Lucene 8.7.0 版本中dvd、dvm文件详解

一、一句话总结

.dvm.dvd 文件共同构成了 Lucene 的 DocValues 数据。

  • .dvm (DocValues Metadata) : 元数据文件,记录了哪些字段拥有 DocValues、它们的类型以及在 .dvd 文件中的位置信息。可以理解为是 .dvd 文件的"目录"或"索引"。
  • .dvd (DocValues Data): 数据文件,包含了所有文档的实际 DocValues 值,但这些值经过了高度压缩和特定格式的编码。

二、深入理解:什么是 DocValues?

在了解文件格式之前,必须先理解 DocValues 是什么。

传统的 Lucene 索引是倒排索引 (Inverted Index) ,它的结构是 词项 -> 文档列表。这非常适合搜索,比如查找包含 "Lucene" 这个词的所有文档。

但是,对于某些操作,倒排索引效率极低,例如:

  • 排序 (Sorting):按价格字段对搜索结果排序。
  • 分面/聚合 (Faceting/Aggregations):按品牌统计每个品牌下有多少商品。
  • 脚本访问字段值:在脚本中直接获取某个文档的特定字段值。

为了高效地执行这些操作,需要一种能够快速从 文档ID -> 字段值 的数据结构。这就是 DocValues ,它是一种正向索引 ,也被称为列式存储 (Columnar Storage)

核心思想:DocValues 将每个字段的值按文档 ID 顺序存储起来,形成一列数据。当需要对某个字段进行排序或聚合时,Lucene 可以像操作数据库的列一样,快速地、顺序地访问所有文档的该字段值,而无需遍历整个倒排索引,从而极大地提升了性能。


三、.dvm 文件详解 (DocValues Metadata)

.dvm 文件是元数据文件,它的体积很小,但至关重要。它就像一本书的目录,告诉你每一章(每个字段的 DocValues)在正文(.dvd 文件)的哪个位置。

它的主要内容包括:

  1. 头部信息 (Header):包含 Lucene 的编解码器名称和版本号,用于校验文件格式的正确性。
  2. 字段条目 (Field-level Entries) :对于索引段 (Segment) 中每一个拥有 DocValues 的字段,.dvm 文件都会有一条记录,包含以下信息:
    • 字段编号 (Field Number):字段在内部的唯一标识符。
    • DocValues 类型 (DocValues Type) :这是最关键的信息之一,决定了 .dvd 文件中数据的编码和解码方式。主要类型有:
      • NUMERIC:64位长整型数字。
      • BINARY:可变长度的字节数组(例如字符串)。
      • SORTED:经过字典编码的单值 BINARY 类型,主要用于排序和单值分面。
      • SORTED_SET:经过字典编码的多值 BINARY 类型,用于多值分面(如商品的多个标签)。
      • SORTED_NUMERIC:经过排序的多值 NUMERIC 类型。
    • 数据偏移量 (Offset) :指示该字段的 DocValues 数据在 .dvd 文件中的起始位置(字节偏移)。
    • 数据长度 (Length) :该字段数据在 .dvd 文件中占用的总字节数。
    • 其他元数据 :根据类型不同,可能还包含额外信息。例如,对于 SORTED 类型,可能包含其值基数(唯一值的数量)等。

工作流程 :当 Lucene 需要访问某个字段(比如 "price")的 DocValues 时,它首先会读取 .dvm 文件,找到 "price" 字段对应的条目,获取其类型(NUMERIC)、在 .dvd 文件中的偏移量和长度。然后,它会直接跳转到 .dvd 文件中的指定位置,并使用与 NUMERIC 类型相匹配的解码器来读取数据。


四、.dvd 文件详解 (DocValues Data)

.dvd 文件是真正存储数据的"大家伙"。它是一个二进制数据块,包含了所有字段的 DocValues 值紧密排列在一起。它本身不包含任何元数据,必须依赖 .dvm 文件才能被正确解析。

其内部数据的存储方式完全取决于字段的 DocValues 类型,并且 Lucene 8.7.0 会使用非常高效的压缩策略来节省磁盘空间和提高读取速度。

以下是不同类型的数据在 .dvd 文件中的典型存储方式:

1. NUMERIC 类型 (例如:价格、年龄、时间戳)

Lucene 不会傻瓜式地为每个文档存储一个 64 位的 long。它会分析这个字段中所有值的特点,选择最优的压缩策略:

  • 最大公约数压缩 (GCD Compression) :如果一个字段的所有数字(例如:1000, 5000, 20000)都有一个共同的除数(例如 1000),Lucene 会先存储这个公约数,然后只存储每个值除以公约数后的商(1, 5, 20)。这对于以毫秒为单位的时间戳非常有效。
  • 表压缩 (Table Compression) :如果一个字段的唯一值数量非常少(低基数),比如一个表示状态的字段只有 0, 1, 2 三个值。Lucene 会创建一个包含 [0, 1, 2] 的小表,然后为每个文档只存储一个指向该表项的极短索引(例如用 2-bit 就可以表示)。
  • Packed Integers:对于一般情况,Lucene 会计算出存储该字段所有值所需的最大位数。例如,如果所有值都在 0-1000 之间,那么只需要 10 bits 就可以表示,而不是 64 bits。Lucene 会将这些 10-bit 的整数紧密地打包在一起,极大地节省了空间。
2. BINARY 类型 (例如:UUID、固定长度的字符串)
  • 固定长度:如果所有值的长度都一样,数据会直接连续存储。
  • 可变长度 :通常会把所有字节数据连接成一个大的字节块,然后用一个额外的元数据结构(也存储在 .dvd 中)来记录每个文档对应值的起始和结束位置。
3. SORTED / SORTED_SET 类型 (例如:商品分类、标签)

这是 DocValues 最强大的应用之一,使用了字典编码 (Dictionary Encoding)

  • 字典 (Dictionary):首先,收集该字段所有的唯一值(例如 "电子产品", "图书", "家居"),对它们进行排序,形成一个字典。
  • 序数 (Ordinals) :然后,对于每个文档,不再存储原始的字符串,而是存储该字符串在字典中的位置索引,这个索引称为序数 (ordinal)。例如,"电子产品" -> 0, "图书" -> 1, "家居" -> 2。
  • 存储 :字典本身和所有文档的序数列表都存储在 .dvd 文件中。因为序数是整数,所以可以再次使用对 NUMERIC 类型的压缩技术。

这种方式的优势是巨大的:

  • 空间节省:长字符串被替换成了短整数。
  • 聚合/分面极快:统计每个分类的商品数量,只需要统计每个序数(0, 1, 2...)出现的次数即可,这是一个纯整数操作,速度飞快。
  • 排序快:比较字符串的排序变成了比较它们序数的大小。

五、一个形象的比喻

把一个 Lucene 索引段想象成一本关于商品的百科全书。

  • 倒排索引 (.tim, .doc等文件):是书末尾的"名词索引"。你想找"手机",索引会告诉你它在第 10, 25, 88 页出现过。
  • DocValues (.dvm, .dvd 文件) :是一张巨大的表格附录。
    • 表格的列头:价格、品牌、上市日期...
    • 表格的行:第1个商品、第2个商品、第3个商品...
    • .dvm 文件:是这张表格的"图例"和"说明"。它告诉你,"价格"是数字类型,在附录数据区的第1-100KB;"品牌"是文本类型,在101-150KB。
    • .dvd 文件:是这张表格的"主体数据区",包含了所有商品的真实价格、品牌序数、日期等,并且这些数据被压得很紧实。

当你需要按价格排序时,Lucene 不会去翻阅整本书,而是直接拿出这张表格,只看"价格"那一列,然后快速地对所有商品进行排序。

当然可以!我们用一个具体的、简化的例子来展示 dvmdvd 文件的存储格式。这会是一个概念上的表示,而非真实的二进制字节流,但它能清晰地揭示其内部工作原理。


六、场景设定

假设我们有一个 Lucene 索引,其中包含 4 个文档(Doc ID 从 0 到 3)。我们关注三个具有 DocValues 的字段:

  1. price (价格): 类型为 NUMERIC
  2. category (分类): 类型为 SORTED (单值文本,用于排序和分面)
  3. tags (标签): 类型为 SORTED_SET (多值文本,用于分面)

原始数据如下:

Doc ID price (NUMERIC) category (SORTED) tags (SORTED_SET)
0 100 "Book" ["new", "bestseller"]
1 200 "Electronics" ["sale"]
2 100 "Book" (无标签)
3 500 "Book" ["new", "hardcover"]

现在,我们来看看 Lucene 8.7.0 会如何将这些数据存入 .dvm.dvd 文件中。


.dvm 文件内容 (元数据)

这个文件非常小,就像一个目录。它的内容在概念上可以看作:

复制代码
// --- .dvm File (Conceptual Representation) ---

[Header: Lucene87Codec, version=8.7.0, ...]
[Number of DocValues Fields: 3]

// --- Entry 1 ---
[Field Name: "price"]
[Field Type: NUMERIC]
[Data Offset in .dvd: 0]
[Data Length in .dvd: L1]  // 假设 price 数据块占 L1 字节

// --- Entry 2 ---
[Field Name: "category"]
[Field Type: SORTED]
[Data Offset in .dvd: L1] // 紧跟在 price 数据之后
[Data Length in .dvd: L2]

// --- Entry 3 ---
[Field Name: "tags"]
[Field Type: SORTED_SET]
[Data Offset in .dvd: L1 + L2] // 紧跟在 category 数据之后
[Data Length in .dvd: L3]

[Footer: Checksum, etc.]

解读:

当 Lucene 需要 category 字段的 DocValues 时,它会:

  1. 读取 .dvm 文件。
  2. 找到名为 "category" 的条目。
  3. 得知其类型是 SORTED,数据位于 .dvd 文件中从字节 L1 开始,长度为 L2 的区域。
  4. 然后,它会带着这些信息去 .dvd 文件中读取和解码数据。

.dvd 文件内容 (实际数据)

这个文件是一个连续的二进制数据块,由三个部分拼接而成,每个部分对应一个字段。

复制代码
// --- .dvd File (Conceptual Representation) ---

// =========================================================================
// == BLOCK 1: 'price' Data (Offset: 0, Length: L1)
// == Type: NUMERIC
// =========================================================================

// Lucene发现所有值 (100, 200, 100, 500) 的最大公约数(GCD)是100。
// 于是采用GCD压缩。

[Metadata for price block: {type: GCD_COMPRESSED, gcd_value: 100}]

// 存储每个原始值除以100后的商。原始值: [100, 200, 100, 500] -> 商: [1, 2, 1, 5]
// 这些小整数可以用 Packed Integers 高效存储。
[Packed Integers Block for quotients: 1, 2, 1, 5]

// =========================================================================
// == BLOCK 2: 'category' Data (Offset: L1, Length: L2)
// == Type: SORTED
// =========================================================================

// Lucene发现该字段只有两个唯一值:"Book" 和 "Electronics"。
// 于是采用字典编码。

// 1. 字典 (Dictionary) 部分: 存储唯一的、排好序的值。
//    序数(ordinal) 0 -> "Book"
//    序数(ordinal) 1 -> "Electronics"
[Dictionary Block: (length=4, "Book"), (length=11, "Electronics")]

// 2. 序数 (Ordinals) 部分: 为每个文档存储其值对应的序数。
//    Doc 0: "Book" -> 0
//    Doc 1: "Electronics" -> 1
//    Doc 2: "Book" -> 0
//    Doc 3: "Book" -> 0
//    序数列表为 [0, 1, 0, 0]。这些小整数用 Packed Integers 存储。
[Packed Integers Block for ordinals: 0, 1, 0, 0]

// =========================================================================
// == BLOCK 3: 'tags' Data (Offset: L1+L2, Length: L3)
// == Type: SORTED_SET
// =========================================================================

// 这比SORTED更复杂,因为它需要处理多值和空值。同样使用字典编码。

// 1. 字典 (Dictionary) 部分: 收集所有唯一的标签并排序。
//    "bestseller", "hardcover", "new", "sale"
//    序数 0 -> "bestseller"
//    序数 1 -> "hardcover"
//    序数 2 -> "new"
//    序数 3 -> "sale"
[Dictionary Block: (len, "bestseller"), (len, "hardcover"), (len, "new"), (len, "sale")]

// 2. 序数列表 (Ordinals List) 部分: 将所有文档的所有标签的序数连接成一个长列表。
//    Doc 0: ["new", "bestseller"] -> [2, 0]
//    Doc 1: ["sale"] -> [3]
//    Doc 2: (无) -> []
//    Doc 3: ["new", "hardcover"] -> [2, 1]
//    连接后的完整序数列表: [2, 0, 3, 2, 1]
[Packed Integers Block for all ordinals: 2, 0, 3, 2, 1]

// 3. 文档到序数的查找表 (Doc-to-Ordinals Lookup) 部分:
//    这是关键!它告诉我们每个文档的序数在上面那个长列表中的起止位置。
//    它是一个包含 (文档数+1) 个指针的列表。
//    - Doc 0 的序数从索引 0 开始。
//    - Doc 1 的序数从索引 2 开始 (因为Doc 0有两个标签)。
//    - Doc 2 的序数从索引 3 开始 (因为Doc 1有一个标签)。
//    - Doc 3 的序数从索引 3 开始 (因为Doc 2没有标签)。
//    - 列表的结尾在索引 5 (因为Doc 3有两个标签,3+2=5)。
//    这个查找表就是: [0, 2, 3, 3, 5]
[Packed Integers Block for lookup table: 0, 2, 3, 3, 5]
工作流程回顾

让我们看看如何获取 Doc 3tags

  1. Lucene 查找 .dvm,定位到 tags 字段在 .dvd 中的数据块。
  2. 进入 tags 数据块,首先找到文档到序数的查找表
  3. 要找 Doc 3,就查看查找表的第 3 和第 4 个元素,得到 35
  4. 这表示 Doc 3 的序数位于序数列表 的索引 35 (不含5) 之间,即索引为 34 的位置。
  5. 序数列表 中取出索引 34 的值,得到 [2, 1]
  6. 拿着序数 21,去 tags 数据块的字典中查找。
  7. 序数 2 对应 "new",序数 1 对应 "hardcover"。
  8. 最终得到结果:["new", "hardcover"]

这个例子清晰地展示了 dvmdvd 文件如何协同工作,以及 Lucene 如何利用 GCD 压缩、字典编码、Packed Integers 和查找表等多种技术,以极高的效率存储和检索 DocValues 数据。


总结

在 Lucene 8.7.0 中,.dvm.dvd 文件是实现高效排序、聚合和字段值访问的关键。它们通过列式存储和多种智能压缩算法,以极高的空间和时间效率存储了字段的正向索引数据,是 Lucene 高性能特性的重要基石,也是 Elasticsearch 和 Solr 等搜索引擎能够实现快速聚合分析功能的核心技术所在。

相关推荐
是犹橐籥10 小时前
头歌Educoder答案 Lucene - 全文检索入门
搜索引擎·全文检索·lucene
cyh男1 天前
Lucene 8.7.0 版本中docFreq、totalTermFreq、getDocCount等方法的含义
lucene
cyh男2 天前
Lucene 8.7.0 版本中doc、tim、tip、tmd文件详解
lucene
极限实验室10 天前
搜索百科(1):Lucene —— 打开现代搜索世界的第一扇门
搜索引擎·lucene
一路向北North12 天前
lucene渲染未命中最匹配的关键词和内容
搜索引擎·全文检索·lucene
risc12345622 天前
【lucene】advanceshallow就是遍历跳表的,可以看作是跳表的遍历器
lucene
cyh男22 天前
Lucene 8.7.0 版本的索引文件格式
搜索引擎·全文检索·lucene
risc12345623 天前
【lucene核心】impacts的由来
lucene
在未来等你24 天前
Elasticsearch面试精讲 Day 5:倒排索引原理与实现
elasticsearch·搜索引擎·面试·全文检索·lucene·分词·倒排索引