做后端查 ES 数据,光捞出几条记录有啥用?领导要 "近 7 天各省份用户消费 TOP3""每个商品分类的平均客单价",总不能手动算吧?这时候 ES 聚合就像救兵,分分钟把杂乱数据拧成你要的报表 ------ 今天咱就把这玩意儿从 "懵圈知识点" 变成 "抄作业技能"!
一、先唠明白:啥是 ES 聚合?------ 数据的 "自动统计员"
你可以把 ES 聚合理解成 "数据加工厂": raw 数据进去,经过 "分类 - 计算 - 汇总" 三道工序,直接输出你要的统计结果。比如:
- 电商场景:按 "商品分类" 分堆,算每堆的 "销量总和""均价"
- 日志场景:按 "错误级别" 分组,统计每组的 "出现次数""最早发生时间"

简单说,聚合就是让 ES 帮你干 "人工算表" 的活,还比你快 100 倍!
二、ES 聚合分 3 类?------ 桶、度量、管道各干各的活
别被 "聚合分类" 吓住,其实就是三个分工明确的小工具,用仓库管货类比一下就懂:
聚合类型 | 作用 | 仓库版类比 |
---|---|---|
Bucket(桶) | 给数据 "分圈子",同属性的进一个桶 | 按 "家电""服装""食品" 给货物分区放 |
Metric(度量) | 给每个桶 "算指标",比如求和、平均值 | 查 "家电区有多少件货""服装区平均单价" |
Pipeline(管道) | 对 "聚合结果" 再聚合,二次加工 | 算 "所有分区的平均货量"(基于前面算的各分区货量) |
记住:先有桶,再有度量,管道是 "聚合的聚合" ,逻辑链条超清晰!
三、Bucket 聚合:给数据 "分圈子",同频的进一个桶
Bucket 聚合的核心是 "分组"------ 你定个规则,ES 就把数据往不同桶里扔,桶里装的都是符合规则的 data。
举个栗子:给手机数据按品牌分桶
假设你有个phone_index索引,存了手机的brand(品牌)、price(价格),想按品牌分组看数据:
json
// DSL请求:按brand分桶,桶名叫"brand_group"
GET /phone_index/_search
{
"size": 0, // 关键!只要聚合结果,不要原始数据
"aggs": {
"brand_group": { // 桶的名字,自定义
"terms": { // 最常用的分桶类型:按字段值分组
"field": "brand.keyword", // 注意:text类型字段要加.keyword
"size": 10 // 返回前10个桶
}
}
}
}
返回结果会像这样,每个桶就是一个品牌,还告诉你桶里有多少条数据:
json
"aggregations": {
"brand_group": {
"buckets": [
{"key": "苹果", "doc_count": 20}, // 苹果桶有20条数据
{"key": "华为", "doc_count": 18},
{"key": "小米", "doc_count": 15}
]
}
}
小技巧:桶还能嵌套!
比如 "先按品牌分桶,再按手机内存(128G/256G)分小桶",就像 "家电区里再分冰箱区、洗衣机区",用sub_aggs嵌套就行,超灵活!
四、Metric 度量:给每个 "圈子" 做体检,关键指标全拿捏
分好桶了,总不能只知道 "桶里有多少条数据" 吧?Metric 就是帮你算桶里数据的 "关键指标",常用的就 4 种,记牢不踩坑:
度量类型 | 作用 | 栗子 |
---|---|---|
sum(求和) | 算桶里字段的总和 | 苹果桶所有手机的 "总销量" |
avg(平均值) | 算字段的平均值 | 华为桶手机的 "平均价格" |
max/min(最大 / 最小) | 找字段的极值 | 小米桶里 "最便宜的手机价格" |
cardinality(去重计数) | 算字段不重复的数量 | 苹果桶里 "有多少种不同型号" |
实操:分桶 + 度量一起搞
还是手机数据,这次按品牌分桶后,算每个品牌的 "平均价格" 和 "最高价格":
json
GET /phone_index/_search
{
"size": 0,
"aggs": {
"brand_group": { // 先分桶
"terms": {"field": "brand.keyword"},
"aggs": { // 桶里加度量
"avg_phone_price": { // 平均价格
"avg": {"field": "price"}
},
"max_phone_price": { // 最高价格
"max": {"field": "price"}
}
}
}
}
}
返回结果里,每个品牌桶都会带度量值,直接拿给产品看都不用加工:
json
"buckets": [
{
"key": "苹果",
"doc_count": 20,
"avg_phone_price": {"value": 6999.5},
"max_phone_price": {"value": 12999}
}
]
五、管道聚合:聚合结果再 "加工",二次分析更给力
有时候,度量结果还不够!比如你算完每个品牌的平均价格后,还想知道 "所有品牌的平均价格里,最高的是多少"------ 这时候就需要管道聚合,它不碰原始数据,只 "啃" 前面的聚合结果。
举个栗子:找品牌平均价格的最大值
基于上面 "brand_group" 的聚合结果,再算一次 "avg_phone_price" 的最大值:
json
GET /phone_index/_search
{
"size": 0,
"aggs": {
"brand_group": { // 第一步:分桶+度量
"terms": {"field": "brand.keyword"},
"aggs": {"avg_phone_price": {"avg": {"field": "price"}}}
},
"max_avg_price": { // 第二步:管道聚合,基于上面的结果
"max_bucket": { // 找所有桶里某个度量的最大值
"buckets_path": "brand_group>avg_phone_price" // 路径:桶名>度量名
}
}
}
}
返回结果里,max_avg_price就是你要的 "所有品牌平均价的最大值",不用再手动对比了!
六、Java 实操 ES 聚合:代码直接抄,再也不用掉头发
咱后端 er 最终还是要写代码,这里用RestHighLevelClient(ES 官方推荐)实操,实现 "按品牌分桶 + 算平均价格",注释写得超详细,直接粘项目里改改就能用!
1. 先加依赖(Maven)
xml
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.14.0</version> <!-- 版本和你的ES一致! -->
</dependency>
2. 核心代码:聚合查询 + 结果解析
java
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.Fuzziness;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.client.RestClient;
import java.io.IOException;
public class EsAggregationDemo {
public static void main(String[] args) throws IOException {
// 1. 初始化ES客户端(单例!实际项目别每次new)
try (RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder("localhost:9200") // 你的ES地址
)) {
// 2. 构建搜索请求:指定索引
SearchRequest searchRequest = new SearchRequest("phone_index");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 3. 构建聚合:先分桶(brand),再度量(avg price)
// 3.1 桶聚合:按brand.keyword分桶,桶名"brand_bucket"
TermsAggregationBuilder brandBucket = AggregationBuilders
.terms("brand_bucket")
.field("brand.keyword") // text字段必须用keyword
.size(10); // 返回前10个桶
// 3.2 度量聚合:算每个桶的平均价格,度量名"avg_price"
AvgAggregationBuilder avgPriceAgg = AggregationBuilders
.avg("avg_price")
.field("price"); // 要计算的字段
// 3.3 把度量聚合嵌套到桶聚合里
brandBucket.subAggregation(avgPriceAgg);
// 4. 配置搜索源:只要聚合结果,不要原始数据
sourceBuilder.aggregation(brandBucket);
sourceBuilder.size(0); // 关键!不然返回大量原始数据,浪费资源
searchRequest.source(sourceBuilder);
// 5. 执行请求,拿到响应
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
// 6. 解析聚合结果(重点!别搞混层级)
// 6.1 先拿到桶聚合结果
Terms brandTerms = response.getAggregations().get("brand_bucket");
// 6.2 遍历每个桶,提取数据
for (Terms.Bucket bucket : brandTerms.getBuckets()) {
String brand = bucket.getKeyAsString(); // 桶的key:品牌名
long phoneCount = bucket.getDocCount(); // 桶里的数据量:该品牌手机数量
// 6.3 拿到桶里的度量结果
Avg avgPrice = bucket.getAggregations().get("avg_price");
double avgPriceValue = avgPrice.getValue(); // 平均价格
// 打印结果,实际项目里可以存数据库或返回给前端
System.out.printf(
"品牌:%s,手机数量:%d,平均价格:%.2f元%n",
brand, phoneCount, avgPriceValue
);
}
}
}
}
3. 关键注意点(避坑指南)
- 字段类型:分桶用 keyword,度量用数值型
text 类型字段会分词,不能直接分桶,必须加.keyword;度量字段得是 int、double 这类数值型,不然算不了。
- size=0:只要聚合结果
不加的话,ES 会默认返回 10 条原始数据,完全没用还浪费性能。
- 客户端版本:和 ES 集群一致
版本不匹配会报各种奇怪错误,比如 7.x 客户端连 6.x 集群会报错。
总结:ES 聚合就这三步,学会直接上手
- 定桶规则:用 Bucket 把数据分成你要的组(按品牌、按省份、按时间);
- 加度量指标:用 Metric 算每个桶的关键数据(求和、平均、最大);
- 二次加工(可选) :用 Pipeline 对聚合结果再分析(找最大的平均价、算所有桶的总和)。
Java 代码照着上面抄,再结合自己的业务改改字段名,基本就能搞定 90% 的 ES 聚合场景!
家人们,你们平时用 ES 聚合有没有踩过坑?比如分桶分不对、度量结果不准?或者还有啥想看的 ES 知识点?评论区聊聊,下次咱接着唠!