日付ヒストグラム集約

このマルチバケット集約は通常の ヒストグラム に似ていますが、日付または日付範囲の値でのみ使用できます。日付はElasticsearch内部で長整数値として表現されるため、通常の histogram を日付に使用することも可能ですが、精度はそれほど高くありません。2つのAPIの主な違いは、ここでは間隔を日付/時間の式を使用して指定できることです。時間ベースのデータは特別なサポートが必要です。なぜなら、時間ベースの間隔は常に固定の長さではないからです。

ヒストグラムと同様に、値は最も近いバケットに切り捨てられます。たとえば、間隔がカレンダー日である場合、2020-01-03T07:00:01Z2020-01-03T00:00:00Z に切り捨てられます。値は次のように切り捨てられます:

Java

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

カレンダーと固定間隔

日付ヒストグラム集約を構成する際、間隔は2つの方法で指定できます:カレンダーを考慮した時間間隔と固定時間間隔。

カレンダーを考慮した間隔は、夏時間が特定の日の長さを変更すること、月ごとに異なる日数があること、うるう秒が特定の年に追加される可能性があることを理解しています。

対照的に、固定間隔は常にSI単位の倍数であり、カレンダーの文脈に基づいて変更されることはありません。

カレンダー間隔

カレンダーを考慮した間隔は、calendar_interval パラメータで構成されます。month のように単位名を使用してカレンダー間隔を指定することも、1M のように単一の単位量として指定することもできます。たとえば、day1d は同等です。2d のような複数の量はサポートされていません。

受け入れられるカレンダー間隔は:

  • minute, 1m
  • すべての分は00秒から始まります。1分は、指定されたタイムゾーンでの最初の分の00秒と次の分の00秒の間の間隔であり、間にうるう秒があればそれを補正します。したがって、時間の開始と終了時に分と秒の数は同じです。
  • hour, 1h
  • すべての時間は00分と00秒から始まります。1時間(1h)は、指定されたタイムゾーンでの最初の時間の00:00分と次の時間の00:00分の間の間隔であり、間にうるう秒があればそれを補正します。したがって、時間の開始と終了時に分と秒の数は同じです。
  • day, 1d
  • すべての日は、通常00:00:00(真夜中)である最も早い時間から始まります。1日(1d)は、指定されたタイムゾーンでの日の開始と次の日の開始の間の間隔であり、間に時間の変更があればそれを補正します。
  • week, 1w
  • 1週間は、指定されたタイムゾーンでの開始曜日:時間:分:秒と、次の週の同じ曜日と時間の間の間隔です。
  • month, 1M
  • 1か月は、指定されたタイムゾーンでの月の開始日と時間と、次の月の同じ日と時間の間の間隔であり、月の日と時間が開始と終了で同じであることを保証します。注意:月よりも長い offset を使用する場合、日が異なる可能性があります。
  • quarter, 1q
  • 1四半期は、指定されたタイムゾーンでの月の開始日と時間と、3か月後の同じ日と時間の間の間隔であり、月の日と時間が開始と終了で同じであることを保証します。
  • year, 1y
  • 1年は、指定されたタイムゾーンでの月の開始日と時間と、次の年の同じ日と時間の間の間隔であり、日付と時間が開始と終了で同じであることを保証します。

カレンダー間隔の例

例として、カレンダー時間での月のバケット間隔を要求する集約は次のとおりです:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "month"
  9. }
  10. }
  11. },
  12. )
  13. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: 'month'
  10. }
  11. }
  12. }
  13. }
  14. )
  15. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "month"
  9. }
  10. }
  11. }
  12. }`)),
  13. es.Search.WithSize(0),
  14. es.Search.WithPretty(),
  15. )
  16. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "month",
  9. },
  10. },
  11. },
  12. });
  13. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "month"
  8. }
  9. }
  10. }
  11. }

カレンダー単位の倍数を使用しようとすると、集約は失敗します。なぜなら、単一のカレンダー単位のみがサポートされているからです:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "2d"
  9. }
  10. }
  11. },
  12. )
  13. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: '2d'
  10. }
  11. }
  12. }
  13. }
  14. )
  15. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "2d"
  9. }
  10. }
  11. }
  12. }`)),
  13. es.Search.WithSize(0),
  14. es.Search.WithPretty(),
  15. )
  16. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "2d",
  9. },
  10. },
  11. },
  12. });
  13. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "2d"
  8. }
  9. }
  10. }
  11. }

Js

  1. {
  2. "error" : {
  3. "root_cause" : [...],
  4. "type" : "x_content_parse_exception",
  5. "reason" : "[1:82] [date_histogram] failed to parse field [calendar_interval]",
  6. "caused_by" : {
  7. "type" : "illegal_argument_exception",
  8. "reason" : "The supplied interval [2d] could not be parsed as a calendar interval.",
  9. "stack_trace" : "java.lang.IllegalArgumentException: The supplied interval [2d] could not be parsed as a calendar interval."
  10. }
  11. }
  12. }

固定間隔

固定間隔は、fixed_interval パラメータで構成されます。

カレンダーを考慮した間隔とは対照的に、固定間隔はSI単位の固定数であり、カレンダー上の位置に関係なく決して逸脱しません。1秒は常に 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日間の固定で近似できます:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "fixed_interval": "30d"
  9. }
  10. }
  11. },
  12. )
  13. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. fixed_interval: '30d'
  10. }
  11. }
  12. }
  13. }
  14. )
  15. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "fixed_interval": "30d"
  9. }
  10. }
  11. }
  12. }`)),
  13. es.Search.WithSize(0),
  14. es.Search.WithPretty(),
  15. )
  16. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. fixed_interval: "30d",
  9. },
  10. },
  11. },
  12. });
  13. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "fixed_interval": "30d"
  8. }
  9. }
  10. }
  11. }

サポートされていないカレンダー単位(たとえば、週)を使用しようとすると、例外が発生します:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "fixed_interval": "2w"
  9. }
  10. }
  11. },
  12. )
  13. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. fixed_interval: '2w'
  10. }
  11. }
  12. }
  13. }
  14. )
  15. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "fixed_interval": "2w"
  9. }
  10. }
  11. }
  12. }`)),
  13. es.Search.WithSize(0),
  14. es.Search.WithPretty(),
  15. )
  16. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. fixed_interval: "2w",
  9. },
  10. },
  11. },
  12. });
  13. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "fixed_interval": "2w"
  8. }
  9. }
  10. }
  11. }

Js

  1. {
  2. "error" : {
  3. "root_cause" : [...],
  4. "type" : "x_content_parse_exception",
  5. "reason" : "[1:82] [date_histogram] failed to parse field [fixed_interval]",
  6. "caused_by" : {
  7. "type" : "illegal_argument_exception",
  8. "reason" : "failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized",
  9. "stack_trace" : "java.lang.IllegalArgumentException: failed to parse setting [date_histogram.fixedInterval] with value [2w] as a time value: unit is missing or unrecognized"
  10. }
  11. }
  12. }

日付ヒストグラムの使用ノート

指定された終了時間が存在しない場合、実際の終了時間は指定された終了時間の後の最も近い利用可能な時間です。

広く分散したアプリケーションは、夏時間を12:01 A.M.で開始および停止する国のような変動を考慮する必要があります。これにより、毎年日曜日の1分の後に土曜日の59分が続くことになります。また、国が国際日付変更線を越えることを決定する場合もあります。そのような状況は、不規則なタイムゾーンオフセットを簡単に見せることがあります。

常に、特に時間変更イベントの周りで厳密なテストを行うことで、時間間隔の指定が意図したものであることを確認できます。

予期しない結果を避けるために、すべての接続されたサーバーとクライアントは、信頼できるネットワーク時間サービスに同期する必要があります。

分数の時間値はサポートされていませんが、別の時間単位にシフトすることで対処できます(たとえば、1.5h90m として指定できます)。

また、時間単位 解析によってサポートされている略語を使用して時間値を指定することもできます。

キー

内部的に、日付はエポックからのミリ秒で表される64ビットの数として表現されます(1970年1月1日真夜中UTC)。これらのタイムスタンプは、バケットのkey名として返されます。key_as_string は、format パラメータ仕様を使用してフォーマットされた日付文字列に変換された同じタイムスタンプです:

format を指定しない場合、フィールドマッピングで指定された最初の日付 フォーマット が使用されます。

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "1M",
  9. "format": "yyyy-MM-dd"
  10. }
  11. }
  12. },
  13. )
  14. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: '1M',
  10. format: 'yyyy-MM-dd'
  11. }
  12. }
  13. }
  14. }
  15. )
  16. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "1M",
  9. "format": "yyyy-MM-dd"
  10. }
  11. }
  12. }
  13. }`)),
  14. es.Search.WithSize(0),
  15. es.Search.WithPretty(),
  16. )
  17. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "1M",
  9. format: "yyyy-MM-dd",
  10. },
  11. },
  12. },
  13. });
  14. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "1M",
  8. "format": "yyyy-MM-dd"
  9. }
  10. }
  11. }
  12. }
表現力豊かな日付 フォーマットパターン をサポートします。

レスポンス:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "sales_over_time": {
  5. "buckets": [
  6. {
  7. "key_as_string": "2015-01-01",
  8. "key": 1420070400000,
  9. "doc_count": 3
  10. },
  11. {
  12. "key_as_string": "2015-02-01",
  13. "key": 1422748800000,
  14. "doc_count": 2
  15. },
  16. {
  17. "key_as_string": "2015-03-01",
  18. "key": 1425168000000,
  19. "doc_count": 2
  20. }
  21. ]
  22. }
  23. }
  24. }

タイムゾーン

Elasticsearchは日付時刻を協定世界時(UTC)で保存します。デフォルトでは、すべてのバケット化と丸めもUTCで行われます。time_zone パラメータを使用して、バケット化に異なるタイムゾーンを使用することを示します。

タイムゾーンを指定すると、次のロジックが使用されて、ドキュメントが属するバケットが決定されます:

Java

  1. 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" として表示されます。

タイムゾーンは、+01:00-08:00 のようなISO 8601 UTCオフセットとして、または America/Los_Angeles のようなIANAタイムゾーンIDとして指定できます。

次の例を考えてみましょう:

Python

  1. resp = client.index(
  2. index="my-index-000001",
  3. id="1",
  4. refresh=True,
  5. document={
  6. "date": "2015-10-01T00:30:00Z"
  7. },
  8. )
  9. print(resp)
  10. resp1 = client.index(
  11. index="my-index-000001",
  12. id="2",
  13. refresh=True,
  14. document={
  15. "date": "2015-10-01T01:30:00Z"
  16. },
  17. )
  18. print(resp1)
  19. resp2 = client.search(
  20. index="my-index-000001",
  21. size="0",
  22. aggs={
  23. "by_day": {
  24. "date_histogram": {
  25. "field": "date",
  26. "calendar_interval": "day"
  27. }
  28. }
  29. },
  30. )
  31. print(resp2)

Ruby

  1. response = client.index(
  2. index: 'my-index-000001',
  3. id: 1,
  4. refresh: true,
  5. body: {
  6. date: '2015-10-01T00:30:00Z'
  7. }
  8. )
  9. puts response
  10. response = client.index(
  11. index: 'my-index-000001',
  12. id: 2,
  13. refresh: true,
  14. body: {
  15. date: '2015-10-01T01:30:00Z'
  16. }
  17. )
  18. puts response
  19. response = client.search(
  20. index: 'my-index-000001',
  21. size: 0,
  22. body: {
  23. aggregations: {
  24. by_day: {
  25. date_histogram: {
  26. field: 'date',
  27. calendar_interval: 'day'
  28. }
  29. }
  30. }
  31. }
  32. )
  33. puts response

Go

  1. {
  2. res, err := es.Index(
  3. "my-index-000001",
  4. strings.NewReader(`{
  5. "date": "2015-10-01T00:30:00Z"
  6. }`),
  7. es.Index.WithDocumentID("1"),
  8. es.Index.WithRefresh("true"),
  9. es.Index.WithPretty(),
  10. )
  11. fmt.Println(res, err)
  12. }
  13. {
  14. res, err := es.Index(
  15. "my-index-000001",
  16. strings.NewReader(`{
  17. "date": "2015-10-01T01:30:00Z"
  18. }`),
  19. es.Index.WithDocumentID("2"),
  20. es.Index.WithRefresh("true"),
  21. es.Index.WithPretty(),
  22. )
  23. fmt.Println(res, err)
  24. }
  25. {
  26. res, err := es.Search(
  27. es.Search.WithIndex("my-index-000001"),
  28. es.Search.WithBody(strings.NewReader(`{
  29. "aggs": {
  30. "by_day": {
  31. "date_histogram": {
  32. "field": "date",
  33. "calendar_interval": "day"
  34. }
  35. }
  36. }
  37. }`)),
  38. es.Search.WithSize(0),
  39. es.Search.WithPretty(),
  40. )
  41. fmt.Println(res, err)
  42. }

Js

  1. const response = await client.index({
  2. index: "my-index-000001",
  3. id: 1,
  4. refresh: "true",
  5. document: {
  6. date: "2015-10-01T00:30:00Z",
  7. },
  8. });
  9. console.log(response);
  10. const response1 = await client.index({
  11. index: "my-index-000001",
  12. id: 2,
  13. refresh: "true",
  14. document: {
  15. date: "2015-10-01T01:30:00Z",
  16. },
  17. });
  18. console.log(response1);
  19. const response2 = await client.search({
  20. index: "my-index-000001",
  21. size: 0,
  22. aggs: {
  23. by_day: {
  24. date_histogram: {
  25. field: "date",
  26. calendar_interval: "day",
  27. },
  28. },
  29. },
  30. });
  31. console.log(response2);

コンソール

  1. PUT my-index-000001/_doc/1?refresh
  2. {
  3. "date": "2015-10-01T00:30:00Z"
  4. }
  5. PUT my-index-000001/_doc/2?refresh
  6. {
  7. "date": "2015-10-01T01:30:00Z"
  8. }
  9. GET my-index-000001/_search?size=0
  10. {
  11. "aggs": {
  12. "by_day": {
  13. "date_histogram": {
  14. "field": "date",
  15. "calendar_interval": "day"
  16. }
  17. }
  18. }
  19. }

タイムゾーンを指定しない場合、UTCが使用されます。これにより、これらのドキュメントはすべて同じ日バケットに配置され、2015年10月1日の真夜中UTCで始まります:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "by_day": {
  5. "buckets": [
  6. {
  7. "key_as_string": "2015-10-01T00:00:00.000Z",
  8. "key": 1443657600000,
  9. "doc_count": 2
  10. }
  11. ]
  12. }
  13. }
  14. }

time_zone の値は、指定されたタイムゾーンの各日の真夜中を表します。

Python

  1. resp = client.search(
  2. index="my-index-000001",
  3. size="0",
  4. aggs={
  5. "by_day": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "day",
  9. "time_zone": "-01:00"
  10. }
  11. }
  12. },
  13. )
  14. print(resp)

Ruby

  1. response = client.search(
  2. index: 'my-index-000001',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. by_day: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: 'day',
  10. time_zone: '-01:00'
  11. }
  12. }
  13. }
  14. }
  15. )
  16. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("my-index-000001"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "by_day": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "day",
  9. "time_zone": "-01:00"
  10. }
  11. }
  12. }
  13. }`)),
  14. es.Search.WithSize(0),
  15. es.Search.WithPretty(),
  16. )
  17. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "my-index-000001",
  3. size: 0,
  4. aggs: {
  5. by_day: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "day",
  9. time_zone: "-01:00",
  10. },
  11. },
  12. },
  13. });
  14. console.log(response);

コンソール

  1. GET my-index-000001/_search?size=0
  2. {
  3. "aggs": {
  4. "by_day": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "day",
  8. "time_zone": "-01:00"
  9. }
  10. }
  11. }
  12. }

最初のドキュメントは2015年9月30日のバケットに入りますが、2番目のドキュメントは2015年10月1日のバケットに入ります:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "by_day": {
  5. "buckets": [
  6. {
  7. "key_as_string": "2015-09-30T00:00:00.000-01:00",
  8. "key": 1443574800000,
  9. "doc_count": 1
  10. },
  11. {
  12. "key_as_string": "2015-10-01T00:00:00.000-01:00",
  13. "key": 1443661200000,
  14. "doc_count": 1
  15. }
  16. ]
  17. }
  18. }
  19. }
key_as_string の値は、指定されたタイムゾーンの各日の真夜中を表します。

多くのタイムゾーンは、夏時間のために時計を変更します。これらの変更が発生する瞬間に近いバケットは、calendar_intervalfixed_interval から期待されるよりもわずかに異なるサイズになる可能性があります。たとえば、CET タイムゾーンでのDST開始を考えてみましょう:2016年3月27日午前2時に、時計は1時間進められ、午前3時の現地時間になりました。daycalendar_interval として使用すると、その日のバケットは通常の他のバケットの24時間ではなく、23時間のデータしか保持しません。短い間隔(たとえば、fixed_interval12h)でも同様で、DSTシフトが発生する3月27日の朝には、11時間のバケットしかありません。

オフセット

offset パラメータを使用して、各バケットの開始値を指定された正の(+)または負のオフセット(-)の期間で変更します。たとえば、1h は1時間、1d は1日です。詳細な時間の持続オプションについては、時間単位 を参照してください。

たとえば、day の間隔を使用すると、各バケットは真夜中から真夜中まで実行されます。offset パラメータを +6h に設定すると、各バケットは午前6時から午前6時まで実行されるようになります:

Python

  1. resp = client.index(
  2. index="my-index-000001",
  3. id="1",
  4. refresh=True,
  5. document={
  6. "date": "2015-10-01T05:30:00Z"
  7. },
  8. )
  9. print(resp)
  10. resp1 = client.index(
  11. index="my-index-000001",
  12. id="2",
  13. refresh=True,
  14. document={
  15. "date": "2015-10-01T06:30:00Z"
  16. },
  17. )
  18. print(resp1)
  19. resp2 = client.search(
  20. index="my-index-000001",
  21. size="0",
  22. aggs={
  23. "by_day": {
  24. "date_histogram": {
  25. "field": "date",
  26. "calendar_interval": "day",
  27. "offset": "+6h"
  28. }
  29. }
  30. },
  31. )
  32. print(resp2)

Ruby

  1. response = client.index(
  2. index: 'my-index-000001',
  3. id: 1,
  4. refresh: true,
  5. body: {
  6. date: '2015-10-01T05:30:00Z'
  7. }
  8. )
  9. puts response
  10. response = client.index(
  11. index: 'my-index-000001',
  12. id: 2,
  13. refresh: true,
  14. body: {
  15. date: '2015-10-01T06:30:00Z'
  16. }
  17. )
  18. puts response
  19. response = client.search(
  20. index: 'my-index-000001',
  21. size: 0,
  22. body: {
  23. aggregations: {
  24. by_day: {
  25. date_histogram: {
  26. field: 'date',
  27. calendar_interval: 'day',
  28. offset: '+6h'
  29. }
  30. }
  31. }
  32. }
  33. )
  34. puts response

Go

  1. {
  2. res, err := es.Index(
  3. "my-index-000001",
  4. strings.NewReader(`{
  5. "date": "2015-10-01T05:30:00Z"
  6. }`),
  7. es.Index.WithDocumentID("1"),
  8. es.Index.WithRefresh("true"),
  9. es.Index.WithPretty(),
  10. )
  11. fmt.Println(res, err)
  12. }
  13. {
  14. res, err := es.Index(
  15. "my-index-000001",
  16. strings.NewReader(`{
  17. "date": "2015-10-01T06:30:00Z"
  18. }`),
  19. es.Index.WithDocumentID("2"),
  20. es.Index.WithRefresh("true"),
  21. es.Index.WithPretty(),
  22. )
  23. fmt.Println(res, err)
  24. }
  25. {
  26. res, err := es.Search(
  27. es.Search.WithIndex("my-index-000001"),
  28. es.Search.WithBody(strings.NewReader(`{
  29. "aggs": {
  30. "by_day": {
  31. "date_histogram": {
  32. "field": "date",
  33. "calendar_interval": "day",
  34. "offset": "+6h"
  35. }
  36. }
  37. }
  38. }`)),
  39. es.Search.WithSize(0),
  40. es.Search.WithPretty(),
  41. )
  42. fmt.Println(res, err)
  43. }

Js

  1. const response = await client.index({
  2. index: "my-index-000001",
  3. id: 1,
  4. refresh: "true",
  5. document: {
  6. date: "2015-10-01T05:30:00Z",
  7. },
  8. });
  9. console.log(response);
  10. const response1 = await client.index({
  11. index: "my-index-000001",
  12. id: 2,
  13. refresh: "true",
  14. document: {
  15. date: "2015-10-01T06:30:00Z",
  16. },
  17. });
  18. console.log(response1);
  19. const response2 = await client.search({
  20. index: "my-index-000001",
  21. size: 0,
  22. aggs: {
  23. by_day: {
  24. date_histogram: {
  25. field: "date",
  26. calendar_interval: "day",
  27. offset: "+6h",
  28. },
  29. },
  30. },
  31. });
  32. console.log(response2);

コンソール

  1. PUT my-index-000001/_doc/1?refresh
  2. {
  3. "date": "2015-10-01T05:30:00Z"
  4. }
  5. PUT my-index-000001/_doc/2?refresh
  6. {
  7. "date": "2015-10-01T06:30:00Z"
  8. }
  9. GET my-index-000001/_search?size=0
  10. {
  11. "aggs": {
  12. "by_day": {
  13. "date_histogram": {
  14. "field": "date",
  15. "calendar_interval": "day",
  16. "offset": "+6h"
  17. }
  18. }
  19. }
  20. }

真夜中から始まる単一のバケットの代わりに、上記のリクエストは午前6時から始まるバケットにドキュメントをグループ化します:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "by_day": {
  5. "buckets": [
  6. {
  7. "key_as_string": "2015-09-30T06:00:00.000Z",
  8. "key": 1443592800000,
  9. "doc_count": 1
  10. },
  11. {
  12. "key_as_string": "2015-10-01T06:00:00.000Z",
  13. "key": 1443679200000,
  14. "doc_count": 1
  15. }
  16. ]
  17. }
  18. }
  19. }

各バケットの開始 offset は、time_zone 調整が行われた後に計算されます。

カレンダー間隔における長いオフセット

通常、calendar_interval よりも小さい単位でオフセットを使用することが一般的です。たとえば、間隔が日であるときに時間のオフセットを使用したり、間隔が月であるときに日数のオフセットを使用したりします。カレンダー間隔が常に標準の長さである場合、または offset がカレンダー間隔の1単位未満である場合(たとえば、+24hdays+28d の月の場合)、各バケットは繰り返し開始します。たとえば、+6hdays は、すべてのバケットが毎日午前6時に開始されることになります。ただし、+30h は、標準から夏時間に変更される日を越える場合を除いて、午前6時に開始されるバケットをもたらします。

この状況は、各月が隣接する月の少なくとも1つの長さが異なるため、月に対してはより顕著です。これを示すために、2022年の1月から8月の各月の20日の日付フィールドを持つ8つのドキュメントを考えてみましょう。

カレンダー間隔の月に対して日付ヒストグラムをクエリすると、レスポンスは各月に1つのバケットを返し、それぞれに単一のドキュメントが含まれます。各バケットには、月の最初の日にちに名前が付けられたキーがあり、オフセットが加えられます。たとえば、+19d のオフセットは、2022-01-20 のような名前のバケットをもたらします。

コンソール

  1. "buckets": [
  2. { "key_as_string": "2022-01-20", "key": 1642636800000, "doc_count": 1 },
  3. { "key_as_string": "2022-02-20", "key": 1645315200000, "doc_count": 1 },
  4. { "key_as_string": "2022-03-20", "key": 1647734400000, "doc_count": 1 },
  5. { "key_as_string": "2022-04-20", "key": 1650412800000, "doc_count": 1 },
  6. { "key_as_string": "2022-05-20", "key": 1653004800000, "doc_count": 1 },
  7. { "key_as_string": "2022-06-20", "key": 1655683200000, "doc_count": 1 },
  8. { "key_as_string": "2022-07-20", "key": 1658275200000, "doc_count": 1 },
  9. { "key_as_string": "2022-08-20", "key": 1660953600000, "doc_count": 1 }
  10. ]

オフセットを +20d に増やすと、各ドキュメントは前の月のバケットに表示され、すべてのバケットキーは通常と同じ日付で終わります。ただし、+28d にさらに増やすと、以前の2月のバケットは "2022-03-01" になります。

コンソール

  1. "buckets": [
  2. { "key_as_string": "2021-12-29", "key": 1640736000000, "doc_count": 1 },
  3. { "key_as_string": "2022-01-29", "key": 1643414400000, "doc_count": 1 },
  4. { "key_as_string": "2022-03-01", "key": 1646092800000, "doc_count": 1 },
  5. { "key_as_string": "2022-03-29", "key": 1648512000000, "doc_count": 1 },
  6. { "key_as_string": "2022-04-29", "key": 1651190400000, "doc_count": 1 },
  7. { "key_as_string": "2022-05-29", "key": 1653782400000, "doc_count": 1 },
  8. { "key_as_string": "2022-06-29", "key": 1656460800000, "doc_count": 1 },
  9. { "key_as_string": "2022-07-29", "key": 1659052800000, "doc_count": 1 }
  10. ]

オフセットを増やし続けると、30日間の月も次の月にシフトし、8つのバケットのうち3つが他の5つとは異なる日になります。実際、続けていくと、同じ月に2つのドキュメントが表示されるケースが見つかります。元々30日間隔のドキュメントが同じ31日間のバケットにシフトされる可能性があります。

たとえば、+50d の場合、次のようになります:

コンソール

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

したがって、offsetcalendar_interval バケットサイズで使用する際には、間隔サイズよりも大きなオフセットを使用することの結果を理解することが常に重要です。

さらに例:

  • たとえば、各年が2月5日に始まる年次ヒストグラムを持つことが目標である場合、calendar_intervalyear とし、offset+33d とすれば、各年は同じようにシフトされます。なぜなら、オフセットには毎年同じ長さの1月だけが含まれているからです。ただし、目標が年が3月5日に始まることの場合、この手法は機能しません。なぜなら、オフセットには4年ごとに長さが変わる2月が含まれているからです。
  • 年の最初の月の中の日付から始まる四半期ヒストグラムが必要な場合、機能しますが、オフセットが1か月より長くなると、開始日が2か月目に移動し、四半期はすべて異なる日付で始まります。

キー付きレスポンス

keyed フラグを true に設定すると、各バケットに一意の文字列キーが関連付けられ、範囲が配列ではなくハッシュとして返されます:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "1M",
  9. "format": "yyyy-MM-dd",
  10. "keyed": True
  11. }
  12. }
  13. },
  14. )
  15. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sales_over_time: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: '1M',
  10. format: 'yyyy-MM-dd',
  11. keyed: true
  12. }
  13. }
  14. }
  15. }
  16. )
  17. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sales_over_time": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "1M",
  9. "format": "yyyy-MM-dd",
  10. "keyed": true
  11. }
  12. }
  13. }
  14. }`)),
  15. es.Search.WithSize(0),
  16. es.Search.WithPretty(),
  17. )
  18. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sales_over_time: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "1M",
  9. format: "yyyy-MM-dd",
  10. keyed: true,
  11. },
  12. },
  13. },
  14. });
  15. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sales_over_time": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "1M",
  8. "format": "yyyy-MM-dd",
  9. "keyed": true
  10. }
  11. }
  12. }
  13. }

レスポンス:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "sales_over_time": {
  5. "buckets": {
  6. "2015-01-01": {
  7. "key_as_string": "2015-01-01",
  8. "key": 1420070400000,
  9. "doc_count": 3
  10. },
  11. "2015-02-01": {
  12. "key_as_string": "2015-02-01",
  13. "key": 1422748800000,
  14. "doc_count": 2
  15. },
  16. "2015-03-01": {
  17. "key_as_string": "2015-03-01",
  18. "key": 1425168000000,
  19. "doc_count": 2
  20. }
  21. }
  22. }
  23. }
  24. }

スクリプト

ドキュメント内のデータが集約したい内容と正確に一致しない場合は、ランタイムフィールド を使用します。たとえば、プロモーション販売の収益は販売日から1日後に認識されるべきです:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. runtime_mappings={
  5. "date.promoted_is_tomorrow": {
  6. "type": "date",
  7. "script": "\n long date = doc['date'].value.toInstant().toEpochMilli();\n if (doc['promoted'].value) {\n date += 86400;\n }\n emit(date);\n "
  8. }
  9. },
  10. aggs={
  11. "sales_over_time": {
  12. "date_histogram": {
  13. "field": "date.promoted_is_tomorrow",
  14. "calendar_interval": "1M"
  15. }
  16. }
  17. },
  18. )
  19. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. runtime_mappings: {
  6. 'date.promoted_is_tomorrow' => {
  7. type: 'date',
  8. script: "\n long date = doc['date'].value.toInstant().toEpochMilli();\n if (doc['promoted'].value) {\n date += 86400;\n }\n emit(date);\n "
  9. }
  10. },
  11. aggregations: {
  12. sales_over_time: {
  13. date_histogram: {
  14. field: 'date.promoted_is_tomorrow',
  15. calendar_interval: '1M'
  16. }
  17. }
  18. }
  19. }
  20. )
  21. puts response

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. runtime_mappings: {
  5. "date.promoted_is_tomorrow": {
  6. type: "date",
  7. script:
  8. "\n long date = doc['date'].value.toInstant().toEpochMilli();\n if (doc['promoted'].value) {\n date += 86400;\n }\n emit(date);\n ",
  9. },
  10. },
  11. aggs: {
  12. sales_over_time: {
  13. date_histogram: {
  14. field: "date.promoted_is_tomorrow",
  15. calendar_interval: "1M",
  16. },
  17. },
  18. },
  19. });
  20. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "runtime_mappings": {
  4. "date.promoted_is_tomorrow": {
  5. "type": "date",
  6. "script": """
  7. long date = doc['date'].value.toInstant().toEpochMilli();
  8. if (doc['promoted'].value) {
  9. date += 86400;
  10. }
  11. emit(date);
  12. """
  13. }
  14. },
  15. "aggs": {
  16. "sales_over_time": {
  17. "date_histogram": {
  18. "field": "date.promoted_is_tomorrow",
  19. "calendar_interval": "1M"
  20. }
  21. }
  22. }
  23. }

パラメータ

order 設定を使用して返されるバケットの順序を制御し、min_doc_count 設定に基づいて返されるバケットをフィルタリングできます(デフォルトでは、ドキュメントに一致する最初のバケットと最後のバケットの間のすべてのバケットが返されます)。このヒストグラムは、データ自体を超えてヒストグラムの境界を拡張する extended_bounds 設定と、指定された境界にヒストグラムを制限する hard_bounds 設定もサポートしています。詳細については、Extended BoundsHard Bounds を参照してください。

欠損値

missing パラメータは、値が欠損しているドキュメントをどのように扱うかを定義します。デフォルトでは、無視されますが、値があるかのように扱うことも可能です。

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. aggs={
  5. "sale_date": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "year",
  9. "missing": "2000/01/01"
  10. }
  11. }
  12. },
  13. )
  14. print(resp)

Ruby

  1. response = client.search(
  2. index: 'sales',
  3. size: 0,
  4. body: {
  5. aggregations: {
  6. sale_date: {
  7. date_histogram: {
  8. field: 'date',
  9. calendar_interval: 'year',
  10. missing: '2000/01/01'
  11. }
  12. }
  13. }
  14. }
  15. )
  16. puts response

Go

  1. res, err := es.Search(
  2. es.Search.WithIndex("sales"),
  3. es.Search.WithBody(strings.NewReader(`{
  4. "aggs": {
  5. "sale_date": {
  6. "date_histogram": {
  7. "field": "date",
  8. "calendar_interval": "year",
  9. "missing": "2000/01/01"
  10. }
  11. }
  12. }
  13. }`)),
  14. es.Search.WithSize(0),
  15. es.Search.WithPretty(),
  16. )
  17. fmt.Println(res, err)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. aggs: {
  5. sale_date: {
  6. date_histogram: {
  7. field: "date",
  8. calendar_interval: "year",
  9. missing: "2000/01/01",
  10. },
  11. },
  12. },
  13. });
  14. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "aggs": {
  4. "sale_date": {
  5. "date_histogram": {
  6. "field": "date",
  7. "calendar_interval": "year",
  8. "missing": "2000/01/01"
  9. }
  10. }
  11. }
  12. }
date フィールドに値がないドキュメントは、値が 2000-01-01 であるドキュメントと同じバケットに入ります。

順序

デフォルトでは、返されるバケットはその key 昇順でソートされますが、order 設定を使用して順序を制御できます。この設定は、order と同じ機能をサポートします。

曜日で集約するためのスクリプトの使用

結果を曜日で集約する必要がある場合は、曜日を返す ランタイムフィールドterms 集約を実行します:

Python

  1. resp = client.search(
  2. index="sales",
  3. size="0",
  4. runtime_mappings={
  5. "date.day_of_week": {
  6. "type": "keyword",
  7. "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
  8. }
  9. },
  10. aggs={
  11. "day_of_week": {
  12. "terms": {
  13. "field": "date.day_of_week"
  14. }
  15. }
  16. },
  17. )
  18. print(resp)

Js

  1. const response = await client.search({
  2. index: "sales",
  3. size: 0,
  4. runtime_mappings: {
  5. "date.day_of_week": {
  6. type: "keyword",
  7. script:
  8. "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))",
  9. },
  10. },
  11. aggs: {
  12. day_of_week: {
  13. terms: {
  14. field: "date.day_of_week",
  15. },
  16. },
  17. },
  18. });
  19. console.log(response);

コンソール

  1. POST /sales/_search?size=0
  2. {
  3. "runtime_mappings": {
  4. "date.day_of_week": {
  5. "type": "keyword",
  6. "script": "emit(doc['date'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
  7. }
  8. },
  9. "aggs": {
  10. "day_of_week": {
  11. "terms": { "field": "date.day_of_week" }
  12. }
  13. }
  14. }

レスポンス:

コンソール-結果

  1. {
  2. ...
  3. "aggregations": {
  4. "day_of_week": {
  5. "doc_count_error_upper_bound": 0,
  6. "sum_other_doc_count": 0,
  7. "buckets": [
  8. {
  9. "key": "Sunday",
  10. "doc_count": 4
  11. },
  12. {
  13. "key": "Thursday",
  14. "doc_count": 3
  15. }
  16. ]
  17. }
  18. }
  19. }

レスポンスには、曜日を相対的にキーとして持つすべてのバケットが含まれます:月曜日は1、火曜日は2…日曜日は7です。