文章目录
-
- [一、 引言](#一、 引言)
-
- [1.1 倒排索引极其适合"搜索"](#1.1 倒排索引极其适合“搜索”)
- [1.2 倒排索引不适合"分析"](#1.2 倒排索引不适合“分析”)
- [二、 列式存储(Columnar Storage)](#二、 列式存储(Columnar Storage))
-
- [2.1 行式存储 vs 列式存储](#2.1 行式存储 vs 列式存储)
- [2.2 为什么聚合和排序需要列式存储?](#2.2 为什么聚合和排序需要列式存储?)
- [三、 FieldData vs DocValues:新旧之争](#三、 FieldData vs DocValues:新旧之争)
- [四、 深度解密:DocValues 的磁盘存储格式](#四、 深度解密:DocValues 的磁盘存储格式)
-
- [4.1 数值类型压缩策略(Numeric DocValues)](#4.1 数值类型压缩策略(Numeric DocValues))
-
- [A. 增量编码(Delta Encoding)](#A. 增量编码(Delta Encoding))
- [B. 最大公约数压缩(GCD Compression)](#B. 最大公约数压缩(GCD Compression))
- [C. 表压缩(Table Encoding)](#C. 表压缩(Table Encoding))
- [4.2 字符串类型存储(Binary/Sorted DocValues)](#4.2 字符串类型存储(Binary/Sorted DocValues))
- [五、 最佳实践与性能调优](#五、 最佳实践与性能调优)
- 总结
摘要 :ElasticSearch 之所以强大,不仅在于它能通过倒排索引实现毫秒级的全文检索,更在于它能通过DocValues实现高效的聚合(Aggregation)、排序(Sorting)和脚本计算。本文将深入 Apache Lucene 内核,拆解正排索引的设计哲学、列式存储的优势以及磁盘上的编码格式。
一、 引言
在深入 DocValues 之前,我们必须先理解它解决的问题。ElasticSearch 最引以为豪的是倒排索引(Inverted Index)。
1.1 倒排索引极其适合"搜索"
倒排索引是一个 Term -> List<DocId> 的映射关系。
例如,搜索关键词 pixel:
| Term | Doc IDs |
|---|---|
| pixel | [1, 3] |
| phone | [1, 2, 3] |
Lucene 可以极其快速地定位包含 pixel 的文档是 ID 为 1 和 3 的文档。
1.2 倒排索引不适合"分析"
但是,如果我们想执行以下操作:
- 排序:按价格从低到高排序。
- 聚合:计算所有手机的平均价格。
问题出现了 :倒排索引是"词"到"文档"的映射。而排序和聚合需要的是"文档"到"值"的映射(即:DocId -> Value)。
如果仅依靠倒排索引,Lucene 必须遍历所有 Term,加载整个倒排表,并在内存中构建文档到值的映射。这被称为 Un-inverting(反向索引),在数据量大时,这会消耗巨大的堆内存(Heap),导致 OOM 或频繁 GC。
二、 列式存储(Columnar Storage)
为了解决上述性能瓶颈,Lucene 引入了 DocValues 。它本质上是一个正排索引 ,采用列式存储结构。
2.1 行式存储 vs 列式存储
- 行式存储(Row-oriented):如 MySQL 的 InnoDB,将一行数据的所有字段存储在一起。
- 列式存储(Columnar-oriented):将同一字段的所有值存储在一起。
列式存储 (DocValues)
ID Column: 1, 2, 3...
Name Column: Pixel, iPhone, Galaxy...
Price Column: 699, 999, 899...
行式存储 (如 MySQL)
Doc1: ID, Name, Price
Doc2: ID, Name, Price
Doc3: ID, Name, Price
2.2 为什么聚合和排序需要列式存储?
-
缓存局部性(Cache Locality) :
当计算平均价格时,CPU 只需要处理
Price这一列的数据。在列式存储中,这些数据在物理磁盘和内存中是连续排列的。这意味着 CPU 可以通过预取(Prefetching)高效地加载数据,极大地减少磁盘寻道和内存缺页中断。 -
压缩率极高 :
同一列的数据类型通常相同(例如都是数字),且数值分布往往具有规律性。这使得 Lucene 可以应用极其激进的压缩算法(如 Delta 编码、GCD 压缩等),从而显著减少磁盘 I/O。
三、 FieldData vs DocValues:新旧之争
在 ES 早期版本中,主要依靠 FieldData ,而现在默认使用 DocValues。
| 特性 | FieldData | DocValues |
|---|---|---|
| 创建时机 | 查询时(Query Time)动态构建 | 索引时(Index Time)构建写入磁盘 |
| 存储位置 | JVM Heap 内存 | 磁盘文件(依靠 OS FileSystem Cache) |
| 性能 | 加载极慢,查询快(纯内存) | 加载无感知,查询极快(内存+磁盘) |
| 缺点 | 容易导致 OOM,GC 压力大 | 占用额外的磁盘空间 |
| 适用场景 | 极少使用(仅 text 字段) | 默认开启(numeric, keyword, date 等) |
思维导图:DocValues 的生命周期
DocValues
构建阶段
IndexWriter buffer
Segment Flush
.dvd, .dvm
读取阶段
mmap 映射
OS Page Cache
随机访问 / 迭代器
应用场景
排序
聚合
脚本
四、 深度解密:DocValues 的磁盘存储格式
DocValues 并非简单地将数字写入文件,而是根据数据分布特征动态选择最佳编码。
DocValues 主要涉及两个文件:
.dvm(DocValues Metadata): 索引文件,记录数据的偏移量。.dvd(DocValues Data): 实际的数据存储文件。
4.1 数值类型压缩策略(Numeric DocValues)
假设我们有一列价格数据:[100, 101, 102, 103]。Lucene 会尝试以下策略:
A. 增量编码(Delta Encoding)
Lucene 不存储原始值,而是存储最小值和偏移量。
- Base: 100
- Values:
[0, 1, 2, 3] - 优势:原数值可能需要 8 字节(long),压缩后仅需几个比特。
B. 最大公约数压缩(GCD Compression)
假设数据为:[100, 200, 300, 400]。
- GCD: 100
- Values:
[1, 2, 3, 4] - 优势:通过除法大幅缩小数值范围。
C. 表压缩(Table Encoding)
如果数值的唯一值(Unique Values)很少(< 256个),Lucene 会构建一个字典表。
- Table:
[0: 100, 1: 999] - Stored:
[0, 0, 1, 0, 1...] - 优势:直接将数值转换为极小的 ID。
存储流程图解:
唯一值极少
有公约数
数值紧凑
原始数值流: 100, 105, 100, 110
分析数值特征
Table Encoding
GCD Compression
Delta Encoding
Bit Packing (位打包)
写入 .dvd 文件
4.2 字符串类型存储(Binary/Sorted DocValues)
对于 Keyword 类型,DocValues 采用 SortedDocValues 格式。它结合了"字典编码"和"数值映射"。
- 去重与排序:首先提取所有唯一的字符串,按字典序排列,生成一个字典(Dictionary)。
- ID 映射:将文档中的字符串替换为字典中的对应的 Ordinal(序号)。
- 存储 :
- Terms 部分:存储去重后的字符串列表(前缀压缩)。
- Ordinals 部分:存储文档 ID 对应的序号(数值类型,应用上述数值压缩策略)。
示例:
文档: Doc1: "Apple", Doc2: "Banana", Doc3: "Apple"
- Dictionary :
0: "Apple", 1: "Banana" - Ordinals Stream :
0, 1, 0
这使得字符串的排序和聚合变成了纯数字的操作,效率极高。
五、 最佳实践与性能调优
理解了原理,我们就能更好地使用 ElasticSearch。
-
不需要聚合的字段,禁用 DocValues :
如果你明确知道某个字段只需要被搜索(倒排索引),永远不需要排序或聚合,可以在 Mapping 中设置
doc_values: false。这能节省磁盘空间并加快索引速度。jsonPUT my_index { "mappings": { "properties": { "session_id": { "type": "keyword", "doc_values": false } } } } -
关注磁盘 I/O 和 Page Cache :
DocValues 极度依赖操作系统的文件系统缓存(Page Cache)。确保给 ES 留足堆外内存(Off-heap memory),通常建议 JVM Heap 设置为物理内存的 50%,剩下的 50% 留给 Lucene 使用。
-
稀疏字段的代价 :
Lucene 默认会对所有文档生成 DocValue 占位。虽然现代 Lucene 对稀疏数据有优化(使用
ADVANCE迭代器),但大量空置字段仍会带来轻微的元数据开销。
总结
Apache Lucene 通过 DocValues 完美补全了倒排索引在分析领域的短板。
- 它采用列式存储,将随机 I/O 转化为顺序 I/O。
- 它利用位打包、GCD、Delta 等算法将磁盘占用降到最低。
- 它利用OS Cache,让大数据量的聚合分析在毫秒级完成。
理解 DocValues 的底层原理,不仅能帮助我们写出更高效的 DSL,更能让我们在面对海量数据分析架构设计时游刃有余。