4. 索引映射与分析器:Mapping 设计原则、自定义 Analyzer、分词器选型
好,咱们进入第四章。这一章聊的是搜索引擎里最容易被低估、但坑最多的两个东西:Mapping 和 Analyzer。
说白了,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 定分词。两者配合好,搜索系统就成功了一半。
好了,这一章就到这里。下一章我们会聊索引的写入优化——说白了,就是怎么让数据更快地进到搜索引擎里。到时候见。