4. 索引映射与分析器:Mapping 设计原则、自定义 Analyzer、分词器选型

好,咱们进入第四章。这一章聊的是搜索引擎里最容易被低估、但坑最多的两个东西:MappingAnalyzer

说白了,Mapping 就是告诉搜索引擎“你的数据长什么样”,Analyzer 就是告诉它“怎么把一句话拆成能搜的词”。这两件事没做好,后面性能再好的集群也白搭。我见过太多团队,上线前 Mapping 随便写,上线后才发现搜不出来东西,或者搜出来的全是垃圾——嗯,那时候再改,代价就大了。

4.1 Mapping 设计原则:别偷懒,字段类型定生死

先说说 Mapping。我个人习惯,在项目一开始就花半小时把 Mapping 定死。为什么?因为 Elasticsearch 的 Mapping 一旦写入数据,大部分字段类型就不能改了。你想想看,一个字段一开始是 text,后来想改成 keyword,对不起,只能重建索引。

我在项目中遇到过这样一个场景:有个日志系统,一开始把时间字段存成了 text,结果想按时间范围查询时,发现根本没法做 range 查询。最后只能新建索引,重新导数据,折腾了一整晚。

所以,Mapping 设计有几个核心原则:

  • 能用 keyword 就别用 text:keyword 适合精确匹配、聚合、排序。text 适合全文搜索。如果你只是存一个状态码、用户ID、标签,用 keyword。
  • 关闭不需要的索引:每个字段默认都会建索引,但有些字段你根本不会搜它。比如一个日志的原始内容,你可能只存不搜。那就把 "index": false 关掉,省空间、省性能。
  • 控制字段数量:别把几十个字段一股脑全塞进去。字段越多,倒排索引越大,写入越慢。我建议一个索引的字段数控制在 50 以内,超过这个数,你就该考虑拆分了。
  • 使用 dynamic mapping 要谨慎:默认 ES 会自动推断字段类型,但自动推断经常翻车。比如一个数字字段,如果第一条数据是字符串,它就会当成 text,后面再写数字就报错。我建议生产环境一律关掉 dynamic: true,改成 dynamic: "strict",这样新增字段会直接报错,逼你手动定义。

核心原则总结:Mapping 是搜索引擎的“数据契约”。写清楚、写严谨,后面少踩坑。

4.2 自定义 Analyzer:为什么默认的不够用?

默认的 Standard Analyzer 能应付大部分英文场景,但中文就不行了。中文分词是个大问题——"南京市长江大桥",你让机器切,它可能切成"南京/市长/江大桥",也可能切成"南京市/长江大桥"。哪个对?看业务。

我记得有一次做电商搜索,用户搜"苹果手机",结果返回了"苹果"和"手机"两个词的结果,但用户其实想要的是"苹果手机"这个整体。这就是分词粒度的问题。

所以,自定义 Analyzer 几乎是必选项。一个完整的 Analyzer 由三部分组成:

  • Character Filters:先对原始文本做预处理,比如去掉 HTML 标签、替换特殊字符。
  • Tokenizer:核心,决定怎么切词。比如按空格切、按字典切、按规则切。
  • Token Filters:对切好的词做二次处理,比如转小写、去停用词、加同义词。

下面是一个自定义 Analyzer 的示例,我把它用在了一个新闻搜索系统里:

{
  "settings": {
    "analysis": {
      "char_filter": {
        "html_strip": {
          "type": "html_strip"
        }
      },
      "tokenizer": {
        "ik_smart_syno": {
          "type": "ik_smart",
          "use_smart": true
        }
      },
      "filter": {
        "my_synonym": {
          "type": "synonym",
          "synonyms": [
            "苹果, apple => 苹果",
            "手机, 电话 => 手机"
          ]
        },
        "my_stop": {
          "type": "stop",
          "stopwords": ["的", "了", "是", "在"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_filter": ["html_strip"],
          "tokenizer": "ik_smart_syno",
          "filter": ["lowercase", "my_stop", "my_synonym"]
        }
      }
    }
  }
}

这个 Analyzer 做了几件事:先去掉 HTML 标签,然后用 IK 分词器做智能切词,再转小写、去掉中文停用词、最后把同义词合并。比如用户搜"apple",会被映射成"苹果",这样搜英文也能找到中文内容。

小技巧:自定义 Analyzer 一定要在创建索引时定义好,不能事后修改。如果非要改,只能重建索引。

4.3 分词器选型:中文、英文、混合场景怎么选?

分词器选型,说白了就是看你的数据是什么语言、什么场景。我按常见场景给你列个表:

场景 推荐分词器 说明
纯英文 Standard / English Standard 够用,English 会加词干提取(如 running → run)
中文通用 IK(ik_smart / ik_max_word) ik_smart 粗粒度,ik_max_word 细粒度。搜索用 smart,索引用 max_word
中文专业领域 HanLP / THULAC 支持自定义词典,适合医疗、法律等专业场景
中英文混合 IK + 自定义词典 IK 对英文支持一般,建议加英文同义词 filter
拼音搜索 pinyin 插件 用户输入"zhongguo"能搜到"中国"
N-gram N-gram Tokenizer 适合模糊匹配、拼写纠错,但索引膨胀严重

我个人最常用的是 IK 分词器。为什么?因为它支持自定义词典,而且社区活跃。我在做电商搜索时,会把品牌名、产品型号加到自定义词典里,比如"iPhone14ProMax",如果不加词典,IK 会切成"iPhone/14/Pro/Max",加了词典后,它就是一个完整的词。

避坑指南:我曾经在项目中直接用默认的 Standard Analyzer 处理中文,结果用户搜"自行车"搜不到"单车",因为 Standard 是按字切的,根本不知道"自行车"是一个词。后来加了 IK 和同义词才解决。所以,中文场景千万别偷懒,一定要用专门的中文分词器。

4.4 实战:如何测试和调优你的 Analyzer?

写好了 Analyzer,怎么知道它好不好用?ES 提供了一个 _analyze API,可以直接测试分词效果。我每次写完 Analyzer,都会先跑一遍这个 API,看看切出来的词对不对。

POST /my_index/_analyze
{
  "analyzer": "my_analyzer",
  "text": "南京市长江大桥"
}

返回结果会告诉你每个词是什么、在原文中的位置。如果发现切错了,就调整词典或分词器配置。

调优时,我一般关注几个指标:

  • 召回率:用户搜一个词,能不能找到所有相关文档?如果找不到,可能是分词太粗了。
  • 精准率:搜出来的结果是不是用户想要的?如果一堆无关结果,可能是分词太细了。
  • 索引大小:分词越细,索引越大。如果索引膨胀到不可接受,就得考虑用粗粒度分词。

嗯,这里有个经验值:对于中文搜索,索引时用细粒度(ik_max_word),搜索时用粗粒度(ik_smart)。这样既能保证召回,又能保证精准。怎么实现?在 Mapping 里给字段配两个 analyzer:一个用于索引,一个用于搜索。

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      }
    }
  }
}

这个配置,我几乎在每个中文搜索项目里都会用。效果稳定,性能也过得去。

一句话总结:Mapping 定结构,Analyzer 定分词。两者配合好,搜索系统就成功了一半。

好了,这一章就到这里。下一章我们会聊索引的写入优化——说白了,就是怎么让数据更快地进到搜索引擎里。到时候见。