読者です 読者をやめる 読者になる 読者になる

ElasticsearchのScroll APIをためしてみた

elasticsearch kotlin Scroll

気になっていたElasticsearchのScroll APIの使用感を記録します。最近の開発でScroll APIを採用したい欲求がありましたが、使用感を調べる前で採用は見送りました。このままだと気になったまま使わないことになりそうなので、この機会にまとめます。

www.elastic.co

※ version 2.4をつかいました。

Scroll APIは通常のSearch requestのoffset/limitでページング取得をしないため処理中のデータ抜けが防げるメリットがあります。またScroll APIは初回リクエスト時の結果をスナップショットすることで安定した応答速度を担保します。
スナップショットをとるためリアルタイムのデータ処理の利用には向いていません。(スナップショットの挙動について試してみたので後述しています)

どんなふうに使うか?

通常のクエリとscroll=1mを加えたリクエストを送ります。(size=1にしています)

curl -XGET 'http://localhost:9200/_search?scroll=1m&size=1&pretty' -d '
{
"query" : { "match" : { "category_id" : 100 } }
}'

次のような検索結果(1件)と合わせて_scroll_idが返ってきます。

{
  "_scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NTs4OkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7OTpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzEwOkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7MTE6RlFCOTVUYkhSbGFGblFWUGdVai1hdzsxMjpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzA7",
・・・
  "hits" : {
    "total" : 52,
・・・
  }
}

2件目の取得を行うために/_search/scrollのエンドポイントへscroll_idをRequest Bodyに加えてリクエストします。クエリは必要ありません。

curl -XGET 'http://localhost:9200/_search/scroll?pretty' -d '
{
"scroll_id": "cXVlcnlUaGVuRmV0Y2g7NTs4OkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7OTpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzEwOkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7MTE6RlFCOTVUYkhSbGFGblFWUGdVai1hdzsxMjpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzA7"
}'
  • 2回目以降はscroll_idを送ることで初回のリクエスト時に送った検索条件の結果が返ってきます。
  • 3回目以降も同様にscroll_idを送ることで3件目、4件目、5件目・・・と結果を取得できます。
  • 初回にsize=1としたため、2回目以降の結果も1件になります。
  • またscroll=1mとしたことで初回にリクエストした検索条件の結果を1分間の有効期限でスナップショットが取られます。

使い終わったscroll_idは破棄をする

スナップショットを残して置くのはコストがかかるためscrollが終われば次のようにscroll_idをクリアします。

curl -XDELETE localhost:9200/_search/scroll -d '
{
    "scroll_id" : ["cXVlcnlUaGVuRmV0Y2g7NTs4OkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7OTpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzEwOkZRQjk1VGJIUmxhRm5RVlBnVWotYXc7MTE6RlFCOTVUYkhSbGFGblFWUGdVai1hdzsxMjpGUUI5NVRiSFJsYUZuUVZQZ1VqLWF3OzA7"]
}'

複数のscroll_idをまとめてクリアもできます。

Scroll APIを使うときのメモ

  • Scroll APIの初回のリクエストはscroll_idを取得するためのものではなく、検索結果に加えてscroll_idが返ってくる。
  • Aggregationを含んだリクエストの場合、Aggregationの結果は初回のみ返ってくる。
  • ソート条件に制約がなければsort orderは_docにすることで安定した応答速度が得られる。

kotlin + 公式Elasticsearch ClientでScroll APIをためしてみる

せっかくなのでkotlinでコードからScroll APIをためしてみました。使ったクライアントは公式のElasticsearch Clientです。

www.elastic.co

※ version 2.4.3をつかいました

スナップショットは本当に有効なのか?

scroll=1mと設定してインデックスされたデータをscroll取得している間に、新しいソースをインデックスしても取得結果のtotal件数に変化がないか試してみました。

以下のような流れで検証します。

scrollIdが取得できれば再帰的にログ出力を繰り返し、その間に新しいソースを1件追加していきます。

実行した結果は次のようになりました。

[INFO ] totalCount={104}, id={AVkna34Rhpv5RJ12skTc}   // 初回取得時のtotal件数は104[INFO ] complete add source id={AVkqcnqbMJXjH5tvLcGB}  //新しいソースの追加が成功
[INFO ] totalCount={104}, id={AVkna_83hpv5RJ12skTd}   // total件数は初回取得時の104件から変わらずスナップショットが有効であることが確認できた
[INFO ] complete add source id={AVkqcntGMJXjH5tvLcGC}
[INFO ] totalCount={104}, id={AVknbG17hpv5RJ12skTf}
・・・

Scroll APIの仕様のとおりスナップショットが有効の状態であれば新しいソースを追加したとしてもスナップショットを指すscroll_idでリクエストをすると全体の件数は変わらないことが確認できました。

まとめ

  • ページング処理ではないため取りこぼしがない。(そもそも初回時点のスナップショットのデータを対象にスクロールするので取りこぼしはないと思われる)
  • offset/limitのページング処理のコードがなくなり、コードがシンプルになった。
  • スナップショットの有効期限が切れた場合、SearchContextMissingExceptionがスローされる。
  • SearchContextMissingExceptionを捕捉して新規スクロールを始める必要があるが、例外が起きた時点のソースIDを始点にスクロールを開始する・・・なかなか例外処理は複雑。
  • 取得したscroll_idを用いた次のスクロールに有効期限を添えることで常にスナップショットの有効期限を更新すれば例外処理を避けることができそう。
  • いまのプロジェクトに導入したくなってきたが、本番での処理時間を測定した上で最適な有効期限の設定をする必要があるため様子見する。

ソースを公開しています

今回検証したソースコードを公開しています。

github.com