🔍

[翻訳] クエリパフォーマンスの最適化: OpenSearch インデックスリクエストキャッシュの実装

に公開

https://opensearch.org/blog/understanding-index-request-cache/

検索ユーザーにとって、速度と効率は不可欠です。OpenSearch はさまざまなメカニズムを通じてこれらを実現していますが、その中でも最も重要なものの 1 つがインデックスリクエストキャッシュです。本記事では、このキャッシュの実装方法と、クエリパフォーマンスの最適化の仕組みについて説明します。

インデックスリクエストキャッシュとは?

インデックスリクエストキャッシュは、シャードレベルでクエリ結果を保存することで OpenSearch の検索クエリを高速化するように設計されています。以下の図にその仕組みを示します。

頻繁に実行されるクエリをキャッシュすることで、同じクエリを繰り返し実行する必要がなくなります。このアプローチは、特定のインデックスやパターンを対象とするクエリに特に効果的で、応答時間とシステム全体の効率を向上させます。キャッシュは、データが変更されるとエントリを自動的にクリアし、常に最新の情報のみが返されるようにします。

キャッシュポリシー

すべての検索リクエストがインデックスリクエストキャッシュでキャッシュされるわけではありません。size=0 を指定した検索リクエストはデフォルトでキャッシュされます。これらのリクエストでは、結果の総数やヒット数などのメタデータのみがキャッシュされます。

以下のリクエストはキャッシュされません。

  • 非決定的なリクエスト: Math.random() のような関数や、nownew Date() のような相対時間を含む検索
  • スクロールリクエストと Profile API リクエスト
  • DFS クエリからフェッチするリクエスト: この検索タイプはインデックスコンテンツと上書きされた統計の両方に依存するため、統計が異なる場合 (シャードの更新など) にスコアが不正確になります

個々の検索リクエストに対してキャッシュを有効にする場合は、request_cache クエリパラメータを true に設定します。

GET /students/_search?request_cache=true
{
  "query": {
    "match": {
      "name": "doe john"
    }
  }
}

キャッシュエントリの理解

各キャッシュエントリは、Key → BytesReference のキーと値のペアです。

Key は次の 3 つの要素で構成されています。

  1. CacheEntity: IndexShardCacheEntity には IndexShard が含まれています。この参照により、キーがそのシャードに紐付けられます。
  2. ReaderCacheKeyId: シャードの現在の状態を示す一意の識別子です。この参照は、シャードの状態が変更されたとき (ドキュメントの追加、削除、更新、またはリフレッシュ後など) に変更されます。
  3. BytesReference: バイト形式で保存された実際の検索クエリです。

これら 3 つのコンポーネントが組み合わさることで、以下の図に示すように、各キーが特定のシャードを対象とする特定のクエリを一意に識別できるようになります。このプロセスにより、シャードの状態が最新であることが確認され、古いデータの取得が防止されます。

キャッシュへのエントリの保存

キャッシュ可能なクエリはすべて getOrCompute メソッドを呼び出します。このメソッドは、キャッシュから事前計算された値を取得するか、計算後の結果をキャッシュに保存します。

以下に getOrCompute メソッドの実装を示します。

function getOrCompute(CacheEntity, DirectoryReader, cacheKey) {
    // ステップ 1: シャードの現在の状態識別子を取得する
    readerCacheKeyId = DirectoryReader.getDelegatingCacheKey().getId()

    // ステップ 2: キャッシュエントリの一意のキーを作成する
    key = new Key(CacheEntity, cacheKey, readerCacheKeyId)

    // ステップ 3: 結果がすでにキャッシュにあるかどうかを確認する
    value = cache.computeIfAbsent(key)

    // ステップ 4: 結果が計算された場合 (キャッシュから取得されなかった場合)、クリーンアップリスナーを登録する
    if (cacheLoader.isLoaded()) {
        cleanupKey = new CleanupKey(CacheEntity, readerCacheKeyId)
        OpenSearchDirectoryReader.addReaderCloseListener(DirectoryReader, cleanupKey)
    }

    // ステップ 5: キャッシュされた結果または計算された結果を返す
    return value
}

キャッシュの無効化

IndexReader はインデックスの特定時点のビューを提供します。インデックスの内容を変更する操作は、新しい IndexReader を作成し、古い IndexReader を閉じます。古い IndexReader によって作成されたキャッシュエントリは古くなるため、クリーンアップが必要です。

CleanupKey

CleanupKey は削除される Key に対応します。それらの関係を以下の図に示します。

IndexReader が閉じられると、対応する CleanupKey が KeysToClean というセットに追加されます。

BytesReference はキャッシュされたデータ自体を表すため、CleanupKey では使用されません。どのエントリをクリーンアップすべきかを識別するためには必要ないからです。CleanupKey は削除するエントリを識別するだけであり、その内容には関心がありません。

キャッシュエントリは以下の操作により無効になる可能性があります。

  • リフレッシュ/マージ: リフレッシュまたはマージ操作は新しい IndexReader を作成し、それによりキャッシュエントリが無効になります。
  • キャッシュのクリア: クリアキャッシュ API 操作は、指定されたインデックスのすべてのリクエストキャッシュエントリを無効にします。クリアキャッシュ API は次のように呼び出すことができます: POST /my-index/_cache/clear?request=true

IndexReader を無効にする操作、または明示的にキャッシュをクリアする操作は、以下の図に示すように、対応する CleanupKey を KeysToClean というコレクションに追加します。

キャッシュのクリーンアップ

OpenSearch は毎分、CacheCleaner というバックグラウンドジョブを別のスレッドで実行します。このジョブは cleanCache メソッドを呼び出し、すべてのキャッシュエントリを反復処理して、各 Key を KeysToClean 内の CleanupKey にマッピングし、以下の図に示すように対応するエントリを削除します。

以下に cleanCache メソッドの実装を示します。

function cleanCache() {
    // ステップ 1: クリーンアップするキーのセットを初期化する
    currentKeysToClean = new Set()
    currentFullClean = new Set()

    // ステップ 2: クリーンアップが必要なキーのリストを処理する
    for each cleanupKey in keysToClean {
        keysToClean.remove(cleanupKey)
        if (shard is closed or cacheClearAPI called) {
            currentFullClean.add(cleanupKey.entity.getCacheIdentity())
        } else {
            currentKeysToClean.add(cleanupKey)
        }
    }

    // ステップ 3: キャッシュを処理し、識別されたキーを削除する
    for each key in cache.keys() {
        if (currentFullClean.contains(key.entity.getCacheIdentity()) or
            currentKeysToClean.contains(new CleanupKey(key.entity, key.readerCacheKey))) {
            cache.remove(key)
        }
    }

    // ステップ 4: キャッシュを更新する
    cache.refresh()
}

まとめ

インデックスリクエストキャッシュは、OpenSearch の効率性を確保する上で重要な役割を果たしています。その仕組みを理解することで、パフォーマンスを最適化し、より自信を持って設定を調整することができます。

OpenSearch プロジェクトはコミュニティの貢献によって成り立っています。改善のための提案がある場合は、ぜひプロジェクトへの貢献をご検討ください。皆様のご意見は、この検索技術の未来を形作るのに役立ちます。

OpenSearch Project

Discussion