2569 字
约 8 分钟
17
S13-X03 · IK 分词器:中文检索的第一道分水岭

用 standard 分析器给中文建索引,跑一次 _analyze 就能看见问题:中华人民共和国 被切成七个单字 term,中华 能匹配,人民共和国 却匹配不上。这不是 mapping 写错,是分析器选错——英文靠空格分词,中文没有空格,standard 内置的 UAX #29 分词规则对汉字只做单字边界切分。

分水岭在这一步:选错分析器,后面所有查询调优都建在错的地基上。下面按排障顺序走:输出形态 → 粒度 → 词典 → 查询侧配置 → "搜不到"。


1. 先用 _analyze 看清切分形态

三个分析器跑同一句话,形态差异一目了然。ES 8.x 里 _analyze 的请求体可以直接给 analyzer:

curl -s -XPOST 'http://localhost:9200/_analyze?pretty' \
  -H 'Content-Type: application/json' -d'
{
  "analyzer": "standard",
  "text": "中华人民共和国国歌"
}'

典型响应的 tokens 数组是七个单字 token,start_offset/end_offset 连续递增,position 每次加 1:

{
  "tokens": [
    { "token": "中", "start_offset": 0, "end_offset": 1, "position": 0 },
    { "token": "华", "start_offset": 1, "end_offset": 2, "position": 1 }
  ]
}

完整响应共七个 token。

换成 ik_smart,典型输出是 中华人民共和国、国歌 两个 token。换成 ik_max_word,同一句话会被切成 中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国 这一组重叠 term。

分析器 切分依据 中华人民共和国 的 term 数 能被"人民共和国"命中
standard Unicode 分词规则,汉字单字切 7(单字) 否
ik_smart 词典最少切分,不重叠 1 否,整词不拆
ik_max_word 词典穷举所有可成词组合 6+ 是
keyword 不分词,整串入库 1(原文) 否,只支持整串精确匹配

上面的 term 列表描述的是形态,不是"实测输出"。不同版本的 IK 主词典收词不同,ik_max_word 的具体 term 数量会变。核对方式就是自己跑一次 _analyze。


2. ik_smart 与 ik_max_word:粒度与各自适用

两种粒度不是好与坏,是召回与精度的换向:

维度 ik_smart ik_max_word
切分策略 最少切分,不重叠 穷举,可重叠
同一文本 term 数 少 多(常见 2–6 倍)
索引体积 / 召回率 小、低 大、高
查询评分 集中 重叠 term 会推高匹配分数

常见组合是索引侧 ik_max_word、查询侧 ik_smart:索引保留全部切法,查询只取最少歧义的一种。因为 max_word 的 term 集合是 smart 的超集,smart 切出的词必然能在索引里找到。

常见组合只是常见,不是唯一:

组合 index / search 适用
召回优先 ik_max_word / ik_max_word 短 query、口语化搜索、词库覆盖不全
体积优先 ik_smart / ik_smart 日志类、字段多、写入量大
平衡 ik_max_word / ik_smart 商品标题、文章标题等中等长度文本
反例 ik_smart / ik_max_word 不要用,查询 term 可能不在索引里,直接漏召回

最后一行最容易写错。约束只有一条:索引的 term 集合必须包含查询的 term 集合。


3. 自定义词典:文件、编码、热更新

配置入口是 $ES_HOME/config/IKAnalyzer.cfg.xml,8.x 里常见于 config/analysis-ik/ 或插件包内的 config,以你装的插件包路径为准:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>IK Analyzer 扩展配置</comment>
  <entry key="ext_dict">custom/mydict.dic</entry>
  <entry key="ext_stopwords">custom/stopword.dic</entry>
  <entry key="remote_ext_dict">http://dict.internal/ik/mydict.dic</entry>
  <entry key="remote_ext_stopwords">http://dict.internal/ik/stopword.dic</entry>
</properties>

词典文件是一行一词的纯文本,可选追加词频与词性:

中华人民共和国 100 n
社会主义核心价值观 50 nz
向量数据库 30 n
配置项 值形态 相对路径基准 生效方式
ext_dict 单文件或 ; 分隔多文件 config 目录 重启或触发词典重载
ext_stopwords 同上 config 目录 同上
remote_ext_dict http(s) URL — 定时轮询,间隔约 60s
remote_ext_stopwords http(s) URL — 同上

热更新的机制要说准:监测线程按固定间隔拉远端 URL,只有响应头里的 Last-Modified 或 ETag 变化时才重新下载。所以自建词典服务必须让静态文件带上这两个头,否则内容改了、时间戳没变,客户端永远认为没更新。

nginx 侧要显式写 if_modified_since exact;(etag on 默认已开),不写的话秒级修改会被合并成同一个 Last-Modified。

词典变更只影响之后写入的文档,已有倒排不会自动重切,作用到历史数据要 _reindex。


4. analyzer 与 search_analyzer 为什么要分开配

一个 text 字段其实有两个分析器:写入用 analyzer,查询用 search_analyzer,不写则继承前者。

PUT /article
{
  "settings": {
    "number_of_shards": 1,
    "analysis": {
      "analyzer": {
        "ik_index":  { "type": "custom", "tokenizer": "ik_max_word", "filter": ["lowercase"] },
        "ik_search": { "type": "custom", "tokenizer": "ik_smart",
                       "filter": ["lowercase", "my_synonym"] }
      },
      "filter": {
        "my_synonym": { "type": "synonym_graph", "synonyms_path": "analysis/synonym.txt", "updateable": true }
      }
    }
  },
  "mappings": {
    "properties": {
      "title":   { "type": "text", "analyzer": "ik_index", "search_analyzer": "ik_search" },
      "content": { "type": "text", "analyzer": "ik_index", "search_analyzer": "ik_search" },
      "tags":    { "type": "keyword" }
    }
  }
}

分开配的三个理由:

理由 说明
粒度不对称 索引要宽,不漏词;查询要窄,不引噪声
查询侧才需要同义词 synonym_graph 只应出现在查询链路,放索引侧会污染倒排
查询侧才需要额外 filter 停用词、拼音、纠错通常只在查询时加,避免写入时丢信息

两侧都写 ik_max_word 时,重叠 term 会同时参与打分,短 query 排序容易乱。


5. "索引建了却搜不到"的三个真实原因

原因一:粒度方向反了。 index 用 ik_smart、search 用 ik_max_word。查询 人民共和国 被切成 人民、共和国,而索引里只存了整词 中华人民共和国,两个 term 都不存在,零命中。

原因二:用 term 查 text 字段。 term 不分词,拿整串去倒排里精确找 term,只有索引里恰好存在这个完整 term 才命中。

原因三:词典是后加的,历史文档没重建。 新词只对新增文档生效。

定位方法把两侧 term 都打出来对比:

GET /article/_analyze
{ "field": "title", "text": "社会主义核心价值观" }

field 参数用的是索引侧分析器;要看查询侧就显式指定 analyzer 名:

GET /article/_analyze
{ "analyzer": "ik_search", "text": "社会主义核心价值观" }

把两次的 tokens[].token 取出来做集合比较:查询侧每个 token 都能在索引侧找到,才可能有召回。


6. synonym_graph 在搜索链路里的顺序

custom 分析器的执行顺序固定:char_filter → tokenizer → filter 数组按序执行。synonym_graph 是 token filter,位置决定成败。

位置 结果
lowercase 之前 大写形式的同义词规则匹配不上
lowercase 之后、stop 之前 推荐
stemmer 之后 词干形态与词典 key 不一致,规则失效

synonym_graph 与老的 synonym 的区别:前者保留位置信息(positionLength),支持多词同义词与跨词扩展;后者只能做单 token 替换。查询侧一律用 synonym_graph。

它在 8.x 支持 updateable: true,配合 synonyms_path 可用 POST /_reload_search_analyzers 在线重载;改 analyzer 定义本身仍需关闭索引。


7. 排错表

现象 原因 处理
节点起不来,日志报 ClassNotFoundException: org.wltea.analyzer... 插件版本与 ES 版本不完全一致 版本号逐位对齐,重装对应版本
elasticsearch-plugin install 报版本校验失败 包内 plugin-descriptor.properties 的 elasticsearch.version 不匹配 同上,不要改描述文件绕过
词典加了词,_analyze 仍切不出来 文件不是 UTF-8,或带 BOM(首词前有 \uFEFF) file mydict.dic 确认;sed -i '1s/^\xEF\xBB\xBF//' 去 BOM
重启后词典仍不生效 ext_dict 相对路径基准是 config,文件放错位置;IK 对不存在的文件静默忽略 改用绝对路径,或确认文件在 config/custom/
remote_ext_dict 内容改了不生效 远端没返回 ETag/Last-Modified 检查响应头,确认时间戳随内容变化
新词只在部分文档命中 词典变更不回溯历史倒排 _reindex 重建,或建新索引后切别名
短 query 命中过多、排序乱 两侧都是 ik_max_word,重叠 term 重复计分 查询侧换 ik_smart,或用 minimum_should_match 约束

8. 边界:什么时候不该继续用 IK

场景 更合适的选择 理由
前缀补全、搜索建议 completion suggester 或 edge_ngram IK 不做前缀索引
拼音搜索 analysis-pinyin 叠在 IK 之后 拼音是独立维度,不该塞进主词典
词库每天大量变动 评估重建成本,或把可变部分挪到 synonym_graph 词典变更要重建索引,热更新只解决加载
字段是 ID、枚举、标签 keyword 分词对这类字段只带来误命中

一句话:IK 解决"中文怎么切成词",不解决"词怎么被找到"。后者要靠 analyzer 分层、同义词、查询写法配合,缺一个都表现为"搜不到"。


9. 自测

  1. standard 把 中华人民共和国 切成几个 token,依据的是什么规则?
  2. 索引侧 ik_smart、查询侧 ik_max_word 会出什么后果,为什么?
  3. remote_ext_dict 的内容改了但客户端不更新,先查哪个响应头?
  4. 为什么 synonym_graph 放在索引侧会出问题?
  5. _analyze 带 field 和带 analyzer 分别看的是哪一侧的分词结果?
  6. 给已有索引加了新词,老文档为什么仍然搜不到,正确做法是什么?
S13-X03 · IK 分词器:中文检索的第一道分水岭
https://www.clxhxhhr.top/posts/5656/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。