今天咱们就聊聊ES聚合查询之 Bucket 聚合详解!最近搞 Elasticsearch,发现这玩意儿的聚合功能简直很强大,好好盘一盘!

准备数据
先整点数据,假设有个电商商品索引,里面有商品名称、品类、价格、销售数量这些信息。咱用下面这串代码,咔咔创建个索引,再塞几条数据进去。大家可以直接用我下面的数据去测试,可要记得我的版本是8.x
PUT /products
{
"mappings": {
"properties": {
"name": { "type": "text" },
"category": { "type": "keyword" },
"price": { "type": "float" },
"quantity": { "type": "integer" }
}
}
}
POST /products/_bulk
{"index":{"_id":1}}
{"name":"iPhone 15","category":"电子产品","price":7999.0,"quantity":100}
{"index":{"_id":2}}
{"name":"T - shirt","category":"服装","price":99.0,"quantity":500}
{"index":{"_id":3}}
{"name":"AirPods Pro","category":"电子产品","price":1999.0,"quantity":300}
{"index":{"_id":4}}
{"name":"牛仔裤","category":"服装","price":199.0,"quantity":200}
{"index":{"_id":5}}
{"name":"巧克力","category":"食品","price":29.0,"quantity":800}
标准聚合:最基础的分类统计
要说桶聚合里最基础、最常用的,还得是 terms 聚合。就好比你有一堆混在一起的水果,苹果、香蕉、橘子,你想知道每种水果有多少个, terms 聚合就负责把它们分门别类,一个个数清楚。
GET /products/_search
{
"size": 0,
"aggs": {
"product_categories": {
"terms": {
"field": "category.keyword"
}
}
}
}
这代码啥意思呢? size: 0 就是说咱不看具体的商品文档,只看聚合结果; aggs 是聚合操作的地盘; product_categories 是咱给这次聚合起的名字,方便后面查看; terms 聚合指定了 category.keyword 字段,意思就是按照商品品类来分组。执行完,结果就像这样:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 5,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"product_categories": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "电子产品",
"doc_count": 2
},
{
"key": "服装",
"doc_count": 2
},
{
"key": "食品",
"doc_count": 1
}
]
}
}
}
这下清楚了,“电子产品” 有 2 件,“服装” 有 2 件,“食品” 有 1 件,一目了然!
多个聚合:一次搞定多项统计
实际干活的时候,哪能只统计一个数据啊?就像去超市,你不光想知道每种水果有多少,还想知道这些水果总共多少钱,平均每个多重。这时候,多个聚合就派上用场了!
GET /products/_search
{
"size": 0,
"aggs": {
"product_categories": {
"terms": {
"field": "category.keyword"
},
"aggs": {
"total_sales": {
"sum": {
"field": "price"
}
},
"average_quantity": {
"avg": {
"field": "quantity"
}
}
}
}
}
}
这里在 product_categories 桶聚合下面,又嵌套了 total_sales(统计总销售额)和 average_quantity(统计平均销售数量)两个指标聚合。先按品类分好组,再分别计算每个组的销售额和平均销售数量。执行完,结果一看,每个品类卖了多少钱、平均卖出去多少件,全在这儿了!
聚合的嵌套:玩点花活
前面都是小打小闹,聚合的嵌套才是真的 “花活”!想象一下,你不光想知道每个品类有多少商品,还想知道每个品类里,不同价格区间的商品数量,这就相当于给每个大分类又分了小类。
GET /products/_search
{
"size": 0,
"aggs": {
"product_categories": {
"terms": {
"field": "category.keyword"
},
"aggs": {
"price_ranges": {
"range": {
"field": "price",
"ranges": [
{ "to": 100 },
{ "from": 100, "to": 1000 },
{ "from": 1000 }
]
}
}
}
}
}
}
先通过 terms 聚合按品类分组,再在每个品类的桶里,用 range 聚合按价格区间分组。执行完,结果里每个品类下面又列出了不同价格区间的商品数量,这数据详细得,老板看了都得说声 “牛哇”!
动态脚本的聚合:自定义规则
有时候,基础的聚合规则满足不了咱们的需求,就像你想吃个特别口味的蛋糕,现成的没有,就得自己动手做。这时候动态脚本聚合就闪亮登场了!
比如,根据商品的价格和销售数量,自定义一个 “价值指数”,再根据这个指数分组。
GET /products/_search
{
"size": 0,
"aggs": {
"value_index_ranges": {
"range": {
"script": {
"source": "doc['price'].value * doc['quantity'].value",
"lang": "painless"
},
"ranges": [
{ "to": 10000 },
{ "from": 10000, "to": 100000 },
{ "from": 100000 }
]
}
}
}
}
通过 painless 脚本计算出 “价值指数”,再用 range 聚合分组。执行完,不同 “价值指数” 区间的商品数量就出来了,是不是感觉打开了新世界的大门?
按分类学习 Bucket 聚合
前面都是从知识点角度讲的,现在咱们换个姿势,按不同分类来看看桶聚合。
前置条件的过滤:filter
filter 不符合条件的数据根本不让进。比如,咱们只想统计价格大于 500 的商品品类数量,就可以这么写:
GET /products/_search
{
"size": 0,
"aggs": {
"expensive_products": {
"filter": {
"range": {
"price": {
"gt": 500
}
}
},
"aggs": {
"product_categories": {
"terms": {
"field": "category.keyword"
}
}
}
}
}
}
先过滤出价格大于 500 的商品,再按品类分组统计。这就好比你只想看贵的商品有哪些品类,其他便宜的直接忽略,是不是很方便?
对 filter 进行分组聚合:filters
filters 聚合比 filter 更 “贪心”,它可以同时设置多个过滤条件,把数据分到不同的桶里。比如,咱们分别统计价格大于 1000 的电子产品和价格小于 200 的服装的商品数量:
GET /products/_search
{
"size": 0,
"aggs": {
"filtered_categories": {
"filters": {
"filters": {
"expensive_electronics": {
"bool": {
"must": [
{ "term": { "category.keyword": "电子产品" } },
{ "range": { "price": { "gt": 1000 } } }
]
}
},
"cheap_clothes": {
"bool": {
"must": [
{ "term": { "category.keyword": "服装" } },
{ "range": { "price": { "lt": 200 } } }
]
}
}
}
},
"aggs": {
"count_products": {
"value_count": {
"field": "name"
}
}
}
}
}
}
设置两个过滤条件,分别筛选出符合条件的数据,再统计数量。这就像把不同要求的商品分别装进不同的小口袋,清清楚楚。
对 number 类型聚合:Range
range 聚合专门对付数值类型的数据,就像个分糖果的小朋友,把不同数量的糖果放到不同的盒子里。比如,统计不同价格区间内的商品销售总额:
GET /products/_search
{
"size": 0,
"aggs": {
"price_ranges_sales": {
"range": {
"field": "price",
"ranges": [
{ "to": 100 },
{ "from": 100, "to": 1000 },
{ "from": 1000 }
]
},
"aggs": {
"total_sales": {
"sum": {
"field": "price"
}
}
}
}
}
}
按价格区间分好桶,再计算每个桶里商品的总销售额,哪个价格区间卖得好,一眼就看出来了!
对 IP 类型聚合:IP Range
当数据里有 IP 地址的时候, ip_range 聚合就派上用场了。比如在网络日志分析中,统计不同 IP 地址段的访问次数,就像统计不同小区的访客数量。
PUT /network_logs
{
"mappings": {
"properties": {
"ip_address": { "type": "ip" }
}
}
}
POST /network_logs/_bulk
{"index":{"_id":1}}
{"ip_address":"192.168.1.10"}
{"index":{"_id":2}}
{"ip_address":"192.168.2.20"}
{"index":{"_id":3}}
{"ip_address":"10.0.0.5"}
GET /network_logs/_search
{
"size": 0,
"aggs": {
"ip_ranges": {
"ip_range": {
"field": "ip_address",
"ranges": [
{ "to": "192.168.1.255" },
{ "from": "192.168.2.0", "to": "192.168.2.255" },
{ "from": "10.0.0.0" }
]
}
}
}
}
先创建索引、添加数据,再用 ip_range 聚合按 IP 地址段分组统计,哪个 IP 段访问得多,心里有数了!
对日期类型聚合:Date Range
date_range 聚合在处理日期数据的时候超有用,就像把不同时间段的日记整理到不同的本子里。比如,统计不同时间段内的商品订单数量:
PUT /orders
{
"mappings": {
"properties": {
"order_date": { "type": "date" }
}
}
}
POST /orders/_bulk
{"index":{"_id":1}}
{"order_date":"2024-01-10T00:00:00Z"}
{"index":{"_id":2}}
{"order_date":"2024-02-15T00:00:00Z"}
{"index":{"_id":3}}
{"order_date":"2024-03-20T00:00:00Z"}
GET /orders/_search
{
"size": 0,
"aggs": {
"order_date_ranges": {
"date_range": {
"field": "order_date",
"ranges": [
{ "to": "2024-02-01" },
{ "from": "2024-02-01", "to": "2024-03-01" },
{ "from": "2024-03-01" }
]
}
}
}
}
按日期范围分好桶,看看哪个时间段订单多,运营策略是不是就有方向了?
对柱状图功能:Histogram
histogram 聚合就像个画图小能手,专门用来生成柱状图。比如,统计每 100 元价格区间内的商品数量,形成价格分布柱状图:
GET /products/_search
{
"size": 0,
"aggs": {
"price_histogram": {
"histogram": {
"field": "price",
"interval": 100
}
}
}
}
interval 参数指定分组间隔为 100 元,执行完,不同价格区间的商品数量分布情况,用柱状图一展示,直观得很!
关于 ES 桶聚合就唠到这儿!是不是感觉也没那么难?赶紧去实操一下, 欢迎评论区留言
评论区