按时间统计

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

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

  • 今年每月销售多少台汽车?

  • 这只股票最近 12 小时的价格是多少?

  • 我们网站上周每小时的平均响应延迟时间是多少?

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

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

可以用通常的 histogram 进行时间分析吗?

从技术上来讲,是可以的。 通常的 histogram bucket(桶)是可以处理日期的。 但是它不能自动识别日期。 而用 date_histogram ,你可以指定时间段如 1 个月 ,它能聪明地知道 2 月的天数比 12 月少。 date_histogram 还具有另外一个优势,即能合理地处理时区,这可以使你用客户端的时区进行图标定制,而不是用服务器端时区。

通常的 histogram 会把日期看做是数字,这意味着你必须以微秒为单位指明时间间隔。另外聚合并不知道日历时间间隔,使得它对于日期而言几乎没什么用处。

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

GET /cars/transactions/_search
{
   "size" : 0,
   "aggs": {
      "sales": {
         "date_histogram": {
            "field": "sold",
            "interval": "month", (1)
            "format": "yyyy-MM-dd" (2)
         }
      }
   }
}
  1. 时间间隔要求是日历术语 (如每个 bucket 1 个月)。

  2. 我们提供日期格式以便 buckets 的键值便于阅读。

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

结果既符合预期又有一点出人意料(看看你是否能找到意外之处):

{
   ...
   "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
            }
         ]
...
}

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

返回空 Buckets

注意到结果末尾处的奇怪之处了吗?

是的,结果没错。 我们的结果少了一些月份! 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" : { (2)
                "min" : "2014-01-01",
                "max" : "2014-12-31"
            }
         }
      }
   }
}
  1. 这个参数强制返回空 buckets。

  2. 这个参数强制返回整年。

这两个参数会强制返回一年中所有月份的结果,而不考虑结果中的文档数目。 min_doc_count 非常容易理解:它强制返回所有 buckets,即使 buckets 可能为空。

extended_bounds 参数需要一点解释。 min_doc_count 参数强制返回空 buckets,但是 Elasticsearch 默认只返回你的数据中最小值和最大值之间的 buckets。

因此如果你的数据只落在了 4 月和 7 月之间,那么你只能得到这些月份的 buckets(可能为空也可能不为空)。因此为了得到全年数据,我们需要告诉 Elasticsearch 我们想要全部 buckets, 即便那些 buckets 可能落在最小日期 之前 或 最大日期 之后

extended_bounds 参数正是如此。一旦你加上了这两个设置,你可以把得到的结果轻易地直接插入到你的图形库中,从而得到类似 汽车销售时间图 的图表。

汽车销售时间图
Figure 37. 汽车销售时间图

扩展例子

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

GET /cars/transactions/_search
{
   "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
                     }
                  }
               ]
            }
         },
...
}

我们把结果绘成图,得到如 按品牌分布的每季度销售额 所示的总销售额的折线图和每个品牌(每季度)的柱状图。

按品牌分布的每季度销售额
Figure 38. 按品牌分布的每季度销售额

潜力无穷

这些很明显都是简单例子,但图表聚合其实是潜力无穷的。 如 Kibana—​用聚合构建的实时分析面板 展示了 Kibana 中用各种聚合构建的面板。

Kibana—​用聚合构建的实时分析面板
Figure 39. Kibana—​用聚合构建的实时分析面板

因为聚合的实时性,类似这样的面板很容易查询、操作和交互。这使得它们成为需要分析数据又不会构建 Hadoop 作业的非技术人员的理想工具。

当然,为了构建类似 Kibana 这样的强大面板,你可能需要更深的知识,比如基于范围、过滤以及排序的聚合。


书籍推荐