ElasticSearch核心引擎Apache Lucene(二):正排索引的奥秘

文章目录

    • [一、 引言](#一、 引言)
      • [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 为什么聚合和排序需要列式存储?

  1. 缓存局部性(Cache Locality)

    当计算平均价格时,CPU 只需要处理 Price 这一列的数据。在列式存储中,这些数据在物理磁盘和内存中是连续排列的。这意味着 CPU 可以通过预取(Prefetching)高效地加载数据,极大地减少磁盘寻道和内存缺页中断。

  2. 压缩率极高

    同一列的数据类型通常相同(例如都是数字),且数值分布往往具有规律性。这使得 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 主要涉及两个文件:

  1. .dvm (DocValues Metadata): 索引文件,记录数据的偏移量。
  2. .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 格式。它结合了"字典编码"和"数值映射"。

  1. 去重与排序:首先提取所有唯一的字符串,按字典序排列,生成一个字典(Dictionary)。
  2. ID 映射:将文档中的字符串替换为字典中的对应的 Ordinal(序号)。
  3. 存储
    • Terms 部分:存储去重后的字符串列表(前缀压缩)。
    • Ordinals 部分:存储文档 ID 对应的序号(数值类型,应用上述数值压缩策略)。

示例:

文档: Doc1: "Apple", Doc2: "Banana", Doc3: "Apple"

  • Dictionary : 0: "Apple", 1: "Banana"
  • Ordinals Stream : 0, 1, 0

这使得字符串的排序和聚合变成了纯数字的操作,效率极高。


五、 最佳实践与性能调优

理解了原理,我们就能更好地使用 ElasticSearch。

  1. 不需要聚合的字段,禁用 DocValues

    如果你明确知道某个字段只需要被搜索(倒排索引),永远不需要排序或聚合,可以在 Mapping 中设置 doc_values: false。这能节省磁盘空间并加快索引速度。

    json 复制代码
    PUT my_index
    {
      "mappings": {
        "properties": {
          "session_id": { 
            "type": "keyword",
            "doc_values": false 
          }
        }
      }
    }
  2. 关注磁盘 I/O 和 Page Cache

    DocValues 极度依赖操作系统的文件系统缓存(Page Cache)。确保给 ES 留足堆外内存(Off-heap memory),通常建议 JVM Heap 设置为物理内存的 50%,剩下的 50% 留给 Lucene 使用。

  3. 稀疏字段的代价

    Lucene 默认会对所有文档生成 DocValue 占位。虽然现代 Lucene 对稀疏数据有优化(使用 ADVANCE 迭代器),但大量空置字段仍会带来轻微的元数据开销。


总结

Apache Lucene 通过 DocValues 完美补全了倒排索引在分析领域的短板。

  • 它采用列式存储,将随机 I/O 转化为顺序 I/O。
  • 它利用位打包、GCD、Delta 等算法将磁盘占用降到最低。
  • 它利用OS Cache,让大数据量的聚合分析在毫秒级完成。

理解 DocValues 的底层原理,不仅能帮助我们写出更高效的 DSL,更能让我们在面对海量数据分析架构设计时游刃有余。

相关推荐
TracyCoder1231 小时前
ElasticSearch核心引擎Apache Lucene(一):倒排索引底层实现
elasticsearch·apache·lucene
那起舞的日子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