2665 字
约 8 分钟
13
S13-X11 · 中文分词:词典与自定义词

搜"手机充电器"搜不到标题写着"充电器 手机快充"的文档,搜"笔记本电脑"只命中"电脑"二字——这类问题八成不在打分,而在分词。ES 出厂不带中文分词器,standard 对中文逐字切;装错、装晚、或索引建在装之前,整个字段就按字索引了,之后怎么调 boost 都救不回来。


1. 中文为什么必须专门分词

现象 例子 standard 的切法 后果
无空格 中文句子 逐字切 手/机/充/电/器 词边界消失,短语查询退化成字交集
歧义切分 "南京市长江大桥" 逐字,"市长"被误配 假命中:搜"市长"能搜到南京的桥
未登录词 产品名、人名、缩写 逐字,词整体不存在 该词永远无法作为一个 term 命中
混合文本 iPhone15充电器 字母串保留、汉字逐字 中英边界处理不一致

直接症状:match_phrase 失去意义(任意相邻字都算短语),term 查询只能拿单字查,高亮拆成单字碎片。


2. 用 _analyze 看清切分结果

# ① 单独测 tokenizer
curl -s "localhost:9200/_analyze?pretty" -H 'Content-Type: application/json' -d '
{"tokenizer":"standard","text":"南京市长江大桥"}'

# ② 测已建索引某字段的真实切分(走 mapping 里的 analyzer / search_analyzer)
curl -s "localhost:9200/kb_article/_analyze?pretty" -H 'Content-Type: application/json' -d '
{"field":"title","text":"iPhone15 充电器快充"}'
返回字段 含义 排障价值
tokens[].token 切出的 term 直接看词有没有被切碎
position 位置增量 短语查询依赖它,重叠/同义词会改变它
start_offset / end_offset 原文字符偏移 高亮错位时看这里
type 词的内部类型(如 CN_WORD) 确认走的是 ik 而非 standard

第二条才是有效验证:指定 field 走的是该字段真实配置,只测 tokenizer 看不出 mapping 配错。


3. ik 的两种模式与"索引细、查询粗"

模式 切法 产出 term 数 用在哪
ik_max_word 最细粒度,穷举所有成词组合 多 索引期
ik_smart 粗粒度,最长匹配优先 少 查询期
PUT /kb_article
{
  "mappings": {
    "properties": {
      "title":   { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
      "content": { "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" },
      "sku":     { "type": "keyword" }
    }
  }
}

理由:索引期切细,让"充电器""快充"这类子词都能被单独召回;查询期切粗,避免 query 碎成大量无意义 term 导致 should 子句爆炸与假命中。反过来(索引粗、查询细)会出现"查询词在索引里不存在",直接零召回。

硬要求是索引分析与查询分析产出一致的 term 形态;ik 大版本变更、或只改 analyzer 没改 search_analyzer,都会破坏它。


4. 词典的三种来源

<!-- config/IKAnalyzer.cfg.xml,ik 插件读取 -->
<?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/mystop.dic</entry>
  <entry key="remote_ext_dict">http://cfg.internal/ik/dict.txt</entry>
  <entry key="remote_ext_stopwords">http://cfg.internal/ik/stop.txt</entry>
</properties>
来源 位置 生效方式 适用
内置主词典 插件 jar 内 main.dic 随插件版本 通用词,不随手改
扩展词典 ext_dict config/custom/*.dic 重启或重新加载 业务专有名词,量小稳定
远程词典 remote_ext_dict HTTP 地址 热更新,插件按固定间隔轮询 词表频繁变化、多节点统一

热更新靠 HTTP Last-Modified / ETag 变化判定(轮询间隔按插件版本核对,量级在分钟级)。多节点必须拉同一 URL;词典放单机本地文件,扩节点后必然出现"某些节点切得不一样"。

# 词典必须 UTF-8 无 BOM,一行一词
file -i custom/mydict.dic
head -c 3 custom/mydict.dic | hexdump -C     # 输出 efbbbf 即 BOM,须去掉

BOM 的后果很隐蔽:文件第一个词永远匹配不上,其余词正常。表现是"加了一个词,就它不生效"。


5. 自定义词该不该加

加词的收益 加词的代价
专有名词作为一个 term 精确召回 词典膨胀,内存与启动时间上升
短语查询、高亮边界正确 可能破坏原有切分(长词优先吃掉短词)
同义/别名可控 与内置词典冲突时行为不确定

判断顺序:

  1. 该词在语料里是否高频且整体语义(如"向量检索""蓝绿发布")→ 加;
  2. 是否只是两个常用词的偶然相邻(如"的方""了不")→ 不加,会让正常切分变形;
  3. 加前后用 _analyze 对比同一批样例,确认只有目标词变化;
  4. 词表版本化,与索引版本一起记录,避免"某天开始搜不准"却查不到改动。

6. 停用词:不等于低频词

PUT /kb_article/_settings
{ "analysis": { "filter": { "my_stop": { "type": "stop",
  "stopwords": ["的", "了", "是", "在", "_none_"] } } } }
误区 事实
停用词 = 低频词 停用词是高频且无区分度的词;低频词恰恰是检索目标
停用词越多越省 去掉"不""没"会翻转语义("不支持"→"支持")
停用词表可随意改 索引期与查询期停用词不一致 → term 对不上,静默零召回

_none_ 是 Lucene 里"清空默认停用词表"的占位符;自定义表会整体替换默认表,不是追加。停用词改动后必须重建索引,只改 settings 无效。


7. 同义词:synonym 与 synonym_graph

维度 synonym synonym_graph
使用位置 索引期或查询期 仅查询期
多词映射 展开为并列 term,位置不精确 保留图结构,支持"多词→多词"
索引体积 索引期展开会膨胀 不影响索引
典型用途 简单等价词 "NY" => "New York"、"手机" => "移动电话"

配置形态是在查询期分析器上挂一个 synonym_graph filter:

"filter": { "syn_q": { "type": "synonym_graph",
  "synonyms": ["充电器,充电头,适配器", "笔记本 => 笔记本电脑"] } }

规则优先级:=> 是单向映射,, 是等价展开。写错方向的后果很具体——把 笔记本 => 笔记本电脑 写成 笔记本电脑 => 笔记本,搜"笔记本"就再也不命中"笔记本电脑"。

查询期展开的代价:一个 query 变成多个 should,长同义词链会让子句数暴涨。控制手段是限制每条规则的词数、避免链式传递(A=B, B=C 会连同 A=C 一起展开),并对同义词组做上限校验。


8. ngram 与 edge_ngram:子串与前缀

PUT /kb_article/_settings
{ "analysis": {
    "tokenizer": { "edge_2_10": { "type": "edge_ngram", "min_gram": 2, "max_gram": 10,
                                 "token_chars": ["letter", "digit"] } },
    "analyzer": {
      "idx_prefix": { "type": "custom", "tokenizer": "edge_2_10" },
      "qry_prefix": { "type": "custom", "tokenizer": "keyword" } } } }

(mapping 里把 brand 配成 analyzer: idx_prefix + search_analyzer: qry_prefix。)

方案 索引膨胀 适用 注意
edge_ngram(前缀) 中,与 max_gram 线性相关 搜索框联想、品牌前缀 查询侧必须用 keyword/standard,不能再用 edge_ngram
ngram(任意子串) 高,与 n 的平方相关 商品编码、车型号模糊匹配 组合数爆炸,慎用于长文本
keyword + prefix 查询 无 结构化编号 只能前缀,不能中缀

最常见的翻车是查询期也用了 edge_ngram:query 被切成 2–10 长度的一堆碎片,命中率虚高、排序混乱。


9. 一致性检查清单

curl -s "localhost:9200/_cat/plugins?v"                      # 插件版本须与 ES 一致
curl -s "localhost:9200/kb_article/_mapping?pretty" | grep -A2 analyzer
curl -s "localhost:9200/kb_article/_analyze?pretty" -H 'Content-Type: application/json' -d '
{"field":"title","text":"南京市长江大桥"}'                      # 索引/查询两套分析器对照
检查项 通过标准 失败表现
插件版本 = ES 版本 _cat/plugins 版本号一致 节点启动失败或分析器不存在
字段 analyzer 不是 standard 中文被逐字切
search_analyzer 与索引期同族 term 对不上,零召回
词典编码 UTF-8 无 BOM 首个词不生效

10. 排错

现象 原因 处理
装了 ik 但切分没变 索引先建、插件后装;mapping 还是 standard 新建索引并 reindex,插件必须在建索引前就位
加的词不生效 文件带 BOM / 路径不在 config 下 / 未重载 去 BOM,改 ext_dict 相对路径,重启或触发热更新
搜"笔记本"命中"笔记本电脑"却搜不到"笔记本"本身 同义词方向写反 检查 => 左右
查询报 too_many_clauses 同义词/分词导致子句爆炸 减少同义词链,或调 indices.query.bool.max_clause_count
在 keyword 字段上配了分词器 keyword 不切分,配置被忽略 该字段本就不该分词,改查 term

最后一条展开说:keyword 字段整段原样存储,挂 analyzer 不报错也不起作用。要做子串匹配应改字段类型或加 text 子字段(含 fields 多字段)。


11. 边界

场景 更合适的做法 理由
中文全文检索 ik + 索引细/查询粗 通用且成熟
词表频繁更新 远程词典热更新 免重启、多节点一致

一句话:分词决定 term 空间,打分只是在这个空间里做选择。术语集合错了,后面所有调优都在错误的集合上做优化。


12. 自测

  1. standard 分词器对中文的处理方式会带来哪些具体检索症状?
  2. 为什么惯例是"索引期 ik_max_word、查询期 ik_smart"?反过来会怎样?
  3. 远程词典热更新的触发依据是什么?多节点部署要注意什么?
  4. 词典文件带 BOM 时,症状为什么只出现在第一个词上?
  5. synonym 与 synonym_graph 在使用位置上有什么硬性区别?
  6. 在 keyword 字段上配置 analyzer 会发生什么?
S13-X11 · 中文分词:词典与自定义词
https://www.clxhxhhr.top/posts/5850/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。