LoginSignup
7
0

More than 1 year has passed since last update.

そのScriptクエリは本当に避けるべきなのか

Last updated at Posted at 2021-12-06

はじめに

こんにちは。aucfanのエンジニアの@tmot です。

aucfanでは大量のオークションデータをElasticsearch(以下ES)に保存しており、このデータの検索結果や集計によってサービスに価値を提供しています。
我々が提供する集計機能においては、対象の全データについて合計や平均を算出する単純な集計だけでなく、特定のデータのみを対象とするような若干複雑な集計も必要となってきます。このような場合にはScriptクエリ(スクリプトの記述によって条件を指定する方法)が非常に頼りになるのですが、検索速度のチューニングの記事では可能なら避けた方がよいとされています。

今回は、落札率の集計 を例に、Scriptクエリ を使用する場合とそうでない場合でどの程度パフォーマンスが違うのかを実験してみたいと思います。

デモデータを使用した実験

ES内のオークションデータを出品者ごとに分類し、落札率を集計します。
落札率の算出方法は以下の通りです。

落札率 = 落札された出品数 / 総出品数

ESにはKibanaを介してリクエストを送り、レスポンス速度などを観測します。

環境

Elasticsearch: 7.13.2
Kibana: 7.13.2

データセット

オークションデータを単純化したデータセット(レコード数 1000万件)をESに作成しました。

以下のフィールドを持ちます。

フィールド名 データ型 説明 備考
auctionId keyword 出品ID auction-000000000~auction-009999999で重複なし
sellerId keyword 出品者ID seller-00 ~ seller-50の51通りのランダム
isSold boolean 落札されたかどうか 落札された場合はtrue
今回は出品者によらず全体の3割程度がtrueになる
price scaled_float 価格 scaled_factor = 100
データの例
{
  "auctionId" : "auction-000000000",
  "isSold" : false,
  "price" : 0,
  "sellerId" : "seller-13"
}

パターン1: Scriptクエリ を使用する場合

クエリ

sellerIdごとにバケットを作成し、各バケット内のレコード総数 (_count)と落札されたレコード数 (soldFilter内の_count)からScriptで落札率を算出します。

Scriptクエリでの落札率集計リクエスト
GET test-index-000001/_search?request_cache=false
{
  "size": 0,
  "aggs": {
    "sellerAggregation": {
      "composite": { // sellerIdごとのbucket aggregation
        "sources": [
          {
            "sellers": {
              "terms": {
                "field": "sellerId"
              }
            }
          }
        ],
        "size": 100
      },
      "aggs": {
        "soldFilter": {
          "filter": {
            "term": {
              "isSold": true
            }
          }
        },
        "bidRate": { // 落札率をscriptで集計
          "bucket_script": {
            "buckets_path": {
              "itemCount": "_count",
              "soldCount": "soldFilter>_count"
            },
            "script": """
            if(params.itemCount == 0) {return 0;}
            (float)params.soldCount / (float)params.itemCount;
            """
          }
        },
        "sortedBy": { // 落札率の降順でソート
          "bucket_sort": {
            "sort": [
              {
                "bidRate": {
                  "order": "desc"
                }
              }
            ],
            "size": 51,
            "from": 0
          }
        }
      }
    }
  }
}

レスポンス内容

以下のようなレスポンスが得られます。

レスポンス
{
    ...
    "aggregations" : {
        "sellerAggregation" : {
            "after_key" : {
                "sellers" : "seller-50"
            },
            "buckets" : [
            {
                "key" : {
                    "sellers" : "seller-08"
                },
                "doc_count" : 196610,
                "soldFilter" : {
                    "doc_count" : 59321
                },
                "bidRate" : {
                    "value" : 0.30171912908554077
                }
            },
            {
                "key" : {
                    "sellers" : "seller-21"
                },
                "doc_count" : 196345,
                "soldFilter" : {
                    "doc_count" : 59203
                },
                "bidRate" : {
                    "value" : 0.30152538418769836
                }
            },
            {
                "key" : {
                    "sellers" : "seller-06"
                },
                "doc_count" : 196061,
                "soldFilter" : {
                    "doc_count" : 59101
                },
                "bidRate" : {
                    "value" : 0.30144190788269043
                }
            },
            {
                "key" : {
                    "sellers" : "seller-23"
                },
                "doc_count" : 196118,
                "soldFilter" : {
                    "doc_count" : 59114
                },
                "bidRate" : {
                    "value" : 0.30142056941986084
                }
            },
            {
                "key" : {
                    "sellers" : "seller-24"
                },
                "doc_count" : 195351,
                "soldFilter" : {
                    "doc_count" : 58860
                },
                "bidRate" : {
                    "value" : 0.30130380392074585
                }
            },
            ...
            ]
        }
    }
}

aggregations.sellerAggregation.bucketsのリストの要素が出品者ごとの集計結果となっており、bidRateがその出品者のデータの落札率です。

{
  "key" : {
    "sellers" : "seller-08"
  },
  "doc_count" : 196610,
  "soldFilter" : {
    "doc_count" : 59321
  },
  "bidRate" : {
    "value" : 0.30171912908554077
  }
}

パターン2: Scriptクエリ を使用しない場合

クエリ

今回の設定だと、isSoldのboolean値を0,1で表した値の平均値 が落札率と一致するので、avgを使って以下のようにも書けそうです。

真偽値のavgによる落札率集計リクエスト
GET test-index-000001/_search?request_cache=false
{
  "size": 0,
  "aggs": {
    "sellerAggregation": {
      "composite": { // sellerIdごとのbucket aggregation
        "sources": [
          {
            "sellers": {
              "terms": {
                "field": "sellerId"
              }
            }
          }
        ],
        "size": 100
      },
      "aggs": {
        "bidRate": { // 落札率をisSold(真偽値)の平均値として集計
          "avg": {
              "field": "isSold"
          }
        },
        "sortedBy": { // 落札率の降順でソート
          "bucket_sort": {
            "sort": [
              {
                "bidRate": {
                  "order": "desc"
                }
              }
            ],
            "size": 51,
            "from": 0
          }
        }
      }
    }
  }
}

レスポンス内容

以下のような結果が返ってきます。

レスポンス
{
    ...
    "aggregations" : {
        "sellerAggregation" : {
            "after_key" : {
                "sellers" : "seller-50"
            },
            "buckets" : [
            {
                "key" : {
                    "sellers" : "seller-08"
                },
                "doc_count" : 196610,
                "bidRate" : {
                    "value" : 0.30171913941305123,
                    "value_as_string" : "true"
                }
            },
            {
                "key" : {
                    "sellers" : "seller-21"
                },
                "doc_count" : 196345,
                "bidRate" : {
                    "value" : 0.3015253762509868,
                    "value_as_string" : "true"
                }
            },
            {
                "key" : {
                    "sellers" : "seller-06"
                },
                "doc_count" : 196061,
                "bidRate" : {
                    "value" : 0.30144189818474865,
                    "value_as_string" : "true"
                }
            },
            {
                "key" : {
                    "sellers" : "seller-23"
                },
                "doc_count" : 196118,
                "bidRate" : {
                    "value" : 0.30142057332830235,
                    "value_as_string" : "true"
                }
            },
            {
                "key" : {
                    "sellers" : "seller-24"
                },
                "doc_count" : 195351,
                "bidRate" : {
                    "value" : 0.3013038069935654,
                    "value_as_string" : "true"
                }
            },
            ...
            ]
        }
    }
}

実行時間の比較

両パターンのクエリ を10回ずつ (_cache/clearを挟みながら)実行し、ES内部での処理時間を比較します。

パターン1の処理時間 (ms)
Scriptクエリ を使用する場合
パターン2の処理時間 (ms)
Scriptクエリ を使用しない場合
1回目 389 359
2回目 300 341
3回目 334 336
4回目 307 396
5回目 265 280
6回目 539 372
7回目 256 292
8回目 268 315
9回目 266 276
10回目 268 302
平均 319.2 326.9
標準偏差 83.02 38.59

この結果でt検定(両側・非等分散)をするとP値が0.805となり、有意差があるとは言えませんでした。

実データを使用した実験

デモデータではあまりにもデータ数が少ないと思われたので、オークファンのサービスで実際に使われている実際のデータの1週間分に対して同等の集計を実施してみました。対象データ数が増えることで差が顕れるでしょうか。

実行時間の比較

パターン1の処理時間 (ms)
Scriptクエリ を使用する場合
パターン2の処理時間 (ms)
Scriptクエリ を使用しない場合
1回目 6024 6833
2回目 6591 6904
3回目 7282 6472
4回目 7072 7850
5回目 5581 7199
6回目 6552 6345
7回目 6923 6189
8回目 5989 7868
9回目 6527 6955
10回目 6735 6388
平均 6527.6 6900.3
標準偏差 500.92 565.27

t検定(両側・等分散)のP値は0.156となり、こちらも有意差があるとは言えません。。

おわりに

今回の落札率の集計の設定においては、Scriptクエリを使う場合とそうでない場合の応答速度の差は見られませんでした。
実際の業務ではScriptクエリ をなくして速度が改善したケースもあれば、Scriptを使わないようにするためにデータ構造を変えてまで対応してもあまり良い効果が得られなかったケースもあるので、そのScriptクエリは本当に避けるべきなのかという勘所を掴んで上手に付き合っていくのがよいかなと思います。

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0