Skip to content

elasticsearch多级聚合

前言

由于更熟悉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定义了非常丰富的分桶方式,更多分桶规则可参见官方文档>>

还有各种聚合计算,也请移步到官方文档>>

分享到:
Published in程序猿的东西

Be First to Comment

发表评论