代码改变世界

lucene 8.7.0 版本中的倒排索引、数字、DocValues三种类型的查询性能对比 - 教程

2025-10-03 10:24  tlnshuju  阅读(1)  评论(0)    收藏  举报

大家来详细对比一下 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 ListBKD Tree列式存储
最擅长的查询文本搜索、关键词精确匹配数值/日期/地理位置的范围过滤排序、聚合、脚本访问字段值
查询性能Term 越稀有越快对范围大小不敏感,对数级复杂度,非常快用于过滤时性能极差(线性扫描)
不擅长的场景数值范围查询、排序、聚合文本搜索任何形式的搜索/过滤
典型查询TermQuery, BooleanQueryPointRangeQuery不用于查询,用于 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 性能优化的关键。