Skip to content
第 9 章 ⏱ 13 分钟阅读

第 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());
});

八、本章小结 ​

要点关键
Metricavg / sum / max / min / stats / cardinality
Bucketterms / range / date_histogram
嵌套在 terms 内嵌 avg、top_hits
Pipeline对已有聚合再聚合
size: 0聚合时不需要原始 hit
shard_size提高 terms 精度
text 聚合不支持,用 keyword

动手练习 ​

  1. 按 category 分组,统计每个分类的文章数和平均 views
  2. 按天统计最近 7 天的文章数,绘制时间序列
  3. 用嵌套聚合 + top_hits,返回每分类下阅读量 Top 3

下一章:第 10 章:MongoDB 入门 →

本站基于 VitePress 构建 · 由 StackHub 团队维护