ES中的桶聚合

桶聚合

在前面几篇博客中介绍的聚合指标是指符合条件的文档字段的聚合,有时还需要根据某些维度进行聚合。例如在搜索酒店时,按照城市、是否满房、标签和创建时间等维度统计酒店的平均价格。这些字段统称为"桶",在同一维度内有一个或者多个桶。例如城市桶,有"北京"、"天津"等,是否满房桶,有"满房"和"非满房"。

1.1 单维度同聚合

最简单的桶聚合是单维度桶聚合,指的是按照一个维度对文档进行分组聚合。在桶聚合时,聚合的桶也需要匹配,匹配的方式有termsfilterranges等。这里只介绍比较有代表性的terms查询和ranges查询。

1.1.1 terms聚合

terms聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keywordboolkeyword数组等适合精确匹配的数据类型,因此不能对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_boundsum_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,其值分别为truefalse。另外,这两个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中添加分组,每个分组使用fromto表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。

示例如下:

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参数来设置距离的单位,可以选择mikm,分别表示米和千米。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}}
相关推荐
用户785127814707 分钟前
Python代码获取京东商品详情原数据 API 接口(item_get_app)
后端
JAVA数据结构9 分钟前
BPMN-Activiti-简单流程委托
后端
sivdead16 分钟前
智能体记忆机制详解
人工智能·后端·agent
拉不动的猪39 分钟前
图文引用打包时的常见情景解析
前端·javascript·后端
该用户已不存在1 小时前
程序员的噩梦,祖传代码该怎么下手?
前端·后端
间彧1 小时前
Redis缓存穿透、缓存雪崩、缓存击穿详解与代码实现
后端
摸鱼的春哥1 小时前
【编程】是什么编程思想,让老板对小伙怒飙英文?Are you OK?
前端·javascript·后端
Max8122 小时前
Agno Agent 服务端文件上传处理机制
后端
调试人生的显微镜2 小时前
苹果 App 怎么上架?从开发到发布的完整流程与使用 开心上架 跨平台上传
后端
顾漂亮2 小时前
Spring AOP 实战案例+避坑指南
java·后端·spring