Elasticsearch 倒排索引原理
Elasticsearch 倒排索引原理
候选人小林在面试时被问到:"ES 的全文搜索为什么这么快?底层靠什么实现的?"
小林说:"因为 ES 用了倒排索引。"
面试官追问:"倒排索引的数据结构是什么样的?"
小林说:"就是...文档和词的对应关系..."
面试官继续追问:"那 Posting List 怎么存的?为什么 ES 的倒排索引能存几十亿文档还不爆内存?"
小林卡住了。
面试官又问:"FST 是什么?Term Index 为什么能加速查询?"
小林彻底答不上来。
【面试官心理】 这道题我是在探测候选人对搜索引擎核心数据结构的理解深度。知道"倒排索引"这个词的占 90%,能说出 Posting List 压缩算法的占 20%,能讲清楚 FST 的占 5%。能答到最后的,基本都有搜索引擎或者底层存储的开发经验。
一、正排索引 vs 倒排索引 🔴
1.1 直观类比
【直观类比】想象你要在一本书里找"HashMap"这个词出现了几次:
- 正排索引:翻到每一页,数这一页有没有 HashMap(逐页扫描)
- 倒排索引:先查书的索引目录,找到"HashMap"这个词条,上面写着"第 3, 15, 87, 203...页"(直接定位)
倒排索引的查询复杂度从 O(N) 降到了 O(1)(精确匹配)或者 O(log M)(范围查询,M 为词条数量)。
1.2 数据结构对比
正排索引(Forward Index):
倒排索引(Inverted Index):
1.3 追问链
第一层:概念理解 面试官问:"倒排索引和正排索引的区别是什么?" 候选人答:"文档到词的映射 vs 词到文档的映射..." 考察点:基本概念
第二层:数据结构 面试官追问:"Term Dictionary、Term Index、Posting List 分别是什么?" 候选人答:...(P6 分水岭) 考察点:底层数据结构理解
第三层:压缩算法 面试官追问:"几十亿文档的 Posting List 怎么存?有哪些压缩算法?" 候选人答:...(P7 区分点) 考察点:存储优化、工程实践
第四层:查询流程 面试官追问:"ES 收到一个 match 查询,底层是怎么找到目标文档的?" 候选人答:... 考察点:全链路理解
二、倒排索引核心组件 🟡
2.1 三大组件
Lucene 的倒排索引由三个核心组件构成:
2.2 Term Dictionary 与 Term Index
Term Dictionary 就是把所有词条按字典序排列,支持二分查找。但问题是:如果词条有 10 亿个,二分查找需要 30 次 IO(log2(1e9) ≈ 30)。
Term Index 用 FST 在内存中构建一个前缀树,类似 Trie 树,但更紧凑:
FST 的特点是:
- 空间占用极小:相同前缀共享节点,比 Trie 节省 50%~80% 空间
- 查询极快:只需沿着 FST 走完查询词的路径,就能定位到 Term Dictionary 的位置
📖 点击展开:FST 的构建过程
FST(Finite State Transducer)在 Lucene 中用于将词条映射到任意值(比如文档频率)。
构建过程:
- 将所有 Term 按字典序排序
- 从第一个 Term 开始,逐个插入 FST 节点
- 相同前缀合并,不同后缀作为新路径
- 每个终止节点存储一个"输出值"(如 postings 指针偏移量)
FST 的查询时间复杂度是 O(query_length),与数据规模无关。
三、Posting List 压缩算法 🔴
3.1 Frame of Reference(FOR)
这是 ES 倒排索引最核心的压缩技术之一,面试中被追问的频率极高。
核心思想:相邻 docID 之间的差值(delta)比 docID 本身小得多,用差值存储可以大幅节省空间。
103 → 1898 的差值,最大需要 12 bit(log2(25003) ≈ 15),而不是存储原始 docID 需要的 15 bit。
但这里还有优化空间:FOR 将差值按固定 bit 数(block = 128 个文档)分组,每组使用统一的 bit 宽度:
3.2 Roaring Bitmap
ES 在处理"哪些文档匹配了这个 filter"时,使用 Roaring Bitmap 而不是 Posting List。它将 docID 按 65536(2^16)为区间分块:
为什么不用连续数组?
- 如果某个词只出现在 docID 1 和 1亿,那么需要存储 1亿 个空位
- Bitmap 用 2^16 个 bit(8KB)就能覆盖这个区间的所有文档,无论稀疏还是稠密
为什么不用 Roaring Bitmap 做所有 Posting List?
- 稠密 Posting List(如停用词"the"出现在 80% 文档中)用 Bitmap 更省空间
- 稀疏 Posting List(如长尾词只出现在 3 个文档中)用 FOR 压缩更省空间
- ES 的策略:混合使用,小数据量用数组,大数据量用 Bitmap
ES 7.x 的 postings_format 默认使用 PLiB(compressed_lz4),底层是 FOR + Roaring Bitmap 的组合。Lucene 的工程师在 2014 年做过实验:对于 10 亿文档的索引,这套压缩方案可以将 Posting List 空间从 80GB 压缩到 12GB。
四、跳表加速合并 🟡
4.1 布尔查询的 Posting List 合并
当执行 must 查询时,ES 需要合并多个 Posting List:
朴素算法:遍历 A 的每个元素,在 B 中二分查找。复杂度 O(N log M)。
跳表算法:在 Posting List 上构建多层跳表索引:
复杂度降为 O(N + M),在大数据量下性能提升显著。
五、错误示范
候选人原话:"倒排索引就是记录每个文档里有哪些词,用链表存起来。"
问题诊断:
- 只理解了表层结构,不知道 Term Dictionary / Term Index / Posting List 三层分离
- 不知道压缩算法,不知道 Lucene 做了多少工程优化
- 完全不了解 FST 这个高级数据结构
面试官内心 OS:"这个候选人知道倒排索引这个词,但肯定没看过 Lucene 源码。他不知道 Lucene 的倒排索引是经过 20 年工程打磨的数据结构,从 FST 到 FOR 压缩到 Roaring Bitmap,每一层都有大量的论文和实验支撑。"
六、生产避坑
6.1 分词粒度翻车
线上后果:搜索"苹果手机"搜不到"苹果 iPhone",因为分词器把"苹果"和"iPhone"分成两个词条。
根因:使用了 standard 分词器,对英文做了小写化和单词分割,但中文和英文混合词处理不佳。
解决方案:
- 配置 IK 分词器的
ik_max_word模式穷举所有分词组合 - 或者自定义 synonyms 同义词表
6.2 冷数据查询延迟飙升
线上后果:3 个月前的订单数据查询延迟从 50ms 飙升到 2s,但数据量只有当前月份的 2 倍。
根因:历史数据的 segment 数太多(累计 N 个月的小 segment),查询时要遍历大量 segment。
排查:
解决方案:对冷数据执行 forcemerge:
将一个月的多个小 segment 合并成一个大 segment,查询性能可提升 10~50 倍。
七、工程选型
【面试官心理】 这道题能答到 FST 和 FOR 压缩的候选人,说明他对 Lucene 底层有过深入研究。这种候选人在 P7 层面非常有竞争力——因为 ES 的核心就是 Lucene,能深入到底层数据结构的候选人凤毛麟角。