全文引擎全景
把 Lucene 库与 Elasticsearch 服务的分层钉住了。读者若只看过
存储工程·索引结构
里的倒排示意图,很容易把 Lucene 想成「一个大
HashMap:词→文档列表」。实际上,一条业务记录进入
Lucene 时,先被建模成 Document 与多个
Field;段内再分配紧凑的
docID;同一字段可能同时参与倒排、列式 DocValues
与 stored
正排三条路径——三条路径的开关彼此独立。
本文是系列第 2 篇。目标不是复述 Elasticsearch mapping JSON,而是回答:
Index/Document/Field/docID在 Lucene 9.x/10.x 里各指什么?indexed、stored、docValues(及向量字段)为何必须当成正交维度理解?- 查文档原文、排序聚合、找词项三类访问分别走哪条索引侧面?
版本锚定:Apache Lucene 9.x/10.x 官方 Document、IndexableField、DocValues 文档;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
携带:
- 名称(
name):逻辑字段名,查询与 mapping 对齐。 - 值(
String、BytesRef、数值等)。 - 类型策略:是否分词、是否存原文、是否写 DocValues、向量维度等。
官方 Document 文档中的 Field
构造参数体现三组独立开关(A 级):
| 开关 | 作用 | 典型查询 / 用途 |
|---|---|---|
Indexed(及
IndexOptions) |
写入倒排:词项 → docID 列表 | MatchQuery、PhraseQuery |
| Stored | 写入正排 stored fields | IndexSearcher.storedFields() 取原文 |
| DocValues | 列式按 docID 随机读 | 排序、SortedSetDocValues 聚合、join |
三者正交:常见组合是
indexed + stored(既要搜又要取标题)、indexed + docValues(既要搜又要聚合
keyword)、仅
docValues(不参与评分但用于排序的数值列)。
2.3 docID 与业务主键
段内 docID 是写入顺序分配的整数。合并
Segment 时会重新编号;删除通过 liveDocs
位图标记,不立即物理抹除。
因此:
- 倒排列表里存的是 docID,不是 UUID。
- 应用层
_id(ES)或自建id字段应存为 keyword DocValues 或 stored,查询命中后再docID → 业务 id映射。
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「单一文档流」的分叉。
开放问题:
- 多值字段(一个 doc 多个 tag)在 DocValues 与倒排上的 ord 编码,与 SQL 1:N 规范化谁更易维护(第 10 篇)。
- 向量字段与文本字段同 docID 时,stored / DocValues / dense vector 三套列的合并 IO(第 15 篇)。
- docID 级软删与磁盘回收延迟:读者从 SQL 习惯「删即空」迁移时的容量错觉(第 8 篇)。
7.1 常见误解
- 「一个字段只能干一件事」。
indexed、stored、docValues独立组合;EStext与keyword双字段才是 mapping 层常用模式,不是 Lucene 强制。 - 「docID 就是主键」。docID 随 merge 变化;持久身份在 stored / DocValues。
- 「不 stored 就无法显示搜索结果」。可用
_source(ES)、DocValues 或外部库按主键回表;stored 只是引擎内正排一种。
八、小结
三句话小结
- Lucene 以 Document + 多 Field 接纳业务记录,段内用 docID 串联倒排、stored 与 DocValues。
- 倒排找词,正排(stored / DocValues)按 doc 取列;排序聚合不应走 postings。
- 读 Analyzer 篇 前,先弄清哪些字段分词、哪些仅 keyword——这决定 term 如何进入词典。
参考资料
核心论文
- Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 38(2), 2006。
规范 / 文档
- Apache Lucene 9.x/10.x, Document、IndexableField、DocValues、IndexOptions。
- Elasticsearch 8.x Reference,
Mapping(
text、keyword、doc_values)。
站内
上一篇:全文引擎全景 | 下一篇:分析链 Analyzer | 系列目录
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】DocValues 与 stored fields:排序聚合为何不走倒排
说明 Lucene DocValues 的列式访问模型如何服务排序、聚合与脚本,stored fields 的随机取原文代价,以及 Elasticsearch 聚合管线对 DocValues 的依赖——为第 14 篇预埋。
【全文检索引擎】Lucene · BM25 · Segment · Elasticsearch NRT
补齐搜索架构叙事与 RAG/向量引擎之间的全文检索内核层:以 Lucene 9.x/10.x 拆解 Analyzer、FST、postings、BM25 与 IndexWriter,并以 Elasticsearch 8.x 拆解分片、refresh/translog 与查询路径;OpenSearch 对照与选型收束。
【全文检索引擎】词项词典 FST:前缀压缩与 TermsEnum
说明 Lucene 段内 Terms 词典如何用 FST 做前缀共享与内存映射;介绍 TermsEnum seek 与字段迭代;与 PostgreSQL GIN Entry Tree 对照一句,为 postings 定位奠基。