🔍

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

に公開

本記事は OpenSearch Project Blog "Optimizing query performance: The implementation of the OpenSearch index request cache" の日本語訳です。


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

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

インデックスリクエストキャッシュは、以下の図に示すように、シャードレベルでクエリ結果を保存することで 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 プロジェクトはコミュニティの貢献によって成り立っています。改善のための提案がある場合は、プロジェクトへの貢献を検討してください。あなたの意見は、この検索技術の未来を形作るのに役立ちます。

Discussion