查询执行 默认按 BM25 相关性 在 postings 上找 Top-K;业务还要 按价格排序、按类目聚合、取回标题原文。这三类需求访问模式不同:倒排 优化「词项 → 文档列表」;列式 DocValues 优化「字段 → 每 doc 一值」;stored fields 优化「doc → 字段快照行存」。
本文是系列第 10 篇,闭合 Lucene
库内核的「正排侧面」:为何 sort /
agg 应走 DocValues 而非 postings,stored
随机访问贵在哪里,以及 Elasticsearch
聚合如何在此基础上建管线。
版本锚定:Apache Lucene 9.x / 10.x
DocValuesAPI;Elasticsearch 8.x 聚合默认依赖doc_values: true字段。本篇无聚合吞吐 benchmark。
一、三种正交存储:indexed · stored · docValues
第 2 篇 强调字段开关正交。查询路径上可概括为:
| 开关 | 磁盘结构 | 擅长 | 不擅长 |
|---|---|---|---|
indexed |
倒排 postings | term 检索、BM25 | 按字段值排序全索引 |
docValues |
列式 per-field | sort、agg、脚本列读 | 全文分词检索 |
stored |
行存字段组 | _source / 高亮取原文 |
大规模列扫描 |
flowchart TB
doc["Logical document"]
inv["Inverted postings<br/>term to docIDs"]
dv["DocValues<br/>field column per docID"]
st["Stored fields<br/>row blob per docID"]
doc --> inv
doc --> dv
doc --> st
qterm["Search by term"] --> inv
sortagg["Sort / Agg / Script"] --> dv
fetch["Fetch _source / stored"] --> st
常见误区:「索引了就能排序」——排序要
DocValues(或 fielddata,旧版 ES 堆内存路径);仅
indexed 的 text
字段默认不能用于聚合。
二、DocValues:列式语义与类型
DocValues 在段 flush 时 按字段写列,docID 为隐式行号(与全局 docID 映射由 reader 处理)。Lucene 提供类型族:
| Lucene 类型 | 典型映射 | 访问模式 |
|---|---|---|
NUMERIC |
long / double /
date |
get(docID) O(1) 列读 |
BINARY |
定长字节 | 脚本、自定义 |
SORTED |
单值字符串(字典序) | sort、terms agg |
SORTED_SET |
多值字符串 | 多值 cat 聚合 |
SORTED_NUMERIC |
多值数值 | 多值 metrics |
与倒排对比:postings 对
稀疏 词项→文档极省;DocValues 对
每个 doc
都有值(或显式缺失)的字段,按列顺序读缓存友好,适合
TopFieldCollector、聚合桶计数。
2.1 索引期与查询期
- 索引:
IndexWriter与倒排同一 flush 写入.dvd/.dvm等(依 codec); - 查询:
LeafReader.getNumericDocValues(field)返回NumericDocValues,advanceExact(docID)判断是否缺失; - 缺失 与
null_value(ES mapping)共同决定 sort 中的占位行为。
2.2 为何不直接用 postings 做 terms 聚合
对 keyword 字段,倒排同样列出「词项 → doc
列表」,似乎可做 terms
aggregation。生产上仍优先 DocValues,原因包括:
- 聚合要遍历命中文档集上的字段值,DocValues 按 docID 顺序 一次一列,缓存局部性好;
- 多字段组合 agg(metric + bucket)在 ES 引擎里与 列访问 管线一致;
- text 字段分词后词项爆炸,聚合
text往往是 mapping 错误; - Lucene 对 sorted-set docvalues 的 ord 编码,使 terms agg 可用全局序号压缩(ES 第 14 篇展开)。
sequenceDiagram
participant Q as Query hits
participant DV as DocValues column
participant Agg as Aggregator
Q->>Agg: docID batch
loop per docID in hit set
Agg->>DV: advanceExact(docID)
DV-->>Agg: value or ord
Agg->>Agg: update bucket
end
三、stored fields:随机取原文的代价
stored 把文档的选定字段
序列化成行
写入存储文件。IndexSearcher.storedFields().document(docID)
或 ES fetch 阶段 按命中 docID 取回
_source(若未单独禁用
stored,_source 作为 stored 行为存在)。
3.1 成本模型(定性)
| 因素 | 影响 |
|---|---|
| 字段体积 | 大 _source、附件 base64 线性放大 I/O |
| 命中数 | size 越大,fetch 越多行随机读 |
| 压缩 | LZ4/ZSTD 减磁盘,但仍有 per-doc 解压 |
| 与 query 分离 | ES query then fetch;高
size 时 fetch 可主导延迟(第 13 篇) |
stored 不是列扫描:取 100 个 doc 的
title 要 跳 100
次行;若只要统计 title 长度分布,应走
DocValues/script,而非拉 stored。
3.2 _source 与
stored 字段
Elasticsearch 默认启用 _source 存全文
JSON;也可 store: true 单字段。冗余存储增加
索引体积与 merge 写放大(第 8
篇)。日志场景大报文字段是 circuit breaker
与慢查询常客(第 17 篇)。
3.3 常见误解
- 「聚合结果里自带原文」。terms agg 只给 桶键与 count;要原文需 top_hits 子聚合,仍走 stored,成本高。
- 「disable doc_values
能省很多内存」。聚合/排序会失败或走
fielddata(堆上建列,危险);
keyword通常 doc_values 默认开启 是有意的。 - 「stored 和 docValues 存一份就够」。排序/聚合不能靠 stored 列扫;检索高亮有时要 postings positions + stored 文本 组合。
2.3 列编码与磁盘布局(直觉)
DocValues 在磁盘上 按字段连续存放,同一
docID 在不同字段的列通过 相同行号
关联。codec 对 SORTED 常用 全局字典 +
ord(序号),把重复字符串压成整数 ord,聚合时比较
ord 而非逐次比较 UTF-8 字节。NUMERIC 列可用
delta / zig-zag 等压缩(依 DocValuesFormat
版本而定)。
merge 时 DocValues 与 postings
一并重写(第 8 篇),大字段列会抬高 merge
字节量——keyword 高基数 + 长字符串是
索引体积 与 聚合内存
的双刃剑。
四、排序如何绑定 DocValues
Sort 指定
SortField(字段、类型、缺失策略)。TopFieldCollector
用 字段 DocValues 维护
优先队列(按字段序而非 BM25)。
- 单字段 sort:列读 + 堆,\(O(N \log K)\);
- index.sort(索引时排序):段内 docID
按某字段有序,范围查询可 early
terminate(与
EarlyTerminatingCollector协同); - ES
sort:要求字段doc_values: true(或keyword等默认开启类型)。
与 BM25 搜索对比:sort 不经过 postings
相交分数(除非 function_score
混合);filter + sort 是典型「先位图筛
docID,再列读排序」。
五、Elasticsearch 聚合管线(预埋)
第 14 篇将展开 mapping 与动态模板;本篇只建 机制依赖:
flowchart LR
search["Search context<br/>query hits"]
dvread["DocValues per segment"]
aggtree["Aggregator tree"]
buckets["Buckets / Metrics"]
search --> dvread --> aggtree --> buckets
fetch["Fetch phase optional"] --> stored["Stored / _source"]
search -.-> fetch
| 聚合类型 | 主要读取 | 注意 |
|---|---|---|
terms |
SORTED ord / 值 |
高基数 keyword 内存与断路器 |
date_histogram |
NUMERIC 时间戳 |
时区与 rounding |
sum / avg |
NUMERIC |
缺失值策略 |
cardinality |
HyperLogLog++ 于 hash 列 | 近似,非精确 distinct |
nested |
嵌套 docValues 结构 | mapping 复杂度高 |
全局序数(global ordinals):对
keyword 低基数场景,分片间合并桶依赖
段级 ord 映射;大索引 构建全局
ord 在 refresh 后可能触发延迟(ES 文档 eager
global ordinals)。这是
工程间隙:教科书式「列扫描聚合」未涵盖分布式
ord 合并成本。
六、与 PostgreSQL GIN、列存的坐标
| 系统 | 全文检索 | 排序/聚合 |
|---|---|---|
| Lucene / ES | postings + BM25 | DocValues 列 |
| PG GIN | 倒排 entry | 回表 heap / 索引列 |
| ClickHouse 日志 | token 索引可选 | 列存原生 agg |
postgresql-kernel/16 GIN 与 observability/08 日志栈 在选型篇 第 18 篇 对照;本篇只强调:搜索引擎把「检索」与「分析列」在存储上拆开,不是 accidental complexity。
5.1 text vs
keyword 双字段模式
ES 实践常为「全文检索 + 聚合」复制字段:
{
"properties": {
"status": {
"type": "text",
"fields": {
"keyword": { "type": "keyword", "ignore_above": 256 }
}
}
}
}status(text)→ postings,供match;status.keyword→ DocValues,供termsagg /sort;- 双写增加索引体积,但避免对 analyzed 词项做聚合这一 结构性错误。
七、设计检查清单(mapping 视角)
- 要 filter/sort/agg
的字段:
keyword/numeric+ doc_values(ES 默认对多数类型开启);避免对 analyzedtext聚合。 - 要展示、高亮、RAG 上下文:保留
stored /
_source,控制单 doc 体积。 - 只检索不展示:可
stored: false+doc_values: false仅 indexed(极少见纯检索字段)。 - 高基数 terms agg:评估
shard_size、partition、或换 样本 / 预聚合(第 14、17 篇)。
八、开放问题
- runtime fields 在查询时物化列,便利但与原生 DocValues 的 性能与缓存 争论仍在 ES 文档中;
- 向量字段 DocValues / KNN 索引与倒排 同段 merge 下的列读成本(第 15 篇);
- 湖仓导出(Parquet)与 ES 聚合列语义对齐,用于冷热分层时的 二次聚合一致性。
九、小结
三句话小结
- postings 服务词项检索;DocValues 服务按 docID 读字段列,是 sort/agg/script 的正道。
- stored 服务按 doc 取行,适合
fetch/_source,不适合全索引列统计;大
_source拖慢 fetch 与 merge。 - Elasticsearch 聚合在 Lucene DocValues 上建 Aggregator 树;分布式 terms 还有 global ordinals 额外成本,第 14 篇继续。
下一篇进入 ES 分布式层:Index · Shard · Replica。
参考资料
规范 / 文档
- Apache Lucene
9.x/10.x:
DocValues、SortedDocValues、NumericDocValues、StoredFields。 - Elasticsearch 8.x Reference:Doc
values、Aggregations、Mapping(
doc_values、store、_source)、Field data(legacy 边界)。
综述
- Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 2006(正排/倒排分工背景)。
站内
返回 系列目录 | 上一篇:查询执行 | 下一篇:Shard · Replica
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。
【全文检索引擎】Lucene 文档模型:Field、docID 与正排/倒排
拆解 Lucene Index/Document/Field 与段内 docID 分配;说明 indexed、stored、docValues 三组正交开关;对照正排与倒排访问路径,为 Analyzer 与 postings 篇奠基。
【全文检索引擎】BM25 与 Similarity:公式如何落到 Lucene
从 Robertson & Zaragoza (2009) 的 BM25 公式出发,拆解 Lucene BM25Similarity 如何把 TF 饱和、文档长度归一与 IDF 落到 postings 与 norms;对照 TF-IDF 工程差异,并划定与学习排序的生产边界。
【全文检索引擎】IndexWriter 与 NRT:从缓冲到可打开的 Reader
拆解 Lucene IndexWriter 的 RAM 缓冲、flush 出段与 DirectoryReader.open(IndexWriter) 近实时语义;说明 Searcher 刷新与「刚写入即可搜」在库层与 ES refresh 层各自保证什么。