倒排索引原理:ES 为什么快
倒排索引原理:ES 为什么快
1. 正排索引 vs 倒排索引
想象你在翻阅一本技术书:
- 正排索引(Forward Index) 就像书的目录:从"第 1 章 概述"翻到第 N 章,逐页查找某个术语。MySQL InnoDB 的 B+Tree 索引本质上也是正排——给定主键查出整行数据,但如果你要搜索"哪些行包含关键词 X",它必须全表扫描。
- 倒排索引(Inverted Index) 就像书末的索引页:直接告诉你"Inverted Index 这个术语出现在第 23 页、第 87 页、第 156 页"。Elasticsearch 正是用这种方式,让关键词搜索从 O(n) 变为 O(1)。
| 特性 | 正排索引(MySQL B+Tree) | 倒排索引(ES/Lucene) |
|---|---|---|
| 核心结构 | 主键 → 行数据 | Term → 文档 ID 列表 |
| 等值查询 | O(log n),极快 | 更快,内存跳表 |
| 模糊/全文搜索 | 全表扫描,极慢 | Term 匹配,极快 |
| LIKE '%keyword%' | 全表扫描 | 分词后直接命中 |
| 空间代价 | 小 | 较大(多份索引) |
| 更新代价 | 原地更新 | 先删后增(段合并) |
2. 三层核心结构
从逻辑到物理,Lucene 的倒排索引分为三层:
2.1 Term Dictionary(词典)
存储所有不重复的 Term,按字典序排列。查询时先在这里二分查找定位 Term。
2.2 Term Index(FST 前缀树)
纯粹在内存中对 Term Dictionary 建索引的树形结构。FST(Finite State Transducer)可以同时压缩键和值,大幅节省内存。给定前缀"inverted",FST 直接跳到词典中 inverted 所在区块,无需遍历整个 Term Dictionary。
查询 "search" 的过程:
FST 前缀匹配 "sea" → 定位到词典中 "sea" 附近
→ 二分查找精确匹配 "search"
→ 获取 Posting List 指针
2.3 Posting List(Roaring Bitmap)
存储包含该 Term 的所有文档 ID。Lucene 用 FOR(Frame of Reference)编码 + Roaring Bitmap 双层策略压缩:
- 文档 ID 差值编码后按高 16 位分桶
- 桶内文档数 < 4096 则用有序数组,> 4096 则用位图
- 这套方案在全内存和少量内存之间取得最佳平衡
3. 用两条文档演示构建过程
假设有两条文档:
POST _bulk
{ "index": { "_index": "blog", "_id": 1 } }
{ "title": "Elasticsearch 倒排索引原理" }
{ "index": { "_index": "blog", "_id": 2 } }
{ "title": "倒排索引与 Roaring Bitmap 优化" }
分词假设(ik 分词器):
| 文档 ID | Terms |
|---|---|
| 1 | elasticsearch, 倒排, 索引, 原理 |
| 2 | 倒排, 索引, roaring, bitmap, 优化 |
倒排索引构建结果:
| Term | Posting List |
|---|---|
| elasticsearch | [1] |
| 倒排 | [1, 2] |
| 索引 | [1, 2] |
| 原理 | [1] |
| roaring | [2] |
| bitmap | [2] |
| 优化 | [2] |
当用户搜索"倒排索引"时:
GET blog/_search
{
"query": {
"match": { "title": "倒排索引" }
}
}
ES 拿到倒排索引后计算的方式如下:
- 分词"倒排"→ PostingList [1,2],分词"索引"→ PostingList [1,2]
- 取交集 → [1,2] 都匹配
- 无需遍历任何文档,直接返回文档 1 和 2
再搜索"elasticsearch bitmap":
- "elasticsearch" → [1],"bitmap" → [2]
- 取交集 → 空(如果是 AND 逻辑),或取并集 → [1,2](OR 逻辑)
这就是倒排索引的核心威力——搜索复杂度与文档总数无关,只与关键词涉及的文章数成正比。
4. 为什么 ES 不能替代 MySQL
倒排索引适合搜索,但不适合精确关联和事务:
| 场景 | 适合工具 | 原因 |
|---|---|---|
| 文章全文搜索 | ES | 倒排索引直接命中 |
| 订单与用户 JOIN | MySQL | ES 不支持高效 JOIN |
| 聚合统计(GROUP BY) | 视情况 | ES 聚合更快但内存消耗大 |
| ACID 事务 | MySQL | ES 没有跨分片事务 |
5. 踩坑案例
踩坑 1:将 ES 当 MySQL 用
实际项目中,有团队用 ES 存储用户订单并做多表关联查询。每次搜索都要跨多个父子关联类型,Nested Query 嵌套超过 3 层性能严重下降,GC 频繁,QPS 从 2000 骤降到 50。最终改为 MySQL 存储订单、ES 仅同步商品名+ID 做全文搜索,MySQL 回表补全数据,性能恢复正常。
踩坑 2:分词器选错导致搜不到
电商搜索"iPhone15",用 standard 分词器不会拆分驼峰词,iPhone15 被当成一个整体 term。用户搜"15"时命中不了。换成 ik_max_word 分词器,iPhone15 可以被拆分为 iphone、15,搜索才正常工作。
PUT products
{
"settings": {
"analysis": {
"analyzer": {
"my_analyzer": {
"type": "custom",
"tokenizer": "standard",
"filter": ["lowercase"]
}
}
}
}
}
6. 总结
倒排索引的本质是以空间换时间:以每个 Term 为 Key 预建文档列表,查询时无需遍历全库,直接取出目标文档集合。ES 的强大正来自 Lucene 的 FST + Roaring Bitmap + Segment 三层架构,让海量数据的全文检索达到毫秒级响应。