聚合
当用户使用搜索引擎完成搜索后,在展示结果钟需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。聚合的需求在很多应用程序钟都有所体现,例如在京东搜索“咸鸭蛋”,然后点击搜索界面钟的筛选按钮,在弹出的界面钟可以对当前的搜索结果进行进一步的过滤。例如,可以重价格区间、品牌、分类i、个数等维度分别进行筛选。
为方面介绍后续内容,这里定义酒店的索引如下:
json
复制代码
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" } } } } } }
向索引中写入示例数据
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}}
1.1 聚合指标
在进行聚合搜索时,聚合的指标业务需求不仅是文档数量。例如,在酒店搜索场景中,我们希望看到以当前位置为中心点,周边各个区域酒店的平均加个。
1.1.1 常见的聚合指标
在搜索聚合时,用户可能关注字段的相关统计信息,例如平均值、最大值、最小值及加和值。例如,用户在使用一个二手房交易搜索引擎进行搜索时,可能会关注当前城市各个区域的房产平均价格。用户在搜索酒店时,也可能会关注附近各个区域酒店的最低价格。
ES聚合请求的地址也是索引的搜索地址,可以使用aggs
子句封装聚合请求。
当使用avg
子句进行平均值的聚合时,可以在avg
子句中指定聚合的字段。在默认情况下,查询将匹配所有文档,如果不需要返回匹配的文档信息,最好将返回的文档个数设置为0。这样既可以让结果看起来更整洁,又可以提高查询速度。
下面的DSL将查询所有酒店的平均价格并且不反悔匹配的文档信息。
json
复制代码
GET /hotel_poly/_search { "size": 0, "aggs": { "my_agg": { //聚合名称 "avg": { "field": "price" //计算文档的平均价格 } } } }
ES返回结果如下:
在上面的搜索结果中,索引的5个文档全部命中,由于DSL设置size为0,所以命中文档的信息没有显示。在搜索结果的aggregations子句中存储着聚合结果,其中my_agg是聚合的名称,其对应的value值就是具体聚合结果,即酒店的平均价格。
如果聚合的指标字段不是ES的基本类型,例如object类型,则可以使用点运算符进行引用。下面的DSL演示了该用法:
bash
复制代码
GET /hotel_poly/_search { "size": 0, "aggs":{ "my_agg":{ "avg": { "field": "comment_info.favourable_comment" } } } }
与平均值类似,最大值、最小值及加和值分别使用
max
、min
和sum
子句进行聚合,不再赘述。
以下代码演示了在Java中使用聚合计算平均值的逻辑。
ini
复制代码
@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); }
为了避免多次请求,ES还提供了stats
聚合。stats
聚合可以将对应字段的最大值、最小值、平均值及加和值一起计算并返回计算结果。下面的DSL展示了stats的用法。
bash
复制代码
GET /hotel_poly/_search { "size": 0, "aggs":{ "my_agg":{ "stats": { "field": "price" } } } }
以下代码演示了在Java中使用stats聚合的逻辑。
ini
复制代码
@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); }
1.1.2 空值处理
在索引中的一部分文档很可能其某些字段是缺失的,在介绍空值处理前,首先介绍ES聚合查询提供的value_count
聚合,该聚合用于统计字段非空值的个数。
bash
复制代码
# value_count聚合统计price字段中非空值的个数 GET /hotel_poly/_search { "size": 0, "aggs":{ "my_agg":{ "value_count": { "field": "price" } } } }
通过上述结果可以看到,当前索引中price字段中的非空值有4个。
以下代码演示了在Java中使用value_count
对price字段进行聚合的逻辑。
ini
复制代码
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); }
需要说明的是,如果判断的字段是数组类型,则value_count
统计的是符合条件的所有文档中该字段数组中非空元素个数的总和,而不是数组的个数总和。
下面的DSL用于统计tags字段数组中非空元素个数的总和。
bash
复制代码
#统计tags字段数组钟非空元素个数的总和 GET /hotel_poly/_search { "size": 0, "aggs":{ "my_agg":{ "value_count": { "field": "tags" } } } }
在索引的5个文档中,除去文档003没有tags字段外,其他4个文档的tags字段数组中各有两个元素,因此聚合的值为2×4=82×4=8个,ES返回结果如下:
上面的结果中,aggregations.my_agg.value
的值为8,这和前面计算的数值相等,验证了使用value_count
对数组字段进行聚合时,ES返回的结果是所有数组元素的个数总和。
如果需要以空值字段的数据作为聚合指标对其进行聚合,可以在指标统计中通过missing
参数指定填充值对空值进行填充。
以下示例演示了对price字段进行聚合,并设定了当字段值为空值时使用100进行替代的DSL。
arduino
复制代码
# 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=2156556+100+200+500+800=2156。ES返回结果如下:
以下代码演示了在Java中当聚合指标为空值时指定填充值的逻辑。
ini
复制代码
@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); }