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 でページ送りする

お気軽にご相談ください

お見積りへ お問い合わせへ
▲