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 的文档,其性能非常高。查询过程是:
      1. 在 FST 词典中快速定位 Term (类似 O(logN) 或更快)。
      2. 获取指向 Postings List 的指针。
      3. 遍历 Postings List 得到所有匹配的文档 ID。
    • 性能与 Term 的稀有度相关
      • 稀有 Term (Low-frequency Term):查询极快,因为 Postings List 很短。
      • 常见 Term (High-frequency Term):查询相对较慢,因为 Postings List 很长,需要处理和合并更多的文档 ID。例如,搜索 "的" 会比搜索 "Lucene" 慢得多。
    • 不适合范围查询:对于文本,没有"范围"的概念。对于数字或日期,如果用倒排索引存储(老版本 Lucene 的做法),范围查询会变成一个巨大的布尔查询(OR 连接范围内所有的 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 加载到内存中,后续操作极快。
  • 典型场景 : sort, aggregations, script_fields (in Elasticsearch)


性能对比总结表

特性 / 类型 倒排索引 (Inverted Index) 数字 (Points) DocValues
核心思想 Term -> Docs (反向映射) 多维空间分割树 DocID -> Value (正向映射/列存)
数据结构 FST + Postings List BKD Tree 列式存储
最擅长的查询 文本搜索、关键词精确匹配 数值/日期/地理位置的范围过滤 排序、聚合、脚本访问字段值
查询性能 Term 越稀有越快 对范围大小不敏感,对数级复杂度,非常快 用于过滤时性能极差(线性扫描)
不擅长的场景 数值范围查询、排序、聚合 文本搜索 任何形式的搜索/过滤
典型查询 TermQuery, BooleanQuery PointRangeQuery 不用于查询,用于 sortaggs
磁盘占用 相对较大,尤其包含位置信息时 相对紧凑 相对紧凑,压缩率高
内存占用 词典(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 (OR price:100.0 OR price: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 (以及现代所有版本) 中,这三种结构协同工作,缺一不可:

  1. 使用倒排索引和 Points 来"查找"和"过滤"文档集。这是查询的第一步,目标是尽可能快地缩小文档范围。
  2. 使用 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 性能优化的关键。

相关推荐
cyh男2 天前
Lucene 8.7.0 版本中dvd、dvm文件详解
lucene
是犹橐籥2 天前
头歌Educoder答案 Lucene - 全文检索入门
搜索引擎·全文检索·lucene
cyh男3 天前
Lucene 8.7.0 版本中docFreq、totalTermFreq、getDocCount等方法的含义
lucene
cyh男4 天前
Lucene 8.7.0 版本中doc、tim、tip、tmd文件详解
lucene
极限实验室12 天前
搜索百科(1):Lucene —— 打开现代搜索世界的第一扇门
搜索引擎·lucene
一路向北North14 天前
lucene渲染未命中最匹配的关键词和内容
搜索引擎·全文检索·lucene
risc12345624 天前
【lucene】advanceshallow就是遍历跳表的,可以看作是跳表的遍历器
lucene
cyh男24 天前
Lucene 8.7.0 版本的索引文件格式
搜索引擎·全文检索·lucene
risc12345625 天前
【lucene核心】impacts的由来
lucene