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
相关推荐
Elasticsearch13 小时前
通用表达式语言 ( CEL ): CEL 输入如何改进 Elastic Agent 集成中的数据收集
elasticsearch
海兰2 天前
离线合同结构化提取与检索:LangExtract + 本地DeepSeek + Elasticsearch 9.x
大数据·elasticsearch·django
yumgpkpm3 天前
AI视频生成:Wan 2.2(阿里通义万相)在华为昇腾下的部署?
人工智能·hadoop·elasticsearch·zookeeper·flink·kafka·cloudera
james的分享3 天前
大数据领域核心 SQL 优化框架Apache Calcite介绍
大数据·sql·apache·calcite
莫寒清3 天前
Apache Tika
java·人工智能·spring·apache·知识图谱
Sheffield3 天前
如果把ZooKeeper按字面意思比作动物园管理员……
elasticsearch·zookeeper·kafka
归叶再无青3 天前
web服务安装部署、性能升级等(Apache、Nginx)
运维·前端·nginx·云原生·apache·bash
嗝屁小孩纸3 天前
ES索引重建(零工具纯脚本执行)
大数据·elasticsearch·搜索引擎
Elastic 中国社区官方博客3 天前
使用 Jina Embeddings v5 和 Elasticsearch 构建“与你的网站数据聊天”的 agent
大数据·人工智能·elasticsearch·搜索引擎·容器·全文检索·jina