
4.1 Elasticsearch-桶 + 指标 + 管道 聚合三位一体模型
在 ES5.x 之后,官方把“聚合(Aggregation)”正式拆成三条主线:Bucket、Metric、Pipeline。
这三者不是简单的“分类”,而是可组合、可嵌套、可级联的“三位一体”执行模型:
Bucket 负责“分堆”,Metric 负责“量堆”,Pipeline 负责“再算一遍堆与堆之间的关系”。
理解它们的执行顺序、内存边界和序列化规则,是写出高性能聚合语句的第一步。
- 三条主线的职责与语法骨架
1.1 Bucket(桶)
关键词:terms / range / date_histogram / filters / geohash_grid …
职责:把文档集合按某个维度切成若干子集,每个子集就是一个桶。
返回格式:
"buckets" : [
{ "key" : "iphone", "doc_count" : 1500 },
{ "key" : "samsung", "doc_count" : 1200 }
]
桶本身只带 key 与 doc_count,其余字段由子聚合填充。
1.2 Metric(指标)
关键词:sum / avg / max / min / value_count / stats / extended_stats / cardinality / percentiles …
职责:对“落在当前桶内的文档”做数值统计,返回标量或一组标量。
位置:必须挂在某个 Bucket 或 Pipeline 之下,不能顶层单独出现。
示例:
"aggs": {
"tm": {
"terms": { "field": "brand" },
"aggs": {
"total_sales": { "sum": { "field": "qty" } }
}
}
}
1.3 Pipeline(管道)
关键词:bucket_script / bucket_selector / moving_avg / derivative / cumulative_sum / avg_bucket / max_bucket …
职责:对“兄弟或父级聚合已经算出来的数值结果”做二次加工,不再接触原始文档。
执行阶段:Coordinator 节点,在 Shard 返回最终结果之后、把结果发往客户端之前。
典型场景:
- 计算同比:
derivative对date_histogram的sum做差分; - 过滤噪音:
bucket_selector把doc_count<10的桶整行删掉; - 归一化:
bucket_script把销售额除以总销售额得到占比。
- 执行顺序与内存边界
ES 的聚合请求在 Coordinator 节点拆成两阶段:
阶段 A:Shard 级
每个 Shard 独立建桶、跑 Metric,只把“局部桶”序列化后返回给 Coordinator。
内存消耗 ≈ 桶数 × 每桶子聚合数 × 预估精度(例如 terms 的 shard_size)。
此阶段 Pipeline 不参与。
阶段 B:Coordinator 级
- 把各 Shard 的局部桶做归并(reduce),得到全局桶;
- 对全局桶跑 Pipeline 聚合;
- 把最终桶树序列化为 JSON。
Pipeline 之所以“不回流 Shard”,是因为它只依赖“已经算好的数字”,不需要再访问倒排索引或正排数据。
这也解释了为什么 bucket_script 里只能引用兄弟 Metric 的路径,而不能写 "field": "qty"——脚本运行的时候早就没有 _source 了。
- 路径语法与作用域
Pipeline 通过 buckets_path 引用上游结果,语法像 Unix 文件路径:
"buckets_path": {
"sales": "total_sales", // 同级 Metric
"cost": "parent>cost_sum", // 父级聚合下的 Metric
"last_day": "_count" // 关键字,等价 doc_count
}
作用域规则:
- 只能引用“同一桶树分支”上已经出现过的 Metric;
- 不能跨兄弟分支(除非用
sibling类型 Pipeline); - 不能引用嵌套更深层的 Metric,除非用
>显式指明路径。
- 三位一体组合套路
套路 1:桶 + 指标
最常用,先分桶再统计。
GET order/_search
{
"size": 0,
"aggs": {
"g": {
"date_histogram": {
"field": "order_date",
"calendar_interval": "1d"
},
"aggs": {
"revenue": { "sum": { "field": "amount" } }
}
}
}
}
套路 2:桶 + 指标 + Pipeline(同级加工)
把每日营收再做一次 7 天移动平均,平滑曲线。
"aggs": {
"g": {
"date_histogram": { "field": "order_date", "calendar_interval": "1d" },
"aggs": {
"revenue": { "sum": { "field": "amount" } },
"smooth": {
"moving_avg": { "buckets_path": "revenue", "window": 7 }
}
}
}
}
套路 3:桶 + 指标 + Pipeline(桶过滤)
只保留“销售额占比超过 5%”的品牌桶,其余丢弃。
"aggs": {
"brands": {
"terms": { "field": "brand", "size": 100 },
"aggs": {
"sales": { "sum": { "field": "amount" } },
"ratio_check": {
"bucket_script": {
"buckets_path": { "s": "sales", "total": "_parent._total_sales" },
"script": "params.s / params.total"
}
}
}
},
"cut": {
"bucket_selector": {
"buckets_path": { "r": "brands>ratio_check" },
"script": "params.r >= 0.05"
}
}
}
- 常见踩坑清单
terms聚合默认返回前 10 桶,Pipeline 作用域也只在这 10 桶内;
如果做占比,记得把size调大或改用composite分页,否则分母失真。bucket_script的返回类型必须是数字,不能是字符串或布尔,
否则在 7.x 直接抛ClassCastException。moving_avg在 8.x 被标记为 deprecated,推荐改用moving_fn;
后者支持 Painless 脚本,窗口边界更灵活。- Pipeline 运行在 Coordinator,单节点内存即上限;
如果date_histogram按秒级切分且跨度一年,桶数 > 3 000 万,
很容易把 Coordinating 节点 OOM,务必先做calendar_interval降采样。 bucket_selector只能“整桶删除”,不能“只删字段”;
如果想隐藏某个 Metric 值,用bucket_script返回null即可。
- 小结
Bucket、Metric、Pipeline 三者不是扁平的“功能列表”,而是严格分层、顺序固定的执行链:
Shard 先桶+指标 → Coordinator 归并 → Coordinator 再跑 Pipeline。
只要抓住“路径语法”和“作用域”两条线,就能把任意复杂的业务报表拆成
“先分堆、再量堆、再算堆间关系”的三板斧,既节省 Shard CPU,也避免 Coordinating 节点成为内存漏斗。
更多技术文章见公众号: 大城市小农民
1427

被折叠的 条评论
为什么被折叠?



