土法炼钢兴趣小组的算法知识备份

【全文检索引擎】Lucene 文档模型:Field、docID 与正排/倒排

文章导航

分类入口
databasestorage
标签入口
#lucene#document#field#docid#docvalues#stored-fields#inverted-index#forward-index#segment

目录

全文引擎全景 把 Lucene 库与 Elasticsearch 服务的分层钉住了。读者若只看过 存储工程·索引结构 里的倒排示意图,很容易把 Lucene 想成「一个大 HashMap:词→文档列表」。实际上,一条业务记录进入 Lucene 时,先被建模成 Document 与多个 Field;段内再分配紧凑的 docID;同一字段可能同时参与倒排、列式 DocValues 与 stored 正排三条路径——三条路径的开关彼此独立。

本文是系列第 2 篇。目标不是复述 Elasticsearch mapping JSON,而是回答:

  1. Index / Document / Field / docID 在 Lucene 9.x/10.x 里各指什么?
  2. indexedstoreddocValues(及向量字段)为何必须当成正交维度理解?
  3. 查文档原文、排序聚合、找词项三类访问分别走哪条索引侧面?

版本锚定:Apache Lucene 9.x/10.x 官方 DocumentIndexableFieldDocValues 文档;Elasticsearch 8.x Mapping 中与 text / keyword / doc_values 的对应关系在边界处提及。本篇无性能自测数字。


一、最小故事:一篇博客如何变成段里的行

假设索引一篇博客,业务字段如下:

业务字段 检索需求 返回需求 排序 / 聚合
title 全文分词 列表展示标题 不按 title 排序
body 全文分词 可选摘要
author 精确过滤 展示 terms 聚合
publish_date 范围过滤 展示 按时间排序
tenant_id 精确过滤 分桶统计

在 Lucene 里,一次 addDocument 传入一个 Document 对象,内含多个 Field(或实现 IndexableField 的自定义类型)。IndexWriter 为每个新文档在当前段分配单调递增的 docID\(0, 1, 2, \ldots\)docID 不是业务主键,而是段内(合并后全局)紧凑整数,供 postings 列表、DocValues 列、norms 数组下标使用。

同一条 title 文本若 IndexOptions 开启索引,会经 Analyzer 切成词项写入倒排;若 stored=true,原文另写入 stored fields 文件;若业务要对 author 做聚合,需为该字段启用 DocValues(keyword 类字段在 ES mapping 里通常默认开启 doc values)。


二、核心对象:Index、Document、Field、docID

2.1 Index 与 Segment

Lucene 的 Index 在磁盘上体现为目录中的一组不可变 Segment 加上 segments_N 提交点。逻辑上它是一个「当前可见文档集合」;物理上是多个 Segment 的并集,查询时 IndexSearcher 跨段合并 docID(通过 LeafReaderContext 映射到段内局部 id)。

flowchart TB
  idx["Lucene Index<br/>logical collection"]
  seg0["Segment_0<br/>docs 0..n0-1"]
  seg1["Segment_1<br/>docs 0..n1-1"]
  commit["segments_N commit point"]
  idx --> seg0
  idx --> seg1
  commit --> idx

工程要点:Segment 不可变带来并发读与缓存友好;更新等价于删除旧 doc + 插入新 doc,故业务主键应放在 stored 或 DocValues 里自行维护,不能指望 docID 稳定(第 7–8 篇)。

2.2 Document 与 Field

Document 是字段容器;Field 携带:

官方 Document 文档中的 Field 构造参数体现三组独立开关(A 级):

开关 作用 典型查询 / 用途
Indexed(及 IndexOptions 写入倒排:词项 → docID 列表 MatchQueryPhraseQuery
Stored 写入正排 stored fields IndexSearcher.storedFields() 取原文
DocValues 列式按 docID 随机读 排序、SortedSetDocValues 聚合、join

三者正交:常见组合是 indexed + stored(既要搜又要取标题)、indexed + docValues(既要搜又要聚合 keyword)、仅 docValues(不参与评分但用于排序的数值列)。

2.3 docID 与业务主键

段内 docID 是写入顺序分配的整数。合并 Segment 时会重新编号;删除通过 liveDocs 位图标记,不立即物理抹除。

因此:

flowchart LR
  biz["Business PK<br/>uuid / sku"]
  doc["Document fields"]
  did["Segment docID<br/>0..N-1"]
  inv["Inverted postings"]
  st["Stored fields"]
  dv["DocValues columns"]
  biz --> doc
  doc --> did
  did --> inv
  did --> st
  did --> dv

三、正排 vs 倒排:三条访问路径

storage/29 已介绍倒排「词项→文档」。Lucene 同时维护多种按 docID 随机访问的结构,统称正排侧面(forward / columnar access):

访问意图 数据结构 为何不用倒排
命中后取标题原文 stored fields 倒排不保存未分词整段(除非不切词)
ORDER BY publish_date NumericDocValues 倒排按词项组织,无法按 doc 顺序扫列
terms 聚合 author SortedSetDocValues 聚合需列遍历或 ord 映射
BM25 长度归一 norms(与 indexed 绑定) 每 doc 一个 byte 级长度因子
flowchart TB
  subgraph inverted ["Inverted side"]
    term["Term in field"]
    fst["Terms dictionary FST"]
    post["Postings lists"]
    term --> fst --> post
  end
  subgraph forward ["Forward / columnar side"]
    sf["Stored fields"]
    ndv["NumericDocValues"]
    ssdv["SortedSetDocValues"]
  end
  q["Query"] --> inverted
  hit["Hit docID"] --> forward

短语查询除倒排外还依赖 positions(第 5 篇):IndexOptions 至少要到 DOCS_AND_FREQS_AND_POSITIONS。offsets 用于高亮;payloads 用于自定义打分信号,本系列不展开编码细节。

3.2 norms:BM25 长度归一预埋

与 indexed 文本字段绑定的 norms 是每个 doc 一个压缩字节,编码字段长度信息,供 Similarity(默认 BM25)做长度归一。norms 不是 DocValues,不参与聚合;查询打分阶段由 IndexSearcher 按 docID 读取。省略 norms(FieldType.setOmitNorms(true))会改变相关性行为——长文与短文 TF 不可比,需在 mapping 层显式决策(第 6 篇公式中的 \(|d|\) 分量)。

3.3 向量字段边界(不展开 ANN)

Lucene 9.x+ 支持 KnnVectorField:向量与文本共享同一 docID,但索引侧面是 HNSW 图(或量化变体),不走 term→postings。混合检索时,文本子句仍走 FST/postings,向量子句走 KnnVectorQuery——第 15 篇讨论同 Segment 生命周期;本篇只需记住 向量不是 TextField 的一种 Store 选项,而是独立字段类型。

3.4 IndexOptions 与写放大

IndexOptions 决定倒排里为每个词项存多少信息:

级别 内容 写放大 / 空间
DOCS 仅 docID 最小
DOCS_AND_FREQS + 词频 BM25 需要 freq
DOCS_AND_FREQS_AND_POSITIONS + 位置 短语、邻近查询
DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS + 字符偏移 高亮

选型是显式权衡body 要短语就带 positions;仅过滤存在的 tags 可用 DOCS。Elasticsearch index_options 与 Lucene 枚举对应(8.x Reference Mapping parameters)。


四、Elasticsearch mapping 与 Lucene Field 的边界

Elasticsearch 不暴露 Lucene API,但 shard 内仍是 Document + Field。常见对应(便于从 ES 反推库层):

ES mapping 类型 Lucene 侧直觉
text + analyzer 分词 indexed,默认不 doc values
keyword 不分词 indexed;默认 doc values 开
date / long DocValues + 可选 indexed
stored: true(少见显式) _source 与 stored 策略由 ES 合成

工程间隙:ES 的 _source 是整文档 JSON 副本,不等于 Lucene stored fields 的唯一形态;排序与聚合仍走 DocValues。动态 mapping 爆炸(第 14 篇)本质是无意为每个 JSON 叶子创建过多 indexed / docValues 列


五、与 PostgreSQL GIN 的模型差异(一句)

PG GIN 把「键→行 TID」放在 Entry Tree + Posting List 里,行仍是堆表元组。Lucene 把文档即索引行docID 是段内抽象行号,多字段共享同一 docID 下的不同倒排域(field 名区分)。SQL UPDATE 与 Lucene「删+增」的可见性语义不同——第 18 篇选型回链。


六、代码侧:构造 Document 的最小示例

下列 Java 片段与 Lucene 9.x API 一致(源码级 A 级);需在本地 Lucene 依赖下编译运行,此处仅作模型说明,非本机执行输出。

import org.apache.lucene.document.*;
import org.apache.lucene.index.IndexOptions;

Document doc = new Document();

// 全文:索引 + 存原文 + 位置(短语)
Field title = new TextField("title", "Lucene document model", Field.Store.YES);
title.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
doc.add(title);

// 精确过滤 + 聚合:StringField 默认 indexed + 可分 DocValues(视版本与 FieldType)
doc.add(new StringField("author", "ltl", Field.Store.NO));

// 排序:NumericDocValues
doc.add(new NumericDocValuesField("publish_date", 20260715L));

// 业务主键:keyword 类 DocValues
doc.add(new StringField("id", "post-001", Field.Store.YES));

IndexWriter.addDocument(doc) 之后,Analyzer 只处理需分词的字段;StringField 整段作为一个 term 进入 FST 词典


七、学术脉络与开放问题

Zobel & Moffat(ACM Computing Surveys, 2006)的倒排模型是 term → postings;Lucene 增加 field 维度(同一 term 在不同 field 是不同倒排域),并用 docID 压缩段不可变 适配 JVM 堆外与 OS 页缓存。这是相对经典 IR「单一文档流」的分叉。

开放问题

  1. 多值字段(一个 doc 多个 tag)在 DocValues 与倒排上的 ord 编码,与 SQL 1:N 规范化谁更易维护(第 10 篇)。
  2. 向量字段与文本字段同 docID 时,stored / DocValues / dense vector 三套列的合并 IO(第 15 篇)。
  3. docID 级软删与磁盘回收延迟:读者从 SQL 习惯「删即空」迁移时的容量错觉(第 8 篇)。

7.1 常见误解

  1. 「一个字段只能干一件事」indexedstoreddocValues 独立组合;ES textkeyword 双字段才是 mapping 层常用模式,不是 Lucene 强制。
  2. 「docID 就是主键」。docID 随 merge 变化;持久身份在 stored / DocValues。
  3. 「不 stored 就无法显示搜索结果」。可用 _source(ES)、DocValues 或外部库按主键回表;stored 只是引擎内正排一种。

八、小结

三句话小结

  1. Lucene 以 Document + 多 Field 接纳业务记录,段内用 docID 串联倒排、stored 与 DocValues。
  2. 倒排找词,正排(stored / DocValues)按 doc 取列;排序聚合不应走 postings。
  3. Analyzer 篇 前,先弄清哪些字段分词、哪些仅 keyword——这决定 term 如何进入词典。

参考资料

核心论文

  1. Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 38(2), 2006。

规范 / 文档

  1. Apache Lucene 9.x/10.x, DocumentIndexableFieldDocValuesIndexOptions
  2. Elasticsearch 8.x Reference, Mappingtextkeyworddoc_values)。

站内

  1. 系列第 1 篇第 3 篇 AnalyzerPG GIN

上一篇:全文引擎全景 | 下一篇:分析链 Analyzer系列目录

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-07-15 · database / storage

【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT

补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。


By .