ElasticSearch核心引擎Apache Lucene(一):倒排索引底层实现

文章目录

    • [0. 全局视野:Lucene 索引结构思维导图](#0. 全局视野:Lucene 索引结构思维导图)
    • [1. 倒排索引 (Inverted Index) 的基石](#1. 倒排索引 (Inverted Index) 的基石)
    • [2. Term Dictionary 的极致压缩:FST](#2. Term Dictionary 的极致压缩:FST)
      • [FST 原理图解](#FST 原理图解)
      • [FST 的优势](#FST 的优势)
    • [3. Posting List 的压缩艺术:Frame of Reference (FOR)](#3. Posting List 的压缩艺术:Frame of Reference (FOR))
      • 核心思想:分治 (Divide and Conquer)
      • 形象案例
      • 动态策略:三种容器 (Container)
        • [A. Array Container (数组容器) ------ 人少的时候用](#A. Array Container (数组容器) —— 人少的时候用)
        • [B. Bitmap Container (位图容器) ------ 人多的时候用](#B. Bitmap Container (位图容器) —— 人多的时候用)
        • [C. Run Container (行程容器) ------ 连号的时候用](#C. Run Container (行程容器) —— 连号的时候用)
      • [核心难点图解:为什么界限是 4096?](#核心难点图解:为什么界限是 4096?)
      • 结构示意图
      • [为什么 Roaring Bitmaps 做交集(AND)这么快?](#为什么 Roaring Bitmaps 做交集(AND)这么快?)
        • [情况 1:Bitmap vs Bitmap](#情况 1:Bitmap vs Bitmap)
        • [情况 2:Array vs Array](#情况 2:Array vs Array)
        • [情况 3:Bitmap vs Array](#情况 3:Bitmap vs Array)
    • 总结

在分布式搜索引擎 ElasticSearch (ES) 的光环之下,默默支撑其海量数据毫秒级检索能力的,是底层的核心库 ------ Apache Lucene

很多开发者知道 ES 使用"倒排索引",但对于倒排索引内部如何极致压缩空间、如何利用位运算加速过滤,往往知之甚少。本文将深入 Lucene 文件的内核,拆解其三大核心黑科技:FST (Finite State Transducers)Frame of Reference (FOR) 以及 Roaring Bitmaps


0. 全局视野:Lucene 索引结构思维导图

在深入细节前,我们先通过一张思维导图,俯瞰 Lucene 倒排索引的完整结构。
Lucene 倒排索引
词项字典
Finite State Transducers
前缀共享
后缀共享
O(len(str)) 查找复杂度
极低内存占用
Term Index
在内存中快速定位
指向磁盘 Block
倒排表
DocID 列表
FOR
增量编码
位压缩
词频/位置信息
过滤器与缓存
Roaring Bitmaps
稠密数据
稀疏数据
连续数据


1. 倒排索引 (Inverted Index) 的基石

传统的数据库(如 MySQL)使用正排索引(Forward Index) ,即 ID -> Data。当我们搜索包含关键词 "Apple" 的文章时,数据库需要扫描每一行数据(Full Table Scan)或依赖 B+Tree 的最左前缀匹配。

Lucene 采用倒排索引 ,即 Term -> List<DocID>。它就像书籍末尾的"索引页",告诉我们某个单词出现在了哪几页。

核心组成

一个倒排索引主要由两部分组成:

  1. Term Dictionary (词项字典): 记录所有文档中出现过的单词(Term),并排序。
  2. Posting List (倒排表): 记录每个 Term 对应的文档 ID 集合,以及词频、偏移量等信息。

2. Term Dictionary 的极致压缩:FST

问题场景

假设我们有十亿条数据,分词后的 Term 数量可能达到上千万。

  • 如果将 Term Dictionary 全部存入内存(HashMap/TreeMap),内存会直接爆炸。
  • 如果存入磁盘,每次查找都需要多次磁盘 IO,速度太慢。
  • B+Tree 虽然优秀,但对于 String 类型的 Key,空间利用率不够极致。

解决方案:FST (Finite State Transducers)

FST 是一种有穷状态转换器 。它本质上是一个有向无环图 (DAG)。Lucene 使用 FST 将 Term Dictionary 以极小的内存代价加载到内存(或通过 mmap 映射)。

FST 原理图解

假设我们有三个单词:mop, moth, pop, star, stop, top

FST 会通过共享前缀共享后缀来压缩存储。
m
o
p
t
h
p
o
p
t
o
p
Start
m
o
p
t
h
p
o
p
t
o
p

(注:真实的 FST 还会为每条边赋予权重(Output),用于直接计算该 Term 在磁盘 Block 中的位置)

FST 的优势

  1. 空间换时间?不,是空间时间双赢 :FST 的查找复杂度为 O(len(term)),与字典大小无关。
  2. 极度压缩 :通过共享前缀和后缀,FST 可以将 Term Dictionary 压缩到原大小的 10% ~ 30% 甚至更低。这使得 ES 可以将索引常驻内存(Heap 外内存)。
  3. 前缀匹配神器 :非常适合处理 prefix 查询、Fuzzy 查询。

3. Posting List 的压缩艺术:Frame of Reference (FOR)

核心思想:分治 (Divide and Conquer)

传统的位图(BitSet)是一个巨大的、连续的 0/1 数组。

如果我们要存数字 2000000000(20亿),在传统 BitSet 中,我们需要申请 20亿个 bit 的空间(约 238MB),哪怕里面只存了这一个数字,前面的空间全是 0,极其浪费。

Roaring Bitmaps 的解决思路是:切分。

它将 32 位整数(Integer)切分为两部分:高 16 位低 16 位

  • 高 16 位 (Key):好比"宿舍楼号"。
  • 低 16 位 (Value):好比"房间号"。

32 位整数 = ( 高 16 位 ≪ 16 ) + 低 16 位 32位整数 = (高16位 \ll 16) + 低16位 32位整数=(高16位≪16)+低16位

关键点: 2 16 = 65536 2^{16} = 65536 216=65536。

这意味着,每一个"宿舍楼"(高 16 位确定的桶)里,最多只能容纳 65536 个数字(0 ~ 65535)。

形象案例

假设我们要存三个数:1065540131080

  1. 数字 10
    • 10 / 65536 = 0 10 / 65536 = 0 10/65536=0 (第 0 栋楼)
    • 10 % 65536 = 10 10 \% 65536 = 10 10%65536=10 (房间号 10)
  2. 数字 65540
    • 65540 / 65536 = 1 65540 / 65536 = 1 65540/65536=1 (第 1 栋楼)
    • 65540 % 65536 = 4 65540 \% 65536 = 4 65540%65536=4 (房间号 4)
  3. 数字 131080
    • 131080 / 65536 = 2 131080 / 65536 = 2 131080/65536=2 (第 2 栋楼)
    • 131080 % 65536 = 8 131080 \% 65536 = 8 131080%65536=8 (房间号 8)

Roaring Bitmap 会创建 3 个"桶"(Container),每个桶只负责管理自己那 65536 个数字范围内的事务。


动态策略:三种容器 (Container)

这是 Roaring Bitmap 最聪明的地方。对于每一个"宿舍楼"(Container),它根据里面住的人数(数据稀疏程度),自动选择最省空间的记账方式。

我们有三种记账本(容器类型):

A. Array Container (数组容器) ------ 人少的时候用
  • 场景:这个"宿舍楼"里住的人很少(稀疏)。
  • 结构 :直接用一个 short[] 数组,把房间号列出来。
  • 存储 :存 [10, 25, 300]
  • 占用空间:每个数字占 2 Bytes(short 类型)。
  • 优点:人少时非常省空间。
  • 缺点:查找需要二分查找,O(logN)。
B. Bitmap Container (位图容器) ------ 人多的时候用
  • 场景:这个"宿舍楼"里住的人很多(稠密)。
  • 结构:直接搞一张固定的表格(BitSet),共有 65536 个格子,有人住就打勾(置 1),没人住就空着(置 0)。
  • 占用空间 :固定 8 KB
    • 计算方式: 65536 bits / 8 = 8192 Bytes = 8 KB 65536 \text{ bits} / 8 = 8192 \text{ Bytes} = 8 \text{ KB} 65536 bits/8=8192 Bytes=8 KB。
  • 优点:无论存多少个数,空间固定;查找、判断是否存在只需 O(1)。
C. Run Container (行程容器) ------ 连号的时候用
  • 场景:住的人都是连续的,比如从 10 号房间到 1000 号房间都住了人。
  • 结构:只记"起始房间"和"连续长度"。
  • 存储 :存 (10, 990),表示从 10 开始,后面 990 个都有数据。
  • 优点:对于连续数据压缩率极高。

核心难点图解:为什么界限是 4096?

你可能会问:什么时候从 Array Container 切换到 Bitmap Container?

答案是:4096 个元素。这是通过数学计算得出的"盈亏平衡点"。

我们来算一笔账:

  1. Array Container :存 N 个数,消耗 N × 2 N \times 2 N×2 Bytes。
  2. Bitmap Container:不管存几个数,固定消耗 8192 Bytes (8KB)。

这就变成了一个初中数学题:
N × 2 < 8192 N \times 2 < 8192 N×2<8192
N < 4096 N < 4096 N<4096

  • 结论
    • 当一个 Block 里的数据少于 4096 个时,用 Array 存,因为它占用的空间小于 8KB。
    • 当数据超过 4096 个时,Array 会占用超过 8KB,这时候直接申请一个 8KB 的 Bitmap 反而更省空间!

结构示意图

Low 16 bits (Values)
High 16 bits (Keys)
Ptr
Ptr
Ptr
Roaring Bitmap 对象
Key: 0
Key: 5
Key: 128
Array Container

数据量: 3

内容: [10, 50, 99]

大小: 6 Bytes
Bitmap Container

数据量: 10000

内容: BitSet[10110...]

大小: 8192 Bytes (固定)
Run Container

数据量: ...

内容: (20, 500) -> 20~520

大小: 4 Bytes
Key 0 对应范围 0~65535

Key 5 对应范围 327680~393215


为什么 Roaring Bitmaps 做交集(AND)这么快?

ES 查询 status:active AND type:video 时,就是在做两个 Posting List 的交集。

Roaring Bitmap 的交集运算非常智能,它不是傻傻地挨个比对,而是同类型匹配

情况 1:Bitmap vs Bitmap

这是最快的情况。

  • 操作 :直接对底层的 long[] 数组进行位运算(Bitwise AND)。
  • 原理 :计算机 CPU 做位运算极快,而且现代 CPU 支持 SIMD(单指令多数据流)指令集,一次可以处理多个字长。这基本上是纳秒级的速度。
情况 2:Array vs Array

这是归并排序逻辑。

  • 操作:两个有序数组找相同元素。
  • 优化 :如果两个数组大小差距很大(例如一个有 10 个元素,一个有 2000 个),它不会从头遍历,而是使用二分查找或者**指数查找(Galloping Search)**来跳过不需要比较的数据。
情况 3:Bitmap vs Array

这是查表逻辑。

  • 操作:遍历 Array 中的每一个元素,看它在 Bitmap 中对应的位置是不是 1。
  • 复杂度:O(N),N 是 Array 的长度。因为 Bitmap 查找是 O(1) 的。

总结

当我们在这个瞬间发起一次 ES 查询时,底层发生了一系列精妙的配合:

  1. FST 像一张超浓缩的地图,在内存中瞬间定位到 Term 对应的 Block 指针。
  2. IndexReader 读取磁盘上的 Block,利用 FOR 算法极其紧凑地解压出 Posting List。
  3. 如果存在 Filter 查询,Roaring Bitmaps 将利用 CPU 的位运算能力,以纳秒级的速度完成集合的交、并、差操作。

正是这些对数据结构极致的压榨和优化,造就了 Lucene 乃至 ElasticSearch 的搜索引擎霸主地位。

参考资料:

  • Apache Lucene Core Documentation
  • "Roaring Bitmaps: Implementation of an Optimized Software Library"
  • ElasticSearch: The Definitive Guide
相关推荐
那起舞的日子2 小时前
ElasticSearch系列-1-入门篇
elasticsearch
Java后端的Ai之路2 小时前
【Git版本控制】-趣味解说Git核心知识
大数据·git·elasticsearch
大志哥1232 小时前
使用logstash和elasticsearch实现日志链路(二)
大数据·elasticsearch·搜索引擎
海兰2 小时前
win11下本地部署单节点Elasticsearch9.0+开发
大数据·elasticsearch·jenkins
Elastic 中国社区官方博客11 小时前
使用 Discord 和 Elastic Agent Builder A2A 构建游戏社区支持机器人
人工智能·elasticsearch·游戏·搜索引擎·ai·机器人·全文检索
*crzep1 天前
Elasticsearch使用Apifox发送请求
elasticsearch·apifox
Dxy12393102161 天前
告别重启!Elasticsearch 8.10 杀手级特性:动态同义词(Dynamic Synonyms)深度解析
大数据·elasticsearch·jenkins
宇神城主_蒋浩宇1 天前
最简单的es理解 数据库视角看写 ES 加 java正删改查深度分页
大数据·数据库·elasticsearch
TongSearch1 天前
TongSearch中分片从何而来,又解决了什么问题
java·elasticsearch·tongsearch