日期直方图聚合

编辑

这种多桶聚合与普通的 直方图 类似,但它只能用于日期或日期范围值。由于日期在 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",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "month",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "month"
      }
    }
  }
}

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

resp = client.search(
    index="sales",
    size="0",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "2d",
      },
    },
  },
});
console.log(response);
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",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        fixed_interval: "30d",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sales_over_time": {
      "date_histogram": {
        "field": "date",
        "fixed_interval": "30d"
      }
    }
  }
}

但是,如果我们尝试使用不支持的日历单位(如周),则会收到异常

resp = client.search(
    index="sales",
    size="0",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        fixed_interval: "2w",
      },
    },
  },
});
console.log(response);
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,则使用字段映射中指定的第一个日期 format

resp = client.search(
    index="sales",
    size="0",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "1M",
        format: "yyyy-MM-dd",
      },
    },
  },
});
console.log(response);
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,
    document={
        "date": "2015-10-01T00:30:00Z"
    },
)
print(resp)

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

resp2 = client.search(
    index="my-index-000001",
    size="0",
    aggs={
        "by_day": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "day"
            }
        }
    },
)
print(resp2)
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)
}
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    date: "2015-10-01T00:30:00Z",
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    date: "2015-10-01T01:30:00Z",
  },
});
console.log(response1);

const response2 = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
      },
    },
  },
});
console.log(response2);
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",
    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)
const response = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
        time_zone: "-01:00",
      },
    },
  },
});
console.log(response);
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 时区中的 DST 开始:在 2016 年 3 月 27 日凌晨 2 点,时钟向前拨快 1 小时到当地时间凌晨 3 点。如果您使用 day 作为 calendar_interval,则覆盖当天的桶将仅保存 23 小时的数据,而不是其他桶通常的 24 小时。较短的间隔也是如此,例如 12hfixed_interval,当 DST 偏移发生时,您将在 3 月 27 日早上获得一个 11 小时的桶。

偏移

编辑

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

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

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

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

resp2 = client.search(
    index="my-index-000001",
    size="0",
    aggs={
        "by_day": {
            "date_histogram": {
                "field": "date",
                "calendar_interval": "day",
                "offset": "+6h"
            }
        }
    },
)
print(resp2)
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)
}
const response = await client.index({
  index: "my-index-000001",
  id: 1,
  refresh: "true",
  document: {
    date: "2015-10-01T05:30:00Z",
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 2,
  refresh: "true",
  document: {
    date: "2015-10-01T06:30:00Z",
  },
});
console.log(response1);

const response2 = await client.search({
  index: "my-index-000001",
  size: 0,
  aggs: {
    by_day: {
      date_histogram: {
        field: "date",
        calendar_interval: "day",
        offset: "+6h",
      },
    },
  },
});
console.log(response2);
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
        }
      ]
    }
  }
}

在进行 time_zone 调整之后,计算每个存储桶的起始 offset

日历间隔的较长偏移量

编辑

通常情况下,偏移量使用的单位会小于 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,以前的二月份存储桶现在已变为 "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,并且每一年都会以相同的方式偏移,因为偏移量仅包含一月份,而一月份的长度每年都相同。但是,如果目标是让年份从 3 月 5 日开始,则此技术将不起作用,因为偏移量包含 2 月,而 2 月的长度每四年都会更改。
  • 如果想要一个季度直方图,从一年中第一个月的某个日期开始,它将起作用,但是一旦通过偏移量将起始日期推到第二个月(偏移量长于一个月),所有季度都将从不同的日期开始。

键值响应

编辑

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

resp = client.search(
    index="sales",
    size="0",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sales_over_time: {
      date_histogram: {
        field: "date",
        calendar_interval: "1M",
        format: "yyyy-MM-dd",
        keyed: true,
      },
    },
  },
});
console.log(response);
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
        }
      }
    }
  }
}

脚本

编辑

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

resp = client.search(
    index="sales",
    size="0",
    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
const response = await client.search({
  index: "sales",
  size: 0,
  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",
      },
    },
  },
});
console.log(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"
      }
    }
  }
}

参数

编辑

您可以使用 order 设置来控制返回的存储桶的顺序,并根据 min_doc_count 设置来过滤返回的存储桶(默认情况下,返回与文档匹配的第一个存储桶和最后一个存储桶之间的所有存储桶)。此直方图还支持 extended_bounds 设置,该设置使直方图的范围可以扩展到数据本身之外,以及 hard_bounds 设置,该设置将直方图限制为指定的范围。有关更多信息,请参阅 Extended BoundsHard Bounds

缺失值

编辑

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

resp = client.search(
    index="sales",
    size="0",
    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)
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    sale_date: {
      date_histogram: {
        field: "date",
        calendar_interval: "year",
        missing: "2000/01/01",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "sale_date": {
      "date_histogram": {
        "field": "date",
        "calendar_interval": "year",
        "missing": "2000/01/01" 
      }
    }
  }
}

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

排序

编辑

默认情况下,返回的存储桶按其 key 升序排序,但您可以使用 order 设置来控制排序。此设置支持与 Terms Aggregation 相同的 order 功能。

使用脚本按星期几聚合

编辑

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

resp = client.search(
    index="sales",
    size="0",
    runtime_mappings={
        "date.day_of_week": {
            "type": "keyword",
            "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
        }
    },
    aggs={
        "day_of_week": {
            "terms": {
                "field": "date.day_of_week"
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "sales",
  size: 0,
  runtime_mappings: {
    "date.day_of_week": {
      type: "keyword",
      script:
        "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))",
    },
  },
  aggs: {
    day_of_week: {
      terms: {
        field: "date.day_of_week",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "date.day_of_week": {
      "type": "keyword",
      "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
    }
  },
  "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 表示星期日。