ElasticSearch 聚合

聚合

类似于DSL查询表达式,聚合也有可组合的语法:独立单元的功能可以被混合起来提供你需要的自定义行为。这意味着只需要学习很少的基本概念,就可以得到几乎无尽的组合。

要掌握聚合,你只需要明白两个概念:

  • 桶(Buckets):满足特定条件的文档的集合
  • 指标(Metrics):对桶内的文档进行统计计算

这就是全部了!每个聚合都是一个或者多个桶和零个或者多个指标的组合。翻译成粗略的SQL语句来解释吧:

SELECT COUNT(color)   // (1)
FROM table
GROUP BY color  // (2)
  • (1)COUNT(color)相当于指标
  • (2)GROUP BY color相当于桶

桶的概念上类似于SQL的分组(GROUP BY),而指标则类似于COUNT()、SUM()、MAX()等统计方法。

桶简单来说就是满足特定条件的文档的集合:

  • 一个雇员数据 男性桶 或者 女性桶
  • 奥尔巴尼数据 纽约桶
  • 日期2014-10-28属于 十月桶

当聚合开始被执行,每个文档里面的值通过计算来决定符合哪个桶的条件。如果匹配到,文档将放入相应的桶并接着进行聚合操作。

桶也可以被嵌套在其他桶里面,提供层次化的或者有条件的划分文案。例如,辛辛那提会被放入俄亥俄州这个桶,而 整个 俄亥俄州 桶会被放入美国这个桶。

Elasticsearch 有很多种类型的桶,能让你通过很多种方式来划分文档(时间、最受欢迎的词、年龄区间、地理位置等等)。其实根本都是通过同样的原理进行操作:基于条件来划分文档。

指标

桶能让我们划分文档到有意义的集合,但是最终我们需要的是对这些桶内的文档进行一些指标的计算。分桶是一种达到目的的手段:它提供了一种给文档分组的方法来让我们可以计算感兴趣的指标。

大多数指标是简单的数学运算(例如最小值、平均值、最大值,还有汇总),这些是通过文档的值来计算。在实践中,指标能让你计算像平均薪资,最高出售价格、95%的查询延迟这样的数据。

桶和指标的组合

聚合是由桶和指标组成的。聚合可能只有一个桶,可能只有一个指标,或者可能两个都有。也有可能有一些桶嵌套在其他桶里面。例如,我们可以通过所属国家来划分文档(桶),然后计算每个国家的平均薪酬(指标)。

由于桶可以被嵌套,我们可以实现非常多并且非常复杂的聚合:

  1. 通过国家划分文档(桶)
  2. 然后通过性别划分每个国家(桶)
  3. 然后通过年龄区间划分每种性别(桶)
  4. 最后,为每个年龄区间计算平均薪酬(指标)

最后将告诉你每个<国家,性别,年龄>组合的平均薪酬。所有的这些都在一个请求内完成并且只遍历一次数据!

【示例】

我们将会创建一些对汽车经销商有用的聚合,数据是关于汽车交易的信息:车型、制造商、售价、何时被出售等。

POST /cars/transactions/_bulk
{ "index": {}}
{ "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" }
{ "index": {}}
{ "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" }
{ "index": {}}
{ "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" }
{ "index": {}}
{ "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" }
{ "index": {}}
{ "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" }
{ "index": {}}
{ "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" }

开始构建我们的第一个聚合。汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作:

GET /cars/transactions/_search
{
    "size" : 0,  // (1)
    "aggs" : {   // (2)
        "popular_colors" : {  // (2)
            "terms" : {   // (4)
              "field" : "color"
            }
        }
    }
}
  • (1)我们并不关心搜索结果的具体内容,所以将返回记录数设置为 0 来提高查询速度。 设置 size: 0 与 Elasticsearch 1.x 中使用 count 搜索类型等价。
  • (2) 聚合操作被置于顶层参数aggs之下(如果你愿意,完整形式aggregations同样有效)
  • (3) 然后,可以为聚合制定一个我们想要名称,本例子中是:popular_colors。响应的结果会以我们定义的名字为标签,这样应用就可以解析得到的结果。
  • (4) 最后,定义单个桶的类型 terms。terms桶 是针对某个field的值进行分组,field有几种值就分成几组。terms桶在进行分组时,会为此field中的每种值创建一个新的桶。因为我们告诉它使用 color 字段,所以 terms 桶会为每个颜色动态创建新桶。

【注意】

  1. 当query和aggs一起存在时,会先执行query的主查询,主查询query执行完后会搜出一批结果,而这些结果才会被拿去aggs拿去做聚合。
  2. 另外要注意aggs后面会先接一层自定义的这个聚合的名字,然后才是接上要使用的聚合桶
  3. 要注意此 "terms桶" 和平常用在主查询query中的 "查找terms" 是不同的东西

查看结果:

{
...
   "hits": {      // (1)
      "hits": [] 
   },
   "aggregations": {
      "popular_colors": {    // (2)
         "buckets": [
            {
               "key": "red",   // (3)
               "doc_count":    // (4)
            },
            {
               "key": "blue",
               "doc_count": 2
            },
            {
               "key": "green",
               "doc_count": 2
            }
         ]
      }
   }
}
  • (1) 因为我们设置了size参数,所以不会有hits搜索结果返回。
  • (2)popular_colors聚合是作为aggregations字段的一部分被返回的。
  • (3)每个桶的key都与color字段里找到的唯一词对应。它总会包含doc_count字段,告诉我们包含该词项的文档数量。
  • (4)每个桶的数量代表该颜色的文档数量。

添加度量指标

通常,我们的应用需要提供更复杂的文档度量。 例如,每种颜色汽车的平均价格是多少?

为了获取更多信息,我们需要告诉 ES 使用哪个字段,计算何种度量。 这需要将度量嵌套在桶内, 度量会基于桶内的文档计算统计结果。

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {  // (1)
            "avg_price": {  // (2)
               "avg": {
                  "field": "price" // (3)
               }
            }
         }
      }
   }
}
  • (1)为度量新增aggs层。
  • (2)为度量指定名字:avg_price。
  • (3)最后,为price字段定义avg度量。

正如所见,我们用前面的例子加入了新的 aggs 层。这个新的聚合层让我们可以将 avg 度量嵌套置于 terms 桶内。实际上,这就为每个颜色生成了平均价格。

{
...
   "aggregations": {
      "colors": {
         "buckets": [
            {
               "key": "red",
               "doc_count": 4,
               "avg_price": {   // 响应中的新字段avg_price
                  "value": 32500
               }
            },
            {
               "key": "blue",
               "doc_count": 2,
               "avg_price": {
                  "value": 20000
               }
            }
         ]
      }
   }
...
}

正如 颜色 的例子,我们需要给度量起一个名字( avg_price )这样可以稍后根据名字获取它的值。最后,我们指定度量本身( avg )以及我们想要计算平均值的字段( price )。

尽管响应只发生很小改变,实际上我们获得的数据是增长了。之前,我们知道有四辆红色的车,现在,红色车的平均价格是 $32,500 美元。这个信息可以直接显示在报表或者图形中。

一个aggs裡可以有很多个聚合,每个聚合彼此间都是独立的,因此可以一个聚合拿来统计数量、一个聚合拿来分析数据、一个聚合拿来计算标准差...,让一次搜索就可以把想要做的事情一次做完。

嵌套桶

在我们使用不同的嵌套方案时,聚合的力量才能真正得以显现。在前例中,我们已经看到如何将一个度量嵌入桶中,它的功能已经十分强大了。但真正令人激动的分析来自于将桶嵌套进 另外一个桶 所能得到的结果。 现在,我们想知道每个颜色的汽车制造商的分布。

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {
            "avg_price": {   // (1)
               "avg": {
                  "field": "price"
               }
            },
            "make": {   // (2)
                "terms": {  // (3)
            "field": "make"
                }
            }
         }
      }
   }
}
  • (1)注意前例中的avg_price度量仍然保持原位。
  • (2)另一个聚合make被加入到了color颜色桶中。
  • (3)这个聚合是terms桶,它会为每个汽车制造商生成唯一的桶。

新增的这个 make 聚合,它是一个 terms 桶(嵌套在 colorsterms 桶内)。这意味着它会为数据集中的每个唯一组合生成( colormake )元组。让我们看看返回的响应:

{
...
   "aggregations": {
      "colors": {
         "buckets": [
            {
               "key": "red",
               "doc_count": 4,
               "make": {   // (1)
                  "buckets": [
                     {
                        "key": "honda",  // (2)
                        "doc_count": 3
                     },
                     {
                        "key": "bmw",
                        "doc_count": 1
                     }
                  ]
               },
               "avg_price": {   // (3)
                  "value": 32500 
               }
            },

...
}
  • (1)正如期望的那样,新的聚合嵌入到每个颜色桶中。
  • (2)现在我们看见按不同制造商分解的每种颜色下车辆信息。
  • (3)最终,我们看到前例中的avg_price度量仍然保持不变。

响应结果告诉我们以下几点:

  • 红色车有四辆。
  • 红色车的平均售价是¥32,500美元。
  • 其中三辆是Honda本田制造,一辆是BMW宝马制造。

这里发生了一些有趣的事。 首先,我们可能会观察到之前例子中的 avg_price 度量完全没有变化,还在原来的位置。 一个聚合的每个 层级 都可以有多个度量或桶, avg_price 度量告诉我们每种颜色汽车的平均价格。它与其他的桶和度量相互独立。

这对我们的应用非常重要,因为这里面有很多相互关联,但又完全不同的度量需要收集。聚合使我们能够用一次数据请求获得所有的这些信息。

让我们回到话题的原点,在进入新话题之前,对我们的示例做最后一个修改,为每个汽车生成商计算最低和最高的价格:

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "colors": {
         "terms": {
            "field": "color"
         },
         "aggs": {
            "avg_price": { "avg": { "field": "price" }
            },
            "make" : {
                "terms" : {
                    "field" : "make"
                },
                "aggs" : {  // (1)
                    "min_price" : { "min": { "field": "price"} },  // (2)
                    "max_price" : { "max": { "field": "price"} }   // (3)
                }
            }
         }
      }
   }
}
  • (1)我们需要增加另外一个嵌套的aggs层级。
  • (2)然后包括min最小度量。
  • (3)以及max最大度量。

得到以下输出(只显示部分结果):

{
...
   "aggregations": {
      "colors": {
         "buckets": [
            {
               "key": "red",
               "doc_count": 4,
               "make": {
                  "buckets": [
                     {
                        "key": "honda",
                        "doc_count": 3,
                        "min_price": {
                           "value": 10000   // (1)
                        },
                        "max_price": {
                           "value": 20000  // (2)
                        }
                     },
                     {
                        "key": "bmw",
                        "doc_count": 1,
                        "min_price": {
                           "value": 80000
                        },
                        "max_price": {
                           "value": 80000
                        }
                     }
                  ]
               },
               "avg_price": {
                  "value": 32500
               }
            },
...
  • (1)min和max度量出现在每个汽车制造商(make)下面

有了这两个桶,我们可以对查询的结果进行扩展并得到以下信息:

  • 有四辆红色车。
  • 红色车的平均售价是 $32,500 美元。
  • 其中三辆红色车是 Honda 本田制造,一辆是 BMW 宝马制造。
  • 最便宜的红色本田售价为 $10,000 美元。
  • 最贵的红色本田售价为 $20,000 美元。

条形图

ElasticSearch 聚合_第1张图片

直方图 histogram 特别有用。 它本质上是一个条形图,如果有创建报表或分析仪表盘的经验,那么我们会毫无疑问的发现里面有一些图表是条形图。 创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。

对于仪表盘来说,我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。

GET /cars/transactions/_search
  {
     "size" : 0,
     "aggs":{
        "price":{
           "histogram":{   // (1)
              "field": "price",
              "interval": 20000
           },
           "aggs":{
              "revenue": {
                 "sum": {    // (2)
                   "field" : "price"
                 }
               }
           }
        }
     }
  }
  • (1)histogram桶要求两个参数:一个数值字段以及一个定义桶大小间隔。
  • (2)sum度量嵌套在每个售价区间内,用来显示每个区间内的总收入。

如我们所见,查询是围绕 price 聚合构建的,它包含一个 histogram 桶。它要求字段的类型必须是数值型的同时需要设定分组的间隔范围。 间隔设置为 20,000 意味着我们将会得到如 [0-19999, 20000-39999, ...] 这样的区间。

接着,我们在直方图内定义嵌套的度量,这个 sum 度量,它会对落入某一具体售价区间的文档中 price 字段的值进行求和。 这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱。

响应结果如下:

 {
  ...
     "aggregations": {
        "price": {
           "buckets": [
              {
                 "key": 0,
                 "doc_count": 3,
                 "revenue": {
                    "value": 37000
                 }
              },
              {
                 "key": 20000,
                 "doc_count": 4,
                 "revenue": {
                    "value": 95000
                 }
              },
              {
                 "key": 80000,
                 "doc_count": 1,
                 "revenue": {
                    "value": 80000
                 }
              }
           ]
        }
     }
  }

结果很容易理解,不过应该注意到直方图的键值是区间的下限。键 0 代表区间 0-19,999 ,键 20000代表区间 20,000-39,999 ,等等。

我们可能会注意到空的区间,比如:$40,000-60,000,没有出现在响应中。 histogram 桶默认会忽略它,因为它有可能会导致不希望的潜在错误输出。

按时间统计

ElasticSearch 聚合_第2张图片

如果搜索是在 Elasticsearch 中使用频率最高的,那么构建按时间统计的 date_histogram 紧随其后。 为什么你会想用 date_histogram 呢?

假设你的数据带时间戳。 无论是什么数据(Apache 事件日志、股票买卖交易时间、棒球运动时间)只要带有时间戳都可以进行 date_histogram 分析。当你的数据有时间戳,你总是想在 时间 维度上构建指标分析:

  • 今年每月销售多少台汽车?
  • 这只股票最近 12 小时的价格是多少?
  • 我们网站上周每小时的平均响应延迟时间是多少?

虽然通常的 histogram 都是条形图,但 date_histogram 倾向于转换成线状图以展示时间序列。 许多公司用 Elasticsearch 仅仅 只是为了分析时间序列数据。 date_histogram 分析是它们最基本的需要。

date_histogram 与 通常的 histogram 类似。 但不是在代表数值范围的数值字段上构建 buckets,而是在时间范围上构建 buckets。 因此每一个 bucket 都被定义成一个特定的日期大小 (比如, 1个月 或 2.5 天)。

我们的第一个例子将构建一个简单的折线图来回答如下问题: 每月销售多少台汽车?

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month",   // (1)
            "format": "yyyy-MM-dd"   // (2)
         }
      }
   }
}
  • (1)时间间隔要求是日历术语(如每个bucket1个月)
  • (2)我们提供日期格式以便buckets的键值便于阅读

我们的查询只有一个聚合,每月构建一个 bucket。这样我们可以得到每个月销售的汽车数量。 另外还提供了一个额外的 format 参数以便 buckets 有 "好看的" 键值。 然而在内部,日期仍然是被简单表示成数值。这可能会使得 UI 设计者抱怨,因此可以提供常用的日期格式进行格式化以更方便阅读。

聚合结果展示如下。正如你所见,我们有代表月份的 buckets,每个月的文档数目,以及美化后的 key_as_string。

{
   ...
   "aggregations": {
      "sales": {
         "buckets": [
            {
               "key_as_string": "2014-01-01",
               "key": 1388534400000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-02-01",
               "key": 1391212800000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-05-01",
               "key": 1398902400000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-07-01",
               "key": 1404172800000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-08-01",
               "key": 1406851200000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-10-01",
               "key": 1412121600000,
               "doc_count": 1
            },
            {
               "key_as_string": "2014-11-01",
               "key": 1414800000000,
               "doc_count": 2
            }
         ]
...
}

注意到结果末尾处的奇怪之处了吗?是的,结果没错。 我们的结果少了一些月份! date_histogram (和 histogram 一样)默认只会返回文档数目非零的 buckets。

这意味着你的 histogram 总是返回最少结果。通常,你并不想要这样。对于很多应用,你可能想直接把结果导入到图形库中,而不想做任何后期加工。

事实上,即使 buckets 中没有文档我们也想返回。可以通过设置两个额外参数来实现这种效果:

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month",
            "format": "yyyy-MM-dd",
            "min_doc_count" : 0,    // (1)
            "extended_bounds" : { 
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
         }
      }
   }
}
  • (1)min_doc_count 参数强制返回空 buckets,但是 Elasticsearch 默认只返回你的数据中最小值和最大值之间的 buckets。最大和最小值在extended_bounds参数中设置

【拓展例子】

正如我们已经见过很多次,buckets 可以嵌套进 buckets 中从而得到更复杂的分析。 作为例子,我们构建聚合以便按季度展示所有汽车品牌总销售额。同时按季度、按每个汽车品牌计算销售总额,以便可以找出哪种品牌最赚钱:

{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "quarter",   // (1)
            "format": "yyyy-MM-dd",
            "min_doc_count" : 0,
            "extended_bounds" : {
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
         },
         "aggs": {
            "per_make_sum": {
               "terms": {
                  "field": "make"
               },
               "aggs": {
                  "sum_price": {
                     "sum": { "field": "price" }   // (2)
                  }
               }
            },
            "total_sum": {
               "sum": { "field": "price" }   //(3)
            }
         }
      }
   }
}
  • (1)注意我们把时间间隔从month改成了quarter
  • (2)计算每种品牌的总销售金额
  • (3)也计算全部品牌的汇总销售金额

得到的结果(截去了一大部分)如下:

{
....
"aggregations": {
   "sales": {
      "buckets": [
         {
            "key_as_string": "2014-01-01",
            "key": 1388534400000,
            "doc_count": 2,
            "total_sum": {
               "value": 105000
            },
            "per_make_sum": {
               "buckets": [
                  {
                     "key": "bmw",
                     "doc_count": 1,
                     "sum_price": {
                        "value": 80000
                     }
                  },
                  {
                     "key": "ford",
                     "doc_count": 1,
                     "sum_price": {
                        "value": 25000
                     }
                  }
               ]
            }
         },
...
}

我们把结果绘成图,得到如下图:

ElasticSearch 聚合_第3张图片

你可能感兴趣的:(ElasticSearch 聚合)