💨

Elasticsearchの集計に誤差、僕は何も理解してないことを知った

2025/01/28に公開

ちょっとだけ規模の大きな Elasticsearch を運用している @zaru です。こんにちは。

今回は Elasticsearch で集計を行った際に「ん? 集計結果がずれている、微妙に少ないぞ」という事象に遭遇しました。調べてみると、自分が何も理解していなかったことに気づいたので、同じように困ったり疑問を持っている方の助けになればと思い、メモを残します。

Elasticsearchの集計

Elasticsearch での集計(集約)は、Aggregations という機能を使います。大量のデータに対しても非常に高速に結果を求められるので、厳しい要件を満たしやすい反面、Elasticsearch のインフラコストは高めになりがちです。

まずは Aggregations のクエリ例を紹介します。以下の例では、label_id フィールドごとの件数をカウントし、その上位 3 件の集計結果を取得しています。

GET /entry/_search
{
  "_source": false,
  "size": 0,
  "aggs": {
    "label_count": {
      "terms": {
        "field": "label_id",
        "size": 3 // 集計結果の上位3件を取得する
      }
    }
  }
}

レスポンス例は以下のとおりです。

aggregations.label_count.buckets[] 配列に上位 3 件の label_id が入っており、key がラベル ID、doc_count がそのドキュメント数を表しています。たとえばラベル ID 597082 のドキュメントが 40 件あり、1 位という結果になっています。

{
  "took" : 10,
  "timed_out" : false,
  "_shards" : {
    "total" : 12,
    "successful" : 12,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4220,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "label_count" : {
      "doc_count_error_upper_bound" : 68,
      "sum_other_doc_count" : 4104,
      "buckets" : [
        {
          "key" : 597082, // 1位のラベルID
          "doc_count" : 40 // 件数
        },
        {
          "key" : 632195,
          "doc_count" : 39
        },
        {
          "key" : 568545,
          "doc_count" : 37
        }
      ]
    }
  }
}

一見すると正しく集計できているように見えますが、doc_count_error_upper_bound が 0 より大きい場合には、集計結果に誤差が含まれている可能性 があります。

今回の例では「1位が40件」という結果ですが、実際には58件あり、18件分の誤差が生じていました。

doc_count_error_upper_boundとは

  • doc_count_error_upper_bound
    • 集計結果の誤差上限を示す値
    • 返ってきたランキングのドキュメント数が最大指定件数まで上振れする可能性がある

わかりにくいのですが、要は doc_count_error_upper_bound が0の場合は全データを集計していて誤差はないという判断ができます。逆に0より大きい場合は doc_count_error_upper_bound の数だけ上振れする誤差があり得るということです。

重要なのは可能性があるというだけで、0よりも大きかったとしても誤差がないケースも普通にあります。

なぜ誤差が発生するのか

誤差の原因は、Elasticsearch が複数のシャードで並行処理を行い、その結果をマージしている ことにあります。Elasticsearch は、複数のシャードに対して同じクエリを投げ、それぞれのシャードが持つデータを部分集計した結果を統合して返す仕組みです。

ランキング上位だけを返す場合、シャードごとに偏りが生じると、想定していたラベルが集計から漏れてしまうなどのズレが起きることがあります。

上の図では、ラベルごとの件数をカウントしようとしていますが、シャードごとに「どのラベルが上位なのか」が異なってしまい、「シャードB」では本来3位に来るはずの「ラベルccc」が1位に躍り出ています。このようにシャード単位の「上位N件」の取りこぼしによって、マージ後の集計結果に誤差が生じる場合があります。

なるべく誤差をなくすには

なるべく誤差を減らすには、size の値を大きくする(→各シャードで集計するドキュメント数を増やす)方法がもっとも簡単です。デフォルトでは size = 10(上位 10 件)。これを大きな値に変更すると、そのぶん正しい上位を取りこぼすリスクが減り、誤差が小さくなります。

GET /entry/_search
{
  "_source": false,
  "size": 0,
  "aggs": {
    "label_count": {
      "terms": {
        "field": "label_id",
        "size": 100 // 本当は上位10件しか必要ないけど誤差をなくすために上位100件集計
      }
    }
  }
}

また、size 以外に shard_size というオプション項目もあります。shard_size は各シャードごとで上位何件集計するかの値です。デフォルトでは size * 1.5 + 10(例:size = 10 の場合は 25)で自動計算されます。

各シャードで上位25件を集計した結果をマージして、上位10件抜き出しているということです。

size を必要な数にしておいて shard_size だけを引き上げることでも誤差をなくすことができます。また、size を無闇に上げるよりも shard_size を上げるほうが効率は良いです。

ただし、この対応で100%誤差をなくすことができるわけではない点には注意してください。

もし、正確な数値で集計をしたい場合は「Composite aggregation」という機能を使ってください。こちらは大規模なデータであってもページング処理をしながら集計処理をするため正確かつパフォーマンス良く処理することができます。

まとめ

  • doc_count_error_upper_bound が 0 より大きい場合、上位の集計結果に誤差が含まれる可能性がある
  • sizeshard_size を増やすことで誤差を減らせる
  • 完全に正確な数値が必要な場合は Composite Aggregation を利用する

ローカル環境ではシャードは1個しか作らないので、複数シャード存在する本番環境でようやく誤差に気がつくことができました…。Elasticsearch、ちゃんと理解したい。

ムーザルちゃんねる

Discussion