2026年7月12日 · 5 分钟阅读
Elasticsearch 聚合分析与 Painless Scripting
从指标聚合到桶聚合、管道聚合和直方图,再到 Painless 脚本处理,完整覆盖 ES 的数据分析能力。
搜索只是 ES 的一半能力。另一半是聚合(Aggregations)——它让你在亿级数据上实时完成分组、统计、排序、百分比分析等操作,完全不需要外挂一个 OLAP 引擎。
按执行方式,聚合分为三类:
- 指标聚合(Metric Aggregations):对一组文档计算统计值,类似 SQL 的聚合函数。
- 桶聚合(Bucket Aggregations):把文档分组到不同的桶里,类似 SQL 的 GROUP BY。
- 管道聚合(Pipeline Aggregations):对聚合结果再做一次聚合,类似子查询嵌套。
指标聚合(Metric Aggregations)
基本统计
GET product/_search
{
"size": 0,
"aggs": {
"max_price": { "max": { "field": "price" } },
"min_price": { "min": { "field": "price" } },
"avg_price": { "avg": { "field": "price" } }
}
}
一次性拿到多项统计
GET product/_search
{
"size": 0,
"aggs": {
"price_stats": {
"stats": { "field": "price" }
}
}
}
stats 会同时返回 count、min、max、avg、sum 五个值,非常方便。如果需要百分位,用 percentiles。
桶聚合(Bucket Aggregations)
按字段分组 + 子聚合
计算每个商品类型的平均价格,再找出其中最低的那个:
GET product/_search
{
"size": 0,
"aggs": {
"type_bucket": {
"terms": {
"field": "type.keyword"
},
"aggs": {
"price_avg": {
"avg": { "field": "price" }
}
}
},
"min_bucket": {
"min_bucket": {
"buckets_path": "type_bucket>price_avg"
}
}
}
}
这个查询展示了聚合嵌套的精髓:
type_bucket先按 type 分组。- 在每个组内计算
price_avg。 min_bucket是一个管道聚合,对所有组的price_avg取最小值。
笛卡尔积分组
ES 支持在桶聚合内再嵌套另一个桶聚合,形成类似 SQL 多列 GROUP BY 的效果:
GET product/_search
{
"size": 0,
"aggs": {
"type_bucket": {
"terms": { "field": "type.keyword" },
"aggs": {
"name_bucket": {
"terms": { "field": "name.keyword" }
}
}
}
}
}
先查询后聚合
聚合可以跟在 query 后面,只对满足条件的文档做聚合:
GET product/_search
{
"query": {
"range": { "price": { "gte": 1000 } }
},
"aggs": {
"type_bucket": {
"terms": { "field": "type.keyword" }
}
}
}
聚合排序
按字段名排序
GET product/_search?size=0
{
"aggs": {
"tags_aggs": {
"terms": {
"field": "tags.keyword",
"size": 10,
"order": { "_key": "asc" }
}
}
}
}
_key 按字段值字母序排序,_count 按文档数排序。
嵌套聚合排序
二级分组内各自排序:
GET product/_search?size=0
{
"aggs": {
"first_sort": {
"terms": {
"field": "tags.keyword",
"order": { "_count": "desc" }
},
"aggs": {
"second_sort": {
"terms": {
"field": "type.keyword",
"order": { "_count": "desc" }
}
}
}
}
}
}
按子聚合结果排序
根据自定义统计值来排序——这是最灵活的排序方式:
GET product/_search?size=0
{
"aggs": {
"type_price": {
"terms": {
"field": "type.keyword",
"order": {
"agg_stats>stats.min": "asc"
}
},
"aggs": {
"agg_stats": {
"filter": {
"terms": { "tags.keyword": ["88vip", "tmall"] }
},
"aggs": {
"stats": {
"stats": { "field": "price" }
}
}
}
}
}
}
}
这里的意思是:按各 type 分组,在每个组内先过滤出 tags 为 88vip 或 tmall 的商品,计算它们的价格统计,然后按照这些统计的最小值(stats.min)对整个分组结果排序。
直方图与分布分析
范围直方图(Range)
手动指定区间范围:
GET product/_search
{
"aggs": {
"price_range": {
"range": {
"field": "price",
"ranges": [
{ "from": 0, "to": 1000 },
{ "from": 1000, "to": 2000 },
{ "from": 2000, "to": 3000 },
{ "from": 3000, "to": 4000 }
]
}
}
}
}
自动直方图(Histogram)
指定区间大小,ES 自动分桶:
GET product/_search
{
"aggs": {
"price_range": {
"histogram": {
"field": "price",
"interval": 1000
}
}
}
}
日期直方图(Date Histogram)
按时间维度聚合,支持年、季度、月、周、日、小时等:
GET product/_search
{
"aggs": {
"price_range": {
"date_histogram": {
"field": "date",
"calendar_interval": "month",
"extended_bounds": {
"min": "2023-01",
"max": "2023-06"
}
}
}
}
}
百分位排名(Percentile Ranks)
分析数据分布情况——比如想知道价格在 1000、2000、3000、4000 以内的商品占比:
GET product/_search
{
"aggs": {
"price_range": {
"percentile_ranks": {
"field": "price",
"values": [1000, 2000, 3000, 4000]
}
}
}
}
Painless Scripting:动态处理数据
有些场景没法通过固定 Mapping 解决,比如:更新时动态计算、查询时临时加工字段、复杂条件聚合。这时就需要 Painless——ES 内置的脚本语言。
文档更新
最简单的脚本——给指定文档的价格 +1:
POST product/_update/2
{
"script": {
"source": "ctx._source.price += 1"
}
}
使用参数化脚本——避免拼接字符串、可复用、安全:
POST product/_update/2
{
"script": {
"lang": "painless",
"source": "ctx._source.price += params.num",
"params": { "num": 1 }
}
}
配合 upsert——文档存在则更新,不存在则创建:
POST product/_update/2
{
"script": {
"lang": "painless",
"source": "ctx._source.price += params.num",
"params": { "num": 1 }
},
"upsert": {
"name": "xiaomi nfc phone2",
"type": "c",
"price": 2999,
"date": "2023-05-01",
"desc": "xiaomi 2023 new1",
"tags": ["88vip", "tmall", "newer"]
}
}
动态计算字段
查询时根据现有字段临时生成新字段的值,不改变索引数据:
GET product/_search
{
"script_fields": {
"discount_price": {
"script": {
"source": "doc['price'].value * 0.9"
}
}
}
}
返回多个计算值:
GET product/_search
{
"script_fields": {
"price": { "script": { "source": "doc['price'].value" } },
"discounts": {
"script": {
"source": "[doc['price'].value * 0.9, doc['price'].value * 0.8, doc['price'].value * 0.7]"
}
}
}
}
存储脚本(Stored Script)
把脚本保存到 ES 中,以后通过 id 引用:
POST _scripts/calc_x
{
"script": {
"lang": "painless",
"source": "doc.price.value + params.num"
}
}
// 引用已存储的脚本
GET product/_search
{
"script_fields": {
"discount": {
"script": {
"id": "calc_x",
"params": { "num": 1 }
}
}
}
}
聚合中使用脚本
计算价格 ≤ 2000 的商品中,每个商品 tags 的数量之和:
GET product/_search
{
"query": {
"constant_score": {
"filter": {
"range": { "price": { "lte": 2000 } }
}
}
},
"aggs": {
"tag_agg": {
"sum": {
"script": {
"source": "doc['tags.keyword'].length"
}
}
}
}
}
性能要点
- 聚合优先使用
keyword字段,text字段需要fielddata=true才能聚合,这会把正排索引加载到 JVM 堆中,容易 OOM。 doc_values是正排索引,以列式存储在磁盘上,排序和聚合直接读这个,不需要加载原文档。如果某个字段确定不需要排序或聚合,设"doc_values": false可以省磁盘空间。- 先过滤再聚合,用 filter / constant_score 提前减少聚合数据量,能显著提升性能。
size: 0在聚合查询上设置,表示不需要返回原始文档,只返回聚合结果。- 脚本尽量参数化,避免编译开销——Painless 每次编译脚本有 150 个/分钟的软限制。
总结
- 指标聚合解决”是多少”,桶聚合解决”分几组”,管道聚合解决”组与组之间的关系”。
- 直方图是范围分组的利器:Range 精细可控、Histogram 自动分桶、Date Histogram 按时间线聚合。
- Painless 让 ES 从一个搜索引擎变成了能执行任意逻辑的数据处理引擎,但也要谨慎使用——在大量文档上用脚本做聚合,性能开销不小。