分词,搜索的第一道闸门
上一篇把世界观立好了:文档进索引,索引拆分片。但文档进索引之前,还有一个动作决定生死——分词。倒排索引的键是词条,词条从哪来?从分词器切出来。切错了,倒排就建歪了,后面怎么查都是歪的。
老王的商城上线第一周就遇到灵异现场:搜「拿铁」能出结果,搜「燕麦拿铁」却是零条——商品明明就叫「燕麦拿铁(大杯)」。运营怀疑搜索功能坏了,老王打开 ES 一查,发现问题出在他压根没配分词器:ES 是能跑,但中文搜索是另一回事。
亲眼看看:standard 是怎么切中文的
ES 内置的默认分词器叫 standard,用 _analyze 接口能直接看它怎么切:
POST /_analyze
{
"analyzer": "standard",
"text": "燕麦拿铁"
}
返回词条:燕、麦、拿、铁standard 的规则很简单:英文按空格切,中文按单字切。对英文这没什么问题,对中文这是灾难现场——「燕麦拿铁」不是四个独立的字,是一个商品名。单字切分带来两个后果。
一是召回全靠缘分。match 查询默认是 OR 逻辑,只要文档里含「拿」或「铁」任意一个字就能命中。于是「拿铁」「打铁」「拿手」「铁路」全蹦出来了,结果一大堆,相关的排前面靠的不是算法是运气。
二是单字没有词义。「拿铁」的「铁」和「打铁」的「铁」,在倒排里是同一个词条,语义上却八竿子打不着。字级的倒排,撑不起像样的相关性排序。
想确认某个字段建索引时用的什么分词器,不用翻文档,两个接口就够了:GET /product/_mapping 能看到字段的 analyzer 配置;POST /product/_analyze 加上 field 参数,能用该字段的分词器现场试切。
请出 IK:一细一粗两把刀
中文场景的事实标准是 IK 分词器。装法和所有 ES 插件一样:
bin/elasticsearch-plugin install https://github.com/infinilabs/analysis-ik/releases/download/v8.x.x/elasticsearch-analysis-ik-8.x.x.zip
版本号必须和 ES 完全一致,装完重启集群生效IK 提供两把刀:ik_max_word 细切,能切出来的词全切出来;ik_smart 粗切,不重复、不交叉。同一个词,两种切法:
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "燕麦拿铁"
}
返回词条:燕麦、拿铁、燕、麦、拿、铁
POST /_analyze
{
"analyzer": "ik_smart",
"text": "燕麦拿铁"
}
返回词条:燕麦、拿铁max_word 把「燕麦」「拿铁」都切出来,还把单字留着兜底;smart 只给最可能的两个词。一个求全,一个求精。中文场景别再纠结选哪个——都要,只是用在不同环节,这就引出下面的经典搭配。
经典搭配:细进粗出
两把刀怎么用?社区打磨多年的黄金法则是:索引用 ik_max_word,搜索用 ik_smart。写进 mapping:
PUT /product_v1
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}为什么这样配?索引时细切,倒排里词条丰富,用户不管怎么搜都容易命中——保召回。搜索时粗切,把输入切成少量高质量词条去查——控噪声。不指定 search_analyzer 时,搜索会沿用索引的分词器,那就等于用细刀切查询,噪声又回来了。
反过来配(粗进细出)会出事:索引里只有「燕麦拿铁」整词,用户搜「燕麦」被细切成「燕」「麦」两个单字词条,倒排里压根没有这俩词条,一条都搜不到。一句话记住:字典要厚,问题要短。
词库不认识「生椰拿铁」怎么办
别高兴太早。IK 自带二十多万条主词典,但新词它不认识。老王上新「生椰拿铁」,IK 切成了「生」「椰」「拿铁」——主词典里没有「生椰」这个词。品牌名、网络热词、行业黑话,都得喂词典。
IK 支持词典扩展,配置文件在插件目录的 config/IKAnalyzer.cfg.xml:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<comment>IK Analyzer 扩展配置</comment>
<entry key="ext_dict">custom_ext.dic</entry>
<entry key="remote_ext_dict">http://192.168.2.105:8080/hotdict</entry>
</properties>第一种,本地扩展词典。custom_ext.dic 一行一个词,加上「生椰拿铁」即可,但改完要重启节点才生效。
第二种,远程词典热更新。remote_ext_dict 指向一个 HTTP 地址,IK 每六十秒拉一次内容,发现变化就加载新词,不用重启。老王最后选的就是这条路:词条存数据库,暴露一个接口返回词典文本,运营在后台加「生椰拿铁」,一分钟内搜索就有了。
第三种,停用词词典 ext_stop_words,把「的」「了」这类无意义词挡在倒排外面,用法同理。
注意一个坑:词典更新只影响之后的分词。已建好的倒排不会因为词典变了自动重切,老文档想用上新词典,得重建索引,怎么重建下面马上讲。
改分词器?只能重建索引
老王吃过最大的亏在这里:字段分词器在 mapping 里创建时定死,之后不能改。直接改 analyzer,ES 会甩回来一个 illegal_argument_exception。想换分词器只有一条路:
第一步:新建 product_v2,mapping 换上新分词器
第二步:POST _reindex 把 product_v1 数据搬过去
第三步:别名 product 从 v1 切到 v2,搜索接口无感吃了这次亏,老王立了规矩:索引名带版本号(product_v1、product_v2),对外一律用别名,重建索引是常规操作而不是事故。这个习惯在后面讲 mapping 和分片规划时还会反复救他。
小结
| 场景 | 结论 |
|---|---|
| standard 切中文 | 按单字切,召回靠缘分,中文场景别裸奔 |
| ik_max_word | 细切保召回,建索引用 |
| ik_smart | 粗切控噪声,搜索用 |
| 新词识别 | 本地词典要重启,远程词典六十秒热更新 |
| 改分词器 | 只能重建索引,版本号加别名切换是标配 |
分词搞定,老王以为能歇口气,结果建索引时手一抖把价格字段建成 text 类型——数值字段类型建错,聚合直接废掉,改还改不了。下一篇讲 mapping:字段类型建错,神仙也救不回来,顺便把 text 和 keyword 的区别一次说透。
评论 (0)