日志平台把 JSON 原样打进 Elasticsearch,一周后发现
索引打不开、cluster_block_exception
提示 mapping 字段数触顶——根因往往不是聚合算不动,而是
动态 mapping
为每个未见过的键创建了字段,cluster state 与
fielddata/doc values 元数据
一起膨胀。运维熟悉 terms
聚合,却未必清楚:terms 桶在分片上扫的是 DocValues
列,不是 postings 倒排(第 10
篇);也不清楚协调节点在 第 13 篇
reduce 阶段如何把 各分片局部桶 合成
全局桶。
本文锚定 Elasticsearch 8.x Reference(Aggregations、Mapping、Dynamic mapping、Circuit breaker),描述 doc values 聚合管线、mapping 类型与 multi-field 约束,并以 dynamic mapping 爆炸 作为典型失效模式,为 第 17 篇 排障 预埋机制链。
本文是「全文检索引擎」系列第 14 篇(共 18 篇)。→ 系列目录
版本锚定:Elasticsearch 8.x;默认
index.mapping.total_fields.limit为 1000(可调,非无限)。不写伪造的聚合 QPS 数字。
一、为何聚合走 DocValues 而非倒排
| 访问模式 | 数据结构 | 典型 API |
|---|---|---|
| 「哪些文档含词项 \(t\)」 | 倒排 postings | match、term 查询 |
| 「某字段所有取值的分布」 | 正排 DocValues | terms、date_histogram、avg |
| 「取原文展示」 | stored / _source |
fetch phase |
倒排按 词项 索引,适合
文档集合过滤;聚合要对
每个文档的字段值 做分组或数值归约,扫
列式 DocValues 的缓存局部性更好,且与
Lucene
DocValuesType(SORTED_NUMERIC、SORTED_SET
等)一致(Lucene 9.x Reference DocValues)。
flowchart LR
Q["Query filter<br/>optional bitmap"]
DV["Per-segment DocValues<br/>column scan"]
LB["Shard-local buckets"]
RD["Coordinating reduce<br/>merge buckets"]
Q --> DV
DV --> LB
LB --> RD
查询 + 聚合 时,常见执行是:先用查询
BitSet 过滤 doc id 集合,再在过滤后的 doc
上 读 DocValues
建桶——不是「先聚合再过滤」的魔法顺序。post_filter
与 filter aggregation 的语义差异见 Reference
Aggregations — Filtering
aggregations。
二、分片局部聚合与协调 reduce
与搜索 query phase 类似,聚合在 每个分片 上独立产生 局部结构:
terms:分片本地Map<key, count>(及子聚合树)。date_histogram:分片本地按时间桶计数。percentiles/percentile_ranks:分片本地 TDigest 或正态近似 sketch(合并时 再合并 sketch,非简单相加)。
协调节点 reduce 合并:
| 聚合 | 分片输出 | Reduce |
|---|---|---|
sum / min /
max |
标量 | 算术合并 |
terms |
桶 + doc_count | 同键桶 相加;shard_size
影响长尾键是否丢失 |
cardinality |
HyperLogLog++ | 合并 HLL 寄存器 |
percentiles |
TDigest | 合并 centroids |
sequenceDiagram
participant C as Coordinating node
participant S1 as Shard 1
participant S2 as Shard 2
C->>S1: search + aggs
C->>S2: search + aggs
S1-->>C: buckets {A:10, B:5}
S2-->>C: buckets {A:7, C:3}
Note over C: reduce terms A=17, B=5, C=3
terms
精度:size
控制返回桶数;shard_size 控制每分片
候选桶 数。分片只上传 Top
shard_size 时, 全局低频键
可能在 reduce 前被截断——这是
分布式聚合的已知近似,不是单分片 bug。提高
shard_size 换精度,付
内存与网络(第 17
篇 慢聚合排障点)。
三、Mapping:类型、doc_values 与 multi-field
Mapping 定义字段 如何索引、如何聚合、如何存储(Reference Mapping):
PUT /products
{
"mappings": {
"properties": {
"title": {
"type": "text",
"fields": {
"keyword": { "type": "keyword" }
}
},
"price": { "type": "scaled_float", "scaling_factor": 100 },
"created_at": { "type": "date" }
}
}
}text:分词,建倒排;默认 不 用于terms(需fielddata=true走堆 fielddata,8.x 生产 不推荐)。keyword:不分词,DocValues 友好;排序、聚合、精确过滤 默认走 keyword。- multi-field:同一 JSON 键
title同时有title(text)与title.keyword(keyword)——RAG / 日志里 聚合用.keyword是常见惯例。
| 需求 | 应选类型 | 避免 |
|---|---|---|
| 全文检索 | text + analyzer |
对 text 直接 terms |
| 聚合 / 排序 | keyword、数值、date |
开启 text fielddata |
| 向量 kNN | dense_vector |
与 第 15 篇 混谈 |
doc_values:大多数字段默认开启(index: false
的 stored 字段例外路径见 Reference)。关闭
doc_values 会 禁用
该字段聚合与排序——与 第 10
篇 正交开关一致。
四、Dynamic mapping 与字段爆炸失效模式
Dynamic mapping(默认
true)在见到新 JSON 键时
推断类型 并写入 mapping(Reference
Dynamic mapping)。对 半结构化日志
友好;对 高基数、键名漂移 的 payload
危险:
{ "user": { "id": 1, "trace": "abc" } }
{ "user": { "session": "xyz", "feature_flag_42": true } }若 user 下子键不可预测,动态模板可能为
每个叶子键 创建字段 →
user.id、user.session、user.feature_flag_42…
数量随时间 线性累积。
4.1 触顶机制
index.mapping.total_fields.limit(默认 1000):单索引 mapping 字段数上限,触顶 拒绝新字段 或阻塞写入(视版本与设置)。index.mapping.depth.limit、index.mapping.nested_fields.limit:防深层嵌套滥用。- Cluster state:每索引 mapping 是 state 一部分;字段爆炸 同时 伤害 第 11 篇 的 CSU 与磁盘上的 DocValues / 倒排元数据。
4.2 典型症状链
flowchart TD
DM["Dynamic mapping on"]
JSON["Unbounded JSON keys"]
MF["Mapping field count ↑"]
CS["Cluster state bloat"]
CB["Circuit breaker / indexing block"]
DM --> JSON --> MF --> CS --> CB
| 阶段 | 可观测信号 | 机制 |
|---|---|---|
| 早期 | 单索引 mapping 数百字段 | 动态推断 + 嵌套对象 |
| 中期 | master CPU、CSU 变慢 | 大 mapping diff 广播 |
| 晚期 | total_fields.limit、breaker 触发 |
拒绝索引;查询仍可能拖垮 heap |
architecture/42 强调 分片与索引数 规划;本篇补 单索引 mapping 维度——二者可同时发生(「每天一新索引 + 每索引千字段」)。
4.3 工程缓解(机制层,非 cookbook)
dynamic: false或strict:未知字段丢弃或拒绝,换 显式 schema。flattened类型:整对象压成 单列 键值,牺牲部分查询精度换 字段数有界(Reference Flattened)。runtime字段:查询时计算,不增加索引字段(适合轻量派生,非万能)。- ingest pipeline + rename:在写入侧把漂移键 规范化 到固定字段集。
- 日志栈若只需检索不需复杂聚合,对照 observability/08 日志选型 评估 CH / Loki。
五、数值、日期与 nested 边界
- 数值聚合(
sum、avg、stats)读numericDocValues;scaled_float用固定缩放因子省空间。 date_histogram依赖date或date_nanosDocValues;时区在协调层统一。nested:子文档 独立 DocValues,nested聚合包装子查询,代价高于 flat keyword——深层 nested 与 第 11 篇 分片扇出叠加,是 第 17 篇 重点。
脚本聚合(scripted_metric)可绕开
DocValues,但在 8.x 受 沙箱与 breaker
约束;默认路径仍是 native 聚合 +
DocValues。
六、学术谱系、争论与开放问题
谱系:列式分析(MonetDB / C-store 谱系)→ Lucene DocValues 正排 → ES Search Aggregations 作为分布式 OLAP 轻量子集 → ELK 推动日志 + 聚合一体化 → 可观测专用列存(ClickHouse)在 高基数字段 上反驳「一切皆 ES」(observability/08)。
争论:
- Schema-on-read(动态 mapping)vs Schema-on-write:动态映射降低接入摩擦;字段爆炸证明 无 schema 的代价转移到 cluster state 与 heap——与数据湖 debate 同构(lakehouse 表格式 用元数据约束换可读性)。
textfielddata:历史版本曾用 fielddata 聚合 analyzed 字段;现代默认 禁止 该路径,强制 multi-field——争论已收敛,但老教程仍误导。
开放问题:
- 合并式聚合状态(partial reduce on data
node)在 8.x 仍有限;超大
terms的 全局精确 与 coordination 内存 权衡,缺少与 ClickHouseGROUP BY同硬件的公开对等实验(本篇不编造排名)。 - ES|QL / EQL 与经典 DSL 聚合是否统一 reduce 路径——版本演进快,读者应以 8.x Release Notes 为准。
七、常见误解
| 误解 | 事实 |
|---|---|
「terms 走倒排」 |
默认扫 DocValues;text 需
.keyword 子字段 |
| 「聚合只在协调节点算」 | 分片局部算,协调 reduce |
| 「动态 mapping 只会多占磁盘」 | 先伤 mapping 元数据与 cluster state |
「调高 total_fields.limit 就行」 |
换 更晚触顶,不解决 无界键空间 |
「关闭 _source 就不能聚合」 |
聚合靠 DocValues,与
_source 无关 |
| 「nested 只是 JSON 好看」 | 每个 nested 路径增加 独立正排列 与查询包装成本 |
八、小结
三句话小结:
- Terms / histogram / metric 聚合在分片上读 DocValues 列,可选查询 BitSet 过滤后建 局部桶,协调节点 reduce 合并。
- Mapping
决定字段能否聚合:
text用 multi-field 的 keyword;动态 mapping 对无界 JSON 键会 字段爆炸,触顶total_fields.limit与 breaker。 - 缓解方向是 有界
schema(
dynamic: false、flattened、pipeline 规范化),而非单纯加机器;日志高基数场景应对照 observability/08。
下一篇讨论 BM25 与 dense_vector 混合检索 的接口边界:混合检索。
参考资料
规范(A 级)
- Elasticsearch 8.x Reference,
Aggregations(执行模型、
shard_size、termsprecision)。 - Elasticsearch 8.x Reference, Mapping、Dynamic mapping、Field data types。
- Elasticsearch 8.x Reference, Flattened、Runtime fields。
- Elasticsearch 8.x Reference, Circuit
breaker、
index.mapping.total_fields.limit。 - Apache Lucene 9.x, DocValues(与 第 10 篇 一致)。
论文与综述(A 级)
- Abadi, D. et al., Column-Stores vs. Row-Stores(C-Store / 列式分析谱系,解释 DocValues 聚合路径)。
站内对读(B 级)
- 搜索引擎架构(索引与分片规划)。
- observability/08 日志选型(高基数日志替代栈)。
- 本系列 第 10 篇、第 13 篇、第 17 篇。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【全文检索引擎】DocValues 与 stored fields:排序聚合为何不走倒排
说明 Lucene DocValues 的列式访问模型如何服务排序、聚合与脚本,stored fields 的随机取原文代价,以及 Elasticsearch 聚合管线对 DocValues 的依赖——为第 14 篇预埋。
【全文检索引擎】refresh · flush · translog:近实时与持久化边界
用保证矩阵拆解 Elasticsearch 8.x 的 refresh、flush、translog 各自承诺什么:可搜窗口、磁盘持久化、崩溃恢复;对照 refresh_interval 与 wait_for,并在单节点 8.15 上实测写入可见性。
【全文检索引擎】生产排障:breaker、refresh、分片、慢查询
用症状到机制的决策树覆盖 circuit breaker、refresh 过频、大字段、分片过多与慢查询 explain,逐条回链本系列 Lucene 段与 ES NRT 篇章;不含未跑集群 benchmark。
【全文检索引擎】全文引擎全景:架构叙事与 Lucene 内核之间的一层
定位 Lucene 9.x/10.x 库内核与 Elasticsearch 8.x 服务层相对 architecture/42、storage/29、向量引擎与 PG GIN 的分工;以最小索引故事建立坐标系,交代倒排索引学术谱系与 18 篇路线图。