连载中 4/20

分词器:中文搜索的第一道坎

2026-08-12 · 1985 阅读 · 0 评论 · 0 赞

分词,搜索的第一道闸门

上一篇把世界观立好了:文档进索引,索引拆分片。但文档进索引之前,还有一个动作决定生死——分词。倒排索引的键是词条,词条从哪来?从分词器切出来。切错了,倒排就建歪了,后面怎么查都是歪的。

老王的商城上线第一周就遇到灵异现场:搜「拿铁」能出结果,搜「燕麦拿铁」却是零条——商品明明就叫「燕麦拿铁(大杯)」。运营怀疑搜索功能坏了,老王打开 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 的区别一次说透。

503

10 年全栈工程师 · 503咖啡馆主理人

#Elasticsearch#分词器#IK分词器#词典#中文搜索

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞