前言
由于更熟悉SQL,而ES的聚合语句想必也更复杂,所以通常的聚合查询都在数据库完成。近日,因为一些查询在数据库的字段不全,因此尝试用ES计算聚合。发现ES的聚合查询复杂有复杂的理由,可以实现更丰富的聚合功能。
示例
下面先看一条示例,不用细看,下面有分解动作教学。
GET /twitter/_search { "query": { "range": { "created_at": { "gte": "2019-08-17 00:00:00", "lt": "2019-08-24 00:00:00" } } }, "size" : 0, "aggs": { "user_pubs":{ "terms": { "field": "user_id", "size": 10, "order": { "_count": "desc" } }, "aggs":{ "is_topic" : { "filters": { "filters": { "topic" : { "range" : { "topic_id" : {"gt":0} } }, "other" : { "term" : { "topic_id" : 0 } } } }, "aggs":{ "zan_nums":{ "sum":{ "field": "zan_num" } }, "pics":{ "sum": { "field": "pic_num" } } } } } } } }
这个语句在推文的索引上查询。查询发布最多的10个用户,每个用户再分是否参加话题,然后分别统计是否参加话题的推文下的图片数和点赞数。
SQL的矛盾
首先我们回顾一下SQL怎么做,假设我们有一个表与索引字段一致,并且已经有了字段is_topic来区分是否有参与话题。首先要使用 GROUP BY user_id, is_topic来分组。然后在SELECT语句中使用SUM(pic_num)和SUM(zan_num)来统计。这么做下来统计数据是没有问题了,但是我们怎么按照用户发布数最多排序呢。因为我们要统计用户发布数就得使用GROUP BY user_id,但是又同时要实现细分的统计。这就矛盾了,因为一个SQL只能有一个GROUP BY,聚合函数也只能根据GROUP BY 来计算。
elasticsearch聚合基本概念
然后我们来看看ES的聚合查询功能。
首先ES在聚合查询的时候引入两个概念是桶bucket和指标metric,等同于GROUP BY 和 聚合函数,不得不说学习了ES之后同时学到了很多同义词。知道了这两个东西看ES的文档就有方向了。ES的分桶通常只有一个纬度,等于一层GROUP BY,如果需要多个纬度,可以在分好的桶里面再分桶,等于下一级GROUP BY。同时由于ES的输出结果不像关系型数据库一样的行列表,因此也支持更复杂的输出结果,也就可以实现每层分桶都可以做聚合。
示例分析
现在我们来解剖前面的示例。
我们看第一个aggs,aggs就是aggregations的简写。我们把第一层的聚合语句单独提取出来看看
"aggs": { "user_pubs":{ "terms": { "field": "user_id", "size": 10, "order": { "_count": "desc" } } } }
user_pubs是我们自定义的聚合名称,他对应的对象就用来描述这个聚合。底下跟着的terms就是这个聚合的分桶方法。表示把user_id相等的分在一个桶。然后根据_count逆序排序,提取前10个。到此就实现了我们的第一层聚合。如果没有下面对aggs,我们执行这个查询会得到前10个用户的user_id和每个分桶下的doc_count。现在我们找出了发布最多的前10个用户。
下面我们再看下一层分桶,为了看着清晰,我依然把他单独拎出来
"aggs":{ "is_topic" : { "filters": { "filters": { "topic" : { "range" : { "topic_id" : {"gt":0} } }, "other" : { "term" : { "topic_id" : 0 } } } } } }
同样的is_topic是我们为这次聚合起的名字,这次我们用了另一种分桶方式filters,我们定义了两个过滤器,一个是topic,条件是topic_id>0的文档,另一个other,条件是topic_id==0,这样又将结果分成了两个桶。如果直接执行这个,是把所有查询结果做聚合,但是我们把他嵌套在第一层的聚合下面,就实现了在第一层分好的每个桶中的再分桶。
我们已经完成了GROUP BY user_id, is_topic,下面我们要为分桶结果做统计。其实是两个求和语句。这两个求和语句把分在桶里的多条文档最终聚合成了一个数值,没错,这其实又是一次聚合!那么我们把第三层聚合提取出来看看
"aggs":{ "zan_nums":{ "sum":{ "field": "zan_num" } }, "pics":{ "sum": { "field": "pic_num" } } }
这次没有定义分桶规则,也就是桶里的文档都要用来聚合,所以直接定义两个聚合zan_nums和pics,都是sum类型,下面指定sum运算对应的字段。同样的,这个聚合如果直接执行表示所有文档求和,我们把他套进第二层聚合里面就是针对第二层分桶的结果,在每个桶中聚合。至此我们完成了我们要的需求,最终的查询语句就是最上面那条。
查询结果
当然,这样的一个查询语句,产生的结果也是非常的美好
"aggregations": { "user_pubs": { "doc_count_error_upper_bound": 11, "sum_other_doc_count": 1795, "buckets": [ { "key": 604480, "doc_count": 43, "is_topic": { "buckets": { "other": { "doc_count": 11, "zan_nums": { "value": 17 }, "pics": { "value": 29 } }, "topic": { "doc_count": 32, "zan_nums": { "value": 38 }, "pics": { "value": 75 } } } } }, ... ] } }
我们看到有两个buckets,分别对应两次分桶,我们要的统计结果在最最最里面。。
后记
ES定义了非常丰富的分桶方式,更多分桶规则可参见官方文档>>
还有各种聚合计算,也请移步到官方文档>>