2026年5月29日
2026年5月29日
ElasticsearchのAggregationでデータ集計(アクセスログで学ぶterms・date_histogram・近似値の注意点)
Elasticsearch の Aggregation(集計)は、検索条件に合うドキュメントを、件数・平均・日別の推移などにまとめる機能です。SQL の GROUP BY と集計関数に近いもので、aggs を検索リクエストに書くだけで使えます。この記事では、Nginx のアクセスログを入れたインデックスを例に、よく使う集計の書き方と、結果を読み違えやすいポイント(近似値・タイムゾーン・バケット数の上限)を説明します。
コード例は Kibana の Dev Tools(コンソール)の書式です。対象のインデックスには、次のようなフィールドがある想定です。
@timestamp(date)、status(short)、url_path(keyword)、request_time(float、秒)、client_ip(ip)
集計の3つの種類
Aggregation は大きく 3 種類に分かれます。
- メトリクス集計: 値を 1 つの数値にまとめる。
avg・sum・min・max・stats・percentiles・cardinality(ユニーク数)など - バケット集計: ドキュメントをグループ(バケット)に分ける。
terms(値ごと)・date_histogram(時間ごと)・range(範囲ごと)・filter/filters(条件ごと)など - パイプライン集計: ほかの集計の結果をさらに計算する。
cumulative_sum(累計)・derivative(前との差)・bucket_sortなど
実際には、バケット集計の中にメトリクス集計を入れ子にして「URL ごとの平均応答時間」「日ごとの 5xx 件数」のように組み合わせます。
最初の集計: ステータスコード別の件数
集計だけが欲しいときは "size": 0 を指定し、検索結果(ヒットしたドキュメント本体)を返さないようにします。転送量が減り、応答も速くなります。
GET nginx-logs-*/_search
{
"size": 0,
"query": {
"range": { "@timestamp": { "gte": "now-24h" } }
},
"aggs": {
"by_status": {
"terms": { "field": "status", "size": 10 }
}
}
}
結果の aggregations.by_status.buckets に、key(ステータスコード)と doc_count(件数)の組が件数の多い順に並びます。query で絞った範囲だけが集計の対象になる点が、SQL の WHERE と同じ考え方です。
text フィールドで集計するとエラーになる
terms を text 型のフィールドに使うと、「text フィールドは集計や並べ替えに向かないので既定で無効」という趣旨のエラーになります。text は単語に分割して索引されているため、値全体での集計ができないからです。自動マッピングで作られたインデックスなら .keyword サブフィールド(例: user_agent.keyword)を指定します。fielddata を有効にして回避する方法もありますが、メモリを大きく消費するため、マッピングを見直して keyword を用意するほうが安全です。
日別の推移: date_histogram
時間ごとの集計には date_histogram を使います。間隔の指定は 2 種類あり、混同するとエラーになります。
calendar_interval: 暦に沿った単位。1d(1日)・1w・1M(1か月)など。月の長さの違いも考慮されるfixed_interval: 固定の長さ。30m・12hなど
古い版で使われていた interval は廃止されているため、ほかの記事のコードを流用するときは書き換えます。
GET nginx-logs-*/_search
{
"size": 0,
"query": {
"range": {
"@timestamp": { "gte": "now-7d/d", "lt": "now/d", "time_zone": "+09:00" }
}
},
"aggs": {
"per_day": {
"date_histogram": {
"field": "@timestamp",
"calendar_interval": "1d",
"time_zone": "Asia/Tokyo",
"min_doc_count": 0
},
"aggs": {
"server_errors": {
"filter": { "range": { "status": { "gte": 500 } } }
},
"p95_time": {
"percentiles": { "field": "request_time", "percents": [95] }
},
"running_total": {
"cumulative_sum": { "buckets_path": "_count" }
}
}
}
}
}
この例では、日ごとのバケットの中に「5xx の件数」「応答時間の 95 パーセンタイル」「その日までの累計件数」を入れています。
time_zone の指定は重要です。日時は内部的に UTC で扱われるため、指定しないと日の区切りが日本時間の午前 9 時になり、「深夜のアクセスが前日に計上される」というずれが起きます。range クエリ側の now/d(日の切り捨て)にも同じことが言えるため、両方に指定しています。min_doc_count: 0 は、件数ゼロの日もバケットとして返す設定で、グラフに穴が開くのを防ぎます。
上位の URL と平均応答時間
terms の中にメトリクスを入れ、さらにその値で並べ替えると「遅いページの一覧」が作れます。
GET nginx-logs-*/_search
{
"size": 0,
"query": { "range": { "@timestamp": { "gte": "now-24h" } } },
"aggs": {
"slow_paths": {
"terms": {
"field": "url_path",
"size": 20,
"order": { "avg_time": "desc" },
"min_doc_count": 50
},
"aggs": {
"avg_time": { "avg": { "field": "request_time" } }
}
},
"unique_visitors": {
"cardinality": { "field": "client_ip" }
}
}
}
min_doc_count: 50 で、数回しかアクセスのない URL が平均値の上位を占めるのを避けています。WordPress のサイトでは /wp-admin/admin-ajax.php や /wp-cron.php が上位に来ることが多く、どの処理が重いかを調べる入口になります。
結果を読み違えないための注意点
terms の件数は近似値になることがある
インデックスが複数のシャードに分かれていると、terms は各シャードで上位を集めてから全体で合算します。そのため、シャードによって順位が違う値は件数が少なめに出たり、上位から漏れたりすることがあります。結果に含まれる doc_count_error_upper_bound(誤差の上限)と sum_other_doc_count(上位に入らなかった件数の合計)を見ると、どの程度の影響があるかがわかります。精度を上げたい場合は shard_size を大きくしますが、その分メモリと時間を使います。1 シャードのインデックスではこの誤差は生じません。
cardinality と percentiles も近似
ユニーク数を数える cardinality は、メモリを節約するために近似アルゴリズムを使います。少ない件数ではほぼ正確ですが、件数が多くなると数パーセント程度の誤差が出ることがあります。precision_threshold で、正確に数えたい件数の目安を指定できます。percentiles も同様に近似です。請求金額のように 1 件の違いも許されない用途には使わず、傾向を見る用途にとどめます。
バケットが多すぎるとエラーになる
terms の size を大きくしたり、細かい間隔の date_histogram に terms を入れ子にしたりすると、バケットの総数が search.max_buckets の上限を超えてエラーになります。すべての値を漏れなく取り出したい場合は、composite 集計を使い、結果の after_key を次のリクエストに渡してページ送りします。
GET nginx-logs-*/_search
{
"size": 0,
"aggs": {
"all_paths": {
"composite": {
"size": 1000,
"sources": [ { "path": { "terms": { "field": "url_path" } } } ]
}
}
}
}
2 回目以降は "after": { "path": "前回の after_key の値" } を composite の中に追加します。
重い集計を軽くするには
queryで期間を絞ってから集計する。全期間の集計は、必要なときだけにする- インデックス名のパターンを期間に合わせて狭くする(
nginx-logs-2026.10.*など) - 集計に使うフィールドは
keywordや数値型にしておく(doc_valuesが使われ、メモリ効率がよい) - 同じ集計を何度も実行するダッシュボードでは、更新間隔を必要以上に短くしない
Elasticsearch の版によって、集計の種類や細かな挙動は追加・変更されています(2026年10月現在)。使っている版の公式リファレンスで、各集計のパラメーターを確認してください。
まとめ
- 集計はメトリクス・バケット・パイプラインの 3 種類。バケットの中にメトリクスを入れ子にして使う
- 集計だけが欲しいときは
"size": 0。集計対象はqueryで絞る termsはkeywordか数値のフィールドに使う。textにはエラーになるdate_histogramはcalendar_intervalとfixed_intervalを使い分け、time_zoneを指定するterms・cardinality・percentilesは近似になりうる。誤差の目安を結果から確認する- バケットが多いときは
compositeでページ送りする