第 9 章:ES 聚合
学习目标
- 掌握 metric、bucket、pipeline 三大聚合
- 实现统计、TopN、分组
- 用 Java 完成嵌套聚合
- 避开聚合的常见坑
一、聚合概述
聚合 = SQL 里的 GROUP BY + 聚合函数(COUNT/SUM/AVG),但 ES 比 SQL 更灵活。
两大类:
- Metric 聚合:求单个值(sum、avg、max、min、count、stats)
- Bucket 聚合:分组(terms、range、date_histogram)
二、Metric 聚合
2.1 简单统计
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0, # ② 不要原始 hit,只看聚合结果(省带宽)
"aggs": { # ③ 聚合体(可以多个)
"avg_views": { "avg": { "field": "views" } }, # 浏览数平均值
"max_views": { "max": { "field": "views" } }, # 浏览数最大值
"sum_revenue": { "sum": { "field": "price" } }, # 价格总和
"stats_views": { "stats": { "field": "views" } } # 浏览数五项统计(一次拿全)
}
}stats 一次返回 count / min / max / avg / sum 五个值。
⚠️ 坑 1:
text字段不能聚合,要改成keyword或开fielddata(不推荐)。
2.2 cardinality(去重计数)
bash
"aggs": {
"unique_users": { "cardinality": { "field": "userId" } }
}底层是 HyperLogLog,结果有约 0.4% 误差,但极快。
三、Bucket 聚合
3.1 terms(分组)
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0, # ② 不要原始 hit
"aggs": { # ③ 聚合体
"by_category": { # ④ 按 category 分组(Bucket 聚合)
"terms": { # terms = 分组聚合
"field": "category", # 按 category 字段分组
"size": 10, # 只取前 10 个分组
"order": { "avg_views": "desc" } # 按下面定义的 avg_views 降序排
},
"aggs": { # ⑤ 子聚合(每个分组内部再聚合)
"avg_views": { "avg": { "field": "views" } } # 每个 category 内算浏览数平均值
}
}
}
}返回:
json
{
"aggregations": {
"by_category": {
"buckets": [
{ "key": "tech", "doc_count": 300, "avg_views": { "value": 2500 } },
{ "key": "life", "doc_count": 150, "avg_views": { "value": 800 } },
{ "key": "finance", "doc_count": 100, "avg_views": { "value": 500 } }
]
}
}
}效果:按 category 分组,每组算平均阅读量,按平均阅读量降序。
3.2 range(数值区间)
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0, # ② 不要原始 hit
"aggs": { # ③ 聚合体
"price_ranges": { # ④ 自定义聚合名
"range": { # ⑤ range = 区间聚合(按数值分桶)
"field": "price", # 按 price 字段分
"ranges": [ # ⑥ 自定义分桶区间
{ "to": 50 }, # 桶1:价格 < 50
{ "from": 50, "to": 200 }, # 桶2:50 ≤ 价格 < 200
{ "from": 200 } # 桶3:价格 ≥ 200
]
}
}
}
}返回:
json
{
"aggregations": {
"price_ranges": {
"buckets": [
{ "key": "*-50.0", "to": 50, "doc_count": 120 }, // 50 以下 120 篇
{ "key": "50.0-200.0", "from": 50, "to": 200, "doc_count": 350 }, // 50-200 350 篇
{ "key": "200.0-*", "from": 200, "doc_count": 80 } // 200 以上 80 篇
]
}
}
}业务场景:商品按价格档统计数量(便宜 / 中等 / 贵),画柱状图 / 饼图直接用。
3.3 date_histogram(时间序列)
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0,
"aggs": {
"by_day": { # ② 自定义聚合名
"date_histogram": { # ③ date_histogram = 按时间分桶
"field": "createTime", # 按 createTime 字段分
"calendar_interval": "day", # ④ 间隔单位:天(自然日)
"format": "yyyy-MM-dd" # ⑤ bucket key 的日期格式
}
}
}
}返回:
json
{
"aggregations": {
"by_day": {
"buckets": [
{ "key_as_string": "2026-09-09", "doc_count": 120 },
{ "key_as_string": "2026-09-10", "doc_count": 180 },
{ "key_as_string": "2026-09-11", "doc_count": 200 }
]
}
}
}业务场景:每日新增文章数、订单量、访问量趋势图。
⚠️ 坑 2:
calendar_interval用于自然月/日,fixed_interval用于固定时长(如30m)。混用会出错。
四、嵌套聚合(实战)
需求:每个分类下的文章数 + 平均阅读量 + 阅读量最高的 3 篇。
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0, # ② 不要原始 hit
"aggs": {
"by_category": { # ③ 一级聚合:按 category 分组
"terms": { "field": "category", "size": 5 }, # 只取前 5 个分类
"aggs": { # ④ 二级聚合(每个分类内部再聚合)
"avg_views": { "avg": { "field": "views" } }, # ⑤ 平均阅读量
"top_articles": { # ⑥ 顶级文章聚合
"top_hits": { # top_hits = 取原始文档
"size": 3, # 每组取前 3 篇
"sort": [ { "views": "desc" } ], # 按浏览数降序
"_source": [ "title", "views" ] # 只返回 title 和 views 字段
}
}
}
}
}
}返回:
json
{
"aggregations": {
"by_category": {
"buckets": [
{
"key": "tech", "doc_count": 300,
"avg_views": { "value": 2500 },
"top_articles": {
"hits": {
"hits": [
{ "_source": { "title": "Spring 实战", "views": 9800 } },
{ "_source": { "title": "Redis 入门", "views": 8500 } },
{ "_source": { "title": "MySQL 优化", "views": 7200 } }
]
}
}
}
]
}
}
}top_hits 是个特殊聚合,可以在桶内取原始文档。
五、Pipeline 聚合(对聚合再聚合)
需求:按天统计文章数,再算累计数。
bash
GET /articles/_search # ① 查 articles 索引
{
"size": 0,
"aggs": { # ② 聚合体
"by_day": { # ③ 一级聚合:按天分桶
"date_histogram": { "field": "createTime", "calendar_interval": "day" },
"aggs": { # ④ 二级聚合:每天文章数
"daily_count": { "value_count": { "field": "_id" } } # value_count = 计数
}
},
"cumulative": { # ⑤ Pipeline 聚合(对上面结果再算)
"cumulative_sum": { # 累计求和
"buckets_path": "by_day>daily_count" # 路径:由_day 的 daily_count
} # (语法:父聚合>子聚合)
}
}
}返回:
json
{
"aggregations": {
"by_day": {
"buckets": [
{ "key_as_string": "2026-09-09", "daily_count": { "value": 100 } },
{ "key_as_string": "2026-09-10", "daily_count": { "value": 150 } },
{ "key_as_string": "2026-09-11", "daily_count": { "value": 200 } }
]
},
"cumulative": {
"buckets": [
{ "key_as_string": "2026-09-09", "cumulative_sum": { "value": 100 } }, // 100
{ "key_as_string": "2026-09-10", "cumulative_sum": { "value": 250 } }, // 100 + 150
{ "key_as_string": "2026-09-11", "cumulative_sum": { "value": 450 } } // 100 + 150 + 200
]
}
}
}业务场景:累计用户增长、累计销售额、累计订单数——常见于"日活 / 月活"、"GMV 累计"报表。
六、聚合精度
6.1 shard_size
bash
"terms": {
"field": "category",
"size": 10,
"shard_size": 50 # 每个 shard 取 50,合并出 top10,更准
}⚠️ 坑 3:
terms默认每个分片返回 top 10,合并后可能漏掉边缘值。shard_size设大点提高精度,代价是慢。
6.2 近似 vs 精确
cardinality/percentiles是近似(快)- 真正精确 → 全量查 + 程序聚合(慢)
- 业务选近似即可(99% 场景)
七、Java 实战
java
NativeQuery query = NativeQuery.builder()
.withAggregation("by_category",
Aggregation.of(a -> a.terms(t -> t.field("category").size(10))
.aggregations("avg_views", Aggregation.of(x -> x.avg(v -> v.field("views"))))))
.withMaxResults(0)
.build();
SearchHits<Article> hits = elasticsearchOperations.search(query, Article.class);
ElasticsearchAggregations aggs = (ElasticsearchAggregations) hits.getAggregations();
StringTerms byCategory = aggs.get("by_category");
byCategory.getBuckets().forEach(b -> {
System.out.println(b.getKeyAsString() + ":" + b.getDocCount());
Avg avgViews = b.getAggregations().get("avg_views");
System.out.println(" avg = " + avgViews.value());
});八、本章小结
| 要点 | 关键 |
|---|---|
| Metric | avg / sum / max / min / stats / cardinality |
| Bucket | terms / range / date_histogram |
| 嵌套 | 在 terms 内嵌 avg、top_hits |
| Pipeline | 对已有聚合再聚合 |
size: 0 | 聚合时不需要原始 hit |
shard_size | 提高 terms 精度 |
| text 聚合 | 不支持,用 keyword |
动手练习
- 按
category分组,统计每个分类的文章数和平均views - 按天统计最近 7 天的文章数,绘制时间序列
- 用嵌套聚合 +
top_hits,返回每分类下阅读量 Top 3
下一章:第 10 章:MongoDB 入门 →