ES 聚合爽到飞起!从分桶到 Java 实操,再也不用翻烂文档

做后端查 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. 关键注意点(避坑指南)

  1. 字段类型:分桶用 keyword,度量用数值型

text 类型字段会分词,不能直接分桶,必须加.keyword;度量字段得是 int、double 这类数值型,不然算不了。

  1. size=0:只要聚合结果

不加的话,ES 会默认返回 10 条原始数据,完全没用还浪费性能。

  1. 客户端版本:和 ES 集群一致

版本不匹配会报各种奇怪错误,比如 7.x 客户端连 6.x 集群会报错。

总结:ES 聚合就这三步,学会直接上手

  1. 定桶规则:用 Bucket 把数据分成你要的组(按品牌、按省份、按时间);
  1. 加度量指标:用 Metric 算每个桶的关键数据(求和、平均、最大);
  1. 二次加工(可选) :用 Pipeline 对聚合结果再分析(找最大的平均价、算所有桶的总和)。

Java 代码照着上面抄,再结合自己的业务改改字段名,基本就能搞定 90% 的 ES 聚合场景!

家人们,你们平时用 ES 聚合有没有踩过坑?比如分桶分不对、度量结果不准?或者还有啥想看的 ES 知识点?评论区聊聊,下次咱接着唠!

相关推荐
小高0074 小时前
🚨 2025 最该淘汰的 10 个前端 API!
前端·javascript·面试
Elasticsearch4 小时前
Elastic Observability 中 Discover 的跟踪,用于深入的应用洞察
elasticsearch
大厂码农老A4 小时前
面试官:“聊聊你最复杂的项目?” 为什么90%的候选人第一句就栽了?
java·面试
爱读源码的大都督4 小时前
Java已死?别慌,看我如何用Java手写一个Qwen Code Agent,拯救Java
java·人工智能·后端
黑客飓风5 小时前
从基础功能到自主决策, Agent 开发进阶路怎么走?
面试·log4j·bug
星辰大海的精灵5 小时前
SpringBoot与Quartz整合,实现订单自动取消功能
java·后端·算法
天天摸鱼的java工程师5 小时前
RestTemplate 如何优化连接池?—— 八年 Java 开发的踩坑与优化指南
java·后端
在未来等你5 小时前
Kafka面试精讲 Day 7:消息序列化与压缩策略
大数据·分布式·面试·kafka·消息队列
一乐小哥5 小时前
一口气同步10年豆瓣记录———豆瓣书影音同步 Notion分享 🚀
后端·python