跳到正文

倒排索引与分词 ​

一句话:倒排索引把「文档 → 词」翻成「词 → 文档列表」,用排序的词表 + FST 前缀索引 + 压缩的 posting list 换全文检索的毫秒级定位;代价是写入要维护每个词的倒排,分词策略错了还会直接查不到。

适用版本:Elasticsearch 8.x + Lucene 9.x;ik 插件版本必须与 ES 版本严格一致。

为什么需要 ​

B+ 树索引适合「字段值等于某个值」或「范围」查询,但它存的是整个字段值,不是字段里的词。content LIKE '%检索%' 只能逐行扫描;数据到千万级就不可接受。

倒排索引换了一个方向:先对字段分词,建立「词 → 文档 ID 列表」的映射;查询时先定位词,再取 posting list 求交集。这样「包含某词的文档」从全表扫描变成按词查找。

核心机制 ​

三层结构:Term Dictionary → FST → Posting List ​

  • Term Dictionary:所有 term 按字典序排列,存在磁盘上,通过 mmap 访问。排序是二分查找和范围扫描的前提。
  • Term Index:Lucene 用 FST(Finite State Transducer,有限状态转换器),不是 B 树。FST 把 term 的前缀压缩成状态机,只保留「前缀 → 词表块偏移」,因此可以常驻内存,又支持前缀、范围、通配符查询。
  • Posting List:每个 term 对应的文档列表。DocID 递增,用 delta 编码 + FOR(Frame of Reference)按块位压缩;求交集时用跳表(skip list)跳过不相关块。

为什么不用 HashMap?几十亿 term 的 HashMap 指针和负载因子开销无法常驻内存;排序词表 + FST 既省内存,又天然支持前缀和范围查询。

Posting List 的压缩与求交 ​

原始 DocID: [100, 108, 115, 130, 140]
delta:      [100, 8, 7, 15, 10]       // 只存差值
按 256 个一组,取组内最大位宽做位打包
  • 词频高(如「的」)的 posting list 极长,压缩是必须的;不压缩会直接吃掉磁盘和内存。
  • 多词查询是 posting list 求交集:有序列表 + 跳表,比逐个比对快得多。
  • Roaring bitmap 在 Lucene/ES 里主要用于 filter cache、文档 ID 集合等场景;不要把「倒排索引 = Roaring bitmap」当成通用结论,普通 posting list 的默认压缩是 delta + 位打包。

分词器:索引和查询必须一致 ​

分词器行为适用
standard按空格/标点切,转小写英文、通用
ik_max_word细粒度,尽量多切索引时提高召回
ik_smart粗粒度,最少切分查询时减少 term 数
keyword不分词,整字段一个 term订单号、状态码、标签

常见配置是索引用 ik_max_word、搜索用 ik_smart:索引时多切,保证「北京大学」能被「北京」「大学」命中;查询时少切,减少无意义的 term 和算分噪声。召回不足时,搜索端也可以改用 ik_max_word,代价是查询更慢。

反过来配(索引 smart、搜索 max_word)会出现「索引里没有这个 term」的漏召回,是典型的分词不一致问题。

最小上手 ​

bash
# 版本必须与 ES 完全一致,例如 ES 8.15.0 就装 8.15.0 的 ik
bin/elasticsearch-plugin install \
  https://github.com/infinilabs/analysis-ik/releases/download/v8.15.0/elasticsearch-analysis-ik-8.15.0.zip

# 安装后重启节点
bash
# 验证分词结果
GET _analyze
{
  "analyzer": "ik_max_word",
  "text": "北京大学"
}
# 期望看到 北京 / 大学 / 北京大学 等 term
bash
# 建索引时指定索引和搜索分词器
PUT /articles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "tags": { "type": "keyword" }
    }
  }
}

自定义词典放在 config/IKAnalyzer.cfg.xml 和 extra_main.dic 等文件里;改完需要重启或按插件支持的方式重载,生产上先在测试节点验证。

生产动作 ​

  • 显式 mapping:关闭动态 mapping 或限制 index.mapping.total_fields.limit(默认 1000,以官方文档为准),避免字段爆炸。
  • text + keyword 双字段:需要全文搜索又需要排序/聚合的字符串,用 multi-field:
    json
    { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
  • 验证分词:上线前用 _analyze 同时验证索引分词器和搜索分词器,确保查询词能命中索引 term。
  • 同义词/停用词:放在 token filter 里统一管理;改分词器通常需要 reindex,不能只改 mapping。
  • ik 版本:插件版本与 ES 严格对齐;升级 ES 时先确认 ik 有对应版本。

边界 ​

  • 倒排索引不擅长范围查询:数值范围、日期范围用 BKD 树/points,不是倒排;排序和聚合用 doc_values。
  • 分词不是越细越好:ik_max_word 会产生更多 term,索引更大、查询更慢;按召回和延迟权衡。
  • 索引/搜索分词器不一致会漏召回:这是中文搜索最常见的配置错误。
  • text 字段不能直接聚合/排序:需要 keyword 子字段或 fielddata;fielddata 会占堆,生产慎用。
  • ik 不是语义模型:同义词、错别字、口语化表达仍要词典或向量检索补足。

讲清楚 ​

倒排索引的核心是三层:排序的 Term Dictionary 在磁盘上,FST 把 term 前缀映射到词表块偏移并常驻内存,Posting List 存这个词出现在哪些文档、词频和位置。FST 不是 B 树,它解决的是「几十亿 term 不能全放内存,又要支持前缀和范围查询」的问题。Posting List 用 delta + 位打包压缩,多词查询靠跳表求交集。

分词决定索引里到底有哪些 term。中文没有空格,所以用 ik;常见配置是索引 ik_max_word、搜索 ik_smart,索引多切保召回,搜索少切降噪声。索引和搜索分词器不一致会直接漏召回。生产上先 _analyze 验证,再建显式 mapping;text 负责搜,keyword 负责排序和聚合。

把一门技术讲透到既懂也会用