聚合分析
聚合(Aggregation)是ES除全文检索外最核心的能力之一,它允许我们对数据进行统计、分组、计算和多维度分析操作,实现类似关系型数据库中GROUP BY、COUNT、SUM、AVG等统计功能,同时ES还支持更复杂的多层嵌套聚合、实时计算,因此广泛用于报表、数据分析、用户画像等场景。
本篇笔记中的示例仍沿用上一篇笔记中的products索引,这里就不重复黏贴了。
ES中的聚合统计
ES中的聚合功能中,最常用的主要有两大类,指标聚合和桶聚合。
graph TD
A[聚合 Aggregation]
A --> B[Bucket 桶聚合]
A --> C[Metric 指标聚合]
B --> B1[Terms Aggregation]
B --> B2[Date Histogram Aggregation]
B --> B3[Range Aggregation]
C --> C1[avg / sum / min / max]
C --> C2[value_count]
C --> C3[cardinality]
C --> C4[stats / extended_stats]
C --> C5[percentiles]
Metric 指标聚合:指标聚合是指在整个文档集合或某个桶上计算数值指标,如最大值、最小值、平均值、求和等,类似SQL中的MAX()、AVG()等聚合函数。
Bucket 桶聚合:桶聚合指将文档按某种规则分组,每组叫做一个“桶”,类似SQL中的GROUP BY。
除此之外,ES其实还包含两种高级聚合功能,管道聚合和矩阵聚合。不过这两个功能一直处于技术预览状态,尤其是矩阵聚合在生产环境中较少使用,这里就不展开介绍了。
聚合请求的基本结构
ES中聚合请求通过aggs(或写全称aggregations,两者完全等价)字段来声明,aggs可以与query同时使用,同时使用时聚合只作用于query过滤后的结果集,基本格式如下。
GET /products/_search
{
"query": { ... },
"aggs": {
"聚合名称(自定义)": {
"聚合类型": {
"field": "字段名",
// ... 其它参数
}
}
},
"size": 0
}
聚合统计中,一个小技巧是使用size: 0,它表示不返回命中文档列表只返回聚合结果,这可以节省网络带宽和响应体积,在纯统计报表场景下非常常用。
指标聚合
value_count
value_count统计某个字段有值的文档数量。这里注意如果某个文档的目标字段为空,这种数据是不会被计入的,所谓的“字段为空”是指字段不存在或为null,空字符串不算“字段为空”。此外,还需要注意的是_id和text类型的字段默认不能用于聚合统计,_id不能用于聚合和上一篇笔记中_id不能用于排序的理由是类似的,至于text字段则是因为它分词后数量巨大,ES默认也不开启FieldData。ES中通过开启一些配置确实可以让这些字段参与聚合,但绝不推荐这样做。keyword、数字等类型可以正常用于聚合统计。
GET /products/_search
{
"size": 0,
"aggs": {
"product_count": {
"value_count": { "field": "name.keyword" }
}
}
}
聚合的响应结果中,相关统计结果会被ES放入aggregations字段。
{
// ... 其它字段
"product_count": {
"value": 8
}
}
avg / sum / min / max
avg、sum、min、max是最基础的单值指标聚合,它们的含义与SQL中的同名函数完全一致,分别是计算平均值、计算累加和、计算最小值和计算最大值。下面例子我们计算products索引中所有产品的平均价格、总库存、最低价格和最高价格。
GET /products/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": { "field": "price" }
},
"total_stock": {
"sum": { "field": "stock" }
},
"min_price": {
"min": { "field": "price" }
},
"max_price": {
"max": { "field": "price" }
}
}
}
stats / extended_stats
stats是简写的复合指标聚合,它一次性返回count、min、max、avg、sum五个指标,无需分别声明5个聚合操作。
GET /products/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": { "field": "price" }
}
}
}
extended_stats在stats基础上还会额外返回方差、标准差、平方和等统计量。
GET /products/_search
{
"size": 0,
"aggs": {
"price_extended_stats": {
"extended_stats": { "field": "price" }
}
}
}
cardinality
cardinality用于计算某个字段的近似去重数量,它类似于SQL中的COUNT(DISTINCT field)。下面例子统计索引中共有多少个不同的品牌。
GET /products/_search
{
"size": 0,
"aggs": {
"brand_count": {
"cardinality": { "field": "brand" }
}
}
}
cardinality是近似而非精确统计,它的底层没有真去全量去重统计,而是使用HLL++(HyperLogLog++)算法实现的,这意味着统计结果会有一定误差,这个误差率默认通常在5%以内,大部分场景下远低于这个值,通过precision_threshold参数可以调节精度,但代价是更高的内存消耗。
percentiles
percentiles用于计算字段值的百分位分布,它常用于分析数据的分布。
GET /products/_search
{
"size": 0,
"aggs": {
"price_percentiles": {
"percentiles": {
"field": "price",
"percents": [25, 50, 75, 90, 99]
}
}
}
}
响应结果中,50.0对应中位数价格,90.0表示90%的产品价格低于这个值,其它以此类推,通过这个指标我们可以直观了解价格整体分布情况。
桶聚合
Terms Aggregation
terms是最常用的桶聚合,它按某个字段的值对文档进行分组,每个唯一值对应一个桶,表达的含义类似SQL中的GROUP BY。下面例子按产品类目分组,统计每个类目下的数量。
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10
}
}
}
}
size参数控制返回桶的最大数量,默认值为10,如果你的字段枚举值非常多时就需要根据实际情况调大这个值,否则ES只会返回文档数最多的前N个桶,其余的会被汇总到一个叫sum_other_doc_count的字段中。
terms聚合默认按结果数降序排列,这可以通过order参数修改排序方式。
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10,
"order": { "_key": "asc" }
}
}
}
}
_key表示按桶的键排序,_count表示按文档数排序,此外也可以按嵌套指标聚合的结果排序,这部分将在嵌套聚合中介绍。
Range Aggregation
range聚合按数值区间分桶,每个区间对应一个桶,区间分桶遵循左闭右开[from, to)原则。下面例子将产品按价格区间分组。
GET /products/_search
{
"size": 0,
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 1000 },
{ "from": 1000, "to": 5000 },
{ "from": 5000, "to": 10000 },
{ "from": 10000 }
]
}
}
}
}
Date Histogram Aggregation
date_histogram按时间间隔分桶,它是日志统计、订单量趋势分析等时序类报表的重要聚合方式。下面例子按月统计产品上架数量。
GET /products/_search
{
"size": 0,
"aggs": {
"by_month": {
"date_histogram": {
"field": "created_at",
"calendar_interval": "month",
"format": "yyyy-MM",
"min_doc_count": 0
}
}
}
}
calendar_interval支持minute、hour、day、week、month、quarter、year等时间粒度。min_doc_count: 0表示即使某个时间段内没有文档也会在结果中返回该桶(文档数为0),这在需要绘制连续折线图时非常重要,否则没有数据的时间点会被跳过导致图表断点。
除了calendar_interval我们还可以选择fixed_interval,它用于指定固定时间间隔,如1h、7d等,与calendar_interval的区别是它不用关心月份天数差异问题,适合精确时间窗口的统计。
聚合嵌套与多级聚合
Bucket聚合和Metric聚合可以相互嵌套:Bucket聚合可以嵌套子Metric聚合,实现分组后再统计;Bucket聚合也可以嵌套子Bucket聚合,实现多级分组。
Bucket聚合内嵌套Metric聚合
下面例子按品牌分组,然后在每个品牌桶内分别计算平均价格和总库存。
GET /products/_search
{
"size": 0,
"aggs": {
"by_brand": {
"terms": {
"field": "brand",
"size": 10
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
},
"total_stock": {
"sum": { "field": "stock" }
}
}
}
}
}
嵌套聚合中的排序
嵌套的Metric聚合结果也可以作为外层Bucket聚合的排序依据。下面例子按品牌分组后,按各品牌的平均价格降序排列。
GET /products/_search
{
"size": 0,
"aggs": {
"by_brand": {
"terms": {
"field": "brand",
"size": 10,
"order": { "avg_price": "desc" }
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}
}
}
多级Bucket聚合嵌套
下面例子实现了两级分组,先按类目分组,再在每个类目内按品牌分组,最后在每个分组内计算平均价格。
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"by_brand": {
"terms": {
"field": "brand",
"size": 10
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}
}
}
}
}
聚合与查询结合
聚合始终只作用于命中文档,结合query就可以实现先筛选再统计的效果。下面例子只统计未删除产品的各类目产品数量和平均价格。
GET /products/_search
{
"size": 0,
"query": {
"term": { "is_deleted": false }
},
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10
},
"aggs": {
"avg_price": {
"avg": { "field": "price" }
}
}
}
}
}
空值聚合处理
默认情况下,Metric聚合和Bucket聚合会忽略字段值为null或字段不存在的文档。missing参数可以为缺失值的文档指定一个默认填充值,使这些文档也能参与聚合。下面例子中,如果某个产品文档缺少price字段,则将其视为价格0参与平均价格计算。
GET /products/_search
{
"size": 0,
"aggs": {
"avg_price": {
"avg": {
"field": "price",
"missing": 0
}
}
}
}
在terms桶聚合中,missing参数可以让缺失字段值的文档单独形成一个桶。
GET /products/_search
{
"size": 0,
"aggs": {
"by_category": {
"terms": {
"field": "category",
"size": 10,
"missing": "未分类"
}
}
}
}