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

【全文检索引擎】DocValues 与 stored fields:排序聚合为何不走倒排

文章导航

分类入口
databasestorage
标签入口
#lucene#docvalues#stored-fields#aggregation#sort#columnar#elasticsearch#full-text-search

目录

查询执行 默认按 BM25 相关性 在 postings 上找 Top-K;业务还要 按价格排序、按类目聚合、取回标题原文。这三类需求访问模式不同:倒排 优化「词项 → 文档列表」;列式 DocValues 优化「字段 → 每 doc 一值」;stored fields 优化「doc → 字段快照行存」。

本文是系列第 10 篇,闭合 Lucene 库内核的「正排侧面」:为何 sort / agg 应走 DocValues 而非 postings,stored 随机访问贵在哪里,以及 Elasticsearch 聚合如何在此基础上建管线。

版本锚定:Apache Lucene 9.x / 10.x DocValues API;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 堆内存路径);仅 indexedtext 字段默认不能用于聚合。


二、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 索引期与查询期

2.2 为何不直接用 postings 做 terms 聚合

keyword 字段,倒排同样列出「词项 → doc 列表」,似乎可做 terms aggregation。生产上仍优先 DocValues,原因包括:

  1. 聚合要遍历命中文档集上的字段值,DocValues 按 docID 顺序 一次一列,缓存局部性好;
  2. 多字段组合 agg(metric + bucket)在 ES 引擎里与 列访问 管线一致;
  3. text 字段分词后词项爆炸,聚合 text 往往是 mapping 错误;
  4. 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 _sourcestored 字段

Elasticsearch 默认启用 _source 存全文 JSON;也可 store: true 单字段。冗余存储增加 索引体积与 merge 写放大(第 8 篇)。日志场景大报文字段是 circuit breaker 与慢查询常客(第 17 篇)。

3.3 常见误解

  1. 「聚合结果里自带原文」。terms agg 只给 桶键与 count;要原文需 top_hits 子聚合,仍走 stored,成本高。
  2. 「disable doc_values 能省很多内存」。聚合/排序会失败或走 fielddata(堆上建列,危险);keyword 通常 doc_values 默认开启 是有意的。
  3. 「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)。

与 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 GINobservability/08 日志栈 在选型篇 第 18 篇 对照;本篇只强调:搜索引擎把「检索」与「分析列」在存储上拆开,不是 accidental complexity。

5.1 text vs keyword 双字段模式

ES 实践常为「全文检索 + 聚合」复制字段:

{
  "properties": {
    "status": {
      "type": "text",
      "fields": {
        "keyword": { "type": "keyword", "ignore_above": 256 }
      }
    }
  }
}

七、设计检查清单(mapping 视角)

  1. 要 filter/sort/agg 的字段keyword / numeric + doc_values(ES 默认对多数类型开启);避免对 analyzed text 聚合。
  2. 要展示、高亮、RAG 上下文:保留 stored / _source,控制单 doc 体积。
  3. 只检索不展示:可 stored: false + doc_values: false 仅 indexed(极少见纯检索字段)。
  4. 高基数 terms agg:评估 shard_sizepartition、或换 样本 / 预聚合(第 14、17 篇)。

八、开放问题

  1. runtime fields 在查询时物化列,便利但与原生 DocValues 的 性能与缓存 争论仍在 ES 文档中;
  2. 向量字段 DocValues / KNN 索引与倒排 同段 merge 下的列读成本(第 15 篇);
  3. 湖仓导出(Parquet)与 ES 聚合列语义对齐,用于冷热分层时的 二次聚合一致性

九、小结

三句话小结

  1. postings 服务词项检索;DocValues 服务按 docID 读字段列,是 sort/agg/script 的正道。
  2. stored 服务按 doc 取行,适合 fetch/_source,不适合全索引列统计;大 _source 拖慢 fetch 与 merge。
  3. Elasticsearch 聚合在 Lucene DocValues 上建 Aggregator 树;分布式 terms 还有 global ordinals 额外成本,第 14 篇继续。

下一篇进入 ES 分布式层:Index · Shard · Replica


参考资料

规范 / 文档

  1. Apache Lucene 9.x/10.x:DocValuesSortedDocValuesNumericDocValuesStoredFields
  2. Elasticsearch 8.x Reference:Doc valuesAggregationsMappingdoc_valuesstore_source)、Field data(legacy 边界)。

综述

  1. Zobel, J. & Moffat, A., Inverted Files for Text Search Engines, ACM Computing Surveys 2006(正排/倒排分工背景)。

站内

  1. 文档模型查询执行聚合与 mapping系列目录

返回 系列目录 | 上一篇:查询执行 | 下一篇:Shard · Replica

同主题继续阅读

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


By .