lucene 8.7.0 版本中的倒排索引、数字、DocValues三种类型的查询性能对比
我们来详细对比一下 Lucene 8.7.0 中倒排索引、数字(Points)和 DocValues 这三种核心数据结构在查询性能上的差异。
首先,一个关键的理念是:它们被设计用来解决不同的问题,因此性能对比必须基于具体的应用场景。 将它们放在不适合的场景下对比,性能差异会非常悬殊。
下面我们逐一分析,然后进行总结和场景化对比。
1. 倒排索引 (Inverted Index)
倒排索引是 Lucene 的基石,主要为文本搜索而生。
-
核心数据结构:
- Term Dictionary (词典):一个包含了所有文档中经过分词、处理后的所有 Term (词项) 的有序列表。在 Lucene 8.x 中,通常使用 FST (Finite State Transducer) 结构,它极大地压缩了词典的存储空间,并能快速定位 Term。
- Postings List (倒排列表):对于词典中的每一个 Term,都有一个列表记录了包含该 Term 的所有文档 ID。这个列表还可能包含 Term 在文档中出现的频率 (freq)、位置 (position)、偏移量 (offset) 等信息。
-
主要用途:
- 全文检索:快速找到包含特定词语的文档。例如:
"search engine"
。 - 关键词精确匹配:对于
keyword
类型的字段(不分词),可以快速找到字段值完全匹配的文档。例如:status: "published"
。
- 全文检索:快速找到包含特定词语的文档。例如:
-
查询性能特点:
- 极快:对于查找包含某个 Term 的文档,其性能非常高。查询过程是:
- 在 FST 词典中快速定位 Term (类似 O(logN) 或更快)。
- 获取指向 Postings List 的指针。
- 遍历 Postings List 得到所有匹配的文档 ID。
- 性能与 Term 的稀有度相关:
- 稀有 Term (Low-frequency Term):查询极快,因为 Postings List 很短。
- 常见 Term (High-frequency Term):查询相对较慢,因为 Postings List 很长,需要处理和合并更多的文档 ID。例如,搜索 “的” 会比搜索 “Lucene” 慢得多。
- 不适合范围查询:对于文本,没有“范围”的概念。对于数字或日期,如果用倒排索引存储(老版本 Lucene 的做法),范围查询会变成一个巨大的布尔查询(OR 连接范围内所有的 Term),性能非常低下。
- 极快:对于查找包含某个 Term 的文档,其性能非常高。查询过程是:
-
典型查询:
TermQuery
,BooleanQuery
,PhraseQuery
,MatchQuery
(in Elasticsearch)
2. 数字类型 (Points)
从 Lucene 6.x 开始,引入了 Points
类型来专门处理数值、日期、地理坐标等多维数据。它彻底改变了 Lucene 处理数值范围查询的方式。
-
核心数据结构: BKD 树 (Block K-D Tree)。这是一种为多维空间数据检索优化的平衡树结构。
- 对于数字,是一维 BKD 树。
- 对于地理坐标,是二维 BKD 树。
-
主要用途:
- 数值、日期的范围过滤:例如,查找价格在
[100, 500]
之间的商品,或者2023年的订单。 - 地理空间位置过滤:例如,查找某个点周围5公里内的所有店铺。
- 数值、日期的范围过滤:例如,查找价格在
-
查询性能特点:
- 范围查询极快:BKD 树的结构使得范围查询的复杂度大致为 O(logN),其中 N 是文档总数。它能够非常高效地剪掉不符合范围条件的文档块,无需逐一检查。
- 性能与范围大小无关:查询一个很小的范围(如
[100, 101]
)和一个很大的范围(如[0, 10000]
)的性能差异不大。这与倒排索引形成鲜明对比。 - 精确值查询也很快:相当于一个范围为
[N, N]
的查询。 - 不用于全文检索:它不存储原始文本,也不进行分词。
-
典型查询:
PointRangeQuery
(in Elasticsearch)
3. DocValues
DocValues 是一个“正向”的索引结构,也被称为“列式存储”。它将字段值按文档 ID 进行了组织。
-
核心数据结构: 列式存储 (Columnar Storage)。对于一个字段,它存储的是一个从
文档ID -> 字段值
的映射。- 例如,一个
price
字段的 DocValues 可能看起来像:- Doc 0 -> 10.0
- Doc 1 -> 25.5
- Doc 2 -> 10.0
- …
- 为了优化存储和访问,它会根据数据类型(数字、关键词)进行压缩和编码。
- 例如,一个
-
主要用途:
- 排序 (Sorting):当需要按某个字段排序时,可以直接通过 DocValues 快速获取每个文档的排序键值,无需加载整个文档。
- 聚合/分组 (Faceting/Aggregations):进行聚合计算时(如计算平均价格、按品牌分组统计数量),DocValues 提供了对字段值的快速、连续访问能力,非常高效。
- 脚本访问字段值:在查询或打分脚本中需要获取某个字段的值时,从 DocValues 读取是最高效的方式。
-
查询性能特点:
- 查询(过滤)性能极差:如果直接用 DocValues 来做过滤(例如,查找
price = 10.0
的文档),Lucene 必须遍历所有文档或匹配查询的文档子集,对每个文档都从 DocValues 中读取其值,然后进行比较。这是一个线性的扫描过程(O(N)),非常缓慢,尤其是当文档数量巨大时。 - 排序和聚合性能极好:因为它利用了操作系统的文件系统缓存(OS Cache)。数据是按列连续存储的,访问局部性非常好,可以一次性将整个字段的 DocValues 加载到内存中,后续操作极快。
- 查询(过滤)性能极差:如果直接用 DocValues 来做过滤(例如,查找
-
典型场景:
sort
,aggregations
,script_fields
(in Elasticsearch)
性能对比总结表
特性 / 类型 | 倒排索引 (Inverted Index) | 数字 (Points) | DocValues |
---|---|---|---|
核心思想 | Term -> Docs (反向映射) | 多维空间分割树 | DocID -> Value (正向映射/列存) |
数据结构 | FST + Postings List | BKD Tree | 列式存储 |
最擅长的查询 | 文本搜索、关键词精确匹配 | 数值/日期/地理位置的范围过滤 | 排序、聚合、脚本访问字段值 |
查询性能 | Term 越稀有越快 | 对范围大小不敏感,对数级复杂度,非常快 | 用于过滤时性能极差(线性扫描) |
不擅长的场景 | 数值范围查询、排序、聚合 | 文本搜索 | 任何形式的搜索/过滤 |
典型查询 | TermQuery , BooleanQuery | PointRangeQuery | 不用于查询,用于 sort 和 aggs |
磁盘占用 | 相对较大,尤其包含位置信息时 | 相对紧凑 | 相对紧凑,压缩率高 |
内存占用 | 词典(FST)常驻内存,Postings按需加载 | 索引的内部节点常驻内存,叶子节点按需加载 | 严重依赖 OS Cache,按列加载,对 JVM 堆内存友好 |
场景化性能分析
假设我们有一个包含商品信息的索引,字段有 description
(text), price
(float), category
(keyword)。
场景一:查找所有描述中包含 “durable” 的商品
- 最佳选择: 倒排索引。
- 性能分析: Lucene 在
description
字段的词典中找到 “durable”,然后获取其倒排列表,瞬间得到所有匹配的文档 ID。这是它的核心优势。 - 其他两者: Points 和 DocValues 根本无法完成这个任务。
场景二:查找价格在 100 到 200 之间的所有商品
- 最佳选择: 数字 (Points)。
- 性能分析: Lucene 在
price
字段的 BKD 树上执行范围查询,高效地剪枝,快速找出所有 price 在[100, 200]
区间的文档。性能稳定且快速。 - 其他两者:
- 倒排索引: 如果强行用倒排索引,会转换成一个超长的
BooleanQuery
(ORprice:100.0
ORprice:100.01
…),性能灾难。 - DocValues: 需要扫描所有文档,获取每个文档的 price,再判断是否在范围内。性能极差。
- 倒排索引: 如果强行用倒排索引,会转换成一个超长的
场景三:按价格从高到低对搜索结果进行排序
- 最佳选择: DocValues。
- 性能分析: 假设一个查询已经找到了1000个匹配的文档。为了排序,Lucene 需要知道这1000个文档各自的
price
。通过price
字段的 DocValues,它可以快速、顺序地读取这1000个文档的价格值,然后进行排序。这个过程非常高效,对 JVM 堆内存压力小。 - 其他两者:
- 倒排索引: 无法完成排序。
- Points: BKD 树的设计不是为了高效地按 DocID 随机访问值的,无法用于排序。
场景四:统计每个分类 (category
) 下有多少商品(聚合)
- 最佳选择: DocValues。
- 性能分析: Lucene 遍历所有匹配文档。对于每个文档,通过
category
字段的 DocValues 快速获取其分类值(通常是编码后的数字),然后在一个哈希表中为该分类的计数器加一。由于 DocValues 的列式访问特性,这个过程极快。 - 其他两者: 倒排索引和 Points 都不适合这个任务。
结论
在 Lucene 8.7.0 (以及现代所有版本) 中,这三种结构协同工作,缺一不可:
- 使用倒排索引和 Points 来“查找”和“过滤”文档集。这是查询的第一步,目标是尽可能快地缩小文档范围。
- 使用 DocValues 对“找到的”文档集进行“操作”,如排序、聚合或在脚本中提取值。
一个典型的复杂查询(如在电商网站上搜索)会同时利用到它们:
// 伪代码,类似 Elasticsearch 的查询
{"query": {"bool": {"must": [{ "match": { "description": "durable" } } // 使用倒排索引],"filter": [{ "range": { "price": { "gte": 100, "lte": 200 } } } // 使用 Points]}},"sort": [{ "price": "desc" } // 使用 DocValues],"aggs": {"categories": {"terms": { "field": "category" } // 使用 DocValues}}
}
理解它们的根本区别和适用场景,是进行 Lucene/Elasticsearch 性能优化的关键。