日付ヒストグラム集約
このマルチバケット集約は通常の ヒストグラム に似ていますが、日付または日付範囲の値でのみ使用できます。日付はElasticsearch内部で長整数値として表現されるため、通常の histogram
を日付に使用することも可能ですが、精度はそれほど高くありません。2つのAPIの主な違いは、ここでは間隔を日付/時間の式を使用して指定できることです。時間ベースのデータは特別なサポートが必要です。なぜなら、時間ベースの間隔は常に固定の長さではないからです。
ヒストグラムと同様に、値は最も近いバケットに切り捨てられます。たとえば、間隔がカレンダー日である場合、2020-01-03T07:00:01Z
は 2020-01-03T00:00:00Z
に切り捨てられます。値は次のように切り捨てられます:
Java
bucket_key = Math.floor(value / interval) * interval
カレンダーと固定間隔
日付ヒストグラム集約を構成する際、間隔は2つの方法で指定できます:カレンダーを考慮した時間間隔と固定時間間隔。
カレンダーを考慮した間隔は、夏時間が特定の日の長さを変更すること、月ごとに異なる日数があること、うるう秒が特定の年に追加される可能性があることを理解しています。
対照的に、固定間隔は常にSI単位の倍数であり、カレンダーの文脈に基づいて変更されることはありません。
カレンダー間隔
カレンダーを考慮した間隔は、calendar_interval
パラメータで構成されます。month
のように単位名を使用してカレンダー間隔を指定することも、1M
のように単一の単位量として指定することもできます。たとえば、day
と 1d
は同等です。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
resp = client.search(
index="sales",
size="0",
aggs={
"sales_over_time": {
"date_histogram": {
"field": "date",
"calendar_interval": "month"
}
}
},
)
print(resp)
Ruby
response = client.search(
index: 'sales',
size: 0,
body: {
aggregations: {
sales_over_time: {
date_histogram: {
field: 'date',
calendar_interval: 'month'
}
}
}
}
)
puts response
Go
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)
Js
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"
}
}
}
}
カレンダー単位の倍数を使用しようとすると、集約は失敗します。なぜなら、単一のカレンダー単位のみがサポートされているからです:
Python
resp = client.search(
index="sales",
size="0",
aggs={
"sales_over_time": {
"date_histogram": {
"field": "date",
"calendar_interval": "2d"
}
}
},
)
print(resp)
Ruby
response = client.search(
index: 'sales',
size: 0,
body: {
aggregations: {
sales_over_time: {
date_histogram: {
field: 'date',
calendar_interval: '2d'
}
}
}
}
)
puts response
Go
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)
Js
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"
}
}
}
}
Js
{
"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単位の固定数であり、カレンダー上の位置に関係なく決して逸脱しません。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
resp = client.search(
index="sales",
size="0",
aggs={
"sales_over_time": {
"date_histogram": {
"field": "date",
"fixed_interval": "30d"
}
}
},
)
print(resp)
Ruby
response = client.search(
index: 'sales',
size: 0,
body: {
aggregations: {
sales_over_time: {
date_histogram: {
field: 'date',
fixed_interval: '30d'
}
}
}
}
)
puts response
Go
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)
Js
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"
}
}
}
}
サポートされていないカレンダー単位(たとえば、週)を使用しようとすると、例外が発生します:
Python
resp = client.search(
index="sales",
size="0",
aggs={
"sales_over_time": {
"date_histogram": {
"field": "date",
"fixed_interval": "2w"
}
}
},
)
print(resp)
Ruby
response = client.search(
index: 'sales',
size: 0,
body: {
aggregations: {
sales_over_time: {
date_histogram: {
field: 'date',
fixed_interval: '2w'
}
}
}
}
)
puts response
Go
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)
Js
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"
}
}
}
}
Js
{
"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 A.M.で開始および停止する国のような変動を考慮する必要があります。これにより、毎年日曜日の1分の後に土曜日の59分が続くことになります。また、国が国際日付変更線を越えることを決定する場合もあります。そのような状況は、不規則なタイムゾーンオフセットを簡単に見せることがあります。
常に、特に時間変更イベントの周りで厳密なテストを行うことで、時間間隔の指定が意図したものであることを確認できます。
予期しない結果を避けるために、すべての接続されたサーバーとクライアントは、信頼できるネットワーク時間サービスに同期する必要があります。
分数の時間値はサポートされていませんが、別の時間単位にシフトすることで対処できます(たとえば、1.5h
は 90m
として指定できます)。
また、時間単位 解析によってサポートされている略語を使用して時間値を指定することもできます。
キー
内部的に、日付はエポックからのミリ秒で表される64ビットの数として表現されます(1970年1月1日真夜中UTC)。これらのタイムスタンプは、バケットのkey
名として返されます。key_as_string
は、format
パラメータ仕様を使用してフォーマットされた日付文字列に変換された同じタイムスタンプです:
format
を指定しない場合、フィールドマッピングで指定された最初の日付 フォーマット が使用されます。
Python
resp = client.search(
index="sales",
size="0",
aggs={
"sales_over_time": {
"date_histogram": {
"field": "date",
"calendar_interval": "1M",
"format": "yyyy-MM-dd"
}
}
},
)
print(resp)
Ruby
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
Go
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)
Js
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
パラメータを使用して、バケット化に異なるタイムゾーンを使用することを示します。
タイムゾーンを指定すると、次のロジックが使用されて、ドキュメントが属するバケットが決定されます:
Java
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
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)
Ruby
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
Go
{
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)
}
Js
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
の値は、指定されたタイムゾーンの各日の真夜中を表します。
Python
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)
Ruby
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
Go
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)
Js
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日のバケットに入りますが、2番目のドキュメントは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_interval
や fixed_interval
から期待されるよりもわずかに異なるサイズになる可能性があります。たとえば、CET
タイムゾーンでのDST開始を考えてみましょう:2016年3月27日午前2時に、時計は1時間進められ、午前3時の現地時間になりました。day
を calendar_interval
として使用すると、その日のバケットは通常の他のバケットの24時間ではなく、23時間のデータしか保持しません。短い間隔(たとえば、fixed_interval
の 12h
)でも同様で、DSTシフトが発生する3月27日の朝には、11時間のバケットしかありません。
オフセット
offset
パラメータを使用して、各バケットの開始値を指定された正の(+
)または負のオフセット(-
)の期間で変更します。たとえば、1h
は1時間、1d
は1日です。詳細な時間の持続オプションについては、時間単位 を参照してください。
たとえば、day
の間隔を使用すると、各バケットは真夜中から真夜中まで実行されます。offset
パラメータを +6h
に設定すると、各バケットは午前6時から午前6時まで実行されるようになります:
Python
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)
Ruby
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
Go
{
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)
}
Js
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
}
]
}
}
}
各バケットの開始 offset
は、time_zone
調整が行われた後に計算されます。
カレンダー間隔における長いオフセット
通常、calendar_interval
よりも小さい単位でオフセットを使用することが一般的です。たとえば、間隔が日であるときに時間のオフセットを使用したり、間隔が月であるときに日数のオフセットを使用したりします。カレンダー間隔が常に標準の長さである場合、または offset
がカレンダー間隔の1単位未満である場合(たとえば、+24h
の days
や +28d
の月の場合)、各バケットは繰り返し開始します。たとえば、+6h
の days
は、すべてのバケットが毎日午前6時に開始されることになります。ただし、+30h
は、標準から夏時間に変更される日を越える場合を除いて、午前6時に開始されるバケットをもたらします。
この状況は、各月が隣接する月の少なくとも1つの長さが異なるため、月に対してはより顕著です。これを示すために、2022年の1月から8月の各月の20日の日付フィールドを持つ8つのドキュメントを考えてみましょう。
カレンダー間隔の月に対して日付ヒストグラムをクエリすると、レスポンスは各月に1つのバケットを返し、それぞれに単一のドキュメントが含まれます。各バケットには、月の最初の日にちに名前が付けられたキーがあり、オフセットが加えられます。たとえば、+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つとは異なる日になります。実際、続けていくと、同じ月に2つのドキュメントが表示されるケースが見つかります。元々30日間隔のドキュメントが同じ31日間のバケットにシフトされる可能性があります。
コンソール
"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 }
]
したがって、offset
を calendar_interval
バケットサイズで使用する際には、間隔サイズよりも大きなオフセットを使用することの結果を理解することが常に重要です。
さらに例:
- たとえば、各年が2月5日に始まる年次ヒストグラムを持つことが目標である場合、
calendar_interval
をyear
とし、offset
を+33d
とすれば、各年は同じようにシフトされます。なぜなら、オフセットには毎年同じ長さの1月だけが含まれているからです。ただし、目標が年が3月5日に始まることの場合、この手法は機能しません。なぜなら、オフセットには4年ごとに長さが変わる2月が含まれているからです。 - 年の最初の月の中の日付から始まる四半期ヒストグラムが必要な場合、機能しますが、オフセットが1か月より長くなると、開始日が2か月目に移動し、四半期はすべて異なる日付で始まります。
キー付きレスポンス
keyed
フラグを true
に設定すると、各バケットに一意の文字列キーが関連付けられ、範囲が配列ではなくハッシュとして返されます:
Python
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)
Ruby
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
Go
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)
Js
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
}
}
}
}
}
スクリプト
ドキュメント内のデータが集約したい内容と正確に一致しない場合は、ランタイムフィールド を使用します。たとえば、プロモーション販売の収益は販売日から1日後に認識されるべきです:
Python
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)
Ruby
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
Js
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 Bounds
と Hard Bounds
を参照してください。
欠損値
missing
パラメータは、値が欠損しているドキュメントをどのように扱うかを定義します。デフォルトでは、無視されますが、値があるかのように扱うことも可能です。
Python
resp = client.search(
index="sales",
size="0",
aggs={
"sale_date": {
"date_histogram": {
"field": "date",
"calendar_interval": "year",
"missing": "2000/01/01"
}
}
},
)
print(resp)
Ruby
response = client.search(
index: 'sales',
size: 0,
body: {
aggregations: {
sale_date: {
date_histogram: {
field: 'date',
calendar_interval: 'year',
missing: '2000/01/01'
}
}
}
}
)
puts response
Go
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)
Js
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
設定を使用して順序を制御できます。この設定は、order
と同じ機能をサポートします。
曜日で集約するためのスクリプトの使用
結果を曜日で集約する必要がある場合は、曜日を返す ランタイムフィールド で terms
集約を実行します:
Python
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)
Js
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です。