Elasticsearch 聚合搜索
- 环境准备
- 数据准备
- 基础聚合
- 桶聚合(分组聚合)
- 单维度桶聚合
- 多维度桶聚合
- 聚合方式
- 直接聚合
- 先查询后聚合
- 前过滤器
- 后过滤器
- 聚合排序
- 按照文档计数排序
- 按照聚合指标排序
- 按照分组Key排序
- 聚合分页
- Top hits 聚合
- Collapse 聚合
当用户使用搜索引擎完成搜索后,在展示结果中需要进行进一步的筛选,而筛选的维度需要根据当前的搜索结果进行汇总,这就用到了聚合技术。
环境准备
- Elasticsearch 服务(单机或集群)
- Kibana 服务
如果对ES不了解或没有上述环境,可以看下我之前的博客。
Elasticsearch入门基础和集群部署
Elasticsearch查看集群信息,设置ES密码,Kibana部署
数据准备
PUT /course
{"mappings": {"properties": {"title": {"type": "text"},"teacher_name": {"type": "text"},"price": {"type": "double"},"create_time": {"type": "date","format": ["yyyy-MM-dd HH:mm:ss"]},"tags": {"type": "keyword"},"publish": {"type":"boolean"},"comment_info":{"properties": {"favourable_num":{"type":"integer"},"negative_num":{"type":"integer"}}}}}
}PUT /_bulk
{"index":{"_index":"course","_id":"1"}}
{"title":"Python编程基础","teacher_name":"张三","price":99.99,"create_time":"2023-04-01 10:00:00","tags":["编程","Python"],"publish":true,"comment_info":{"favourable_num":150,"negative_num":3}}
{"index":{"_index":"course","_id":"2"}}
{"title":"Java高级开发","teacher_name":"李四","price":129.5,"create_time":"2023-06-08 15:30:00","tags":["Java","后端开发"],"publish":true,"comment_info":{"favourable_num":200,"negative_num":7}}
{"index":{"_index":"course","_id":"3"}}
{"title":"数据结构与算法","teacher_name":"王五","price":88.88,"create_time":"2023-03-15 14:45:00","tags":["算法","数据结构"],"publish":false,"comment_info":{"favourable_num":50,"negative_num":0}}
{"index":{"_index":"course","_id":"4"}}
{"title":"Web前端开发入门","teacher_name":"赵六","price":79.9,"create_time":"2023-05-20 09:15:00","tags":["HTML","CSS","JavaScript"],"publish":true,"comment_info":{"favourable_num":88,"negative_num":2}}
{"index":{"_index":"course","_id":"5"}}
{"title":"机器学习实战","teacher_name":"孙七","price":159,"create_time":"2023-02-25 11:00:00","tags":["机器学习","人工智能"],"publish":true,"comment_info":{"favourable_num":120,"negative_num":5}}
{"index":{"_index":"course","_id":"6"}}
{"title":"数据库原理与设计","teacher_name":"周八","price":66.6,"create_time":"2023-01-10 13:30:00","tags":["数据库","SQL"],"publish":false,"comment_info":{"favourable_num":30,"negative_num":1}}
{"index":{"_index":"course","_id":"7"}}
{"title":"Android应用开发","teacher_name":"吴九","price":119.88,"create_time":"2023-04-20 17:45:00","tags":["Android","移动开发"],"publish":true,"comment_info":{"favourable_num":105,"negative_num":4}}
{"index":{"_index":"course","_id":"8"}}
{"title":"深度学习探索","teacher_name":"郑十","price":299,"create_time":"2023-05-05 16:00:00","tags":["深度学习","神经网络"],"publish":true,"comment_info":{"favourable_num":180,"negative_num":6}}
{"index":{"_index":"course","_id":"9"}}
{"title":"UI/UX设计精髓","teacher_name":"钱十一","price":55.55,"create_time":"2023-03-20 12:15:00","tags":["UI设计","用户体验"],"publish":true,"comment_info":{"favourable_num":75,"negative_num":2}}
{"index":{"_index":"course","_id":"10"}}
{"title":"云计算技术基础","teacher_name":"孙十二","create_time":"2023-07-01 18:30:00","tags":["云计算","AWS"],"publish":false,"comment_info":{"favourable_num":45,"negative_num":0}}
基础聚合
# 聚合指令
"aggs":{// 指定聚合内容"value_count_price":{// 指定方法"value_count": {//指定聚合字段"field": "price"}}}
基础聚合方法
- 平均值 avg
- 最大 max
- 最小 min
- 计数 value_count
- 求和 sum
- 统计聚合 stats
注意: 上述基础聚合,非空值不会参与计算
例如:十个文档数据,其中有一个文档中字段为空,对其求平均值,结果应该是: 字段不为空的9个文档的平均值。
对于空值:可以使用missing 字段,指定 将空值替换为某个值 来参与计算
桶聚合(分组聚合)
在ES中,MYSQL 中的 Group by 分组 被称为 桶聚合
单维度桶聚合
单维度桶聚合 就是 按照一个维度对文档 进行分组聚合
在桶聚合时匹配方式
- terms
terms聚合是按照字段的实际完整值进行匹配和分组的,它使用的维度字段必须是keyword、bool、keyword数组等适合精确匹配的数据类型,因此不能对text字段直接使用terms聚合,如果对text字段有terms聚合的需求,则需要在创建索引时为该字段增加多字段功能。
- ranges
ranges聚合也是经常使用的一种聚合。它匹配的是数值字段,表示按照数值范围进行分组。用户可以在ranges中添加分组,每个分组用from和to表示分组的起止数值。注意该分组包含起始数值,不包含终止数值。
- filter
filter聚合使用和搜索时使用方法一样,用来过滤出一批数据,一般用于 不影响查询条件的前过滤器
GET /course/_search
{// 桶聚合默认 计算每个桶对应的文档数"size": 0,"aggs":{"agg_terms_tags":{// 默认只返回十个桶,即size为10 "terms": {"field": "tags"}},"agg_terms_publish":{"terms": {"field": "publish","size": 10}},"agg_range_price":{"range": {"field": "price","ranges": [//不指定from (-∞,80){"to": 80},// 左闭右开 [80,100){"from": 80,"to": 100},//不指定to [100,+∞){"from": 100}]}},"agg_filter_price": {"filter": {"match": {"publish": "true"}}}}
}
注意: 这里多了一个 key_as_string 字段。
如果桶字段类型不是keyword类型,ES在聚合时会将桶字段转换为Lucene存储的实际值进行识别。true在Lucene中存储为1,false在Lucene中存储为0。而key_as_string 则用来表示原始值的字符串形式。
在默认情况下,进行桶聚合时如果不指定指标,则ES默认聚合的是文档计数,该值以doc_count为key存储在每一个bucket子句中。
返回的doc_count 是近似值,并不是一个准确数,
因此在聚合外围,ES给出了两个参考值doc_count_error_upper_bound
和 sum_other_doc_count
doc_count error_upper
表示被遗漏的文档数量可能存在的最大值。
sum other doc count
表示除了返回给用户的文档外剩下的文档总数。
多维度桶聚合
通常,在一些复杂业务中,单维度的桶聚合无法满足需求。
往往需要引入多维度的嵌套桶聚合。
例如:分别获取发布和未发布状态 价格 在 (-∞,80),[80,100),[100,+∞) 的 价格平均值
分析上述 需求,按照关系数据库的思路,其实就是 按照 发布状态 和 这三个价格区间进行分组 求每个组的平均价格
对应ES 中的桶聚合,可以转化为 求两个维度的桶聚合,
- 按照 发布状态 进行分组聚合
- 在聚合的结果中,按照价格区间进行分组聚合
- 在最新的聚合桶中 计算平均价格
转化为DSL如下:
GET /course/_search
{"size": 0,"aggs": {// 第一层分组桶:按照发布状态分组"publish_group": {"terms": {"field": "publish"},"aggs": {// 第二层分组:按照价格区间分组"price_range_group": {"range": {"field": "price","ranges": [{"to": 80},{"from": 80,"to": 100},{"from": 100}]},"aggs": {// 聚合计算平均值,也可以算一层"ans_avg": {"avg": {"field": "price"}}}}}}}
}
第一个桶 publish 为 true 时,内部嵌套了 价格区间的桶,每个价格区间内又有计算的 平均值 ans_avg
第二个桶 publish 为 false 时,内部嵌套了 价格区间的桶,每个价格区间内又有计算的 平均值 ans_avg
聚合方式
ES支持灵活的聚合方式,它不仅支持聚合和查询相结合,而且还可以使聚合的过滤条件不影响搜索条件,并且还支持在聚合后的结果中进行过滤选。
直接聚合
直接聚合指的是聚合时的DSL没有query子句,是直接对索引内的所有文档进行聚合。
前面的案例都是使用直接聚合方式
先查询后聚合
与直接聚合相对应,这种查询方式需要增加query子句,query子句和普通的query查询没有区别,参加聚合的文档必须匹配query查询。
对应的 就是 SQL 语言中的 Where语句
例如:查询发布状态的 课程 在 价格区间 (-∞,80),[80,100),[100,+∞) 中的平均价格
GET /course/_search
{"query": {"match": {"publish": "true"}},"size": 0,"aggs": {"price_range_group": {"range": {"field": "price","ranges": [{"to": 80},{"from": 80,"to": 100},{"from": 100}]},"aggs": {"ans_avg": {"avg": {"field": "price"}}}}}
}
前过滤器
有时需要对聚合条件进一步地过滤,但是又不能影响当前的查询条件。
例如:查询全部课程,并计算已经上架的课程的平均价格
因为 未上架的课程 也不能买,所以其平均价格没有意义
这时 需要用到 filter 关键字,在聚合前进行过滤,但不影响query数据
SQL 中没有 前过滤器 关键字,但可以通过 case when end 实现相似的效果
对应DSL语句为:
GET /course/_search
{"size": 0,"aggs": {"my_aggs": {"filter": {"term": {"publish": "true"}},"aggs": {"ans_avg": {"avg": {"field": "price"}}}}}
}
后过滤器
在有些场景中,需要根据条件进行数据查询,但是聚合结果不受影响。
例如:求全部数据的平均值,但只需输出下架的课程
这时 需要使用 post_filter 关键字 进行 聚合后的 后置过滤,但不影响aggs聚合
可以类比 SQL语句中的 having 关键字,分组后进行数据筛选
GET /course/_search
{"size": 10,"post_filter": {"term": {"publish": "false"}}, "aggs": {"ans_avg": {"avg": {"field": "price"}}}
}
注意:
ans_avg 还是计算的 query 查询出来的数据的平均值
而 post_filter 作用于 聚合计算后,再过滤数据,所以 只输出了三个 publish 为false 的数据
聚合排序
按照文档计数排序
可以使用_count 来进行文档计数排序
GET /course/_search
{"size": 0,"aggs": {"agg_terms_publish1": {"terms": {"field": "publish","order": {"_count": "asc"}}}}
}
按照聚合指标排序
可以使用具体的聚合指标名称 来进行排序
GET /course/_search
{"size": 0,"aggs": {"agg_terms_publish1": {"terms": {"field": "publish","order": {"avg_aggs": "desc"}},"aggs": {"avg_aggs": {"avg": {"field": "price"}}}}}
}
按照分组Key排序
在聚合排序时,业务需求可能有按照每个分组的组名称排序的场景。此时可以使用 key来引用分组名称。
按照分组Key的自然顺序升序排列
GET /course/_search
{"size": 0,"aggs": {"agg_terms_publish1": {"terms": {"field": "publish","order": {"_key": "asc"}}}}
}
聚合分页
ES支持同时返回查询结果和聚合结果,前面介绍聚合查询时,查询结果和聚合结果各自封装在不同的子句中。
但有时我们希望聚合的结果按照每组选出前N个文档的方式进行呈现,最常见的一个场景就是电商搜索,如搜索苹果手机6S,搜索结果应该展示苹果手机6S型号中的一款手机即可,而不论该型号手机的颜色有多少种。
另外,当聚合结果和查询结果封装在一起时,还需要考虑对结果分页的问题,此时前面介绍的聚合查询就不能解决这些问题了。
ES提供的Top hits聚合和Collapse聚合可以满足上述需求,但是这两种查询的分页方案是不同的。
Top hits 聚合
Top hits聚合指的是聚合时在每个分组内部,按照某个规则选出前N个文档进行展示。
例如: 搜索 “开发”时,按照上下架分组,每组按照价格升序,展示最便宜的数据
GET /course/_search
{"query": {"match": {"title": "开发"}},"size": 0,"aggs": {"group_publish": {"terms": {"field": "publish"},"aggs": {"top_aggs": {"top_hits": {"size": 1,"sort": {"price": {"order": "asc"}}}}}}}
}
Collapse 聚合
当在索引中有大量数据命中时,Top hits聚合存在效率问题,并且需要用户自行排序。
针对上述问题,ES推出了Collapse聚合,即用户可以在collapse子句中指定分组字段,匹配query的结果按照该字段进行分组,并在每个分组中按照得分高低展示组内的文档。
当用户在query子句外指定from和size时,将作用在Collapse聚合之后,即此时的分页是作用在分组之后的。
GET /course/_search
{"query": {"match": {"title": "开发"}},"from": 0,"size": 2,"collapse": {"field": "price"}
}