daisuzz.log

OpenSearch2.4でサポートされたPoint in Timeを使ってみる

OpenSearch2.4でサポートされたPoint in Time(PIT)を使ったページネーションを試してみる。

前提

OpenSearch 2.4.1

Point in Timeとは

Point in Timeとは、時間的に固定されたデータセットに対してクエリを実行するための仕組み。

from, sizeを使った方法や、search_after単体を使ったページング処理では同じクエリを複数回実行すると、場合によっては異なる結果が返される。

例えば、ドキュメントを取得して1ページ目を表示した後で新しいドキュメントが作成された場合、2ページ目を表示したときに1ページ目に表示されていたドキュメントが2ページ目に表示されてしまう可能性がある。

この問題を避けるためにScrollという仕組みを使う方法もあるが、スクロールはメモリ消費量も多く、ユーザが都度検索操作を行うような場面で利用することは推奨されていない

そこでOpenSearch2.4からサポートされたのがPIT。 PITを作成するAPIを叩くと、その時点のインデックスの情報が、インデックスとは別のセグメント(PIT)として作成される。 このPITを指定してクエリを実行することで、固定されたデータセットに対して処理が実行されるため、同じクエリを複数回実行しても常に同じ結果が返されるようになる。

Launch highlight: Paginate with Point in Time · OpenSearch から抜粋した以下の図がわかりやすい。

https://opensearch.org/assets/media/blog-images/2022-12-09-point-in-time/pitUserDiagram.png

これをページングに使うことで、ページを移動する前後でドキュメントが追加/削除/更新されたとしても、一貫性のある結果を返すことができる。

Point in Timeを使ったページング処理

実際にOpenSearchのDevToolsを使って動作を確認してみる。

事前にbooksというインデックスを作成し、ドキュメントを4件登録しておく。

// リクエスト
GET /books/_search
{
  "query": {
        "match_all": {}
    },
  "sort":[
        {
            "_id": {
                "order": "asc"
            }
        }
    ]
}

// レスポンス
{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "books",
        "_id" : "book001",
        "_score" : null,
        "_source" : {
          "title" : "サンプル1",
          "author" : "daisuzz",
          "pages" : 10
        },
        "sort" : [
          "book001"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book002",
        "_score" : null,
        "_source" : {
          "title" : "サンプル2",
          "author" : "daisuzz",
          "pages" : 20
        },
        "sort" : [
          "book002"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book003",
        "_score" : null,
        "_source" : {
          "title" : "サンプル3",
          "author" : "daisuzz",
          "pages" : 30
        },
        "sort" : [
          "book003"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book004",
        "_score" : null,
        "_source" : {
          "title" : "サンプル4",
          "author" : "daisuzz",
          "pages" : 40
        },
        "sort" : [
          "book004"
        ]
      }
    ]
  }
}

用意したbooksインデックスのPITを作成する。クエリパラメータで指定しているkeep_aliveは必須パラメータで、PITを保持する期間を指定する。今回は5分を指定している。

// リクエスト
POST /books/_search/point_in_time?keep_alive=5m

// レスポンス
{
  "pit_id" : "h8P8QAEFYm9va3MWU3JrVEk4dWJTM2FHNGg0TW0wRFFXZwAWWUw0cDF4RFVTSmlKdEV1ZTkyRjZYdwAAAAAAAAAARBY0aGdZZ01iN1FQNkppQ0cxSFpEN01nARZTcmtUSTh1YlMzYUc0aDRNbTBEUVdnAAA=",
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "creation_time" : 1672985497938
}

作成したPITに対してドキュメントIDの昇順で1ページ目(3件)を取得する検索クエリを実行する。 query.pit.keep_aliveを指定することでPITの保持時間を指定した時間延長できる。今回はクエリを実行するごとに現在時間から5分延長するようにしている。

// リクエスト
GET /_search
{
  "size": 3,
  "query": {
        "match_all": {}
    },
    "pit": {
      "id": "h8P8QAEFYm9va3MWU3JrVEk4dWJTM2FHNGg0TW0wRFFXZwAWWUw0cDF4RFVTSmlKdEV1ZTkyRjZYdwAAAAAAAAAATxY0aGdZZ01iN1FQNkppQ0cxSFpEN01nARZTcmtUSTh1YlMzYUc0aDRNbTBEUVdnAAA=",
      "keep_alive": "5m"
    },
  "sort":[
        {
            "_id": {
                "order": "asc"
            }
        }
    ]
}

// レスポンス
{
  "pit_id" : "h8P8QAEFYm9va3MWU3JrVEk4dWJTM2FHNGg0TW0wRFFXZwAWWUw0cDF4RFVTSmlKdEV1ZTkyRjZYdwAAAAAAAAAATxY0aGdZZ01iN1FQNkppQ0cxSFpEN01nARZTcmtUSTh1YlMzYUc0aDRNbTBEUVdnAAA=",
  "took" : 4,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "books",
        "_id" : "book001",
        "_score" : null,
        "_source" : {
          "title" : "サンプル1",
          "author" : "daisuzz",
          "pages" : 10
        },
        "sort" : [
          "book001"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book002",
        "_score" : null,
        "_source" : {
          "title" : "サンプル2",
          "author" : "daisuzz",
          "pages" : 20
        },
        "sort" : [
          "book002"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book003",
        "_score" : null,
        "_source" : {
          "title" : "サンプル3",
          "author" : "daisuzz",
          "pages" : 30
        },
        "sort" : [
          "book003"
        ]
      }
    ]
  }
}

2ページ目を取得する前に、booksインデックスに対して新しいドキュメントを登録する。

// リクエスト
POST /books/_doc/book005
{
  "title": "サンプル5",
  "author": "daisuzz",
  "pages": 50
}

// レスポンス
{
  "_index" : "books",
  "_id" : "book005",
  "_version" : 4,
  "result" : "created",
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 37,
  "_primary_term" : 1
}

その後2ページ目を取得する。

// リクエスト
GET /_search
{
  "size": 3,
  "query": {
        "match_all": {}
    },
    "pit": {
      "id": "h8P8QAEFYm9va3MWU3JrVEk4dWJTM2FHNGg0TW0wRFFXZwAWWUw0cDF4RFVTSmlKdEV1ZTkyRjZYdwAAAAAAAAAARhY0aGdZZ01iN1FQNkppQ0cxSFpEN01nARZTcmtUSTh1YlMzYUc0aDRNbTBEUVdnAAA=",
      "keep_alive": "5m"
    },
  "sort":[
        {
            "_id": {
                "order": "asc"
            }
        }
    ],
    "search_after":[
      "book003"  
    ]
}

// レスポンス
{
  "pit_id" : "h8P8QAEFYm9va3MWU3JrVEk4dWJTM2FHNGg0TW0wRFFXZwAWWUw0cDF4RFVTSmlKdEV1ZTkyRjZYdwAAAAAAAAAATxY0aGdZZ01iN1FQNkppQ0cxSFpEN01nARZTcmtUSTh1YlMzYUc0aDRNbTBEUVdnAAA=",
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "books",
        "_id" : "book004",
        "_score" : null,
        "_source" : {
          "title" : "サンプル4",
          "author" : "daisuzz",
          "pages" : 40
        },
        "sort" : [
          "book004"
        ]
      }
    ]
  }
}

1ページ目を取得してから新しいドキュメントを登録したが、その後の2ページ目を取得する処理の結果では、hits.total.valueが4のままになっており、かつhits.hitsの中身を見ても、登録したドキュメントは結果に含まれていないことが確認できる。

ちなみにPITを指定せずに検索クエリを実行すると、登録したドキュメントも検索結果に含まれることが確認できる。

// リクエスト
GET /_search
{
  "size": 3,
  "query": {
        "match_all": {}
    },
  "sort":[
        {
            "_id": {
                "order": "asc"
            }
        }
    ],
    "search_after":[
      "book003"  
    ]
}

// レスポンス
{
  "took" : 1111,
  "timed_out" : false,
  "_shards" : {
    "total" : 4,
    "successful" : 4,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "books",
        "_id" : "book004",
        "_score" : null,
        "_source" : {
          "title" : "サンプル4",
          "author" : "daisuzz",
          "pages" : 40
        },
        "sort" : [
          "book004"
        ]
      },
      {
        "_index" : "books",
        "_id" : "book005",
        "_score" : null,
        "_source" : {
          "title" : "サンプル5",
          "author" : "daisuzz",
          "pages" : 50
        },
        "sort" : [
          "book005"
        ]
      },
      {
        "_index" : ".kibana_1",
        "_id" : "config:2.4.1",
        "_score" : null,
        "_source" : {
          "config" : {
            "buildNum" : 4665
          },
          "type" : "config",
          "references" : [ ],
          "migrationVersion" : {
            "config" : "7.9.0"
          },
          "updated_at" : "2023-01-06T02:23:57.010Z"
        },
        "sort" : [
          "config:2.4.1"
        ]
      }
    ]
  }
}

注意点

Redirecting… では、

Point in Time (PIT) with search_after is the preferred pagination method in OpenSearch, especially for deep pagination.

と書かれており、PITとsearch_afterを使ったページネーション処理が公式ドキュメントでは推奨されている。

ただし、Launch highlight: Paginate with Point in Time · OpenSearch では、PITの欠点として、

So far we’ve seen that PIT search is superior to other pagination methods. But what are the drawbacks? First, for a PIT, OpenSearch has to keep the segments even though they might have been merged and are not needed for the live dataset. This leads to an increased heap usage. Second, there is currently no built-in resiliency in a PIT, so if your node goes down, all PIT segments are lost.

と書かれていて、PITを使うことでヒープ使用量が増えたりノードがDOWNしたときはPITが全て失われるようなので、実際に本番環境で使う場合にはその点を検証した上で使う必要がある。