掘金 后端 ( ) • 2024-04-27 12:19

theme: scrolls-light

聚合

当用户使用搜索引擎完成搜索后,在展示结果钟需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。聚合的需求在很多应用程序钟都有所体现,例如在京东搜索“咸鸭蛋”,然后点击搜索界面钟的筛选按钮,在弹出的界面钟可以对当前的搜索结果进行进一步的过滤。例如,可以重价格区间、品牌、分类i、个数等维度分别进行筛选。

4f856c6d62bcc25d863cfd3e3d068a6

为方面介绍后续内容,这里定义酒店的索引如下:

 PUT /hotel_poly
 {
   "settings": {
     "number_of_shards": 1     //指定主分片个数为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"
           }
         }
       }
     }
   }
 }

向索引中写入示例数据

 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}}

1.1 聚合指标

在进行聚合搜索时,聚合的指标业务需求不仅是文档数量。例如,在酒店搜索场景中,我们希望看到以当前位置为中心点,周边各个区域酒店的平均加个。

1.1.1 常见的聚合指标

在搜索聚合时,用户可能关注字段的相关统计信息,例如平均值、最大值、最小值及加和值。例如,用户在使用一个二手房交易搜索引擎进行搜索时,可能会关注当前城市各个区域的房产平均价格。用户在搜索酒店时,也可能会关注附近各个区域酒店的最低价格。

ES聚合请求的地址也是索引的搜索地址,可以使用aggs子句封装聚合请求。

当使用avg子句进行平均值的聚合时,可以在avg子句中指定聚合的字段。在默认情况下,查询将匹配所有文档,如果不需要返回匹配的文档信息,最好将返回的文档个数设置为0。这样既可以让结果看起来更整洁,又可以提高查询速度。

下面的DSL将查询所有酒店的平均价格并且不反悔匹配的文档信息。

 GET /hotel_poly/_search
 {
   "size": 0,
   "aggs": {
     "my_agg": {     //聚合名称
       "avg": {
         "field": "price"    //计算文档的平均价格
       }
     }
   }
 }

ES返回结果如下:

image-20240421103943022

在上面的搜索结果中,索引的5个文档全部命中,由于DSL设置size为0,所以命中文档的信息没有显示。在搜索结果的aggregations子句中存储着聚合结果,其中my_agg是聚合的名称,其对应的value值就是具体聚合结果,即酒店的平均价格。

如果聚合的指标字段不是ES的基本类型,例如object类型,则可以使用点运算符进行引用。下面的DSL演示了该用法:

 GET /hotel_poly/_search
 {
   "size": 0,
   "aggs":{
     "my_agg":{
       "avg": {
         "field": "comment_info.favourable_comment"
       }
     }
   }
 }

image-20240421104222141

与平均值类似,最大值、最小值及加和值分别使用maxminsum子句进行聚合,不再赘述。

以下代码演示了在Java中使用聚合计算平均值的逻辑。

 @Test
 public void getAvgAggSearch() throws IOException {
     //创建搜索请求
     SearchRequest searchRequest = new SearchRequest("hotel_poly");
     SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
     String aggName="my_agg";   //聚合的名称
     //定义avg聚合,指定字段为price
     AvgAggregationBuilder aggregationBuilder = AggregationBuilders.avg(aggName).field("price");
     searchSourceBuilder.aggregation(aggregationBuilder);    //添加聚合
     searchRequest.source(searchSourceBuilder);  //设置查询请求
     //执行查询
     SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
     //获取聚合结果
     Aggregations aggregations = searchResponse.getAggregations();
     Avg avg = aggregations.get(aggName);    //获取avg聚合返回的对象
     String key = avg.getName();                     //获取聚合名称
     double avgValue = avg.getValue();
     System.out.println("key="+key+",aggValue="+avgValue);
 }

image-20240421104806086

为了避免多次请求,ES还提供了stats聚合。stats聚合可以将对应字段的最大值、最小值、平均值及加和值一起计算并返回计算结果。下面的DSL展示了stats的用法。

 GET /hotel_poly/_search
 {
   "size": 0,
   "aggs":{
     "my_agg":{
       "stats": {
         "field": "price"
       }
     }
   }
 }

image-20240421105354075

以下代码演示了在Java中使用stats聚合的逻辑。

 @Test
 public void getStatsAggSearch() throws IOException{
     //创建搜索请求
     SearchRequest searchRequest = new SearchRequest("hotel_poly");
     SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
     String aggName="my_agg";   //聚合的名称
     //定义avg聚合,指定字段为price
     StatsAggregationBuilder aggregationBuilder = AggregationBuilders.stats(aggName).field("price");
     searchSourceBuilder.aggregation(aggregationBuilder);    //添加聚合
     searchRequest.source(searchSourceBuilder);  //设置查询请求
     //执行查询
     SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
     //获取聚合结果
     Aggregations aggregations = searchResponse.getAggregations();
     Stats stats = aggregations.get(aggName);    //获取stats聚合返回的对象
     String key = stats.getName();
     double sumValue=stats.getSum();             //获取聚合加和值
     double avgVal=stats.getAvg();               //获取聚合平均值
     double countVal=stats.getCount();           //获取聚合文档数量值
     double maxVal=stats.getMax();               //获取聚合最大值
     double minVal=stats.getMin();               //获取聚合最小值
     log.info("key={}",key);                     //打印聚合名称
     log.info("sumVal={},avgVal={},countVal={},maxVal={},minVal={}",sumValue,avgVal,countVal,maxVal,minVal);
 }

image-20240421105500261

1.1.2 空值处理

在索引中的一部分文档很可能其某些字段是缺失的,在介绍空值处理前,首先介绍ES聚合查询提供的value_count聚合,该聚合用于统计字段非空值的个数。

 # value_count聚合统计price字段中非空值的个数
 GET /hotel_poly/_search
 {
   "size": 0,
   "aggs":{
     "my_agg":{
       "value_count": {
         "field": "price"
       }
     }
   }
 }

image-20240421105741226

通过上述结果可以看到,当前索引中price字段中的非空值有4个。

以下代码演示了在Java中使用value_count对price字段进行聚合的逻辑。

  public void getValueCountAggSearch() throws IOException{
         //创建搜索请求
         SearchRequest searchRequest = new SearchRequest("hotel_poly");
         SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
         String aggName="my_agg";   //聚合的名称
         //定义avg聚合,指定字段为price
         ValueCountAggregationBuilder aggregationBuilder = AggregationBuilders.count(aggName).field("price");
         searchSourceBuilder.aggregation(aggregationBuilder);    //添加聚合
         searchRequest.source(searchSourceBuilder);  //设置查询请求
         //执行查询
         SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
         //获取聚合结果
         Aggregations aggregations = searchResponse.getAggregations();
         //获取value_count聚合返回的对象
         ValueCount valueCount = aggregations.get(aggName);
         String key = valueCount.getName();
         long count = valueCount.getValue();
         log.info("key={},count={}",key,count);
     }

image-20240421110011904

需要说明的是,如果判断的字段是数组类型,则value_count统计的是符合条件的所有文档中该字段数组中非空元素个数的总和,而不是数组的个数总和

下面的DSL用于统计tags字段数组中非空元素个数的总和。

 #统计tags字段数组钟非空元素个数的总和
 GET /hotel_poly/_search
 {
   "size": 0,
   "aggs":{
     "my_agg":{
       "value_count": {
         "field": "tags"
       }
     }
   }
 }

在索引的5个文档中,除去文档003没有tags字段外,其他4个文档的tags字段数组中各有两个元素,因此聚合的值为$2\times 4=8$个,ES返回结果如下:

image-20240421110404683

上面的结果中,aggregations.my_agg.value的值为8,这和前面计算的数值相等,验证了使用value_count对数组字段进行聚合时,ES返回的结果是所有数组元素的个数总和。

如果需要以空值字段的数据作为聚合指标对其进行聚合,可以在指标统计中通过missing参数指定填充值对空值进行填充。

以下示例演示了对price字段进行聚合,并设定了当字段值为空值时使用100进行替代的DSL。

# missing参数指定填充值对空值进行填充
GET /hotel_poly/_search
{
  "size": 0,
  "aggs": {
    "my_agg": {
     "sum":{
       "field": "price",
       "missing": 100		//计算加和值时将price字段中的空值用100代替
     }
    }
  }
}

在索引中,文档002的price字段为空,因此被填充为100,文档001、003、004和005的price字段分别为556、200、500和800,因此符合聚合的值应该是$556+100+200+500+800=2156$。ES返回结果如下:

image-20240421111203396

以下代码演示了在Java中当聚合指标为空值时指定填充值的逻辑。

@Test
public void getSumAggSearch() throws IOException{
    //创建搜索请求
    SearchRequest searchRequest = new SearchRequest("hotel_poly");
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    String aggName="my_agg";   //聚合的名称
    //定义avg聚合,指定字段为price
    SumAggregationBuilder aggregationBuilder = AggregationBuilders.sum(aggName).field("price");
    aggregationBuilder.missing("100");
    searchSourceBuilder.aggregation(aggregationBuilder);    //添加聚合
    searchRequest.source(searchSourceBuilder);  //设置查询请求
    //执行查询
    SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
    //获取聚合结果
    Aggregations aggregations = searchResponse.getAggregations();
    //获取value_count聚合返回的对象
    Sum sum = aggregations.get(aggName);
    String key=sum.getName();
    double value = sum.getValue();
    log.info("key={},value={}",key,value);
}

image-20240421111302281