数据库基础体系 · 第 125/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

Elasticsearch 中文检索:分词器、词典、同义词、拼音和版本治理

中文检索的难点,不是把字符串放进 Elasticsearch,而是决定:

  • 文档在索引时被拆成哪些词;
  • 用户查询在搜索时被拆成哪些词;
  • 哪些词应该被视为同义;
  • 中文、拼音、首字母和原文是否需要同时支持;
  • 分词器、词典和插件升级后,旧索引是否仍然可读、可检索;
  • 相关性变化如何被验证,而不是凭感觉上线。

这些决策最终都会落到 Elasticsearch 的 text 字段、analyzersearch_analyzer、分词器、词元过滤器、词典和索引生命周期上。


一、先建立模型:文本如何变成可检索的倒排索引

Elasticsearch 的全文检索大致经过以下数据流:

原始文本
  │
  ├─ char_filter:字符预处理
  │
  ├─ tokenizer:把字符流切成 token
  │
  └─ token filter:改写、过滤、归一化 token
        │
        └─ 倒排索引

查询时也会经过类似流程:

用户查询
  │
  └─ search_analyzer
        │
        └─ 查询词项 → 倒排索引匹配 → 相关性计算

1. Analyzer 是一条分析流水线

analyzer 由三类组件组成:

  1. 字符过滤器 char_filter

    • 在分词前处理字符流;
    • 例如把 HTML 标签去掉,或进行字符映射。
  2. 分词器 tokenizer

    • 决定如何把字符流切成 token;
    • 例如按空格切分、按中文词语切分、生成字符二元组。
  3. 词元过滤器 filter

    • 对 token 进行小写化、停用词过滤、同义词展开、拼音转换等。

例如:

{
  "settings": {
    "analysis": {
      "char_filter": {
        "replace_dash": {
          "type": "mapping",
          "mappings": ["- => _"]
        }
      },
      "filter": {
        "my_stop": {
          "type": "stop",
          "stopwords": ["的", "了"]
        }
      },
      "analyzer": {
        "my_analyzer": {
          "type": "custom",
          "char_filter": ["replace_dash"],
          "tokenizer": "standard",
          "filter": ["lowercase", "my_stop"]
        }
      }
    }
  }
}

这条流水线的顺序不能随意理解为“几个功能叠加”。顺序会改变结果:

同义词过滤器 → lowercase

和:

lowercase → 同义词过滤器

可能无法匹配同一条规则,因为同义词规则的解析也受前置组件影响。


二、中文为什么不能直接依赖默认分词

中文文本通常没有空格:

Elasticsearch中文检索
南京市长江大桥
苹果手机壳

英文可以较自然地依赖空格边界,但中文的词边界需要推断。

以:

南京市长江大桥

为例,可能存在多种切分:

南京市 / 长江大桥
南京 / 市长 / 江大桥
南京市 / 长江 / 大桥

不同切分会直接影响召回和相关性。

设文档词项集合为:

Td={t1,t2,,tn}T_d = \{t_1,t_2,\ldots,t_n\}

查询经过搜索分析器后得到:

Tq={q1,q2,,qm}T_q = \{q_1,q_2,\ldots,q_m\}

最简单的全文匹配条件是:

TqTdT_q \cap T_d \neq \varnothing

如果分词器把查询“长江大桥”切成:

长江 / 大桥

而文档被切成:

长江大桥

那么二者可能无法按一个完整词项匹配。反过来,如果使用细粒度切分,召回可能增加,但噪声和短词匹配也会增加。

因此,中文分词不是单纯的“把字符串切开”,而是在以下目标之间取舍:

  • 召回率:相关文档尽量不要漏掉;
  • 精确率:无关文档尽量不要被召回;
  • 短语位置准确性match_phrase 是否能正确判断词序和邻接;
  • 索引大小:词项越多,倒排索引通常越大;
  • 升级稳定性:分词规则变化会改变索引内容和评分。

三、内置中文分词能力:Standard、CJK 与 SmartCN

3.1 Standard tokenizer:通用边界切分,不等于中文词典分词

standard tokenizer 依据 Unicode 文本分词规则进行切分。它适合通用文本,但不能把它当作中文词典分词器。

对中文文本,实际 token 结果应使用 _analyze 验证,不应只根据名称猜测:

curl -X POST 'http://localhost:9200/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "tokenizer": "standard",
    "text": "南京市长江大桥"
  }'

输出中的关键字段通常包括:

{
  "tokens": [
    {
      "token": "南",
      "position": 0
    }
  ]
}

具体 token 数量和行为应以当前 Elasticsearch、Lucene 版本的实际结果为准。重要的是:standard 不提供面向业务词库的中文词语识别能力,也不会自动理解“无线耳机”“新能源汽车”这类业务词。

适用场景通常是:

  • 多语言通用字段;
  • 不依赖中文词语边界的简单检索;
  • 作为实验基线;
  • 配合 n-gram 等策略构造特殊召回字段。

它不适合作为“中文搜索已经解决”的依据。


3.2 CJK tokenizer:面向中日韩文本的字符级或二元组策略

CJK 相关组件针对中日韩文本提供更细粒度的切分方式。常见思路是:

  • 中文、日文、韩文字符按较细粒度处理;
  • 或使用相邻字符二元组,提高部分短语的召回能力;
  • 拉丁字母和数字按照其他规则处理。

可以直接查看当前版本的结果:

curl -X POST 'http://localhost:9200/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "tokenizer": {
      "type": "cjk",
      "tokenizer_type": "bigram"
    },
    "text": "南京市长江大桥"
  }'

二元组策略可能产生类似:

南京 / 京市 / 市长 / 长江 / 江大 / 大桥

这里的重点不是某一版本的具体输出,而是它的机制:词元边界不依赖业务词典,而是使用相邻字符窗口。

优点:

  • 对没有词典的中文文本有较稳定的基础召回;
  • 新词不需要立即维护词典;
  • 适合搜索框、标题召回等对覆盖面要求较高的场景。

缺点:

  • 词项数量增加;
  • “京市”“江大”这样的组合可能没有自然语言意义;
  • 相关性需要额外调优;
  • 不能直接表达“新能源汽车”是一个业务概念。

CJK bigram 是一种检索策略,不是中文语义理解。


3.3 SmartCN:内置插件提供的中文分词器

Elasticsearch 提供 analysis-smartcn 插件,该插件基于 Lucene 的 Smart Chinese Analyzer。它不是默认随集群启用的功能,需要安装与 Elasticsearch 版本匹配的插件。

安装时必须使用与 Elasticsearch 完全匹配的插件版本,并在所有可能承载相关索引的节点安装:

bin/elasticsearch-plugin install analysis-smartcn

安装后通常需要重启节点。插件缺失或版本不兼容时,节点可能无法加载使用该分析器的索引,甚至导致索引无法正常打开。

分析示例:

curl -X POST 'http://localhost:9200/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "smartcn",
    "text": "南京市长江大桥"
  }'

SmartCN 的价值在于提供比通用字符切分更接近中文词语的基础分词能力。但它也有明确边界:

  • 词典和算法不是为你的业务领域定制的;
  • 专业术语、品牌名、内部产品名可能切分不理想;
  • 版本升级可能改变分词结果;
  • 它不等于可随意热更新的业务词典系统。

如果业务需要自行维护词典,通常会使用第三方中文分析插件,或者在写入 Elasticsearch 前进行业务侧分词。后者会牺牲部分 Elasticsearch 原生分析能力,但能获得更明确的版本控制。


四、词典:分词器看到的世界不是业务词库

4.1 词典改变的是分词边界

假设有文本:

我购买了新能源汽车保险

没有业务词典时,可能切成:

我 / 购买 / 了 / 新能源 / 汽车 / 保险

如果业务词典中加入“新能源汽车”,可能变成:

我 / 购买 / 了 / 新能源汽车 / 保险

这不只是 token 数量变化,还会影响:

  • match 的匹配路径;
  • match_phrase 的位置关系;
  • BM25 中词频和文档频率;
  • 高亮显示;
  • 同义词规则是否能命中;
  • 索引大小和查询性能。

4.2 词典更新不只是文件替换

如果索引时使用了词典 A:

新能源汽车 → 新能源汽车

后来词典 B 把它切成:

新能源 / 汽车

那么新旧文档的倒排词项结构可能不同。即使搜索时加载了词典 B,旧文档也不会自动重新生成新的倒排索引。

因此必须区分两个概念:

  • 搜索分析器更新:影响之后的查询解析;
  • 索引分析器更新:要让已有文档按照新规则产生新词项,通常需要重建索引。

一个索引中的字段分析配置不能简单地原地改成另一套逻辑后期待旧数据自动修复。常见的正确流程是:

创建新索引 v2
  ↓
使用新 analyzer 和新词典写入
  ↓
reindex 或从源系统重放
  ↓
抽样验证分词、召回和排序
  ↓
切换 alias
  ↓
观察并保留旧索引

4.3 词典的版本应该进入数据治理

词典至少应具备以下属性:

词典版本:dict-2025-03-01
来源:业务术语审核表
变更类型:新增 / 删除 / 拆分 / 合并
影响字段:title、content
生效方式:重建索引 / 仅搜索侧
验证集:中文检索评测集 v3
回滚方式:切回旧 alias

词典删除尤其危险。例如删除一个词可能使原来一个 token 变成多个 token,也可能让查询失去精确匹配。不能只看“词典文件能否加载”,还要验证分词结果和查询结果。


五、索引分析器与搜索分析器必须分开理解

5.1 analyzer 负责索引,search_analyzer 负责查询

典型映射:

PUT products_v1
{
  "settings": {
    "analysis": {
      "analyzer": {
        "zh_index": {
          "type": "custom",
          "tokenizer": "smartcn",
          "filter": ["lowercase"]
        },
        "zh_search": {
          "type": "custom",
          "tokenizer": "smartcn",
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "zh_index",
        "search_analyzer": "zh_search"
      }
    }
  }
}

查询:

GET products_v1/_search
{
  "query": {
    "match": {
      "name": "无线耳机"
    }
  }
}

执行过程可以概括为:

索引文档:
“无线耳机” → [无线, 耳机]

用户查询:
“无线耳机” → [无线, 耳机]

倒排匹配:
无线、耳机

match 查询会使用字段的搜索分析器。term 查询不会进行分词,适合已经知道精确词项的场景,不适合直接拿中文自然语言输入使用:

GET products_v1/_search
{
  "query": {
    "term": {
      "name": "无线耳机"
    }
  }
}

这条查询要求倒排索引中存在完整的词项 无线耳机。如果索引器产生的是 无线耳机,它就不会像 match 那样自动拆分。

5.2 为什么索引侧和搜索侧可以不同

一种常见策略是:

索引侧:保留更多词项,扩大召回
搜索侧:使用更稳定、更少噪声的查询分析

例如索引侧使用细粒度 tokenizer,搜索侧使用词典分词。这样可能让新词更容易被召回,但也存在词项不对齐的问题,必须通过评测集验证,而不是默认认为“索引切得越细越好”。

另一个常见策略是:

索引侧:普通中文分词
搜索侧:加入同义词

这是因为同义词主要是查询扩展需求,不一定需要把所有变体都写进索引。


六、用 _analyze 检查事实,而不是猜测

创建分析器后,应分别检查:

  1. 字符过滤后文本;
  2. tokenizer 输出;
  3. 最终 token;
  4. token 的 position;
  5. 词典变更前后差异。

示例:

curl -X POST 'http://localhost:9200/products_v1/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "zh_search",
    "text": "无线耳机"
  }'

查询已存在索引的分析器,可以确保测试的是该索引实际使用的配置,而不是本地猜测的配置。

关注以下字段:

{
  "tokens": [
    {
      "token": "无线",
      "position": 0,
      "start_offset": 0,
      "end_offset": 2,
      "type": "<WORD>"
    }
  ]
}
  • token:最终参与匹配的词项;
  • position:短语查询判断词序和距离的重要信息;
  • start_offsetend_offset:高亮和原文偏移相关;
  • type:token 类型,通常用于诊断。

完整算例:

原文:

苹果手机壳

若分词结果为:

苹果(0) / 手机(1) / 壳(2)

查询:

苹果手机

若搜索侧得到:

苹果(0) / 手机(1)

match_phrase 可以利用相邻 position 检查短语。

如果索引结果是:

苹果手机(0) / 壳(1)

而查询结果是:

苹果(0) / 手机(1)

普通 match 仍可能通过部分词项策略得到结果,但 match_phrase 的行为和相关性会不同。这就是“同一个中文字段,分词不同会改变查询语义”的具体表现。


七、同义词:不是简单的字符串替换

7.1 同义词的两种基本关系

同义词规则通常有两种形式。

等价规则

手机, 移动电话

它表达的是一组等价词。是否自动展开、展开方向如何,取决于同义词过滤器和规则配置。

显式映射

笔记本电脑 => 笔记本

它表达有方向的转换:

左侧查询词 → 右侧词项

两者语义不同,不能把所有业务别名都写成等价规则。

例如:

苹果, 苹果公司

可能导致“苹果手机”与“苹果公司年报”互相扩展,产生明显噪声。自然语言中的“相关”不等于检索意义上的“同义”。

7.2 多词同义词需要位置图

考虑规则:

移动电话 => 手机

查询:

移动电话套餐

搜索分析器需要把两个输入词映射成一个替代路径。如果只把 token 当作独立字符串处理,短语位置可能错误。

synonym_graph 的作用是构造 token graph,使多词同义词能够保留替代路径和位置关系。现代 Elasticsearch/Lucene 场景下,多词同义词通常应优先在搜索分析器中使用 synonym_graph,而不是在索引时把查询扩展结果永久写入索引。

概念上的结果可以表示为:

移动 → 电话
  \      \
   \      手机

搜索器可以沿不同路径匹配,而不是把 移动电话手机 粗暴地当作三个互不相关的词。

7.3 为什么通常不建议索引时展开同义词

假设索引时规则为:

手机 => 移动电话

旧规则写入了大量文档后,业务又改成:

手机 => 智能手机

此时:

  • 旧文档已经写入 移动电话
  • 新文档可能写入 智能手机
  • 修改规则不能自动改变旧文档的倒排索引;
  • 删除同义词会要求重新索引才能彻底消除旧词项。

搜索时展开则把规则应用在当前查询上,通常更容易更新和回滚。但搜索侧同义词也要经过评测,因为扩展过多会降低精确率和排序质量。

7.4 同义词规则本身也会被分析器解析

同义词过滤器不是把配置文件当成完全不透明的文本。规则在加载时会经过前置分析组件。因此以下顺序会影响规则含义:

lowercase → synonym_graph

若规则写成:

WiFi, wifi

前置 lowercase 可能使它们归一化为同一个词。

停用词过滤器放在同义词前面也有风险:

stop → synonym_graph

如果规则中含有被停用的词,规则可能被改变,甚至因规则解析结果不合法而加载失败。实际设计时应:

  • 让同义词规则使用已经归一化的形式;
  • 明确停用词是否允许出现在同义词中;
  • 在创建索引和部署规则时执行 _analyze
  • 把“同义词加载成功”作为发布验证的一部分。

八、使用 Synonym Set 时的更新边界

Elasticsearch 支持通过同义词集合管理 API 维护同义词集合。一个示例流程如下:

curl -X PUT 'http://localhost:9200/_synonyms/products_synonyms' \
  -H 'Content-Type: application/json' \
  -d '{
    "synonyms_set": [
      {
        "id": "mobile-phone",
        "synonyms": "手机, 移动电话"
      },
      {
        "id": "laptop",
        "synonyms": "笔记本电脑 => 笔记本"
      }
    ]
  }'

然后在分析器中引用该集合。不同 Elasticsearch 版本对相关参数、更新和重载流程存在版本要求,不能把某一版本的配置直接复制到所有集群。使用前应确认当前版本对 synonyms_setsynonym_graph 和可更新搜索分析器的支持方式。

一个重要约束是:可更新的同义词通常应只放在搜索分析器中,并按照当前版本要求配置可更新属性。更新集合后,还需要确认相关搜索分析器是否已经重新加载。某些环境中需要显式执行搜索分析器重载接口,典型形式为:

curl -X POST 'http://localhost:9200/products_v1/_reload_search_analyzers'

发布流程不能只检查 API 返回成功,还要验证:

curl -X POST 'http://localhost:9200/products_v1/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "zh_search",
    "text": "移动电话"
  }'

然后使用真实查询确认:

GET products_v1/_search
{
  "query": {
    "match": {
      "name": "移动电话"
    }
  }
}

如果同义词集合更新成功,但搜索节点上的分析器没有使用新规则,可能出现“配置中心显示新规则、查询结果仍是旧行为”的状态。生产上应记录:

同义词集合版本
分析器重载时间
各节点确认状态
抽样查询结果
失败节点及恢复动作

同义词集合发布还要考虑规则错误。若新规则无法解析,搜索分析器可能加载失败。正确的恢复路径是:

  1. 保留上一版规则;
  2. 在测试索引或隔离环境先验证;
  3. 发布新规则;
  4. 检查每个节点的分析器状态;
  5. 用固定查询集验证;
  6. 失败时恢复旧规则并重新加载。

九、拼音检索:它解决的是输入方式,不是中文分词本身

用户可能输入:

wuxian erji
wxej

而文档写的是:

无线耳机

拼音检索的目标是建立另一种可匹配表示:

无线耳机
→ wu xian er ji
→ wxej

这与中文分词是两个维度:

  • 中文分词回答“中文文本由哪些词组成”;
  • 拼音转换回答“这些中文字符或词对应哪些拼音表示”。

9.1 Elasticsearch 官方能力与第三方插件要区分

Elasticsearch 官方发行版并不等于内置了任意中文拼音分析插件。常见的 analysis-pinyin 等方案通常来自第三方插件,插件的安装方式、参数名、支持版本和 token 规则应以插件自身文档为准。

不能因为配置中出现:

"type": "pinyin"

就认为这是 Elasticsearch 官方稳定组件。

第三方插件带来额外边界:

  • 每个节点都必须安装兼容版本;
  • Elasticsearch 和插件版本必须匹配;
  • 滚动升级期间要检查新旧节点兼容性;
  • 插件缺失可能导致索引打开或节点启动失败;
  • 插件升级可能改变拼音切分、首字母和重复 token;
  • 快照通常不等于把节点插件自动部署到恢复目标。

9.2 常见索引设计:原文字段与拼音字段并存

不要轻易把原中文字段直接替换为拼音字段。更常见的是多字段:

PUT products_pinyin_v1
{
  "settings": {
    "analysis": {
      "analyzer": {
        "zh_base": {
          "type": "custom",
          "tokenizer": "smartcn",
          "filter": ["lowercase"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "zh_base",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      }
    }
  }
}

这里先保留:

  • name:中文全文检索;
  • name.keyword:精确过滤、聚合、排序。

如果使用第三方拼音插件,通常会额外建立一个拼音字段,例如:

name_pinyin

然后在查询时分别搜索中文字段和拼音字段:

GET products_pinyin_v1/_search
{
  "query": {
    "multi_match": {
      "query": "wxej",
      "fields": [
        "name^3",
        "name_pinyin"
      ]
    }
  }
}

这里的 ^3 是字段 boost,表示中文字段的基础权重高于拼音字段。实际权重必须用评测集确定。

9.3 拼音字段的歧义和噪声

拼音并非一一对应:

可能读作:

xing
hang

如果插件没有结合上下文处理多音字,可能产生错误拼音。姓名、品牌和专有名词尤其容易出错。

首字母也会产生大量碰撞:

无线耳机 → wxej
无锡二中 → wxez

如果用户只输入 wx,结果可能非常宽。应考虑:

  • 首字母字段只承担辅助召回;
  • 完整拼音字段和中文字段分开;
  • 对拼音命中降低权重;
  • 对完整品牌名做 keyword 或专门的别名字段;
  • 用固定查询集验证误召回。

拼音字段的另一个风险是 token 数量可能显著增加。如果同时保留原文、完整拼音、首字母和拆分拼音,索引体积和查询开销都可能增加。不能在没有数据规模和查询分布验证的情况下全部开启。


十、字段设计:不要让一个字段承担所有检索语义

一个商品名称通常至少有以下几种需求:

需求 推荐字段形态
中文全文检索 text
精确相等 keyword
拼音召回 独立拼音 text 字段
品牌、型号过滤 独立 keyword 字段
同义词查询扩展 搜索分析器或查询侧逻辑
前缀联想 专门的 autocomplete 字段或查询设计

示例:

PUT products_v1
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "zh_index",
        "search_analyzer": "zh_search",
        "fields": {
          "keyword": {
            "type": "keyword"
          }
        }
      },
      "brand": {
        "type": "keyword"
      },
      "model": {
        "type": "keyword"
      },
      "name_pinyin": {
        "type": "text",
        "analyzer": "pinyin_index",
        "search_analyzer": "pinyin_search"
      }
    }
  }
}

textkeyword 的区别必须明确:

  • text 会被分析,适合全文匹配;
  • keyword 通常作为一个完整值索引,适合精确匹配、聚合和排序;
  • keyword 不是“更精确的中文全文搜索字段”。

例如:

{
  "term": {
    "name.keyword": "无线耳机"
  }
}

适合判断字段值是否恰好等于“无线耳机”。


十一、短语查询、位置和分词结果之间的关系

matchmatch_phrase 的语义不同。

match

match 会分析查询文本,并根据查询类型匹配产生的词项。它适合自然语言检索。

{
  "query": {
    "match": {
      "name": "无线耳机"
    }
  }
}

match_phrase

match_phrase 除了要求词项匹配,还会利用 token 的 position 判断顺序和距离:

{
  "query": {
    "match_phrase": {
      "name": "无线耳机"
    }
  }
}

假设文档 A:

无线 / 耳机

文档 B:

无线 / 蓝牙 / 耳机

如果查询分析结果为:

无线(position=0) / 耳机(position=1)

那么文档 A 更符合相邻短语。文档 B 是否匹配以及评分如何,取决于查询参数和位置关系。

如果使用了同义词图,多词同义词必须正确产生位置图,否则短语查询可能出现:

  • 本应命中的短语没有命中;
  • 非相邻词被当成相邻;
  • 高亮片段不符合用户预期;
  • 查询扩展后评分异常。

因此,测试中文 analyzer 时不能只检查 token 字符串,还要检查 position


十二、同义词、分词和相关性调优的联动

分词改变的是倒排中的词项;同义词改变的是查询路径;BM25 再根据词项统计量计算相关性。

BM25 的一个常见形式是:

score(q,d)=tqIDF(t)f(t,d)(k1+1)f(t,d)+k1(1b+bdavgdl)\text{score}(q,d) = \sum_{t \in q} IDF(t) \cdot \frac{f(t,d)(k_1+1)} {f(t,d)+k_1\left(1-b+b\cdot\frac{|d|}{avgdl}\right)}

其中:

  • f(t,d)f(t,d):词项 tt 在文档 dd 中的词频;
  • d|d|:文档长度;
  • avgdlavgdl:平均文档长度;
  • IDF(t)IDF(t):词项逆文档频率;
  • k1k_1bb:BM25 参数。

中文分词越细,通常会导致:

  • 文档 token 数量增加;
  • 某些短词在大量文档出现;
  • 词项的 IDF 变化;
  • 文档长度归一化变化;
  • 相同查询在不同 analyzer 下排序变化。

同义词扩展也会改变查询中参与计算的词项数量。例如:

手机 → 手机、移动电话

扩展后可能让包含“移动电话”的文档获得匹配分,但如果扩展词过于常见,可能产生大量低质量结果。

因此,不能只用“是否能搜到”判断 analyzer 是否正确。至少应记录:

查询
期望文档集合
可接受的排序范围
不可接受的误召回
高亮结果
match 与 match_phrase 的差异

如果需要定位某个文档为什么排在前面,可以使用:

GET products_v1/_explain/1
{
  "query": {
    "match": {
      "name": "无线耳机"
    }
  }
}

_explain 可帮助确认:

  • 查询实际分析出了哪些词;
  • 哪些词项命中了;
  • BM25 的贡献来自哪里;
  • boost 和字段权重是否按预期生效。

十三、版本治理:分析器是索引协议的一部分

13.1 为什么 analyzer 版本必须受控

索引中的倒排结构由索引时分析器决定。以下任何变化都可能影响索引内容:

  • tokenizer 类型变化;
  • 词典内容变化;
  • stopword 变化;
  • lowercase、stemmer 等过滤器变化;
  • 同义词规则变化;
  • 第三方插件版本变化;
  • Lucene 或 Elasticsearch 大版本升级导致的分析行为变化。

所以 analyzer 不是普通配置,而更接近一种索引协议:

文档源数据 + analyzer 版本 → 倒排索引

如果同样的源文本在不同版本产生不同 token,旧索引和新索引就不能简单看作同一语义。

13.2 内置组件、插件和业务侧分词的治理差异

内置组件

优点:

  • 随 Elasticsearch 版本管理;
  • 不需要额外安装节点插件;
  • 集群部署相对简单。

风险:

  • 版本升级可能带来行为变化;
  • 不能假设不同大版本分词结果完全一致。

Elasticsearch 官方分析插件

例如 analysis-smartcn。它仍然需要按节点安装,并要求与 Elasticsearch 版本匹配。

第三方分析插件

例如常见的 IK、Pinyin 插件。它们不是 Elasticsearch 核心 API 的统一保证范围,治理要求更高:

Elasticsearch 版本
插件版本
Lucene 兼容性
节点安装清单
索引使用字段
词典版本
升级与回滚方案

业务侧预处理

业务系统在写入前生成:

{
  "name": "无线耳机",
  "name_tokens": ["无线耳机", "无线", "耳机"],
  "name_pinyin": ["wuxianerji", "wxej"]
}

这种方案让 token 结果完全由业务控制,但也意味着:

  • 需要自己定义 token 结构;
  • 需要处理高亮偏移;
  • 需要保证所有写入链路一致;
  • 需要在源数据变更时重新生成派生字段。

没有一种方案天然适合所有业务,关键是明确谁负责分词、谁负责版本发布、谁负责回滚。


十四、索引生命周期:创建、迁移、切换和回滚

14.1 在索引创建时固定分析设置

完整示例:

curl -X PUT 'http://localhost:9200/articles_v1' \
  -H 'Content-Type: application/json' \
  -d '{
    "settings": {
      "analysis": {
        "analyzer": {
          "zh_index": {
            "type": "custom",
            "tokenizer": "smartcn",
            "filter": ["lowercase"]
          },
          "zh_search": {
            "type": "custom",
            "tokenizer": "smartcn",
            "filter": ["lowercase"]
          }
        }
      }
    },
    "mappings": {
      "properties": {
        "title": {
          "type": "text",
          "analyzer": "zh_index",
          "search_analyzer": "zh_search",
          "fields": {
            "keyword": {
              "type": "keyword"
            }
          }
        }
      }
    }
  }'

前置条件:

  • 集群已安装并启用 analysis-smartcn
  • 所有相关节点均有该插件;
  • 插件版本与 Elasticsearch 版本匹配。

如果插件未安装,创建或打开使用 smartcn 的索引会失败。

14.2 使用 alias 进行切换

假设业务访问别名:

articles

先指向旧索引:

POST /_aliases
{
  "actions": [
    {
      "add": {
        "index": "articles_v1",
        "alias": "articles",
        "is_write_index": true
      }
    }
  ]
}

创建 articles_v2 并完成重建后,使用一次原子 alias 操作切换:

POST /_aliases
{
  "actions": [
    {
      "remove": {
        "index": "articles_v1",
        "alias": "articles"
      }
    },
    {
      "add": {
        "index": "articles_v2",
        "alias": "articles",
        "is_write_index": true
      }
    }
  ]
}

切换前至少验证:

curl 'http://localhost:9200/articles_v2/_count'
curl -X POST 'http://localhost:9200/articles_v2/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "zh_search",
    "text": "新能源汽车"
  }'

还要执行固定检索集,检查:

  • 结果数量;
  • 关键文档是否召回;
  • 排名变化;
  • 高亮;
  • 中文、拼音、同义词查询;
  • 失败查询是否有日志和可回滚路径。

14.3 迁移过程中的并发写入

如果重建索引期间源数据仍在变更,单纯执行一次 _reindex 可能产生时间窗口:

读取旧索引快照
  ↓
源数据继续变化
  ↓
新索引缺少后续变更

常见解决方案包括:

  • 暂停写入后重建;
  • 使用源数据库的变更日志补偿;
  • 双写新旧索引;
  • 先全量迁移,再按更新时间或版本号增量补偿;
  • 切换前短暂阻塞写入并校验。

这不是 Elasticsearch 分词器特有的问题,但 analyzer 变更通常必须重建索引,因此并发写入治理不可省略。


十五、失败表现和诊断路径

15.1 “查不到”,但文档明明存在

按以下顺序诊断:

第一步:确认字段类型

curl 'http://localhost:9200/articles_v1/_mapping'

如果字段是 keyword,却使用自然语言全文查询,结果可能不符合预期。

第二步:比较索引侧和查询侧分词

curl -X POST 'http://localhost:9200/articles_v1/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "zh_index",
    "text": "新能源汽车"
  }'

curl -X POST 'http://localhost:9200/articles_v1/_analyze' \
  -H 'Content-Type: application/json' \
  -d '{
    "analyzer": "zh_search",
    "text": "新能源汽车"
  }'

第三步:确认查询类型

  • term 不分词;
  • match 使用搜索分析器;
  • match_phrase 依赖 position;
  • query_string 还涉及语法解析和转义。

第四步:检查实际命中词项

可以使用 profile 查询分析执行路径,或使用 _explain 查看特定文档的得分和匹配原因。

15.2 “加了同义词,结果反而变差”

常见原因:

  • 把近义词写成了绝对等价词;
  • 同义词展开过宽;
  • 多词同义词没有使用图结构;
  • 同义词放在索引侧,旧规则已经污染索引;
  • 查询分析器和索引分析器的词边界不一致;
  • 扩展词的文档频率过高,排序被常见词主导。

诊断时先做:

POST /articles_v1/_analyze
{
  "analyzer": "zh_search",
  "text": "移动电话"
}

不要直接从搜索结果猜规则是否生效。先确认 token 和 position,再检查真实查询。

15.3 “拼音能搜到,但中文结果质量下降”

可能是拼音字段与中文字段混合后权重过高:

"fields": [
  "title",
  "title_pinyin"
]

拼音字段可能召回大量同音或首字母碰撞结果。应分别观察:

"fields": [
  "title^3",
  "title_pinyin^0.5"
]

但具体权重不是固定答案,需要用评测集调整。若拼音只是“用户输入方式兼容”,通常不应让它完全替代中文字段的相关性。

15.4 “升级后同样查询的排序变了”

需要区分:

  • 词项变了;
  • 文档统计量变了;
  • BM25 参数变了;
  • 分片分布和数据量变了;
  • 同义词规则变了;
  • 插件版本变了。

首先对同一批文本在旧版本和新版本运行 _analyze,保存 token、position 和 offset 的差异。然后在相同数据快照上比较查询结果,而不是直接比较两个数据量不同的生产集群。


十六、一个可执行的最小验证集

中文检索发布前,可以准备如下数据:

POST articles_v1/_bulk
{"index":{"_id":"1"}}
{"title":"无线耳机使用说明"}
{"index":{"_id":"2"}}
{"title":"移动电话套餐介绍"}
{"index":{"_id":"3"}}
{"title":"新能源汽车保险"}
{"index":{"_id":"4"}}
{"title":"南京市长江大桥交通信息"}

验证查询至少包括:

无线耳机
移动电话
手机
新能源汽车
新能源 汽车
南京市长江大桥
wuxianerji
wxej

每条查询应记录:

analyze 输出
召回文档
文档排序
高亮片段
是否命中预期
是否出现误召回
耗时和错误

测试不应只覆盖“能否命中”,还应覆盖反例:

苹果手机
苹果公司
行车
银行

这些词可以暴露:

  • 词典边界;
  • 多音字处理;
  • 同义词误扩展;
  • 拼音碰撞;
  • 短词噪声;
  • phrase position 错误。

十七、规范保证、常见实现与经验建议的边界

Elasticsearch 可以明确保证的部分

  • text 字段会按配置的 analyzer 建立全文索引;
  • search_analyzer 用于查询侧分析;
  • keyword 字段按完整值处理;
  • term 查询不会像 match 一样对输入执行全文分析;
  • analyzer 配置属于索引设置和映射的一部分;
  • 多词同义词需要正确处理位置关系,synonym_graph 是相关机制。

依赖版本或插件的部分

  • SmartCN 的具体切分结果;
  • 第三方 IK、Pinyin 插件的参数和 token 输出;
  • 同义词集合更新后的自动重载方式;
  • Lucene 或 Elasticsearch 升级后的分词细节;
  • 插件在滚动升级和快照恢复中的兼容行为。

必须由业务评测决定的部分

  • 使用 SmartCN、CJK bigram 还是第三方分词;
  • 是否添加业务词典;
  • 同义词放在搜索侧还是索引侧;
  • 拼音字段是否保留完整拼音和首字母;
  • 中文字段与拼音字段的权重;
  • 词典更新触发全量重建还是分阶段发布。

结语

中文检索的核心不是选择一个“最强分词器”,而是建立一套可解释的索引语义:

文本规范化
  → 中文分词
  → 业务词典
  → 同义词与拼音扩展
  → 字段与 analyzer 设计
  → 索引重建和版本发布
  → 评测、监控与回滚

分词器决定词项边界,词典决定业务术语如何被理解,同义词决定查询如何扩展,拼音决定用户输入方式如何映射到中文内容,而版本治理决定这些语义变化能否安全地进入生产系统。

只要索引分析器、搜索分析器、词典和插件版本没有被当作同一条检索协议管理,系统就可能出现“配置看起来正确、查询结果却不可解释”的问题。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。