2023-02-10 - 6 聚合

文章介绍了如何使用Elasticsearch(ES)进行聚合搜索,包括指标聚合如平均值、最大值、最小值,桶聚合如terms和ranges,以及桶内指标聚合。此外,还讨论了处理空值、多维度桶嵌套聚合、前过滤器和后过滤器的使用,以及Tophits和Collapse聚合在分页和文档展示上的应用。
摘要由CSDN通过智能技术生成

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

在这里插入图片描述
ES支持丰富的聚合操作,不仅可以使用聚合功能对文档进行计数,还可以计算文档字段的平均值、最大值和最小值等。ES还提供了桶聚合的功能,以便于对多维度数据进行聚合。本章将结合实例介绍这些内容。另外,如果希望搜索结果和聚合结果一起返回,其中绕不开的一个主题就是分页和排序,本章也会对这两部内容进行介绍。

为方便介绍,下面重新定义酒店的索引。

PUT /hotel 
{ 
  "settings": { 
    "number_of_shards": 1                
  },  
  "mappings": { 
    "properties": { 
      "title": {                        
        "type": "text" 
      }, 
     "city": {                        
        "type": "keyword" 
      }, 
      "price": {                      
        "type": "double" 
      }, 
      "create_time": {                  
        "type": "keyword" 
      }, 
      "full_room": {             
        "type": "boolean" 
      },
      "tags": {                      
        "type": "keyword" 
      }
    } 
  } 
}

接着向索引中写入示例数据,具体如下:

POST /_bulk 
{"index":{"_index":"hotel","_id":"001"}} 
{"title": "文雅酒假日酒店","city":"北京","price": 556.00,"create_time": "20200418120000","full_room":true, "tags":["wifi","小型电影院"],"comment_info":{"favourable_comment":20, "negative_comment":10}} 
{"index":{"_index":"hotel","_id":"002"}} 
{"title": "金都嘉怡假日酒店","city":"北京","create_time":"20210315200000", "full_room":false,"tags": ["wifi","免费早餐"],"comment_info":{"favourable_comment":20,"negative_ comment":10}} 
{"index":{"_index":"hotel","_id":"003"}} 
{"title": "金都假日酒店","city":"北京","price": 200.00,"create_time": "20210509160000","full_room":true,"comment_info":{"favourable_comment":20,"negative_comment":10}} 
{"index":{"_index":"hotel","_id":"004"}} 
{"title": "金都假日酒店","city":"天津","price": 500.00,"create_time": "20210218080000","full_room":false,"tags":["wifi","免费车位"]} 
{"index":{"_index":"hotel","_id":"005"}} 
{"title": "文雅精选酒店","city":"天津","price": 800.00,"create_time": "20210101080000","full_room":true,"tags":["wifi","充电车位"],"comment_info":{"favourable_comment" :20,"negative_comment":10}}

1 聚合指标

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

在搜索聚合时,用户可能会关注字段的相关统计信息,例如平均值、最大值、最小值及加和值等。例如,用户在使用一个二手房交易搜索引擎进行搜索时,可能会关注当前城市各个区域的房产平均价格。再例如,用户在搜索酒店时,也可能会关注附近各个区域酒店的最低价格。如图7.2所示,左图为在链家App的地图模式,可以搜索到当前位置附近的二手房平均交易价格;右图为携程App的地图模式,可以搜索到当前位置附近最低价格的酒店。

在这里插入图片描述

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

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

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

在上面的DSL中,设定avg聚合的字段为price字段,并设置size参数的值为0。该DSL被执行后,ES的返回结果如下:

{"hits" : {       //命中的文档列表 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
 "aggregations" : {     //聚合结果 
    "my_agg" : {       //聚合名称 
      "value" : 514.0     //聚合指标值 
    } 
  } 
} 

在上面的搜索结果中,索引中的5个文档全部命中,由于DSL设置size为0,所以命中文档的信息没有显示。在搜索结果的aggregations子句中存储着聚合结果,其中my_agg是聚合的名称,其对应的value值就是具体聚合结果,即酒店的平均价格。
如果聚合的指标字段不是ES的基本类型,例如object类型,则可以使用点运算符进行引用。下面的DSL演示了该用法:

GET /hotel/_search 
{ 
  "size": 0,  
  "aggs": { 
    "my_agg": { 
      "avg": { 
        //使用点运算符引用object类型字段的数据 
        "field": "comment_info.favourable_comment" 
      } 
    } 
  } 
} 

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

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

public void getAvgAggSearch() throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    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+",avgValue="+avgValue); //打印结果 
} 

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

GET /hotel/_search 
{ 
  "size": 0,  
  "aggs": { 
    "my_agg": { 
      "stats": {             //使用stats运算符计算多个指标 
        "field": "price" 
      } 
    } 
  } 
}  

在上面的DSL中,对所有酒店进行了常用统计指标的聚合,查询结果如下:

{ 
  "… 
  "hits" : { 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : { 
     "count" : 4,          //文档数量 
      "min" : 200.0,       //聚合的价格最小值 
      "max" : 800.0,       //聚合的价格最大值 
      "avg" : 514.0,       //聚合的价格平均值 
      "sum" : 2056.0       //聚合的价格加和值 
    } 
  } 
}  

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

public void getStatsAggSearch () throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
   String aggName="my_agg";                           //聚合的名称 
    //定义stats聚合,指定字段为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 sumVal =stats.getSum() ;                   //获取聚合加和值 
    double avgVal =stats.getAvg() ;                  //获取聚合平均值 
   long countVal =stats.getCount() ;                 //获取聚合文档数量值 
   double maxVal =stats.getMax() ;                   //获取聚合最大值 
   double minVal =stats.getMin() ;                   //获取聚合最小值 
    System.out.println("key="+key);                  //打印聚合名称 
   System.out.println("sumVal="+sumVal+",avgVal="+avgVal+",countVal= 
"+countVal+",maxVal="+maxVal+",minVal="+minVal);     //打印结果 
} 

2 空值处理

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

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

执行上述DSL后,ES返回的结果如下:

{ 
  .. 
  "hits" : { 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : {                    //price字段中非空值的个数 
      "value" : 4 
    } 
  } 
} 

通过上述结果可以看到,当前索引中price字段中的非空值有4个。
下面的代码演示了在Java中使用value_count对price字段进行聚合的逻辑。

public void getValueCountAggSearch () throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
   String aggName="my_agg";       //聚合的名称 
    //定义value_count聚合,指定字段为price 
    ValueCountAggregationBuilder aggregationBuilder = AggregationBuilders. 
count(aggName).field("price"); 
    aggregationBuilder.missing("200"); 
    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();              //获取聚合值 
    System.out.println("key="+key+",count="+count);  //打印结果 
}  

需要指出的是,如果判断的字段是数组类型,则value_count统计的是符合条件的所有文档中该字段数组中非空元素个数的总和,而不是数组的个数总和。下面的DSL用于统计tags字段数组中非空元素个数的总和。

GET /hotel/_search 
{ 
  "size": 0,  
  "aggs": { 
    "my_agg": { 
      "value_count": {              //统计tags字段数组中非空元素的个数 
        "field": "tags" 
      } 
    } 
  } 
} 

在索引的5个文档中,除去文档003没有tags字段外,其他4个文档的tags字段数组中各有两个元素,因此聚合的值为2×4=8个。下面来看一下ES返回的内容:

{. 
  "hits" : { 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : { 
      "value" : 8                  //tags字段数组中非空元素的个数 
    } 
  } 
} 

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

如果需要以空值字段的数据作为聚合指标对其进行聚合,可以在指标统计中通过missing参数指定填充值对空值进行填充。以下示例演示了对price字段进行聚合,并设定了当字段值为空值时使用100进行替代的查询请求。

GET /hotel/_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返回的结果如下:

{"hits" : { 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : { 
      "value" : 2156.0          //将price字段中的空值用100代替后的加和结果 
    } 
  } 
}  

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

public void getSumAggSearch () throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
   String aggName="my_agg";                              //聚合的名称 
    //定义sum聚合,指定字段为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(); 
    Sum sum = aggregations.get(aggName);                  //获取sum聚合返回的对象 
   String key=sum.getName();                              //获取聚合名称 
    double sumVal = sum.getValue();                       //获取聚合值 
    System.out.println("key="+key+",count="+sumVal);      //打印结果 
}  

3 桶聚合

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

1 单维度桶聚合

最简单的桶聚合是单维度桶聚合,指的是按照一个维度对文档进行分组聚合。在桶聚合时,聚合的桶也需要匹配,匹配的方式有terms、filter和ranges等。本节只介绍比较有代表性的terms查询和ranges查询,对其他匹配方式感兴趣读者可以阅读相关文档进行学习,这里不再赘述。

terms聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keyword、bool、keyword数组等适合精确匹配的数据类型,因此不能对text字段直接使用terms聚合,如果对text字段有terms聚合的需求,则需要在创建索引时为该字段增加多字段功能。
以下的DSL描述的是按照城市进行聚合的查询:

GET /hotel/_search 
{ 
  "size": 0,  
  "aggs": { 
    "my_agg": { 
      "terms": {                    //按照城市进行聚合 
        "field": "city" 
      } 
    } 
  } 
} 

因为ES支持多桶聚合,所以每个桶聚合需要定义一个名字,此处定义了一个桶聚合,名字为my_agg。在这个桶聚合中使用了一个terms聚合,聚合字段选择了城市,目的是统计各个城市的酒店的文档个数。在聚合外面,因为不希望返回任何文档,所以指定查询返回的文档为0。执行该DSL后,ES返回的结果如下:

{"hits" : {"hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : {                         //单维度聚合的名称 
      "doc_count_error_upper_bound" : 0, //可能被遗漏的文档数量的最大值 
      "sum_other_doc_count" : 0,         //除了返回给用户的文档外剩下的文档总数 
     "buckets" : [                       //聚合桶 
        { 
          "key" : "北京", 
          "doc_count" : 3                //该聚合桶下的文档数量 
        }, 
        { 
          "key" : "天津", 
          "doc_count" : 2                //该聚合桶下的文档数量 
        } 
      ] 
    } 
  } 
}  

在默认情况下,进行桶聚合时如果不指定指标,则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表示被遗漏的文档数量可能存在的最大值,sum_other_doc_count表示除了返回给用户的文档外剩下的文档总数。

以下DSL是按照满房状态进行聚合的查询,注意该字段是bool型:

GET /hotel/_search 
{ 
  "size": 0,  
  "aggs": { 
    "my_agg": { 
      "terms": { 
        "field": "full_room"            //按照满房状态进行聚合 
      } 
    } 
  } 
}   

执行DSL后,ES返回的结果如下:

{"hits" : { 
    "total" : { 
      "value" : 5, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
      "buckets" : [ 
        { 
          "key" : 1, 
          "key_as_string" : "true",    //聚合桶的字符串形式 
          "doc_count" : 3 
        }, 
        { 
          "key" : 0, 
          "key_as_string" : "false",   //聚合桶的字符串形式 
          "doc_count" : 2 
        } 
      ] 
    } 
  } 
}  

从上述结果中可以看到,在满房和非满房的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聚合进行单维度桶聚合的逻辑:

public void getBucketDocCountAggSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    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();         //获取文档个数 
        System.out.println("termsKey=" + bucketKey + ",docCount=" + docCount); 
    } 
}   

除了terms聚合,ranges聚合也是经常使用的一种聚合。它匹配的是数值字段,表示按照数值范围进行分组。用户可以在ranges中添加分组,每个分组用from和to表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。以下DSL演示了使用ranges进行聚合的方法:

GET /hotel/_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返回的结果如下:

{"hits" : {"hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : {           //range聚合名称 
      "buckets" : [        //范围聚合桶 
        { 
          "key" : "*-200.0", 
          "to" : 200.0, 
          "doc_count" : 0 
        }, 
        { 
          "key" : "200.0-500.0", 
          "from" : 200.0, 
          "to" : 500.0, 
          "doc_count" : 1 
        }, 
        { 
          "key" : "500.0-*", 
          "from" : 500.0, 
          "doc_count" : 3 
        } 
      ] 
    } 
  } 
} 

在上面的分组划分中,第一个分组规则为price<200,没有文档与其匹配,因此其doc_count为0;第二个分组规则为200≤price<500,文档003与其匹配,因此其doc_count为1;第三个分组规则为price>500,文档001、004和005与其匹配,因此其doc_count值为3。

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

public void getRangeDocCountAggSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    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)); 
    searchSourceBuilder.aggregation(rangeAgg);  //添加ranges聚合 
    searchRequest.source(searchSourceBuilder);  //设置查询请求 
    //执行查询 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT); 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    Range range = aggregations.get(rangeAggName);//获取range聚合返回的对象 
    for (Range.Bucket bucket : range.getBuckets()) { 
        String bucketKey = bucket.getKeyAsString(); //获取桶名称 
        long docCount = bucket.getDocCount();       //获取聚合文档个数 
        System.out.println("bucketKey=" + bucketKey + ",docCount=" + docCount); 
    } 
}  

有时还需要对单维度桶指定聚合指标,聚合指标单独使用子aggs进行封装,该aggs子句的使用方式和上一节介绍的聚合指标相同。以下请求表示按照城市维度进行聚合,统计各个城市的平均酒店价格:

GET /hotel/_search 
{ 
  "size": 0, 
  "aggs": { 
   "my_agg": {                    //单维度聚合名称 
      "terms": {                  //定义单维度桶 
        "field": "city" 
      }, 
     "aggs": {                    //用于封装单维度桶下的聚合指标 
        "my_sum": {               //聚合指标名称 
          "sum": {                //对price字段进行加和 
            "field": "price", 
            "missing": 200 
          } 
        } 
      } 
    } 
  } 
}  

执行上述DSL后,ES返回的结果如下:

{"hits" : {}, 
  "aggregations" : { 
    "my_agg" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
      "buckets" : [              //单维度桶列表 
        {                        //具体的单维度桶 
          "key" : "北京", 
          "doc_count" : 3, 
          "my_sum" : {           //聚合指标 
            "value" : 956.0 
          } 
        }, 
        {                        //具体的单维度桶 
          "key" : "天津", 
          "doc_count" : 2, 
          "my_sum" : {           //聚合指标 
            "value" : 1300.0 
          } 
        } 
      ] 
    } 
  } 
} 

在上面的结果中,聚合桶的维度是城市,当前索引中城市为“北京”的文档个数为3,城市为“天津”的文档个数为2。将这两组文档的聚合结果在buckets子句中进行了封装,可以根据key字段进行聚合桶的识别,每个聚合的组中既有文档个数又有价格的加和值。

以下代码演示了在Java中使用桶聚合和指标聚合的逻辑:

public void getBucketAggSearch () throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    String termsAggName="my_terms";      //聚合的名称 
    //定义terms聚合,指定字段为城市 
    TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders. 
terms(termsAggName).field("city"); 
    String sumAggName="my_sum";          //sum聚合的名称 
    //定义sum聚合,指定字段为价格 
    SumAggregationBuilder sumAggregationBuilder = AggregationBuilders. 
sum(sumAggName).field("price"); 
    //定义聚合的父子关系 
    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(); 
        System.out.println("termsKey="+termsKey); 
        Sum sum =bucket.getAggregations().get(sumAggName); 
       String key=sum.getName();                         //获取聚合名称 
        double sumVal = sum.getValue();                  //获取聚合值 
        System.out.println("key="+key+",count="+sumVal); //打印结果 
    } 
} 
2 多维度桶嵌套聚合

在某些业务需求中,不仅需要一个维度的桶聚合,而且还可能有多维度桶嵌套聚合的需求。例如在搜索酒店时,可能需要统计各个城市的满房和非满房状态下的酒店平均价格。ES支持嵌套桶聚合,进行嵌套时,可以使用aggs子句进行子桶的继续嵌套,指标放在最里面的子桶内。以下DSL演示了多维度桶的使用方法:

GET /hotel/_search 
{ 
  "size": 0, 
  "aggs": { 
   "group_city": {               //多维度桶名称 
      "terms": { 
        "field": "city" 
      }, 
     "aggs": {                  //单维度桶 
        "group_full_room": { 
          "terms": { 
            "field": "full_room" 
          }, 
          "aggs": {             //聚合指标 
            "my_sum": { 
              "avg": { 
                "field": "price", 
                "missing": 200 
              } 
            } 
          } 
        } 
      } 
    } 
  } 
}  

上述DSL被执行后,ES返回的结果如下:

{"hits" : {}, 
  "aggregations" : { 
    "group_city" : {                              //多维度聚合名称 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
     "buckets" : [                                 //第一层桶聚合列表 
        { 
          "key" : "北京",                          //第一层桶的key 
          "doc_count" : 3, 
          "group_full_room" : {                    //第二层桶聚合 
            "doc_count_error_upper_bound" : 0, 
            "sum_other_doc_count" : 0, 
            "buckets" : [                          //单维度聚合列表 
              { 
                "key" : 1, 
                "key_as_string" : "true", 
                "doc_count" : 2, 
                "my_sum" : {                       //聚合指标 
                  "value" : 378.0 
                } 
              }, 
              { 
                "key" : 0, 
                "key_as_string" : "false", 
                "doc_count" : 1, 
                "my_sum" : { 
                  "value" : 200.0 
                } 
              } 
            ] 
          } 
        }, 
        { 
          "key" : "天津", 
          "doc_count" : 2, 
          "group_full_room" : { 
            "doc_count_error_upper_bound" : 0, 
            "sum_other_doc_count" : 0, 
            "buckets" : [ 
              { 
                "key" : 0, 
                "key_as_string" : "false", 
                "doc_count" : 1, 
                "my_sum" : { 
                  "value" : 500.0 
                } 
              }, 
              { 
                "key" : 1, 
                "key_as_string" : "true", 
                "doc_count" : 1, 
                "my_sum" : { 
                  "value" : 800.0 
                } 
              } 
            ] 
          } 
        } 
      ] 
    } 
  } 
} 

结果中可以看到,第一层的分桶先按照城市分组分为“北京”“天津”;第二层在“北京”“天津”桶下面继续分桶,分为“满房”“非满房”桶,对应的聚合指标即价格的加和值存储在内部的my_sum字段中。

以下代码演示了在Java中使用多维度桶进行聚合的逻辑:

public void getExternalBucketAggSearch () throws IOException{ 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    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 termsArrFullRoom = AggregationBuilders.terms 
(aggNameCity).field("full_room"); 
 
    String sumAggName="my_sum";                    //sum聚合的名称 
    //定义sum聚合,指定字段为价格 
    SumAggregationBuilder sumAgg = AggregationBuilders.sum(sumAggName). 
field("price"); 
 
    //定义聚合的父子关系 
    termsArrFullRoom.subAggregation(sumAgg); 
    termsAggCity.subAggregation(termsArrFullRoom); 
    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(); 
        System.out.println("--------"+"termsKeyCity="+termsKeyCity+ 
"--------"); 
        Terms termsFullRom   =bucket.getAggregations().get(aggNameCity); 
        //遍历第二层bucket 
        for(Terms.Bucket bucketFullRoom:termsFullRom.getBuckets()){ 
            //获取第二层bucket名称 
            String termsKeyFullRoom=bucketFullRoom.getKeyAsString(); 
            System.out.println("termsKeyFullRoom="+termsKeyFullRoom); 
            //获取聚合指标 
            Sum sum =bucketFullRoom.getAggregations().get(sumAggName); 
            String key=sum.getName();          //获取聚合指标名称 
            double sumVal = sum.getValue();    //获取聚合指标值 
            System.out.println("key="+key+",count="+sumVal);  //打印结果 
        } 
    } 
} 

4 聚合方式

ES支持灵活的聚合方式,它不仅支持聚合和查询相结合,而且还可以使聚合的过滤条件不影响搜索条件,并且还支持在聚合后的结果中进行过滤筛选。本节将介绍这些聚合方式。

1 直接聚合

直接聚合指的是聚合时的DSL没有query子句,是直接对索引内的所有文档进行聚合。前面介绍的示例都属于直接聚合,这里不再进行演示。

2 先查询再聚合

与直接聚合相对应,这种查询方式需要增加query子句,query子句和普通的query查询没有区别,参加聚合的文档必须匹配query查询。下面的DSL演示了这种用法:

GET /hotel/_search 
{ 
  "size": 0,  
 "query": {                     //指定查询query逻辑 
    "term": { 
      "city": { 
        "value": "北京" 
      } 
    } 
  }, 
 "aggs": {                     //指定聚合逻辑 
    "my_agg": { 
      "avg": { 
        "field": "price" 
      } 
    } 
  } 
}  

以下代码演示了在Java中先查询再聚合的逻辑:

public void getQueryAggSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
 
    String avgAggName = "my_avg";                      //avg聚合的名称 
    //定义sum聚合,指定字段为价格 
    AvgAggregationBuilder avgAgg = AggregationBuilders.avg(avgAggName). 
field("price"); 
 
    searchSourceBuilder.aggregation(avgAgg);           //添加聚合 
    //构建query查询 
    searchSourceBuilder.query(QueryBuilders.termQuery("city", "北京")); 
    searchRequest.source(searchSourceBuilder);         //设置查询请求 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                               //执行搜索 
    SearchHits searchHits = searchResponse.getHits();  //获取搜索结果集 
    System.out.println("---------------hit--------------"); 
    for (SearchHit searchHit : searchHits) {           //遍历搜索结果集 
        String index = searchHit.getIndex();           //获取索引名称 
        String id = searchHit.getId();                 //获取文档_id 
        Float score = searchHit.getScore();            //获取得分 
        String source = searchHit.getSourceAsString(); //获取文档内容 
        System.out.println("index=" + index + ",id=" + id + ",source=" +  
source);                                               //打印数据 
    } 
    System.out.println("---------------agg--------------"); 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    ParsedAvg avg = aggregations.get(avgAggName);      //获取聚合返回的对象 
    String avgName = avg.getName();                    //获取聚合名称 
    double avgVal = avg.getValue();                    //获取聚合值 
    //打印结果 
    System.out.println("avgName=" + avgName + ",avgVal=" + avgVal); 
} 
3 前过滤器

有时需要对聚合条件进一步地过滤,但是又不能影响当前的查询条件。例如用户进行酒店搜索时的搜索条件是天津的酒店,但是聚合时需要将非满房的酒店平均价格进行聚合并展示给用户。此时不能变更用户的查询条件,需要在聚合子句中添加过滤条件。下面的DSL展示了在聚合时使用过滤条件的用法:

GET /hotel/_search 
{ 
  "size": 0, 
 "query": {                   //指定查询的query逻辑 
    "term": { 
      "city": { 
        "value": "天津" 
      } 
    } 
  }, 
  "aggs": { 
    "my_agg": { 
     "filter": {              //指定过滤器逻辑 
        "term": { 
          "full_room": false 
        } 
      }, 
     "aggs": {                //指定聚合逻辑 
        "my_avg": { 
          "avg": { 
            "field": "price" 
          } 
        } 
      } 
    } 
  } 
} 

执行上述DSL后,ES返回的结果如下:

{"hits" : { 
    "total" : { 
      "value" : 2, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : { 
     "doc_count" : 1,        //只有文档004没有被过滤 
      "my_avg" : { 
        "value" : 500.0 
      } 
    } 
  } 
} 

通过上述结果可以知道,满足查询条件的文档个数为2,命中的文档为004和005,但是在聚合时要求匹配非满房的酒店,只有文档004满足聚合条件,因此酒店的平均值为文档004的price字段值。

以下代码演示了在Java中使用前过滤器的逻辑:

public void getFilterAggSearch() throws IOException {  
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    String filterAggName = "my_terms";               //聚合的名称 
    TermQueryBuilder termQueryBuilder=QueryBuilders.termQuery("full_room", 
true); 
    FilterAggregationBuilder filterAggregationBuilder=AggregationBuilders. 
filter(filterAggName,termQueryBuilder); 
 
    String avgAggName = "my_avg";                    //avg聚合的名称 
    //定义聚合,指定字段为价格 
    AvgAggregationBuilder avgAgg= AggregationBuilders.avg(avgAggName). 
field("price"); 
 
    //为filter聚合添加子聚合 
    filterAggregationBuilder.subAggregation(avgAgg); 
    searchSourceBuilder.aggregation(filterAggregationBuilder); //添加聚合 
    //构建term查询 
    searchSourceBuilder.query(QueryBuilders.termQuery("city", "天津")); 
    searchRequest.source(searchSourceBuilder);      //设置查询请求 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                            //执行搜索 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    //获取sum聚合返回的对象 
    ParsedFilter filter = aggregations.get(filterAggName); 
    Avg avg =filter.getAggregations().get(avgAggName); 
   String key = avg.getName();                   //获取聚合名称 
    double avgVal = avg.getValue();              //获取聚合值 
    System.out.println("key=" + key + ",avgVal=" + avgVal); //打印结果 
} 
4 后过滤器

在有些场景中,需要根据条件进行数据查询,但是聚合的结果集不受影响。例如在酒店搜索场景中,用户的查询词为“假日”,此时应该展现标题中带有“假日”的酒店。但是在该页面中,如果还希望给用户呈现出全国各个城市的酒店的平均价格,这时可以使用ES提供的后过滤器功能。该过滤器是在查询和聚合之后进行过滤的,因此它的过滤条件对聚合没有影响。以下的DSL展示了后过滤器的使用:

GET /hotel/_search 
{ 
  "size": 0,  
 "query": {                      //指定查询的query逻辑 
    "match": { 
      "title": "假日" 
    } 
  }, 
 "post_filter": {                //指定后过滤器逻辑 
    "term": { 
      "city": "北京" 
    } 
  },  
 "aggs": {                       //指定聚合逻辑 
    "my_agg": { 
      "avg": { 
        "field": "price", 
        "missing":200 
      } 
    } 
  } 
}  

在上面的查询中,使用match匹配title中包含“假日”的酒店,并且查询出这些酒店的平均价格,最后使用post_filter设置后过滤器的条件,将酒店的城市锁定为“北京”,执行该DSL后,ES返回的结果如下:

{"hits" : { 
    "total" : { 
      "value" : 3, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ] 
  }, 
  "aggregations" : { 
    "my_agg" : {               //聚合时酒店的城市锁定为“北京” 
      "value" : 364.0 
    } 
  } 
}  

根据查询结果可知,match查询命中了4个文档,对这4个文档的price字段取平均值为364,最后通过post_filter将其中的文档004过滤掉,因此hits子句中的total数量为3。
以下代码演示了在Java中使用后过滤器的逻辑:

public void getPostFilterAggSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
 
    String avgAggName = "my_avg";                  //avg聚合的名称 
    //定义sum聚合,指定字段为价格 
    AvgAggregationBuilder avgAgg= AggregationBuilders.avg(avgAggName). 
field("price"); 
   avgAgg.missing(200);                            //设置默认值为200 
    searchSourceBuilder.aggregation(avgAgg);       //添加聚合 
    //构建term查询 
    searchSourceBuilder.query(QueryBuilders.matchQuery("title", "假日")); 
    TermQueryBuilder termQueryBuilder=QueryBuilders.termQuery("city","北京"); 
    searchSourceBuilder.postFilter(termQueryBuilder); 
    searchRequest.source(searchSourceBuilder);     //设置查询请求 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                           //执行搜索 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    Avg avg =aggregations.get(avgAggName); 
   String key = avg.getName();                     //获取聚合名称 
    double avgVal = avg.getValue();                //获取聚合值 
    System.out.println("key=" + key + ",avgVal=" + avgVal); //打印结果 
} 

5 聚合排序

根据前面的介绍可知,ES对于聚合结果的默认排序规则有时并非是我们期望的。可以使用ES提供的sort子句进行自定义排序,有多种排序方式供用户选择:可以按照聚合后的文档计数的大小进行排序;可以按照聚合后的某个指标进行排序;还可以按照每个组的名称进行排序。下面将介绍以上3种排序功能。

1 按文档计数排序

在聚合排序时,业务需求可能有按照每个组聚合后的文档数量进行排序的场景。此时可以使用_count来引用每组聚合的文档计数进行排序。以下DSL演示了按照城市的酒店平均价格进行聚合,并按照聚合后的文档计数进行升序排列的请求:

GET /hotel/_search 
{ 
  "size": 0, 
  "aggs": { 
    "group_city": { 
      "terms": { 
        "field": "city", 
        "order": {                     //按照文档计数进行升序排列 
          "_count": "asc" 
        } 
      }, 
      "aggs": { 
        "my_avg": { 
          "avg": {                    //使用价格平均值作为聚合指标 
            "field": "price", 
            "missing": 200 
          } 
        } 
      } 
    } 
  } 
}  

执行上述DSL后,ES返回的结果如下:

{"hits" : {}, 
  "aggregations" : { 
    "group_city" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
      "buckets" : [                   //按照文档计数对桶聚合进行排序 
        { 
          "key" : "天津", 
          "doc_count" : 2, 
          "my_avg" : { 
            "value" : 650.0 
          } 
        }, 
        { 
          "key" : "北京", 
          "doc_count" : 3, 
          "my_avg" : { 
            "value" : 318.6666666666667 
          } 
        } 
      ] 
    } 
  } 
}  

以下代码演示了在Java中使用文档计数进行聚合排序的逻辑:

public void getAggDocCountOrderSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    String termsAggName = "my_terms";                  //聚合的名称 
    //定义terms聚合,指定字段为城市 
    TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders. 
terms(termsAggName).field("city"); 
    BucketOrder bucketOrder=BucketOrder.count(true); 
    termsAggregationBuilder.order(bucketOrder); 
    String avgAggName = "my_avg";                      //avg聚合的名称 
    //定义sum聚合,指定字段为价格 
    SumAggregationBuilder avgAgg = AggregationBuilders.sum(avgAggName). 
field("price"); 
    //定义聚合的父子关系 
    termsAggregationBuilder.subAggregation(avgAgg); 
    searchSourceBuilder.aggregation(termsAggregationBuilder); //添加聚合 
    searchRequest.source(searchSourceBuilder);         //设置查询请求 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                               //执行搜索 
    SearchHits searchHits = searchResponse.getHits();  //获取搜索结果集 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    Terms terms = aggregations.get(termsAggName);      //获取聚合返回的对象 
    for (Terms.Bucket bucket : terms.getBuckets()) { 
        String bucketKey = bucket.getKey().toString(); 
        System.out.println("termsKey=" + bucketKey); 
        Sum sum = bucket.getAggregations().get(avgAggName); 
        String key = sum.getName();                    //获取聚合名称 
        double sumVal = sum.getValue();                //获取聚合值 
        System.out.println("key=" + key + ",count=" + sumVal);  //打印结果 
    } 
}  
2 按聚合指标排序

在聚合排序时,业务需求可能有按照每个组聚合后的指标值进行排序的场景。此时可以使用指标的聚合名称来引用每组聚合的文档计数。以下DSL演示了按照城市的酒店平均价格进行聚合,并按照聚合后的平均价格进行升序排列的请求:

GET /hotel/_search 
{ 
  "size": 0, 
  "aggs": { 
    "group_city": { 
      "terms": { 
        "field": "city", 
        "order": {                   //按照聚合指标进行升序排列 
          "my_avg": "asc" 
        } 
      }, 
      "aggs": { 
        "my_avg": {                  //定义聚合指标 
          "avg": { 
            "field": "price", 
            "missing": 200 
          } 
        } 
      } 
    } 
  } 
} 

执行上述DSL后,ES返回的结果如下:

"hits" : {}, 
  "aggregations" : { 
    "group_city" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
     "buckets" : [                   //按照价格平均值对桶聚合升序排列 
        { 
          "key" : "北京", 
          "doc_count" : 3, 
          "my_avg" : { 
            "value" : 318.6666666666667 
          } 
        }, 
        { 
          "key" : "天津", 
          "doc_count" : 2, 
          "my_avg" : { 
            "value" : 650.0 
          } 
        } 
      ] 
    } 
  } 
} 

以下代码演示了在Java中按照聚合指标进行聚合排序的逻辑:

public void getAggMetricsOrderSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    String termsAggName = "my_terms";   //聚合的名称 
    //定义terms聚合,指定字段为城市 
    String avgAggName = "my_avg";              //avg聚合的名称 
    TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders. 
terms(termsAggName).field("city"); 
    BucketOrder bucketOrder=BucketOrder.aggregation(avgAggName,true); 
    termsAggregationBuilder.order(bucketOrder); 
 
    //定义sum聚合,指定字段为价格 
    AvgAggregationBuilder avgAgg = AggregationBuilders.avg(avgAggName). 
field("price"); 
    //定义聚合的父子关系 
    termsAggregationBuilder.subAggregation(avgAgg); 
    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.getKey().toString(); 
        System.out.println("termsKey=" + bucketKey); 
        Avg avg = bucket.getAggregations().get(avgAggName); 
        String key = avg.getName();                //获取聚合名称 
        double avgVal = avg.getValue();            //获取聚合值 
        System.out.println("key=" + key + ",avgVal=" + avgVal);//打印结果 
    } 
} 
3按分组key排序

在聚合排序时,业务需求可能有按照每个分组的组名称排序的场景。此时可以使用_key来引用分组名称。以下DSL演示了按照城市的酒店平均价格进行聚合,并按照聚合后的分组名称进行升序排列的请求:

GET /hotel/_search 
{ 
  "size": 0, 
  "aggs": { 
    "group_city": { 
      "terms": { 
        "field": "city", 
        "order": {                 //按照分组key的自然顺序升序排列 
          "_key": "asc" 
        } 
      }, 
      "aggs": { 
        "my_avg": {                //定义聚合指标 
          "avg": { 
            "field": "price", 
            "missing": 200 
          } 
        } 
      } 
    } 
  } 
}  

执行上述DSL后,ES返回的结果如下:

{"hits" : {}, 
  "aggregations" : { 
    "group_city" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
      "buckets" : [                //按照分组key的自然顺序对桶聚合升序排列 
        { 
          "key" : "北京", 
          "doc_count" : 3, 
          "my_avg" : { 
            "value" : 318.6666666666667 
          } 
        }, 
        { 
          "key" : "天津", 
          "doc_count" : 2, 
          "my_avg" : { 
            "value" : 650.0 
          } 
        } 
      ] 
    } 
  } 
}   

以下代码演示了在Java中按照分组key进行聚合排序的逻辑:

public void getAggKeyOrderSearch() throws IOException { 
    //创建搜索请求 
    SearchRequest searchRequest = new SearchRequest("hotel"); 
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
    String termsAggName = "my_terms";       //聚合的名称 
    //定义terms聚合,指定字段为城市 
    String avgAggName = "my_avg";           //avg聚合的名称 
    TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders. 
terms(termsAggName).field("city"); 
    BucketOrder bucketOrder=BucketOrder.key(true); 
    termsAggregationBuilder.order(bucketOrder); 
 
    //定义sum聚合,指定字段为价格 
    SumAggregationBuilder avgAgg = AggregationBuilders.sum(avgAggName). 
field("price"); 
    //定义聚合的父子关系 
    termsAggregationBuilder.subAggregation(avgAgg); 
    searchSourceBuilder.aggregation(termsAggregationBuilder); //添加聚合 
    searchRequest.source(searchSourceBuilder);         //设置查询请求 
    SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                               //执行搜索 
    SearchHits searchHits = searchResponse.getHits();  //获取搜索结果集 
    //获取聚合结果 
    Aggregations aggregations = searchResponse.getAggregations(); 
    Terms terms = aggregations.get(termsAggName); //获取sum聚合返回的对象 
    for (Terms.Bucket bucket : terms.getBuckets()) { 
        String bucketKey = bucket.getKey().toString(); 
        System.out.println("termsKey=" + bucketKey); 
        Sum sum = bucket.getAggregations().get(avgAggName); 
        String key = sum.getName();         //获取聚合名称 
        double sumVal = sum.getValue();     //获取聚合值 
        System.out.println("key=" + key + ",count=" + sumVal);//打印结果 
    } 
}  

4 聚合分页

ES支持同时返回查询结果和聚合结果,前面介绍聚合查询时,查询结果和聚合结果各自封装在不同的子句中。但有时我们希望聚合的结果按照每组选出前N个文档的方式进行呈现,最常见的一个场景就是电商搜索,如搜索苹果手机6S,搜索结果应该展示苹果手机6S型号中的一款手机即可,而不论该型号手机的颜色有多少种。另外,当聚合结果和查询结果封装在一起时,还需要考虑对结果分页的问题,此时前面介绍的聚合查询就不能解决这些问题了。ES提供的Top hits聚合和Collapse聚合可以满足上述需求,但是这两种查询的分页方案是不同的。本节将介绍Top hits聚合和Collapse聚合,并分别给出这两种查询的分页方案。

1 Top hits聚合

顾名思义,Top hits聚合指的是聚合时在每个分组内部按照某个规则选出前N个文档进行展示。例如,搜索“金都”时,如果希望按照城市分组,每组按照匹配分数降序展示3条文档数据,DSL如下:

GET /hotel/_search 
{ 
  "size": 0, 
  "query": { 
    "match": { 
      "title": "金都" 
    } 
  },  
  "aggs": { 
   "group_city": {                //按照城市进行桶聚合 
      "terms": { 
        "field": "city" 
      }, 
      "aggs": { 
        "my_avg": { 
          "top_hits": {          //指定返回每个桶的前3个文档 
            "size": 3 
          } 
        } 
      } 
    } 
  } 
}  

执行上述查询后,ES返回的结果如下:

{"hits" : {"hits" : [ ] 
  }, 
  "aggregations" : { 
    "group_city" : { 
      "doc_count_error_upper_bound" : 0, 
      "sum_other_doc_count" : 0, 
      "buckets" : [               //每个桶聚合中返回前3个文档 
        { 
          "key" : "北京", 
          "doc_count" : 2, 
          "my_avg" : { 
            "hits" : {"hits" : [ 
                { 
                  "_index" : "hotel", 
                  "_type" : "_doc", 
                  "_id" : "003", 
                  "_score" : 1.0928286, 
                  "_source" : {} 
                }, 
                { 
                  "_index" : "hotel", 
                  "_type" : "_doc", 
                  "_id" : "002", 
                  "_score" : 0.96817136, 
                  "_source" : {} 
              ] 
            } 
          } 
        }, 
        { 
          "key" : "天津", 
          "doc_count" : 1, 
          "my_avg" : { 
            "hits" : { 
              "total" : { 
                "value" : 1, 
                "relation" : "eq" 
              }, 
              "max_score" : 1.0928286, 
              "hits" : [ 
                { 
                  "_index" : "hotel", 
                  "_type" : "_doc", 
                  "_id" : "004", 
                  "_score" : 1.0928286, 
                  "_source" : {} 
                } 
              ] 
            } 
          } 
        } 
      ] 
    } 
  } 
}  

可以看到,在索引中一共有3个文档命中match查询条件,在聚合结果中按照城市分成了两个组“北京”“天津”,在“北京”下面有两个文档命中,并且按照得分将展示文档进行了降序排列,“天津”只有一个文档命中。

以下代码演示了在Java中使用Top hits聚合的逻辑:

public void getAggTopHitsSearch() throws IOException { 
        //创建搜索请求 
        SearchRequest searchRequest = new SearchRequest("hotel"); 
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
        String termsAggName = "my_terms";             //聚合的名称 
 
        TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders. 
terms(termsAggName).field("city"); 
        BucketOrder bucketOrder=BucketOrder.key(true); 
        termsAggregationBuilder.order(bucketOrder); 
 
        String topHitsAggName = "my_top";            //聚合的名称 
        TopHitsAggregationBuilder topHitsAgg=AggregationBuilders.topHits 
(topHitsAggName); 
        topHitsAgg.size(3); 
        //定义聚合的父子关系 
        termsAggregationBuilder.subAggregation(topHitsAgg); 
        //添加聚合 
        searchSourceBuilder.aggregation(termsAggregationBuilder); 
        searchSourceBuilder.query(QueryBuilders.matchQuery("title","金都")); 
        searchRequest.source(searchSourceBuilder);          //设置查询请求 
        SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                                    //执行搜索 
 
        //获取聚合结果 
        Aggregations aggregations = searchResponse.getAggregations(); 
        //获取sum聚合返回的对象 
        Terms terms = aggregations.get(termsAggName); 
        for (Terms.Bucket bucket : terms.getBuckets()) { 
            String bucketKey = bucket.getKey().toString(); 
            System.out.println("termsKey=" + bucketKey); 
            TopHits topHits = bucket.getAggregations().get(topHitsAggName); 
            SearchHit[] searchHits=topHits.getHits().getHits(); 
            for(SearchHit searchHit:searchHits){ 
                System.out.println(searchHit.getSourceAsString()); 
            } 
        } 
    }  

Top hits聚合能满足“聚合的结果按照每组选出N个文档的方式进行呈现”的需求,但是很遗憾,它不能完成自动分页功能。如果在聚合中使用Top hits聚合并期望对数据进行分页,则要求聚合的结果一定不能太多,因为需要由客户端自行进行分页,此时对分页内存的存储能力是一个挑战。可以一次性获取聚合结果并将其存放在内存中或者Redis中,然后自行实现翻页逻辑,完成翻页。假设数据一次性存储到Redis的list结构中,以下示例代码演示了从Redis分页取数据的逻辑:

import org.springframework.stereotype.Service; 
import org.springframework.data.redis.core.StringRedisTemplate; 
import java.util.List; 
 
@Service 
public class CacheServiceImpl { 
    @Resource 
    private StringRedisTemplate stringRedisTemplate; //Redis客户端 
 
    public List<String> getRecommendData(String key, Integer pageNo,  
Integer pageSize) { 
        long start = (pageNo - 1) * pageSize;         //计算分页的起始位置 
        long end = pageNo * pageSize - 1;             //计算分页的终止位置 
        //从Redis中取数据并将数据返回 
        return stringRedisTemplate.opsForList().range(key, start, end); 
    } 
}

2 Collapse聚合

如前面所述,当在索引中有大量数据命中时,Top hits聚合存在效率问题,并且需要用户自行排序。针对上述问题,ES推出了Collapse聚合,即用户可以在collapse子句中指定分组字段,匹配query的结果按照该字段进行分组,并在每个分组中按照得分高低展示组内的文档。当用户在query子句外指定from和size时,将作用在Collapse聚合之后,即此时的分页是作用在分组之后的。以下DSL展示了Collapse聚合的用法:

GET /hotel/_search 
{ 
 "from": 0,                      //指定分页的起始位置 
 "size": 5,                      //指定每页返回的数量 
 "query": {                      //指定查询的query逻辑 
    "match": { 
      "title": "金都" 
    } 
  },  
 "collapse": {                   //指定按照城市进行Collapse聚合 
    "field": "city" 
  } 
} 

执行上述DSL后,ES返回的结果如下:

{"hits" : {"hits" : [ 
      {"_id" : "003", 
        "_score" : 1.0928286, 
        "_source" : {}, 
        "fields" : {             //按照城市进行Collapse聚合 
          "city" : [ 
            "北京" 
          ] 
        } 
      }, 
      {"_id" : "004", 
        "_score" : 1.0928286, 
        "_source" : {}, 
        "fields" : {             //按照城市进行Collapse聚合 
          "city" : [ 
            "天津" 
          ] 
        } 
      } 
    ] 
  } 
}  

从结果中可以看到,与Top hits聚合不同,Collapse聚合的结果是封装在hit中的。在索引中一共有3个文档命中match查询条件,在聚合结果中已经按照城市分成了两个组,即“北京”“天津”,在“北京”下面有两个文档命中,其中得分最高的文档为003,“天津”只有一个文档命中。上述结果不仅能按照得分进行排序,并且具备分页功能。
以下代码演示了在Java中使用Collapse聚合的逻辑:

public void getCollapseAggSearch() throws IOException{ 
     //按照spu进行分组 
     //按照城市进行分组 
     CollapseBuilder collapseBuilder = new CollapseBuilder("city"); 
     SearchRequest searchRequest = new SearchRequest();  //新建搜索请求 
     SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); 
     //新建match查询 
     searchSourceBuilder.query(QueryBuilders.matchQuery("title", "金都")); 
     searchSourceBuilder.collapse(collapseBuilder);      //设置Collapse聚合 
     searchRequest.source(searchSourceBuilder);          //设置查询 
     SearchResponse searchResponse = client.search(searchRequest,  
RequestOptions.DEFAULT);                                 //执行搜索 
     SearchHits searchHits = searchResponse.getHits();   //获取搜索结果集 
     for (SearchHit searchHit : searchHits) {            //遍历搜索结果集 
         String index = searchHit.getIndex();            //获取索引名称 
         String id = searchHit.getId();                  //获取文档_id 
         Float score = searchHit.getScore();             //获取得分 
         String source = searchHit.getSourceAsString();  //获取文档内容 
         System.out.println("index=" + index + ",id=" + id + ",score=" + score  
+ ",source=" + source);                                  //打印数据 
     } 
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值