Elasticsearch 中的聚合(Aggregations)技术详解
本文来自于我关于 Elasticsearch 的系列文章。欢迎阅读、点评与交流~
1、Elasticsearch 深度解析:从核心原理到开发者实战
2、深入理解倒排索引(Inverted Index):搜索引擎的核心数据结构
3、Elasticsearch 中的聚合(Aggregations)技术详解
4、Elasticsearch 列式存储详解:Doc Values 的原理与实践

文章目录
- [Elasticsearch 中的聚合(Aggregations)技术详解](#Elasticsearch 中的聚合(Aggregations)技术详解)
-
- [1. 聚合概述](#1. 聚合概述)
- [2. 聚合的分类](#2. 聚合的分类)
- [3. 核心概念](#3. 核心概念)
- [4. 常用聚合详解](#4. 常用聚合详解)
-
- [4.1 指标聚合(Metric Aggregations)](#4.1 指标聚合(Metric Aggregations))
- [4.2 桶聚合(Bucket Aggregations)](#4.2 桶聚合(Bucket Aggregations))
- [4.3 管道聚合(Pipeline Aggregations)](#4.3 管道聚合(Pipeline Aggregations))
- [5. 聚合语法结构](#5. 聚合语法结构)
- [6. 执行原理与数据结构](#6. 执行原理与数据结构)
-
- [6.1 Doc Values 与 Fielddata](#6.1 Doc Values 与 Fielddata)
- [6.2 分布式执行流程](#6.2 分布式执行流程)
- [6.3 深度聚合与延迟](#6.3 深度聚合与延迟)
- [7. 性能优化与注意事项](#7. 性能优化与注意事项)
-
- [7.1 通用优化建议](#7.1 通用优化建议)
- [7.2 内存与 OOM 风险](#7.2 内存与 OOM 风险)
- [7.3 实时性与缓存](#7.3 实时性与缓存)
- [7.4 精确度与近似值](#7.4 精确度与近似值)
- [8. 典型应用场景](#8. 典型应用场景)
- [9. 示例:复杂聚合实战](#9. 示例:复杂聚合实战)
- [10. 总结](#10. 总结)
Elasticsearch 的聚合(Aggregations)是其核心功能之一,用于对索引中的数据进行 统计分析、分组、计算和转换 ,类似于 SQL 中的 GROUP BY、 COUNT、 SUM、 AVG 以及窗口函数。聚合与搜索查询并行工作,可以在同一个请求中同时返回搜索结果和统计结果,非常适合构建仪表盘、数据报表、实时分析等场景。
1. 聚合概述
- 定义:聚合是一组对文档集合进行数据提取和计算的框架,它基于查询(query)过滤后的文档集,执行各种统计分析。
- 与搜索的关系 :聚合可以独立使用(
size:0只返回聚合结果),也可以与搜索查询结合(返回命中文档的同时返回聚合)。 - 特点 :
- 实时计算(基于倒排索引和 Doc Values)
- 支持嵌套(在桶内再建桶或计算指标)
- 支持管道聚合(对聚合结果再次聚合)
- 分布式并行计算(分片级别聚合 + 协调节点合并)
2. 聚合的分类
Elasticsearch 官方将聚合分为四大类,最常用的是前三类:
| 类型 | 作用 | 常见例子 |
|---|---|---|
| 指标聚合 | 对文档的某个字段进行数学计算,返回单值或多值指标。 | avg, sum, min, max, stats, cardinality |
| 桶聚合 | 将文档划分到不同的桶(分组)中,每个桶代表一个类别或区间。 | terms, range, histogram, date_histogram, filter |
| 管道聚合 | 基于其他聚合的输出结果进行二次聚合,如计算导数、累积和、移动平均等。 | derivative, cumulative_sum, moving_avg, bucket_script |
| 矩阵聚合 | 对多个字段的值进行矩阵运算,如计算相关系数。较少用。 | matrix_stats |
3. 核心概念
- 桶(Bucket) :一组满足特定条件的文档集合。桶聚合会创建多个桶,每个桶有一个键(key)和文档列表(逻辑上)。例如按
country字段分桶,每个国家是一个桶。 - 指标(Metric):对桶内文档的数值字段进行统计计算。指标聚合可以计算平均值、总和、最大值等。
- 嵌套(Nesting):桶内可以嵌套子聚合(子桶或子指标),实现多层级分组统计。例如先按国家分桶,再在每个国家桶内按城市分桶,最后计算平均年龄。
- 元数据(Metadata):可以给聚合添加自定义元数据,便于识别。
4. 常用聚合详解
4.1 指标聚合(Metric Aggregations)
| 聚合名称 | 说明 | 示例 |
|---|---|---|
avg |
平均值 | "avg_price": { "avg": { "field": "price" } } |
sum |
总和 | "total_revenue": { "sum": { "field": "revenue" } } |
min / max |
最小值 / 最大值 | "min_date": { "min": { "field": "timestamp" } } |
stats |
一次性返回 count, min, max, avg, sum |
"price_stats": { "stats": { "field": "price" } } |
extended_stats |
在 stats 基础上增加方差、标准差、标准差范围等 |
"price_ext_stats": { "extended_stats": { "field": "price" } } |
cardinality |
去重计数(近似值,基于 HyperLogLog++) | "unique_users": { "cardinality": { "field": "user_id" } } |
value_count |
统计某字段非空的文档数 | "non_null_prices": { "value_count": { "field": "price" } } |
top_hits |
返回桶内最匹配的若干文档(支持排序、分页、高亮) | 常嵌套在桶聚合中,获取每个分组的前 N 条记录 |
percentiles |
百分位数统计 | "price_percentiles": { "percentiles": { "field": "price" } } |
scripted_metric |
使用脚本自定义指标计算(灵活但消耗大) | 支持 Map/Combine/Reduce 阶段 |
4.2 桶聚合(Bucket Aggregations)
| 聚合名称 | 说明 | 示例 |
|---|---|---|
terms |
根据字段值分桶,返回每个值的文档数。常用于枚举、标签、类别。 | "by_category": { "terms": { "field": "category", "size": 10 } } |
filter |
只创建一个桶,包含匹配给定查询的文档。常用于全局过滤。 | "high_price": { "filter": { "range": { "price": { "gt": 100 } } } } |
filters |
多个过滤器,每个过滤器创建一个桶。 | "price_ranges": { "filters": { "filters": { "cheap": {...}, "expensive": {...} } } } |
range |
按数值范围分桶,可自定义多个区间。 | "price_range": { "range": { "field": "price", "ranges": [ { "to": 50 }, { "from": 50, "to": 100 } ] } } |
date_range |
日期范围分桶,支持日期数学表达式(now-1d 等)。 |
"date_range": { "date_range": { "field": "timestamp", "ranges": [ { "from": "2024-01-01" } ] } } |
histogram |
等间隔数值直方图(固定步长)。 | "price_hist": { "histogram": { "field": "price", "interval": 10 } } |
date_histogram |
时间直方图(固定时间间隔:分钟、小时、天等)。 | "sales_over_time": { "date_histogram": { "field": "timestamp", "calendar_interval": "day" } } |
missing |
创建"缺失字段值"的桶,存放没有该字段或值为 null 的文档。 |
"missing_category": { "missing": { "field": "category" } } |
geohash_grid |
地理点按 Geohash 网格分桶,用于地图聚合。 | "geo_grid": { "geohash_grid": { "field": "location", "precision": 5 } } |
composite |
用于分页遍历多层级聚合结果,类似 SQL 的 GROUP BY 分页。 |
避免 terms 聚合深度分页的性能问题 |
4.3 管道聚合(Pipeline Aggregations)
管道聚合不是从文档直接计算,而是从其他聚合的输出(多桶或单值)中计算。必须明确指定 buckets_path 指向父聚合的结果。
| 聚合名称 | 说明 | 示例场景 |
|---|---|---|
derivative |
计算某指标在相邻桶之间的导数(变化率)。 | 每小时订单数的变化趋势 |
cumulative_sum |
累积和,从第一个桶到当前桶的累加值。 | 年度销售额累计 |
moving_avg / mov_fn |
移动平均(平滑时间序列)。7.x 后推荐 mov_fn 支持自定义函数。 |
股票价格 7 日移动平均 |
avg_bucket |
计算所有桶内某指标的平均值。 | 平均每个类别的销售额 |
sum_bucket |
所有桶内某指标的总和。 | 所有类别销售额的总和 |
min_bucket / max_bucket |
所有桶内某指标的最小值/最大值及其对应的桶键。 | 找出销量最高的类别 |
stats_bucket |
类似 stats,但作用于多桶指标。 |
对每个月的销售额进行统计 |
bucket_script |
使用脚本基于多个聚合结果计算新值(每个桶执行一次)。 | 计算比率 total_sales / total_visitors |
bucket_selector |
过滤掉不符合脚本条件的桶。 | 只保留销售额大于 1000 的月份 |
serial_diff |
计算某指标与前一滞后期的差值。 | 周期差分(去季节趋势) |
5. 聚合语法结构
聚合通过 aggs 或 aggregations 关键字定义,可以嵌套任意深度。
基本结构:
json
{
"size": 0, // 不返回文档,只返回聚合结果
"query": { ... }, // 可选,限定聚合的文档范围
"aggs": {
"my_agg_name_1": { // 自定义聚合名称
"agg_type": { // 聚合类型,如 terms, avg
// 聚合参数
}
},
"my_agg_name_2": {
"agg_type": {
// ...
},
"aggs": { // 子聚合(嵌套)
"sub_agg": { ... }
}
}
}
}
示例:按类别分桶,计算每类商品的平均价格和最高价格,再按品牌子分桶
json
{
"size": 0,
"aggs": {
"categories": {
"terms": { "field": "category", "size": 10 },
"aggs": {
"avg_price": { "avg": { "field": "price" } },
"max_price": { "max": { "field": "price" } },
"brands": {
"terms": { "field": "brand", "size": 5 },
"aggs": {
"brand_avg_price": { "avg": { "field": "price" } }
}
}
}
}
}
}
6. 执行原理与数据结构
6.1 Doc Values 与 Fielddata
- Doc Values:Elasticsearch 默认对非文本字段启用的列式存储结构,用于排序和聚合。它基于磁盘,在索引时生成,聚合时直接读取,内存效率高。
- Fielddata :对于
text字段,默认禁用 Doc Values,聚合时需启用fielddata(将倒排索引加载到内存),非常消耗内存,不推荐 生产环境对text字段做聚合。建议使用keyword类型。
6.2 分布式执行流程
- 协调节点接收聚合请求,将请求广播到所有分片(或根据路由指定分片)。
- 每个分片独立执行聚合:读取本地 Doc Values,计算该分片的桶和指标。
- 协调节点 收集所有分片的部分结果,进行合并、排序、分页等二次处理(例如
terms聚合的size取 Top K,需要分片返回 Top 候选,再全局排序)。 - 返回最终聚合结果。
6.3 深度聚合与延迟
- 对于
terms聚合,如果size很大(如 10000),每个分片需要返回该数量候选桶,协调节点再合并,性能会下降。 - 使用
composite聚合可以分页获取全量分组,避免一次性取大量桶。 date_histogram等时间聚合使用固定间隔,性能较好。
7. 性能优化与注意事项
7.1 通用优化建议
- 使用 Doc Values :确保聚合字段是
keyword、数值或日期类型,不要对text进行聚合。 - 限制桶数量 :在
terms聚合中设置合理的size,使用shard_size控制分片返回的候选桶数。 - 过滤前置 :先通过
query缩小聚合数据范围,减少计算量。 - 并行聚合:Elasticsearch 默认并行执行同级聚合,但过多深层嵌套会占用内存。
- 利用
execution_hint:terms聚合支持map(直接扫描)或global_ordinals(使用全局序数),后者对大基数更快。 - 避免
scripted_metric:除非万不得已,尽量用内置聚合代替脚本聚合。
7.2 内存与 OOM 风险
- 桶聚合的结果需要加载到内存(分片和协调节点),大量桶(如上百万)可能导致 OutOfMemoryError。
- 使用
composite聚合可以流式获取桶,避免一次性加载所有桶。 - 监控节点内存,配置
search.max_buckets(默认 65535)限制单个响应中的桶总数。
7.3 实时性与缓存
- 聚合是实时计算的(每次请求都重新读取文档),但分片查询结果有缓存(request cache)。
- 如果数据不变,可以启用查询缓存提升性能,但注意缓存的粒度是分片级别。
7.4 精确度与近似值
cardinality聚合基于 HyperLogLog++,默认精度可控(precision_threshold),是近似值。percentiles使用 TDigest 算法,默认也是近似值(精度与压缩参数相关)。- 对精度要求极高时,可以增加
precision_threshold或使用脚本精确计算,但性能下降。
8. 典型应用场景
- 电商仪表盘 :按品类统计销售额、订单量、平均价格;按月/周展示销售趋势(
date_histogram+sum)。 - 日志分析 :按
status_code分桶统计各 HTTP 状态码数量;按小时统计错误率(管道聚合计算导数)。 - 用户画像 :计算用户年龄的百分位数;按地区分组统计活跃用户数(
cardinality去重)。 - 监控告警 :使用
avg_bucket计算过去 10 分钟的平均 CPU 使用率,超过阈值触发告警。 - 地理分析 :
geohash_grid聚合在地图上展示热力分布;geo_distance范围聚合分析附近兴趣点数量。
9. 示例:复杂聚合实战
需求:统计最近 7 天,每个商品类别的销售总额、平均折扣、唯一买家数,并计算每天销售额的 7 日移动平均。
json
{
"size": 0,
"query": {
"range": { "order_date": { "gte": "now-7d" } }
},
"aggs": {
"by_category": {
"terms": { "field": "category", "size": 10 },
"aggs": {
"total_sales": { "sum": { "field": "price" } },
"avg_discount": { "avg": { "field": "discount_rate" } },
"unique_buyers": { "cardinality": { "field": "user_id" } },
"sales_over_time": {
"date_histogram": {
"field": "order_date",
"calendar_interval": "day"
},
"aggs": {
"daily_sales": { "sum": { "field": "price" } },
"sales_7_day_ma": {
"moving_avg": { "buckets_path": "daily_sales", "window": 7 }
}
}
}
}
}
}
}
10. 总结
- Elasticsearch 聚合提供了强大的实时数据分析能力,涵盖了从基础统计到复杂管道计算的需求。
- 核心分类:指标聚合 (计算值)、桶聚合 (分组)、管道聚合(二次计算)。
- 性能关键:依赖 Doc Values 列存,避免
text字段聚合;控制桶数量,必要时使用composite分页。 - 适用场景:任何需要在不离线的情况下对大规模数据进行多维分析、趋势计算、报表生成的系统。
理解聚合的工作机制和优化技巧,可以帮助你高效地构建搜索与分析一体化的应用,充分发挥 Elasticsearch 的实时性优势。