桶聚合
在前面几篇博客中介绍的聚合指标是指符合条件的文档字段的聚合,有时还需要根据某些维度进行聚合。例如在搜索酒店时,按照城市、是否满房、标签和创建时间等维度统计酒店的平均价格。这些字段统称为"桶",在同一维度内有一个或者多个桶。例如城市桶,有"北京"、"天津"等,是否满房桶,有"满房"和"非满房"。
1.1 单维度同聚合
最简单的桶聚合是单维度桶聚合,指的是按照一个维度对文档进行分组聚合。在桶聚合时,聚合的桶也需要匹配,匹配的方式有terms
、filter
和ranges
等。这里只介绍比较有代表性的terms
查询和ranges
查询。
1.1.1 terms聚合
terms
聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keyword
、bool
、keyword
数组等适合精确匹配的数据类型,因此不能对text
字段直接使用terms
聚合,如果对text
字段有terms
聚合的需求,则需要在创建索引时为该字段增加多字段功能。
以下的DSL描述的是按照城市进行聚合的查询:
bash
# 按照城市进行聚合
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg": {
"terms": { //按照城市进行聚合
"field": "city"
}
}
}
}
因为ES支持多聚合,所以每个桶聚合需要定义一个名字,此处定义了一个桶聚合,名字为my_agg。在这个桶聚合中使用了一个terms
聚合,聚合字段选择了城市,目的是统计各个城市的酒店的文档个数。在聚合外面,因为不希望返回任何文档,所以指定查询返回的文档为0。执行该DSL后,ES返回的结果如下:
在默认情况下,进行桶聚合时如果不指定指标,则ES默认聚合的是文档计数,该值以doc_count
为key存储在每一个bucket子句中。在聚合结果的buckets的两个bucket中,key字段的值分别为"北京""天津",表示两个bucket的唯一标识;doc_count
字段的值分别为3和2,表示两个bucket的文档计数。返回的doc_count
是近似值,并不是一个准确数,因此在聚合外围,ES给出了连个参考值doc_count_error_upper_bound
和sum_other_doc_count
。
doc_count_error_upper_bound
表示被遗漏的文档数量可能存在的最大值sum_other_doc_count
表示除了返回给用户的文档外剩下的文档总数
以下DSL是按照满房状态进行聚合的查询,注意该字段是bool型:
bash
# 按照满房状态进行查询
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg": {
"terms": {
"field": "full_room"
}
}
}
}
ES返回的结果如下:
在上述结果中可以看到,在满房和非满房的bucket结果中多出了一个字段,名称为key_as_string
,其值分别为true
和false
。另外,这两个bucket的key值分别为1和0.这是因为,如果桶字段类型不是keyword类型,ES在聚合时会将桶字段转换为Lucene存储的实际值进行识别。true在Lucene中存储为1,false在Lucene中存储为0,这就是为什么满房和非满房的key字段分别为1和0的原因。
这种情况给用户的使用带来了一些困惑,因为和原始值的差别比较大。针对这个问题,我们可以使用ES提供的
key_as_string
桶识别字段,它是原始值的字符串形式,和原始值的差别比较小。
在Java中国使用terms聚合进行单维度桶聚合的逻辑如下:
ini
public void getBucketDocCountAggSearch() throws IOException {
//创建搜索请求
SearchRequest searchRequest = new SearchRequest("hotel_poly");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String termsAggName = "my_terms"; //指定聚合的名称
//定义terms聚合,指定字段为城市
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(termsAggName).field("full_room");
//添加聚合
searchSourceBuilder.aggregation(termsAggregationBuilder);
//设置查询请求
searchRequest.source(searchSourceBuilder);
//执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取聚合结果
Aggregations aggregations = searchResponse.getAggregations();
//获取聚合返回的对象
Terms terms = aggregations.get(termsAggName);
for (Terms.Bucket bucket : terms.getBuckets()) {
String bucketKey = bucket.getKeyAsString();//获取桶名称
long docCount = bucket.getDocCount(); //获取文档个数
log.info("termsKey={},docCount={}", bucketKey, docCount);
}
}
1.1.2 ranges聚合
ragnes
聚合也是经常使用的一种聚合。它匹配的是数值字段,表示按照数值范围进行分组。用户可以在ranges中添加分组,每个分组使用from
和to
表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。
示例如下:
vbnet
# ranges聚合
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg":{
"range": {
"field": "price",
"ranges": [ //多个范围桶
{
"to": 200 //不指定from,默认from为0
},
{
"from": 200,
"to": 500
},
{
"from": 500 //不指定to,默认to为该字段最大值
}
]
}
}
}
}
执行上述DSL后,ES返回的结果如下:
在上面的分组划分中,第一个分组规则为price<200,没有文档与其匹配,因此其doc_count
为0;第二个分组规则为200 \le price<500,文档003与其匹配,因此其doc_count
为1;第三个分组规则为price \ge 500,文档001、004和005与其匹配,因此其doc_count
值为3。
以下代码演示了在Java中使用ranges聚合的逻辑:
ini
public void getRangeDocCountAggSearch() throws IOException {
//创建搜索请求
SearchRequest searchRequest = new SearchRequest("hotel_poly");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String rangeAggName = "my_range"; //聚合的名称
//定义ranges聚合,指定字段为price
RangeAggregationBuilder rangeAgg = AggregationBuilders.range(rangeAggName).field("price");
rangeAgg.addRange(new RangeAggregator.Range(null, null, 200d));
rangeAgg.addRange(new RangeAggregator.Range(null, 200d, 500d));
rangeAgg.addRange(new RangeAggregator.Range(null, 500d, null));
//添加ranges聚合
searchSourceBuilder.aggregation(rangeAgg);
//设置查询请求
searchRequest.source(searchSourceBuilder);
//执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取聚合结果
Aggregations aggregations = searchResponse.getAggregations();
Range range = aggregations.get(rangeAggName);
for (Range.Bucket bucket : range.getBuckets()) {
String bucketKey = bucket.getKeyAsString(); //获取桶名称
long docCount = bucket.getDocCount(); //获取聚合文档个数
log.info("termsKey={},docCount={}", bucketKey, docCount);
}
}
有时还需要对单维度桶指定聚合指标,聚合指标单独使用子aggs
进行封装。
以下请求表示按照城市维度进行聚合,统计各个城市的平均酒店价格:
csharp
# 按照城市维度进行聚合,统计各个城市的平均酒店价格
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg": { //单维度聚合名称
"terms": { //定义单维度桶
"field": "city"
},
"aggs": { //用于封装单维度桶下的聚合指标
"my_sum": { //聚合指标名称
"sum": { //最price字段进行加和
"field": "price",
"missing": 200
}
}
}
}
}
}
在上面的结果中,聚合桶的维度是城市,当前索引中城市为"北京"的文档个数为3,城市为"天津"的文档个数为2.将这两组文档的聚合结果在buckets子句中进行了封装,可以根据key字段进行聚合桶的识别,每个聚合的组中既有文档个数又有价格的加和值。
在Java中使用桶聚合和指标聚合的逻辑如下:
ini
public void getBucketAggSearch() throws IOException{
//创建搜索请求
SearchRequest searchRequest = new SearchRequest("hotel_poly");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
String termsAggName="my_terms"; //聚合的名称
//定义terms聚合,指定字段为城市
TermsAggregationBuilder termsAggregationBuilder=AggregationBuilders.terms(termsAggName).field("city");
//sum聚合的名称
String sumAggName="my_sum";
//定义sum聚合,指定字段为价格
SumAggregationBuilder sumAggregationBuilder=AggregationBuilders.sum(sumAggName).field("price");
sumAggregationBuilder.missing(200);
//定义聚合的父子关系
termsAggregationBuilder.subAggregation(sumAggregationBuilder);
//添加聚合
searchSourceBuilder.aggregation(termsAggregationBuilder);
//设置查询请求
searchRequest.source(searchSourceBuilder);
//执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取聚合结果
Aggregations aggregations = searchResponse.getAggregations();
Terms terms = aggregations.get(termsAggName); //获取聚合返回的对象
for (Terms.Bucket bucket : terms.getBuckets()) {
String termsKey = bucket.getKey().toString();
log.info("termsKey={}",termsKey);
Sum sum=bucket.getAggregations().get(sumAggName);
String key=sum.getName(); //获取聚合名称
double sumVal = sum.getValue(); //获取聚合值
log.info("key={},count={}",key,sumVal);
}
}
1.2 多维度桶嵌套聚合
在某些业务需求中,不仅需要一个维度的桶聚合,而且还可能有多维度桶嵌套聚合的需求。例如在搜索酒店时,可能需要统计各个城市的满房和非满房状态下的酒店平均价格。ES支持嵌套桶聚合,进行嵌套时,可以使用aggs
子句进行子桶的继续嵌套,指标放在最里面的子桶内。
以下DSL演示多维度桶的使用方法:
csharp
# 多维度桶嵌套聚合
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"group_city": { //多维度桶名称
"terms": {
"field": "city"
},
"aggs": { //单维度桶
"group_full_room": {
"terms": {
"field": "full_room"
},
"aggs": { //聚合指标
"my_sum":{
"sum": {
"field": "price",
"missing": 200
}
}
}
}
}
}
}
}
ES返回结果如下:
从结果中可以看到,第一层的分桶先按照城市分组分为"北京""天津";第二层在"北京""天津"桶下面继续分桶,分为"满房"和"非满房"桶,对应的聚合指标即价格的加和值存储在内部的my_sum字段中。
在Java中使用多维度桶进行聚合的逻辑如下:
scss
public void getExternalBucketAggSearch() throws IOException{
//创建搜索请求
SearchRequest searchRequest = new SearchRequest("hotel_poly");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//按城市聚合的名称
String aggNameCity="my_terms_city";
//定义terms聚合,指定字段为城市
TermsAggregationBuilder termsAggCity=AggregationBuilders.terms(aggNameCity).field("city");
//按满房状态聚合的名称
String aggNameFullRoom="my_terms_full_room";
//定义terms聚合,指定字段为满房状态
TermsAggregationBuilder termsAggFullRoom=AggregationBuilders.terms(aggNameFullRoom).field("full_room");
//sum聚合的名称
String sumAggName="my_sum";
//定义sum聚合,指定字段为价格
SumAggregationBuilder sumAgg=AggregationBuilders.sum(sumAggName).field("price").missing(200);
//定义聚合的父子关系
termsAggFullRoom.subAggregation(sumAgg);
termsAggCity.subAggregation(termsAggFullRoom);
//添加聚合
searchSourceBuilder.aggregation(termsAggCity);
//设置查询请求
searchRequest.source(searchSourceBuilder);
//执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取聚合结果
Aggregations aggregations = searchResponse.getAggregations();
//获取聚合返回的对象
Terms terms=aggregations.get(aggNameCity);
for (Terms.Bucket bucket : terms.getBuckets()) { //遍历第一层bucket
//获取第一层bucket名称
String termsKeyCity = bucket.getKey().toString();
log.info("-------termsKeyCity={}--------",termsKeyCity);
Terms termsFullRoom = bucket.getAggregations().get(aggNameFullRoom);
//遍历第二层bucket
for (Terms.Bucket bucketFullRoom : termsFullRoom.getBuckets()) {
//获取第二层bucket名称
String termsKeyFullRoom = bucketFullRoom.getKeyAsString();
log.info("termsKeyFullRoom={}",termsKeyFullRoom);
//获取聚合指标
Sum sum=bucketFullRoom.getAggregations().get(sumAggName);
//获取聚合指标名称
String key=sum.getName();
//获取聚合指标值
double sumVal=sum.getValue();
log.info("key={},count={}",key,sumVal);
}
}
}
1.3 地理距离聚合
按照地理距离聚合是一个非常实用的功能,例如在搜索酒店时,可能需要对附近的酒店个数先预览一下:查看距离当前位置2km范围内、2~3km范围内、5km范围内的酒店个数。
用户可以使用geo_distance
聚合进行地理距离聚合,通过field
参数来设置距离计算的字段,可以在origin
子句中设定距离的原点,通过unit
参数来设置距离的单位,可以选择mi
和km
,分别表示米和千米。ranges
子句用来对距离进行阶段性的分组,该子句的使用方式和前面介绍的range聚合类似。
示例DSL如下:
csharp
# 地理距离聚合
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg":{
"geo_distance": {
"field": "location",
"origin": { //指定聚合的中心点经纬度
"lat": 39.915143,
"lon": 116.4039
},
"unit": "km", //指定聚合时的距离计量单位
"ranges": [ //指定每一个聚合桶的距离范围
{
"to": 3
},
{
"from": 3,
"to": 10
},
{
"from":10
}
]
}
}
}
}
在上述DSL中,给定了一个地理位置,此处使用ranges
聚合对距离该位置的就带你划分了3个分组的桶:第1个桶为3km范围内;第2个桶为3~10km;第3个桶为大于等于10km。执行上述DSL后,ES返回结果如下:
其中,在aggregations
结果子句中对应查询的分组有3个bucket桶,表示按照距离划分的3个组,每个bucket桶内分别给出了key和文档数量等信息。
也可以指定聚合指标进行地理距离聚合,下面的DSL将按照bucket分桶聚合酒店的最低价格:
css
# 指定聚合指标进行地理距离聚合
GET /hotel_poly/_search
{
"size": 0,
"aggs": {
"my_agg": {
"geo_distance": {
"field": "location",
"origin": {
"lat": 39.915143,
"lon": 116.4039
},
"unit": "km",
"ranges": [
{
"to": 3
},
{
"from": 3,
"to": 10
},
{
"from": 10
}
]
},
"aggs": {
"my_min": {
"min": {
"field": "price",
"missing": 100
}
}
}
}
}
}
ES返回结果如下:
在上面的结果中,ES给出了各个地理距离分组的名称及最低价格。
在Java中使用地理距离分组并计算最低价格的逻辑如下:
java
public void getGeoDistanceAggSearch() throws IOException{
//创建搜索请求
SearchRequest searchRequest = new SearchRequest("hotel_poly");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//聚合的名称
String geoDistanceAggName="location";
//定义GeoDistance聚合
GeoDistanceAggregationBuilder geoDistanceAgg = AggregationBuilders.geoDistance(geoDistanceAggName, new GeoPoint(39.915143, 116.4039));
geoDistanceAgg.unit(DistanceUnit.KILOMETERS);
geoDistanceAgg.field("location");
//指定分桶范围规则
geoDistanceAgg.addRange(new GeoDistanceAggregationBuilder.Range(null,0d,3d));
geoDistanceAgg.addRange(new GeoDistanceAggregationBuilder.Range(null,3d,10d));
geoDistanceAgg.addRange(new GeoDistanceAggregationBuilder.Range(null,10d,null));
//min聚合的名称
String minAggName="my_min";
//定义sum聚合,指定字段为价格
MinAggregationBuilder minAgg=AggregationBuilders.min(minAggName).field("price");
minAgg.missing(100); //指定默认值
//定义聚合的父子关系
geoDistanceAgg.subAggregation(minAgg);
searchSourceBuilder.aggregation(geoDistanceAgg); //添加聚合
searchRequest.source(searchSourceBuilder); //设置查询请求
//执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
//获取聚合结果
Aggregations aggregations = searchResponse.getAggregations();
//获取GeoDistance聚合返回的对象
ParsedGeoDistance range=aggregations.get(geoDistanceAggName);
for (Range.Bucket bucket : range.getBuckets()) {
//获取bucket名称的字符串形式
String termsKey = bucket.getKeyAsString();
log.info("termsKey={}",termsKey);
ParsedMin min = bucket.getAggregations().get(minAggName);
String key = min.getName(); //获取聚合名称
double minVal = min.getValue(); //获取聚合值
log.info("key={},min={}",key,minVal); //打印结果
}
}
数据源
索引结构
bash
PUT /hotel_poly
{
"settings": {
"number_of_shards": 1
},
"mappings": {
"properties": {
"title":{
"type": "text"
},
"city":{
"type": "keyword"
},
"price":{
"type": "double"
},
"create_time":{
"type": "date"
},
"full_room":{
"type": "boolean"
},
"location":{
"type": "geo_point"
},
"tags":{
"type": "keyword"
},
"comment_info":{
"properties": {
"favourable_comment":{
"type":"integer"
},
"negative_comment":{
"type":"integer"
}
}
}
}
}
}
酒店数据
bash
POST /_bulk
{"index":{"_index":"hotel_poly","_id":"001"}}
{"title":"文雅假日酒店","city":"北京","price":556.00,"create_time":"20200418120000","full_room":true,"location":{"lat":39.938838,"lon":106.449112},"tags":["wifi","小型电影院"],"comment_info":{"favourable_comment":20,"negative_comment":10}}
{"index":{"_index":"hotel_poly","_id":"002"}}
{"title":"金都嘉怡假日酒店","city":"北京","create_time":"20210315200000","full_room":false,"location":{"lat":39.915153,"lon":116.4030},"tags":["wifi","免费早餐"],"comment_info":{"favourable_comment":20,"negative_comment":10}}
{"index":{"_index":"hotel_poly","_id":"003"}}
{"title":"金都假日酒店","city":"北京","price":200.00,"create_time":"20210509160000","full_room":true,"location":{"lat":40.002096,"lon":116.386673},"comment_info":{"favourable_comment":20,"negative_comment":10}}
{"index":{"_index":"hotel_poly","_id":"004"}}
{"title":"金都假日酒店","city":"天津","price":500.00,"create_time":"20210218080000","full_room":false,"location":{"lat":39.155004,"lon":117.203976},"tags":["wifi","免费车位"]}
{"index":{"_index":"hotel_poly","_id":"005"}}
{"title":"文雅精选酒店","city":"天津","price":800.00,"create_time":"20210101080000","full_room":true,"location":{"lat":39.178447,"lon":117.219999},"tags":["wifi","充电车位"],"comment_info":{"favourable_comment":20,"negative_comment":10}}