
1.9 Elasticsearch-轻量聚合:terms、avg、max、min
——在“搜”完之后立刻“算”,不拖慢毫秒级响应
0 为什么需要“轻量聚合”
在日志、指标、商品、订单等典型场景中,前端往往要在返回命中结果的同时,把“分布”“极值”“均值”一并带出。如果把这些计算下推到应用层,就意味着一次搜索请求后要拉回全部明细数据——带宽、内存、GC 都会爆炸。Elasticsearch 把聚合(aggregation)做成和搜索同一套 DSL、同一个倒排索引、同一套分布式执行框架,使得“搜+算”在毫秒级完成。对 99% 的线上场景,我们不需要写脚本、不需要 MapReduce,只要记住四个最常用、最轻量的聚合入口:terms、avg、max、min。本节就围绕它们展开,给出语法、注意点、性能陷阱与调优技巧。
1 terms:分组统计,一行 DSL 出柱状图
1.1 基础语法
GET /order-2025-11/_search
{
"size": 0,
"aggs": {
"status_dist": {
"terms": {
"field": "status.keyword",
"size": 10
}
}
}
}
- size=0 表示不返回命中文档,只要聚合结果
- terms 聚合会按照 doc_count 降序返回前 10 个桶(bucket)
返回片段:
"aggregations": {
"status_dist": {
"buckets": [
{ "key": "paid", "doc_count": 284731 },
{ "key": "canceled", "doc_count": 41209 }
]
}
}
前端可直接把 key 当 X 轴、doc_count 当 Y 轴画柱状图。
1.2 精准度陷阱:shard_size 与最小计数
terms 是分布式聚合:每个分片先本地取 Top N,再由协调节点二次归并。如果分片之间数据分布倾斜,归并结果可能漏掉真正的全局 Top N。
- 官方默认 size=10 时,shard_size=size×1.5+10,可手动调大:
"terms": {
"field": "ip",
"size": 100,
"shard_size": 1000
}
- 对绝对精准场景(如财务对账)用
composite聚合分页遍历,或把size设成2147483647(全量)并打开execution_hint:map,但会牺牲内存。
1.3 排序与指标内嵌
terms 桶内部还能再挂子聚合,实现“分组后算均值/最大/最小”:
"aggs": {
"status_dist": {
"terms": {
"field": "status.keyword",
"order": { "avg_amount": "desc" }
},
"aggs": {
"avg_amount": { "avg": { "field": "amount" } }
}
}
}
协调节点会先生成桶,再按子聚合结果重排桶顺序,同样走内存堆。
2 avg:均值聚合,一行搞定
"aggs": {
"avg_latency": {
"avg": {
"field": "latency"
}
}
}
- 支持
missing参数指定缺省值,避免空桶被忽略:
"avg": {
"field": "score",
"missing": 0
}
- 对
float、scaled_float、integer都有效;若字段是keyword会直接抛异常。 - 内部使用
double累加,极端精度场景(金融分厘)需自行写script用BigDecimal,但性能掉 5×。
3 max / min:极值聚合,秒级出结果
"aggs": {
"max_price": { "max": { "field": "price" } },
"min_price": { "min": { "field": "price" } }
}
- 极值聚合走 BKD 树(数值、日期、IP 字段)或 全局序数(keyword),时间复杂度 O(log n),几乎不受数据量级影响。
- 对日期字段还能直接拿原始字符串:
"max": { "field": "@timestamp" }
返回 "value_as_string": "2025-11-01T12:00:00.000Z",前端可直接渲染。
4 轻量 ≠ 免费:内存与并发控制
-
fielddata 禁区
terms 默认用 global ordinals 在 segment 级别做字典压缩,如果字段是text且没开keyword多字段,ES 会尝试加载 fielddata,直接把倒排拉成正排,老年代瞬间暴涨。解决:- mapping 里一律保留
keyword子字段; - 对
text做聚合前先用sub-field:
"terms": { "field": "title.keyword" } - mapping 里一律保留
-
桶爆炸
高基数字段(如 user_id、ip、trace_id)做 terms 聚合,桶数量 = 唯一值数量,协调节点需要把全部分片桶回传,内存占用 = 桶数 × 32 byte 左右。- 打开
execution_hint:map把聚合下推到数据节点,减少序列化开销; - 用
composite分页,每次 1 万桶流式吐出; - 实在需要全量,用 transform 先预聚合到中间索引。
- 打开
-
并发限流
高并发 dashboard 同时拉十几种聚合,容易把数据节点 CPU 打满。- 开启 search.max_buckets(默认 65536)与 search.allow_expensive_queries;
- 对只读集群打开 adaptive_selection,让协调节点把请求优先发往负载低的分片副本。
5 实战:一条 DSL 同时拿“总数、均值、最大、最小、分布”
GET /nginx-access-2025-11-01/_search
{
"size": 0,
"query": { "range": { "@timestamp": { "gte": "now-1h" } } },
"aggs": {
"total": { "value_count": { "field": "bytes" } },
"avg_bytes": { "avg": { "field": "bytes" } },
"max_bytes": { "max": { "field": "bytes" } },
"min_bytes": { "min": { "field": "bytes" } },
"status_code_dist": {
"terms": { "field": "status", "size": 10 }
}
}
}
一次请求返回:
- 过去 1 小时总请求数
- 平均、最大、最小响应字节
- 状态码分布
前端 Grafana 可以直接映射到 SingleStat 与 PieChart,无需二次加工。
6 小结
terms、avg、max、min 四个聚合覆盖了 80% 的线上统计需求:
- terms 负责“分组”,注意 shard_size 与桶爆炸;
- avg/max/min 负责“数值”,依赖 BKD 树,毫秒级;
- 一律用 keyword 子字段,禁用 fielddata;
- 高基数用 composite 或 transform 兜底;
- 一条 DSL 可以把“搜索+多维指标”一次性带回,真正做到“轻量”。
掌握这四个入口,你就拥有了在 Elasticsearch 里“搜完立刻算”的最低成本方案。下一节,我们将在此基础上引入 percentiles、stats、extended_stats,让指标统计再上一个维度。
更多技术文章见公众号: 大城市小农民
1211

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



