倒排索引是什么?
结论先行:倒排索引是“词项 → 文档列表”的映射,与“文档 → 词项”的正排相反。它由三部分组成:词典(term dictionary,有序存放全部词项)、倒排表(posting list,每个词项对应的文档 ID 列表)、词项索引(term index,词典的前缀索引,常用 FST 实现并常驻内存)。检索时先查词项索引定位词典,再取倒排表求交,最后按文档 ID 回表取原文。
一、结构总览
文档集合
doc1: "分布式 锁 实现"
doc2: "分布式 事务"
doc3: "Redis 分布式 锁"
分词后建立倒排
term posting list
────────────────────────────
分布式 → [1, 2, 3]
锁 → [1, 3]
实现 → [1]
事务 → [2]
Redis → [3]
term index (FST) ──> term dictionary (有序) ──> posting list (压缩)
常驻内存 磁盘 + 文件缓存 磁盘 + 跳表
二、三级结构各自解决什么
| 层次 | 数据结构 | 解决的问题 |
|---|---|---|
| term index | FST(有限状态转换器) | 词典很大,无法全放内存,用前缀共享压缩 |
| term dictionary | 有序数组/字典树 | 二分或前缀定位到具体 term |
| posting list | 有序 docId 数组 + 跳表 | 多个 term 求交、按 docId 顺序回表 |
| 压缩编码 | FOR(帧内差值)、Roaring Bitmap | 倒排表可能上亿条,必须压缩 |
举例:分布式 与 锁 求交,得到 [1, 3];如果查询是 分布式 AND 锁 AND NOT 事务,再与 [2] 做差集。
三、正排与倒排的分工
| 方向 | 结构 | 用途 |
|---|---|---|
| 正排(doc → values) | DocValues 列存 | 排序、聚合、脚本取值 |
| 倒排(term → docs) | 倒排索引 | 关键词检索、过滤 |
ES 同时维护两套:检索走倒排,排序聚合走 DocValues,所以“既能搜又能排”。
四、动手验证
// 看分词结果,确认 term 是怎么产生的
GET /article/_analyze
{
"analyzer": "ik_max_word",
"text": "分布式锁的实现"
}
// 看查询命中了哪些 term、怎么算分
GET /article/_search
{
"explain": true,
"query": { "match": { "title": "分布式锁" } }
}
_analyze 输出 token 列表,explain 输出每个 term 的 IDF、TF 与最终得分,是排查“为什么这条没排前面”的标准手段。
五、倒排索引的代价
| 代价 | 说明 |
|---|---|
| 写入放大 | 每个字段的每个 term 都要写倒排,写入量远大于原文 |
| 存储膨胀 | 倒排 + 正排 + 原始 _source,通常是原文的 2~3 倍 |
| 更新昂贵 | 段不可变,更新等于重建文档,旧文档延迟删除 |
| 分词依赖 | 中文分词质量直接决定召回率,词典要维护 |
常见追问
- 追问:为什么倒排表要有序?有序才能用跳表做跳跃求交,也才能按 docId 顺序批量回表,减少随机 IO。
- 追问:FST 为什么省内存?它把词典的所有前缀做成状态机共享,几十万词项的索引可以压到几 MB。
- 追问:中文一定要装分词器吗?默认 standard 分词器按单字切,“分布式锁”会切成四个字,召回高但精度差;生产中文场景基本都用 ik 或 jieba 类分词器。
小结
- 倒排 = 词典 + 倒排表 + 词项索引,方向是 term → docs。
- FST 压缩词典索引,跳表加速求交,FOR/Roaring 压缩倒排表。
- 检索走倒排,排序聚合走 DocValues,两者互补。
- 倒排索引用存储和写入成本,换检索速度。