主题
倒排索引与分词
一句话:倒排索引把「文档 → 词」翻成「词 → 文档列表」,用排序的词表 + 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": "北京大学"
}
# 期望看到 北京 / 大学 / 北京大学 等 termbash
# 建索引时指定索引和搜索分词器
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 负责排序和聚合。