日期直方图聚合编辑

这种多桶聚合类似于普通的 直方图,但它只能用于日期或日期范围值。由于日期在 Elasticsearch 中以长整型值的形式存储,因此可以使用普通的 histogram 对日期进行聚合,但精度可能不高。这两个 API 的主要区别在于,这里可以使用日期/时间表达式指定间隔。基于时间的需要特殊支持,因为基于时间的间隔并不总是固定长度的。

与直方图类似,值会被 向下 舍入到最接近的桶中。例如,如果间隔为一个日历日,则 2020-01-03T07:00:01Z 会被舍入到 2020-01-03T00:00:00Z。值的舍入方式如下

bucket_key = Math.floor(value / interval) * interval

日历间隔和固定间隔编辑

在配置日期直方图聚合时,间隔可以通过两种方式指定:日历感知时间间隔和固定时间间隔。

日历感知间隔会理解夏令时会改变特定日期的长度,月份有不同的天数,闰秒可以添加到特定年份。

相反,固定间隔始终是 SI 单位的倍数,并且不会根据日历上下文而改变。

日历间隔编辑

日历感知间隔使用 calendar_interval 参数配置。可以使用单位名称(例如 month)或单个单位数量(例如 1M)来指定日历间隔。例如,day1d 等效。不支持多个数量,例如 2d

接受的日历间隔为

minute, 1m
所有分钟都从 00 秒开始。一分钟是指定时区中第一个分钟的 00 秒和下一个分钟的 00 秒之间的间隔,补偿任何介于其中的闰秒,以便小时后的分钟数和秒数在开始和结束时相同。
hour, 1h
所有小时都从 00 分钟 00 秒开始。一小时 (1h) 是指定时区中第一个小时的 00:00 分钟和下一个小时的 00:00 分钟之间的间隔,补偿任何介于其中的闰秒,以便小时后的分钟数和秒数在开始和结束时相同。
day, 1d
所有日期都从最早可能的时间开始,通常是 00:00:00(午夜)。一天 (1d) 是指定时区中一天的开始时间和下一天的开始时间之间的间隔,补偿任何介于其中的时间变化。
week, 1w
一周是指定时区中 day_of_week:hour:minute:second 的开始时间和下一周的相同星期几和时间的间隔。
month, 1M
一个月是指定时区中一个月中一天的开始时间和下一个月中的同一天和时间的间隔,以便月份和一天中的时间在开始和结束时相同。请注意,如果 offset 超过一个月,则日期可能不同。
quarter, 1q
一个季度是指定时区中一个月中一天的开始时间和三个月后的同一天和时间的间隔,以便月份和一天中的时间在开始和结束时相同。
year, 1y
一年是指定时区中一个月中一天的开始时间和下一年的同一天和时间的间隔,以便日期和时间在开始和结束时相同。

日历间隔示例编辑

例如,以下是一个请求以日历时间为单位的月份间隔的聚合

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "month",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'month'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "month"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month"
      }
    }
  }
}

如果尝试使用日历单位的倍数,则聚合将失败,因为只支持单个日历单位

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "2d",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '2d'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "2d"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "2d"
      }
    }
  }
}
{
  "error" : {
    "root_cause" : [...],
    "type" : "x_content_parse_exception",
    "reason" : "[1:82] [date_histogram] failed to parse field [calendar_interval]",
    "caused_by" : {
      "type" : "illegal_argument_exception",
      "reason" : "The supplied interval [2d] could not be parsed as a calendar interval.",
      "stack_trace" : "java.lang.IllegalArgumentException: The supplied interval [2d] could not be parsed as a calendar interval."
    }
  }
}

固定间隔编辑

固定间隔使用 fixed_interval 参数配置。

与日历感知间隔相反,固定间隔是 SI 单位的固定数量,并且永远不会偏离,无论它们落在日历上的哪个位置。一秒钟始终由 1000ms 组成。这允许以支持单位的任何倍数来指定固定间隔。

但是,这意味着固定间隔无法表达其他单位,例如月份,因为月份的持续时间不是固定数量。尝试指定月份或季度等日历间隔将引发异常。

固定间隔接受的单位为

毫秒 (ms)
单个毫秒。这是一个非常非常小的间隔。
秒 (s)
定义为每个 1000 毫秒。
分钟 (m)
定义为每个 60 秒(60,000 毫秒)。所有分钟都从 00 秒开始。
小时 (h)
定义为每个 60 分钟(3,600,000 毫秒)。所有小时都从 00 分钟 00 秒开始。
天 (d)
定义为 24 小时(86,400,000 毫秒)。所有日期都从最早可能的时间开始,通常是 00:00:00(午夜)。

固定间隔示例编辑

如果我们尝试重新创建之前的“月”calendar_interval,我们可以用 30 个固定天来近似它

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date",
                    "fixed_interval": "30d",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          fixed_interval: '30d'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "fixed_interval": "30d"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "fixed_interval": "30d"
      }
    }
  }
}

但是,如果我们尝试使用不支持的日历单位,例如周,我们将得到一个异常

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {"field": "date", "fixed_interval": "2w"}
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          fixed_interval: '2w'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "fixed_interval": "2w"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "fixed_interval": "2w"
      }
    }
  }
}
{
  "error" : {
    "root_cause" : [...],
    "type" : "x_content_parse_exception",
    "reason" : "[1:82] [date_histogram] failed to parse field [fixed_interval]",
    "caused_by" : {
      "type" : "illegal_argument_exception",
      "reason" : "failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized",
      "stack_trace" : "java.lang.IllegalArgumentException: failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized"
    }
  }
}

日期直方图使用说明编辑

在所有情况下,当指定的结束时间不存在时,实际结束时间是指定的结束时间之后最接近的可用时间。

分布广泛的应用程序还必须考虑诸如在凌晨 12:01 开始和停止夏令时的国家/地区,因此一年中会出现一分钟的星期日,然后是 59 分钟的星期六,以及决定跨越国际日期变更线的国家/地区。这种情况会使不规则时区偏移看起来很容易。

与往常一样,严格的测试,尤其是在时间变化事件周围,将确保您的时间间隔规范符合您的预期。

为了避免出现意外结果,所有连接的服务器和客户端必须同步到可靠的网络时间服务。

不支持小数时间值,但可以通过切换到另一个时间单位来解决此问题(例如,1.5h 可以改为指定为 90m)。

还可以使用 时间单位 解析支持的缩写来指定时间值。

编辑

在内部,日期表示为一个 64 位数字,表示自纪元(1970 年 1 月 1 日午夜 UTC)以来的毫秒数。这些时间戳作为桶的 key 名称返回。 key_as_string 是使用 format 参数规范将相同时间戳转换为格式化的日期字符串。

如果未指定 format,则使用字段映射中指定的第一个日期 格式

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "1M",
                    "format": "yyyy-MM-dd",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '1M',
          format: 'yyyy-MM-dd'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "1M",
	        "format": "yyyy-MM-dd"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "1M",
        "format": "yyyy-MM-dd" 
      }
    }
  }
}

支持表达式的日期 格式模式

响应

{
  ...
  "aggregations": {
    "sales_over_time": {
      "buckets": [
        {
          "key_as_string": "2015-01-01",
          "key": 1420070400000,
          "doc_count": 3
        },
        {
          "key_as_string": "2015-02-01",
          "key": 1422748800000,
          "doc_count": 2
        },
        {
          "key_as_string": "2015-03-01",
          "key": 1425168000000,
          "doc_count": 2
        }
      ]
    }
  }
}

时区编辑

Elasticsearch 将日期时间存储在协调世界时 (UTC) 中。默认情况下,所有分组和舍入也在 UTC 中进行。使用 time_zone 参数指示分组应使用不同的时区。

当指定时区时,将使用以下逻辑来确定文档所属的桶

bucket_key = localToUtc(Math.floor(utcToLocal(value) / interval) * interval))

例如,如果间隔为一个日历日,时区为 America/New_York,则日期值 2020-01-03T01:00:01Z 将按以下方式处理

  1. 转换为 EST:2020-01-02T20:00:01
  2. 舍入到最接近的间隔:2020-01-02T00:00:00
  3. 转换回 UTC:2020-01-02T05:00:00:00Z

当为桶生成 key_as_string 时,键值将存储在 America/New_York 时间中,因此它将显示为 "2020-01-02T00:00:00"

可以将时区指定为 ISO 8601 UTC 偏移量,例如 +01:00-08:00,或作为 IANA 时区 ID,例如 America/Los_Angeles

考虑以下示例

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    body={"date": "2015-10-01T00:30:00Z"},
)
print(resp)

resp = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    body={"date": "2015-10-01T01:30:00Z"},
)
print(resp)

resp = client.search(
    index="my-index-000001",
    size="0",
    body={
        "aggs": {
            "by_day": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "day",
                }
            }
        }
    },
)
print(resp)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    date: '2015-10-01T00:30:00Z'
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    date: '2015-10-01T01:30:00Z'
  }
)
puts response

response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day'
        }
      }
    }
  }
)
puts response
{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T00:30:00Z"
	}`),
		es.Index.WithDocumentID("1"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T01:30:00Z"
	}`),
		es.Index.WithDocumentID("2"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Search(
		es.Search.WithIndex("my-index-000001"),
		es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day"
	      }
	    }
	  }
	}`)),
		es.Search.WithSize(0),
		es.Search.WithPretty(),
	)
	fmt.Println(res, err)
}
PUT my-index-000001/_doc/1?refresh
{
  "date": "2015-10-01T00:30:00Z"
}

PUT my-index-000001/_doc/2?refresh
{
  "date": "2015-10-01T01:30:00Z"
}

GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day"
      }
    }
  }
}

如果未指定时区,则使用 UTC。这将导致这两个文档都放入同一个日期桶中,该桶从 2015 年 10 月 1 日午夜 UTC 开始

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-10-01T00:00:00.000Z",
          "key":           1443657600000,
          "doc_count":     2
        }
      ]
    }
  }
}

如果指定 time_zone-01:00,则该时区的午夜是 UTC 午夜之前的一个小时

resp = client.search(
    index="my-index-000001",
    size="0",
    body={
        "aggs": {
            "by_day": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "day",
                    "time_zone": "-01:00",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day',
          time_zone: '-01:00'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("my-index-000001"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day",
	        "time_zone": "-01:00"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day",
        "time_zone": "-01:00"
      }
    }
  }
}

现在,第一个文档将落入 2015 年 9 月 30 日的桶中,而第二个文档将落入 2015 年 10 月 1 日的桶中

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-09-30T00:00:00.000-01:00", 
          "key": 1443574800000,
          "doc_count": 1
        },
        {
          "key_as_string": "2015-10-01T00:00:00.000-01:00", 
          "key": 1443661200000,
          "doc_count": 1
        }
      ]
    }
  }
}

key_as_string 值表示指定时区中每一天的午夜。

许多时区会为了夏令时调整他们的时钟。在这些变化发生时刻附近的时段,桶的大小可能与您从 calendar_intervalfixed_interval 预期的大小略有不同。例如,考虑 CET 时区的夏令时开始:2016 年 3 月 27 日凌晨 2 点,时钟向前拨动了 1 小时,变为当地时间凌晨 3 点。如果您使用 day 作为 calendar_interval,则覆盖该天的桶将只包含 23 小时的数据,而不是其他桶通常的 24 小时。对于更短的间隔,例如 fixed_interval12h,也是如此,您在 3 月 27 日上午夏令时转换发生时,将只有一个 11 小时的桶。

偏移量edit

使用 offset 参数通过指定的正 (+) 或负偏移量 (-) 持续时间来更改每个桶的起始值,例如 1h 表示一个小时,或 1d 表示一天。有关更多可能的持续时间选项,请参阅 时间单位

例如,当使用 day 的间隔时,每个桶从午夜到午夜运行。将 offset 参数设置为 +6h 会将每个桶更改为从上午 6 点到上午 6 点运行。

resp = client.index(
    index="my-index-000001",
    id="1",
    refresh=True,
    body={"date": "2015-10-01T05:30:00Z"},
)
print(resp)

resp = client.index(
    index="my-index-000001",
    id="2",
    refresh=True,
    body={"date": "2015-10-01T06:30:00Z"},
)
print(resp)

resp = client.search(
    index="my-index-000001",
    size="0",
    body={
        "aggs": {
            "by_day": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "day",
                    "offset": "+6h",
                }
            }
        }
    },
)
print(resp)
response = client.index(
  index: 'my-index-000001',
  id: 1,
  refresh: true,
  body: {
    date: '2015-10-01T05:30:00Z'
  }
)
puts response

response = client.index(
  index: 'my-index-000001',
  id: 2,
  refresh: true,
  body: {
    date: '2015-10-01T06:30:00Z'
  }
)
puts response

response = client.search(
  index: 'my-index-000001',
  size: 0,
  body: {
    aggregations: {
      by_day: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'day',
          offset: '+6h'
        }
      }
    }
  }
)
puts response
{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T05:30:00Z"
	}`),
		es.Index.WithDocumentID("1"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Index(
		"my-index-000001",
		strings.NewReader(`{
	  "date": "2015-10-01T06:30:00Z"
	}`),
		es.Index.WithDocumentID("2"),
		es.Index.WithRefresh("true"),
		es.Index.WithPretty(),
	)
	fmt.Println(res, err)
}

{
	res, err := es.Search(
		es.Search.WithIndex("my-index-000001"),
		es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "by_day": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "day",
	        "offset": "+6h"
	      }
	    }
	  }
	}`)),
		es.Search.WithSize(0),
		es.Search.WithPretty(),
	)
	fmt.Println(res, err)
}
PUT my-index-000001/_doc/1?refresh
{
  "date": "2015-10-01T05:30:00Z"
}

PUT my-index-000001/_doc/2?refresh
{
  "date": "2015-10-01T06:30:00Z"
}

GET my-index-000001/_search?size=0
{
  "aggs": {
    "by_day": {
      "date_histogram": {
        "field":     "date",
        "calendar_interval":  "day",
        "offset":    "+6h"
      }
    }
  }
}

上面的请求将文档分组到从上午 6 点开始的桶中,而不是从午夜开始的单个桶。

{
  ...
  "aggregations": {
    "by_day": {
      "buckets": [
        {
          "key_as_string": "2015-09-30T06:00:00.000Z",
          "key": 1443592800000,
          "doc_count": 1
        },
        {
          "key_as_string": "2015-10-01T06:00:00.000Z",
          "key": 1443679200000,
          "doc_count": 1
        }
      ]
    }
  }
}

每个桶的起始 offset 是在进行 time_zone 调整后计算的。

跨越日历间隔的长时间偏移量edit

通常使用小于 calendar_interval 的单位的偏移量。例如,当间隔为天时使用小时的偏移量,或者当间隔为月时使用天的偏移量。如果日历间隔始终为标准长度,或者 offset 小于日历间隔的一个单位(例如,对于 days 小于 +24h,或者对于月小于 +28d),则每个桶将具有重复的起始时间。例如,对于 days+6h 将导致所有桶每天从上午 6 点开始。但是,+30h 也将导致桶从上午 6 点开始,除了跨越从标准时间到夏令时或反之的日期。

对于月份来说,这种情况更为明显,因为每个月至少与相邻月份之一的长度不同。为了说明这一点,请考虑八个文档,每个文档都包含一个日期字段,分别对应 2022 年 1 月到 8 月的每个月的第 20 天。

当查询月份的日历间隔的日期直方图时,响应将返回每个月一个桶,每个桶包含一个文档。每个桶将有一个以月份的第一天命名的键,加上任何偏移量。例如,+19d 的偏移量将导致桶具有像 2022-01-20 这样的名称。


"buckets": [
  { "key_as_string": "2022-01-20", "key": 1642636800000, "doc_count": 1 },
  { "key_as_string": "2022-02-20", "key": 1645315200000, "doc_count": 1 },
  { "key_as_string": "2022-03-20", "key": 1647734400000, "doc_count": 1 },
  { "key_as_string": "2022-04-20", "key": 1650412800000, "doc_count": 1 },
  { "key_as_string": "2022-05-20", "key": 1653004800000, "doc_count": 1 },
  { "key_as_string": "2022-06-20", "key": 1655683200000, "doc_count": 1 },
  { "key_as_string": "2022-07-20", "key": 1658275200000, "doc_count": 1 },
  { "key_as_string": "2022-08-20", "key": 1660953600000, "doc_count": 1 }
]

将偏移量增加到 +20d,每个文档将出现在前一个月的桶中,所有桶键都以相同的月份日期结尾,就像平常一样。但是,进一步增加到 +28d,以前是 2 月的桶现在变成了 "2022-03-01"


"buckets": [
  { "key_as_string": "2021-12-29", "key": 1640736000000, "doc_count": 1 },
  { "key_as_string": "2022-01-29", "key": 1643414400000, "doc_count": 1 },
  { "key_as_string": "2022-03-01", "key": 1646092800000, "doc_count": 1 },
  { "key_as_string": "2022-03-29", "key": 1648512000000, "doc_count": 1 },
  { "key_as_string": "2022-04-29", "key": 1651190400000, "doc_count": 1 },
  { "key_as_string": "2022-05-29", "key": 1653782400000, "doc_count": 1 },
  { "key_as_string": "2022-06-29", "key": 1656460800000, "doc_count": 1 },
  { "key_as_string": "2022-07-29", "key": 1659052800000, "doc_count": 1 }
]

如果我们继续增加偏移量,30 天的月份也将移到下一个月,因此 8 个桶中的 3 个与其他 5 个桶的日期不同。事实上,如果我们继续下去,我们会发现两个文档出现在同一个月份的情况。最初相隔 30 天的文档可以被移到同一个 31 天的月份桶中。

例如,对于 +50d,我们看到


"buckets": [
  { "key_as_string": "2022-01-20", "key": 1642636800000, "doc_count": 1 },
  { "key_as_string": "2022-02-20", "key": 1645315200000, "doc_count": 2 },
  { "key_as_string": "2022-04-20", "key": 1650412800000, "doc_count": 2 },
  { "key_as_string": "2022-06-20", "key": 1655683200000, "doc_count": 2 },
  { "key_as_string": "2022-08-20", "key": 1660953600000, "doc_count": 1 }
]

因此,在使用 offsetcalendar_interval 桶大小时,始终要了解使用大于间隔大小的偏移量带来的后果。

更多示例

  • 例如,如果目标是创建一个每年从 2 月 5 日开始的年度直方图,您可以使用 calendar_intervalyearoffset+33d,并且每年的偏移量将相同,因为偏移量只包含 1 月,而 1 月的长度每年都相同。但是,如果目标是让年份从 3 月 5 日开始,则此技术将不起作用,因为偏移量包含 2 月,而 2 月的长度每四年变化一次。
  • 如果您想要一个从一年中第一个月的某个日期开始的季度直方图,它将起作用,但是一旦您将开始日期推到第二个月,即偏移量超过一个月,所有季度将从不同的日期开始。

键控响应edit

keyed 标志设置为 true 会将唯一的字符串键与每个桶关联,并将范围作为哈希而不是数组返回。

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "1M",
                    "format": "yyyy-MM-dd",
                    "keyed": True,
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date',
          calendar_interval: '1M',
          format: 'yyyy-MM-dd',
          keyed: true
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sales_over_time": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "1M",
	        "format": "yyyy-MM-dd",
	        "keyed": true
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "1M",
        "format": "yyyy-MM-dd",
        "keyed": true
      }
    }
  }
}

响应

{
  ...
  "aggregations": {
    "sales_over_time": {
      "buckets": {
        "2015-01-01": {
          "key_as_string": "2015-01-01",
          "key": 1420070400000,
          "doc_count": 3
        },
        "2015-02-01": {
          "key_as_string": "2015-02-01",
          "key": 1422748800000,
          "doc_count": 2
        },
        "2015-03-01": {
          "key_as_string": "2015-03-01",
          "key": 1425168000000,
          "doc_count": 2
        }
      }
    }
  }
}

脚本edit

如果文档中的数据与您想要聚合的内容不完全匹配,请使用 运行时字段。例如,如果促销销售的收入应该在销售日期后的第二天确认

resp = client.search(
    index="sales",
    size="0",
    body={
        "runtime_mappings": {
            "date.promoted_is_tomorrow": {
                "type": "date",
                "script": "\n        long date = doc['date'].value.toInstant().toEpochMilli();\n        if (doc['promoted'].value) {\n          date += 86400;\n        }\n        emit(date);\n      ",
            }
        },
        "aggs": {
            "sales_over_time": {
                "date_histogram": {
                    "field": "date.promoted_is_tomorrow",
                    "calendar_interval": "1M",
                }
            }
        },
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    runtime_mappings: {
      'date.promoted_is_tomorrow' => {
        type: 'date',
        script: "\n        long date = doc['date'].value.toInstant().toEpochMilli();\n        if (doc['promoted'].value) {\n          date += 86400;\n        }\n        emit(date);\n      "
      }
    },
    aggregations: {
      sales_over_time: {
        date_histogram: {
          field: 'date.promoted_is_tomorrow',
          calendar_interval: '1M'
        }
      }
    }
  }
)
puts response
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "date.promoted_is_tomorrow": {
      "type": "date",
      "script": """
        long date = doc['date'].value.toInstant().toEpochMilli();
        if (doc['promoted'].value) {
          date += 86400;
        }
        emit(date);
      """
    }
  },
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date.promoted_is_tomorrow",
        "calendar_interval": "1M"
      }
    }
  }
}

参数edit

您可以使用 order 设置控制返回的桶的顺序,并根据 min_doc_count 设置过滤返回的桶(默认情况下,返回第一个匹配文档的桶和最后一个桶之间的所有桶)。此直方图还支持 extended_bounds 设置,它允许将直方图的边界扩展到数据本身之外,以及 hard_bounds,它将直方图限制在指定的边界内。有关更多信息,请参阅 扩展边界硬边界

缺失值edit

missing 参数定义了如何处理缺少值的文档。默认情况下,它们会被忽略,但也可以将它们视为具有值。

resp = client.search(
    index="sales",
    size="0",
    body={
        "aggs": {
            "sale_date": {
                "date_histogram": {
                    "field": "date",
                    "calendar_interval": "year",
                    "missing": "2000/01/01",
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      sale_date: {
        date_histogram: {
          field: 'date',
          calendar_interval: 'year',
          missing: '2000/01/01'
        }
      }
    }
  }
)
puts response
res, err := es.Search(
	es.Search.WithIndex("sales"),
	es.Search.WithBody(strings.NewReader(`{
	  "aggs": {
	    "sale_date": {
	      "date_histogram": {
	        "field": "date",
	        "calendar_interval": "year",
	        "missing": "2000/01/01"
	      }
	    }
	  }
	}`)),
	es.Search.WithSize(0),
	es.Search.WithPretty(),
)
fmt.Println(res, err)
POST /sales/_search?size=0
{
  "aggs": {
    "sale_date": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "year",
        "missing": "2000/01/01" 
      }
    }
  }
}

date 字段中没有值的文档将与具有值 2000-01-01 的文档落入同一个桶中。

顺序edit

默认情况下,返回的桶按其 key 升序排序,但您可以使用 order 设置控制顺序。此设置支持与 术语聚合 相同的 order 功能。

使用脚本按星期几聚合edit

当您需要按星期几聚合结果时,请在返回星期几的 运行时字段 上运行 terms 聚合。

resp = client.search(
    index="sales",
    size="0",
    body={
        "runtime_mappings": {
            "date.day_of_week": {
                "type": "keyword",
                "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))",
            }
        },
        "aggs": {"day_of_week": {"terms": {"field": "date.day_of_week"}}},
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    runtime_mappings: {
      'date.day_of_week' => {
        type: 'keyword',
        script: "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
      }
    },
    aggregations: {
      day_of_week: {
        terms: {
          field: 'date.day_of_week'
        }
      }
    }
  }
)
puts response
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "date.day_of_week": {
      "type": "keyword",
      "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))"
    }
  },
  "aggs": {
    "day_of_week": {
      "terms": { "field": "date.day_of_week" }
    }
  }
}

响应

{
  ...
  "aggregations": {
    "day_of_week": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "Sunday",
          "doc_count": 4
        },
        {
          "key": "Thursday",
          "doc_count": 3
        }
      ]
    }
  }
}

响应将包含所有具有相对星期几作为键的桶:1 表示星期一,2 表示星期二……7 表示星期日。