OpenSearchのベクトル検索(k-NN)とは?仕組み・チューニング・運用を徹底解説

OpenSearchのベクトル検索(k-NN)の仕組みを基礎から徹底解説。nmslib・faiss・luceneの3エンジンの違い、knn_vectorとHNSWグラフがLuceneセグメント単位で作られる構造、refresh/mergeと書き込みコスト、ef_searchとwarmup、フィルタ検索とexact fallback、native memoryとcircuit breaker、遅い時のチューニング手順、監視、導入事例まで。

OpenSearchでベクトル検索(k-NN)を使うと決めたなら、その仕組みを理解しておくことが安定運用の前提になる。本記事は、ベクトル検索基盤の内部構造を解き明かす3部作シリーズの第3回である。ANN(近似最近傍探索)そのものの原理――HNSW・IVF・PQ・DiskANNといったアルゴリズムがなぜ生まれ、recallと速度とメモリをどうトレードオフするか――は、 ANN基礎編 で体系的に解説済みという前提に立つ。本記事では、その原理がOpenSearchという「全文検索エンジンにベクトル検索を後から統合したシステム」の中で、具体的にどのような設計判断・実装・運用上の注意点として現れるかを、公式ドキュメントとk-NNプラグイン自体のソースコードの両方に基づいて掘り下げる。

前回の Milvus内部構造編 では、ベクトル検索専用に設計された分散システムとしてのMilvusを見た。OpenSearchは出自がまったく異なる。Elasticsearchからのフォークという経緯を持ち、Lucene由来のセグメントアーキテクチャの上にk-NNプラグインとしてANNを統合してきた。この出自の違いは、書き込み経路、検索経路、メモリ管理、フィルタとの組み合わせ方など、あらゆる場面に影響を及ぼしている。

以下ではまず、k-NNプラグインがnmslibからfaiss・luceneへとどう発展してきたかの歴史を辿り(1章)、OpenSearchの階層構造とANNグラフの配置(2章)、書き込み経路とセグメントのライフサイクル(3章)、検索経路とwarmup(4章)、フィルタとメモリ管理(5章)、性能障害の切り分け(6章)、観測すべきメトリクス(7章)、そして公開事例(8章)へと進む。

本シリーズ「ANNとベクトル検索基盤の教科書」(全3回)

  1. ANN基礎編
  2. Milvus内部構造編
  3. OpenSearch内部構造編(本記事)

1. OpenSearchのベクトル検索の歴史: nmslib・faiss・luceneの3エンジン

OpenSearch における近似最近傍探索(ANN: Approximate Nearest Neighbor)機能は、単一の設計で生まれたものではない。Elasticsearch からのフォークという特殊な出自と、複数のベクトル検索ライブラリを段階的に取り込んできた歴史が積み重なって、現在の「エンジン(engine)」という抽象化にたどり着いている。この章では k-NN プラグインの変遷を時系列で追い、なぜ現在 lucene / faiss / nmslib という3つのエンジンが存在し、なぜ nmslib が非推奨になったのかを理解する。

Open Distro 時代の k-NN プラグインと nmslib

k-NN 機能の原型は、AWS が Elasticsearch 向けに提供していた OSS ディストリビューション「Open Distro for Elasticsearch」の k-NN プラグインとして2019年に登場した。当時採用されたのは NMSLIB(Non-Metric Space Library)という HNSW(Hierarchical Navigable Small World)実装で、これが k-NN プラグインが最初にサポートしたアルゴリズムである。2021年、Elastic 社が Elasticsearch と Kibana のライセンスを Elastic License に変更したことを受けて、AWS を中心とするコミュニティが Elasticsearch 7.10.2 をフォークし OpenSearch プロジェクトを開始した。k-NN プラグインもこのフォークにそのまま引き継がれ、OpenSearch 1.0 以降も nmslib ベースの近似 k-NN 検索がまず提供された。

faiss エンジンの追加と Lucene のネイティブ対応

OpenSearch 1.2 で、Meta(旧 Facebook)が開発する類似度検索ライブラリ Faiss の IVF(Inverted File Index)実装が k-NN プラグインに統合された。faiss はその後 HNSW・PQ(Product Quantization)・SQ(Scalar Quantization)・バイナリベクトルなど OpenSearch のベクトル圧縮機能の中心的な実装基盤に成長していく。

もう一つの転機は Lucene 自体のネイティブベクトル対応である。Apache Lucene は 9.1 で KnnVectorsFormat という codec 拡張ポイントを追加し、セグメント単位で HNSW グラフをネイティブに構築・検索できるようになった。OpenSearch はこれを 2.2 で lucene エンジンとして取り込み、2.4(Lucene 9.4 ベース)では Lucene の KnnFloatVectorQuery を用いた効率的なフィルタ付き k-NN 検索(efficient filtering)にも対応した。これにより、JNI 経由でネイティブライブラリを呼び出す faiss/nmslib とは異なり、Lucene のヒープ管理下で完結する軽量な ANN エンジンという選択肢が生まれた。

nmslib の非推奨化

k-NN プラグインのソースコード KNNEngine.java を見ると、NMSLIB の定義には @Deprecated(since = "2.19.0", forRemoval = true) というアノテーションが付与されており、さらに restrictedFromVersion として Version.V_3_0_0 が設定されている(isRestricted(Version indexVersionCreated) は、インデックス作成時のバージョンが 3.0.0 以降であれば true を返す)。つまり OpenSearch 2.19 で正式に非推奨化が宣言され、3.0 以降に新規作成するインデックスでは nmslib エンジンの使用が制限される。3.0 より前に作られた既存の nmslib インデックスは引き続き読み書き・検索が可能だが、新規インデックスでは faiss か lucene を選ぶ必要がある。ドキュメント上も engine パラメータの説明には一貫して「nmslib(非推奨)」という表記が使われている。

Vector Engine としての現在(3.x系)

2026年7月時点のドキュメントサイト(docs.opensearch.org)の設定を見ると、最新版は OpenSearch 3.7.0(Lucene 10.4.0 ベース)であり、k-NN リポジトリの CHANGELOG.md では 3.8 に向けた開発(Lucene 10.5.0 へのアップグレードなど)が進行中である。3.x 系では k-NN 機能は単なる「検索プラグインの一機能」から「Vector Engine」というブランドで再定義され、以下のような拡張が積み重なっている。

  • derived source(3.0〜): index.knn.derived_source.enabled により、ベクトルを _source に重複保存せず、update/reindex などの機能を保ったままディスク使用量を削減する。
  • remote index build / GPU ビルド(3.0 プレビュー〜): NVIDIA cuVS の CAGRA アルゴリズムを Faiss 経由で利用し、ベクトルをオブジェクトストレージへアップロードして GPU フリート側でグラフを構築、CPU互換形式に変換して取り込む「デカップルされた GPU 加速インデクシング」の仕組みが knn.remote_index_build.* 設定として提供される。
  • memory-optimized search(3.1〜): faiss+HNSW インデックスをネイティブメモリに全展開せず mmap 経由でオンデマンドに読み込む方式(詳細は5章)。
  • disk-based vector search(2.17〜): 量子化とリスコアリングを組み合わせた低コストモード。

このように、engine という概念そのものは「nmslib(過去の遺産、非推奨)→ faiss(大規模データの主力、圧縮・GPU対応の中心)→ lucene(中小規模、フィルタ性能とシンプルさが売り)」という役割分担へと収斂してきた。次章以降では、この3エンジン共通の基盤である OpenSearch のアーキテクチャに踏み込む。

2. OpenSearch k-NNのアーキテクチャ: HNSWグラフはLuceneセグメント単位で作られる

OpenSearch の ANN 機能を正しく理解するには、まず OpenSearch(および基盤の Apache Lucene)が持つ「クラスタ→ノード→インデックス→シャード→セグメント」という階層構造を押さえる必要がある。ベクトル検索固有の挙動のほとんどは、この階層のどこで何が起きているかに起因するからである。

階層構造のおさらい

OpenSearch の1つの**インデックス(index)は、内部的に複数のシャード(shard)に分割される。各シャードは実体としては1つの Apache Lucene インデックスであり、Lucene インデックスはさらに複数のセグメント(segment)**から構成される。セグメントは一度書き込まれると不変(immutable)であり、ドキュメントの更新・削除は新しいセグメントの追加や論理削除フラグの付与によって実現される。この「セグメントは不変」という性質が、ANN グラフの扱いを理解する上で決定的に重要になる。

最重要ポイント: ANN インデックスは「セグメントごと」に作られる

k-NN プラグインの公式パフォーマンスチューニングガイドは、次の3原則を明言している。

Vector indexes are created per knn_vector field/Lucene segment pair. Queries execute sequentially on segments in the shard. The coordinating node selects the final size neighbors from the neighbors returned by each shard.

つまり、HNSW グラフ(あるいは IVF クラスタ)は「シャード単位」ではなく「knn_vector フィールドと Lucene セグメントの組」ごとに独立して構築される。1つのシャードに10個のセグメントがあれば、そのシャードには最大10個の独立した ANN グラフが存在することになる。検索時には各セグメントのグラフをそれぞれ探索し、その結果をシャード内でマージしてから、コーディネートノードがシャード間の結果を統合する。この設計のため、セグメント数が増えるほど検索は「グラフをN回探索してマージする」コストが積み重なり、遅くなる(詳しくは3章・6章)。

faiss/nmslib のネイティブメモリと lucene のヒープ

faiss と nmslib は C++ で書かれたライブラリであり、JNI(Java Native Interface)経由で呼び出される。そのため、これらのエンジンが構築する HNSW グラフや IVF クラスタは JVM ヒープの外側にある**ネイティブメモリ(off-heap)**に配置され、NativeMemoryCacheManager というキャッシュ機構によって管理される(5章・6章で詳述)。一方で lucene エンジンは Lucene の KnnVectorsFormat をそのまま使うため、グラフは Lucene が管理するファイル(セグメントファイルの一部)として書き出され、検索時には Lucene の通常のメモリマップ機構や、OpenSearch 3.1 以降の memory-optimized search の仕組みでアクセスされる。ドキュメント上、OpenSearch は「セグメントファイルへの書き込みにカスタム codec を使い、その codec が基盤となる k-NN 検索ライブラリの読める形式でベクトルデータを書く」と説明している。

ここまでの階層構造とエンジンごとのグラフ配置の違いをまとめると図2-1のようになる。

OpenSearchのクラスタ→ノード→インデックス→シャード→セグメントという階層と、各セグメントに独立したHNSWグラフが構築される様子。右の凡例はfaiss/nmslibのネイティブメモリ配置とluceneのセグメントファイル配置の違いを示す

図2-1: OpenSearchの階層構造とANNグラフの構築単位。ANNグラフはknn_vectorフィールド×セグメントごとに独立して作られ、faiss/nmslibはネイティブメモリに、luceneはセグメントファイルにグラフを配置する。

マッピング定義の実例

knn_vector フィールドは method オブジェクトで、アルゴリズム名(name)・距離空間(space_type)・エンジン(engine)・エンジン固有パラメータ(parameters)を指定する。

PUT test-index
{
  "settings": {
    "index": {
      "knn": true,
      "knn.algo_param.ef_search": 100
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 1024,
        "method": {
          "name": "hnsw",
          "space_type": "l2",
          "engine": "faiss",
          "parameters": {
            "ef_construction": 128,
            "m": 24
          }
        }
      }
    }
  }
}

index.knntrue にすることで、そのインデックスの knn_vector フィールドに対してネイティブライブラリインデックス(あるいは Lucene の KNN 構造)が構築される。false のままだと、ベクトルは doc values として保存はされるが近似 k-NN 検索機能そのものが無効になる。space_type はトップレベルにも method 内にも書けるが、method 内の指定が優先される。mef_construction はグラフの密度と構築コストを決めるパラメータで、詳細な意味は ANN基礎編 の HNSW の解説と同一である。次章では、この method 定義に基づいて実際にセグメントが作られていく「書き込み経路」を追う。

3. OpenSearchの書き込みの仕組み: refresh・mergeとHNSWグラフ構築コスト

前章で「ANN グラフはセグメントごとに作られる」と述べたが、それはいつ・どのように起きるのか。ここでは OpenSearch の書き込み経路(indexing path)を、通常の転置インデックスと k-NN 特有の挙動の両面から追う。

indexing → in-memory buffer → refresh → segment

ドキュメントを _bulk などで書き込むと、まず Lucene の in-memory buffer(IndexWriter のバッファ)に蓄積される。この時点ではまだディスク上のセグメントにはなっておらず、検索対象にもならない。バッファの内容が新しい Lucene セグメントとしてディスクに書き出され、検索可能になるトリガーが refresh である。OpenSearch のデフォルトの refresh_interval は1秒で、これは「準リアルタイム検索(near real-time search)」を実現するための設計である。

k-NN 特有の点は、この refresh によるセグメント生成のタイミングで、そのセグメント内の knn_vector フィールドに対する HNSW グラフ(または IVF クラスタ)がビルドされるということである。ベクトルのグラフ構築は転置インデックスの構築よりもはるかに CPU コストが高いため、refresh の頻度がそのまま「書き込みスループット対グラフ構築コスト」のトレードオフに直結する。

refresh_interval と鮮度・書き込みコストのトレードオフ

公式のインデクシング性能チューニングガイドは、大量データを一括投入する際には refresh_interval を無効化するか長く設定することを推奨している。

PUT /{index_name}/_settings
{
    "index" : {
        "refresh_interval" : "-1"
    }
}

refresh_interval を短く(あるいはデフォルトの1秒)保ったまま大量書き込みを行うと、小さなセグメントが大量に生成され、その一つひとつに対して HNSW グラフ構築が走ることになる。これは CPU 負荷を増大させるだけでなく、後述するようにセグメント数の増加そのものが検索性能を悪化させる。バルクロード完了後は refresh_interval を元に戻すことを忘れてはならない。

merge とグラフの再構築

セグメントは不変なので、削除されたドキュメントの掃除や小さなセグメントの統合は merge によって行われる。merge が起きると、複数の入力セグメントから1つの新しいセグメントが生成され、そのセグメントの knn_vector フィールドについても新しい HNSW グラフが1からビルドされる(古いセグメントのグラフはそのまま使い回せない)。これは公式ドキュメントが “(Expert level) Build vector data structures on demand” として紹介する高度なテクニックの前提にもなっている。すなわち、初回の一括ロード後に force merge を行いたい場合、通常の運用ではグラフが2回(通常のセグメント生成時と force merge 時)ビルドされてしまうため、index.knn.advanced.approximate_threshold-1 に設定してグラフ構築自体を止めた状態で bulk indexing→force merge を行い、最後に 0 に戻すという手順が推奨される。

POST test-index/_forcemerge?max_num_segments=1

max_num_segments=1 はシャード内のセグメントを最終的に1つにまとめる指定であり、検索時にグラフ探索・マージのオーバーヘッドを最小化する。

refresh によるセグメント生成と merge によるグラフ再構築の関係を時系列で整理すると図3-1のようになる。

上段はrefresh(1秒間隔)のたびにin-memory bufferから新しいセグメントが生まれHNSWグラフが構築される様子、下段はmergeで複数の旧セグメントが1つのmerged segmentに統合されグラフが1から再構築される様子を示すタイムライン図

図3-1: refreshによるセグメント生成(上段)とmergeによるグラフ再構築(下段)。セグメント数が増えるほど検索時のグラフ探索コストが積み重なる。

セグメントが増えると検索が遅くなる理由

検索性能チューニングガイドは次のように明言している。

To improve search performance, you must keep the number of segments under control. Lucene’s IndexSearcher searches over all of the segments in a shard to find the ‘size’ best results. Having one segment per shard provides optimal performance with respect to search latency.

2章で述べた通り ANN グラフはセグメント単位で独立しているため、セグメント数が N 個あれば検索時には N 回のグラフ探索(各セグメントで最大 k 件を取得)とその結果のシャード内マージが発生する。セグメントが小さく数が多いほど、1回あたりのグラフ探索のオーバーヘッド(グラフの読み込み、エントリポイントからの探索)が相対的に大きくなり、全体レイテンシが増加する。一方で、極端に少ない・大きすぎるセグメントは recall(再現率)の面で不利になることもある。パフォーマンスチューニングガイドは「多数の小さなセグメントを検索して集約する方が、少数の巨大なセグメントを検索して集約するより recall が高くなる傾向がある」とも述べており、レイテンシと recall はセグメント数を挟んでトレードオフの関係にある。

index.knn 設定とビルドスレッド

グラフ構築の並列度は knn.algo_param.index_thread_qty(クラスタ設定、デフォルトは32コア未満のシステムで1、32コア以上で4)で制御される。値を上げるとインデクシングスループットは上がるが、ネイティブライブラリのインデックス構築は CPU 負荷が高いため、必要以上に増やすと他の処理(検索など)を圧迫する。

PUT _cluster/settings
{
  "persistent": {
    "knn.algo_param.index_thread_qty": 2
  }
}

次章では、こうして作られたセグメントに対して検索がどのように実行されるか、query phase と fetch phase の2段階に分けて見ていく。

4. OpenSearchのk-NN検索の仕組み: ef_search・k・sizeとwarmup API

OpenSearch の検索は(k-NN に限らず)常に query phasefetch phase の2段階で構成される。query phase では各シャードがマッチしたドキュメントの ID とスコアだけを計算してコーディネートノードに返し、コーディネートノードが全シャードの結果をマージして上位 size 件を決定したのち、fetch phase でその上位件数分だけ _source などの実データを該当シャードから取得する。この2段階設計自体は k-NN 特有ではないが、query phase の中身が k-NN では「セグメントごとの HNSW/IVF 探索」に置き換わる点が本質的に異なる。この一連の流れを図示すると図4-1のようになる。

左列がquery phase(コーディネートノードから各シャードへ分配、各セグメントのHNSW探索、shard内マージ、コーディネートノードでのグローバルマージ)、右列がfetch phase(該当シャードのみへの_source取得リクエストとレスポンス返却)を番号付き矢印で示す2カラム図

図4-1: query phase(左)とfetch phase(右)の流れ。query phaseではセグメントごとのHNSW探索→shard内マージ→グローバルマージでsize件を確定し、fetch phaseでは該当シャードのみから_sourceを取得する。

シャード単位の検索からセグメント単位の探索、そしてマージへ

k とサイズ(k, size)の関係についてドキュメントは次のように定義している。faiss/nmslib エンジンでは、k は「シャード内の全セグメントを通じて返される最大ドキュメント数」を表すのに対し、Lucene エンジンでは k は「シャードごとに返されるドキュメント数」を表す。つまり faiss/nmslib は各セグメントの探索結果をシャード内で早期にマージしながら k 件に絞り込むのに対し、Lucene は各セグメントがそれぞれ最大 k 件を返し、それをシャード内でマージしてから size 件に絞る、という違いがある。

size | k | 主シャード数 | シャードあたりのセグメント数 | Faiss/NMSLIB の返却数 | Lucene の返却数
10   | 1 | 1            | 4                              | 4                     | 1
10   | 10| 1            | 4                              | 10                    | 10
10   | 1 | 2            | 4                              | 8                     | 2

ksize が等しい場合は両エンジンとも同じ件数を返すが、ksize より小さい場合は挙動が異なる。実運用では size(最終的に欲しい件数)と k(各セグメント/シャードでの探索件数)を混同しないことが重要である。最終的に、各シャードはコーディネートノードに size 件を返し、コーディネートノードは size * シャード数 件の中からグローバルに上位 size 件を選び出す。

ef_search の指定方法

Faiss の HNSW 探索の広さは ef_search(動的候補リストのサイズ)で制御される。指定方法は2通りある。

  1. インデックス設定として: index.knn.algo_param.ef_search(動的設定、デフォルト100)
  2. 検索リクエストの method_parameters として、クエリ単位で上書き
GET my-index/_search
{
  "query": {
    "knn": {
      "my_vector_field": {
        "vector": [1.5, 2.5, 3.5],
        "k": 5,
        "method_parameters": {
          "ef_search": 512
        }
      }
    }
  }
}

なお Lucene エンジンの HNSW 実装は ef_search を無視し、検索リクエストの k の値を動的に ef_search として使うため、Lucene エンジンでは ef_search の設定自体が不要である、とドキュメントは明記している。

warmup API はなぜ必要か

近似 k-NN 用のネイティブライブラリインデックス(グラフファイル)は、他の Lucene セグメントファイルと並んでディスクに保存される特殊なファイルである。検索を実行するには、k-NN プラグインはこのファイルをネイティブメモリに読み込む必要がある。まだメモリに読み込まれていない状態で検索リクエストが来ると、その場でファイルを読み込むことになり、初回クエリのレイテンシが跳ね上がる。この問題を避けるため、_plugins/_knn/warmup API が用意されている。

GET /_plugins/_knn/warmup/index1,index2,index3?pretty

このAPIは指定したインデックスの全シャード(プライマリ・レプリカ双方)にあるネイティブライブラリファイルをすべてネイティブメモリにロードする。冪等な操作であり、既にロード済みのセグメントに対しては何もしない。ベストプラクティスとして、ドキュメントは「warmup 中に merge を走らせない」ことを強く推奨している。merge によって古いセグメント A・B が削除され新しいセグメント C が生まれると、warmup でロードした A・B はキャッシュから見えなくなり、C については改めて初回ロードのペナルティが発生するためである。同様に、warmup 対象に新規ドキュメントを書き込むと検索可能になるまでロードが妨げられるため、warmup は「インデクシング完了後・検索開始前」に実行するのが正しい使い方である。

OpenSearch 3.1 以降、memory-optimized search(5章)が有効な場合、warmup API は全データをネイティブメモリに展開するのではなく、検索に必要な最小限の情報(Faiss インデックスファイルへの読み込みストリームのオープンなど)だけをロードするよう変化する。これにより warmup 自体は軽量になるが、ウォームアップの目的(初回クエリのレイテンシ削減)は変わらず有効である。

stats API での確認

warmup 後、どれだけのグラフがメモリに載っているかは _plugins/_knn/stats で確認できる(詳細は6章)。

GET /_plugins/_knn/stats/graph_memory_usage,circuit_breaker_triggered?pretty

次章では、フィルタ付き検索がこの「セグメントごとの ANN 探索」とどう組み合わさるか、そして exact search へのフォールバック、ネイティブメモリの制約、ディスクベース検索の仕組みを見ていく。

5. フィルタ付きベクトル検索と省メモリ化: exact fallback・量子化・on_diskモード

フィルタ付き k-NN 検索の3方式

OpenSearch の公式ドキュメントは、フィルタ付きベクトル検索を次の4つの方式(適用タイミングで見ると pre / 検索中 / post の3系統)に整理している。

方式フィルタの適用タイミング検索の種類対応エンジン/メソッド
Efficient k-NN filtering検索中(pre/post のハイブリッド)近似lucene(hnsw)、faiss(hnsw, ivf)
Boolean filter検索後(post-filtering)近似lucenefaissnmslib(非推奨)
post_filter パラメータ検索後(post-filtering)近似lucenefaissnmslib(非推奨)
Scoring script filter検索前(pre-filtering)厳密(exact)全ベクトルフィールド対応だが低スケール

pre-filtering(scoring script) は先にフィルタでドキュメント集合を絞り込み、その部分集合に対して総当たりの exact k-NN を実行する。フィルタが強く効いて対象が数万件程度まで絞られる場合は高速だが、対象集合が大きいとスケールしない。post-filtering は ANN 検索を先に実行してから結果にフィルタをかけるため、フィルタが強いと k 件に満たない結果しか返らないことがある。efficient filtering はこの中間で、ANN のグラフ探索そのものにフィルタ条件を織り込みながら進める方式であり、k 件をできるだけ確保しつつ ANN の速度を活かす。

efficient filtering の exact fallback ロジック

efficient filtering の内部動作は k-NN プラグインのソースコード src/main/java/org/opensearch/knn/index/query/KNNWeight.java に実装されている。この searchLeaf メソッドは1セグメントごとに次の判断を行う。

  1. フィルタ適用後のドキュメント数(filterCardinality、ドキュメントでは P)が k 以下であれば、そもそも ANN を経由せず直接 exact search を行う(isFilterIdCountLessThanK)。
  2. index.knn.advanced.filtered_exact_search_threshold(コード上の定数 ADVANCED_FILTERED_EXACT_SEARCH_THRESHOLD、デフォルト値は「未設定」を表す -1)が設定されている場合、その閾値がフィルタ後のドキュメント数以上であれば exact search を行う。
  3. 閾値が未設定の場合は、MAX_DISTANCE_COMPUTATIONS(KNNConstants.java に定義される定数で値は 2048000)という「許容される最大距離計算回数」と filterCardinality * dimension(推定距離計算回数)を比較し、後者が上回らなければ exact search を行う。
  4. 上記のいずれにも該当せず ANN(efficient filtering)を実行した結果、フィルタ後のドキュメント数 Pk 以上あったにもかかわらず返却件数 Rk 未満だった場合(isFilteredExactSearchRequireAfterANNSearch)、k 件を確保するためにフィルタ済みドキュメント ID 集合に対する exact search へフォールバックする。

この4段階の判定ロジックをフローチャートにすると図5-1のようになる。

フィルタ後件数PとkやMAX_DISTANCE_COMPUTATIONS(2,048,000)などの閾値を比較する3つのひし形分岐がYesならexact searchへ、Noならば次の分岐またはANN実行(efficient filtering)へ進み、ANN実行後にR<kかつP≥kであれば再びexact searchへフォールバックするフローチャート

図5-1: efficient filteringにおけるexact search fallbackの判定フロー(KNNWeight.javasearchLeafロジックに基づく)。3つの事前条件のいずれかを満たせばexact searchに直行し、いずれも満たさなければANNを実行したうえで結果件数不足時にフォールバックする。

この最後のフォールバックは OpenSearch 3.5 で index.knn.faiss.efficient_filter.disable_exact_search という設定によって無効化できるようになった。レイテンシを優先し、k 件に満たない結果でもよいワークロードでは、この設定を true にすることで追加の exact search をスキップできる。ソースコード上もこの分岐は KNNSettings.isKnnIndexFaissEfficientFilterExactSearchDisabled(...) の戻り値を見て、true であれば isExactSearchRequirefalse を返すよう実装されている。

OpenSearch 3.1 以降、Faiss+HNSW で memory-optimized search が有効な場合には、Lucene 由来の ACORN フィルタリング最適化(グラフ探索中にフィルタでスパースになった近傍の先の近傍まで辿る手法)も適用され、デフォルトではフィルタ後に60%を超えるドキュメントが残る場合は最適化自体をスキップし、90%未満の近傍しかフィルタを満たさない場合に近傍のさらに先まで探索範囲を広げる。

native memory circuit breaker

faiss/nmslib のグラフはネイティブメモリに保持されるため、JVM のヒープとは別に専用のサーキットブレーカーで保護されている。

  • knn.memory.circuit_breaker.limit(動的、デフォルト 50%): ネイティブライブラリインデックス用のメモリ上限。100GBのマシンで JVM が32GB使う場合、残り68GBの50%、つまり34GBが上限になる。ノード属性 node.attr.knn_cb_tier を使うことでノードごとに異なる上限を設定することもできる。
  • knn.circuit_breaker.unset.percentage(動的、デフォルト 75): 一度サーキットブレーカーが作動した後、メモリ使用率がこの割合(knn.memory.circuit_breaker.limit に対する割合)を下回るまで knn.circuit_breaker.triggeredfalse に戻さない、というヒステリシス制御。

このロジックの実体は src/main/java/org/opensearch/knn/index/KNNCircuitBreaker.java にある。クラスタの選出済みマネージャノードが2分間隔(CB_TIME_INTERVAL = 2 * 60 秒)でスケジュールされたタスクを実行し、NativeMemoryCacheManager のキャッシュ容量が閾値に達しているかを確認する。作動時は最も長く使われていないグラフ(LRU)がキャッシュから追い出され(evict)、全ノードでキャッシュ使用率が unset.percentage を下回れば knn.circuit_breaker.triggeredfalse に戻す。

グラフのメモリ見積もり式

公式ドキュメントはエンジンごとのメモリ使用量を次の式で見積もっている(dimension は次元数、m は HNSW の最大接続数、num_vectors はベクトル数)。

\[\text{HNSW(float)} \approx 1.1 \times (4d + 8m) \times \text{num\_vectors} \ \text{bytes}\] \[\text{IVF(float)} \approx 1.1 \times \big((4d \times \text{num\_vectors}) + (4 \times \text{nlist} \times d)\big) \ \text{bytes}\]

byte ベクトルの場合は係数が variable で、HNSW は \(1.1 \times (d + 8m) \times \text{num\_vectors}\) 、binary ベクトル(次元は8の倍数であることが必須)の場合は \(1.1 \times (d/8 + 8m) \times \text{num\_vectors}\) になる。レプリカを使うとベクトル総数は実質2倍になる点にも注意が必要である。

quantization(SQ/PQ/binary)

メモリ使用量を抑えるため、OpenSearch は複数の量子化技術を提供する。compression_level フィールドマッピングパラメータで指定でき、対応するエンジンと圧縮率は次の通りである。

compression_level対応エンジン
1xfaiss, lucene, nmslib(非推奨)
2xfaiss
4xlucene
8xfaiss
16xfaiss
32xfaiss, lucene

内部的には SQ(32bit float → 16bit や 1bit への削減)、PQ(ベクトルを m 個のサブベクトルに分割し code_size ビットのコードブックで符号化)、バイナリ量子化(float→bit)などが組み合わされる。

quantize + rescore の2フェーズ検索

量子化されたインデックスでは、recall を保つために2フェーズのリスコアリングが使われる。第1フェーズで量子化ベクトルを使い oversample_factor * k 件を近似検索で取得し、第2フェーズでそれらドキュメントのフル精度ベクトルをディスクから読み込んで再スコアリングし、上位 k 件に絞り込む。

GET /my-vector-index/_search
{
  "size": 2,
  "query": {
    "knn": {
      "target-field": {
        "vector": [2, 3, 5, 6],
        "k": 2,
        "rescore": { "oversample_factor": 1.2 }
      }
    }
  }
}

OpenSearch 2.17 で導入された disk-based vector search は、mode: "on_disk" を指定するだけでこの「量子化 + リスコアリング」を既定構成として有効にする。デフォルトの compression_level32x で、デフォルトの oversample_factor2.0 である。

PUT my-vector-index
{
  "settings": { "index": { "knn": true } },
  "mappings": {
    "properties": {
      "my_vector_field": {
        "type": "knn_vector",
        "dimension": 8,
        "space_type": "innerproduct",
        "mode": "on_disk",
        "compression_level": "16x"
      }
    }
  }
}

on_disk mode は「低レイテンシ優先」の in_memory(デフォルト、faiss・量子化なし)と対を成す「低コスト優先」のモードであり、float データ型のみサポートされる。OpenSearch 3.1 以降、on_disk かつ 1x 圧縮を組み合わせると、後述する memory-optimized search が自動的に有効になる。

memory-optimized search(3.1)

memory-optimized search は、Faiss+HNSW インデックスをネイティブメモリに全展開する代わりに、インデックスファイルを mmap し OS のページキャッシュを介してオンデマンドに読み込む方式である。索引時の挙動には影響せず、検索時のメモリ管理方式だけを変える。IVF・PQ には未対応で、Faiss+HNSW にのみ適用できる。2.19 より前に作られたインデックスは、このモードを有効にしても常に全展開される。量子化とは独立した仕組みであり、disk-based search(圧縮あり)とは異なるが、両者を組み合わせることも可能である。

6. OpenSearchのベクトル検索が遅い時のチューニング: 障害切り分け手順

ここまでの内容を踏まえ、実運用で「ベクトル検索が遅い」と感じたときにどこを見るべきかを整理する。

stats API で確認すべき指標

_plugins/_knn/stats はノードレベル・クラスタレベルの両方の統計を返す。特に注目すべきフィールドは以下の通りである。

GET /_plugins/_knn/stats?pretty
  • graph_memory_usage / graph_memory_usage_percentage: ノード上でネイティブライブラリインデックスが使っているメモリ量とキャッシュ容量に対する割合。
  • hit_count / miss_count: 既にメモリに載っているグラフへのアクセス(hit)か、初回ロードが必要なアクセス(miss)か。miss が多い場合、warmup が効いていないか、キャッシュ容量不足で追い出しが多発している可能性がある。
  • eviction_count: メモリ制約またはアイドル時間超過によってキャッシュから追い出されたグラフの数(明示的なインデックス削除によるものはカウントされない)。
  • cache_capacity_reached: knn.memory.circuit_breaker.limit に到達したかどうか。
  • circuit_breaker_triggered: サーキットブレーカーが作動中かどうか(クラスタレベル)。
  • indices_in_cache: インデックスごとのグラフ使用メモリ量・グラフ数。特定のインデックスがキャッシュを圧迫していないか確認できる。
  • load_exception_count / graph_query_errors: ロード失敗やクエリエラーの発生状況。

セグメント数の確認

3章・4章で述べた通り、セグメント数の増加は検索レイテンシに直結する。_cat/segments API でシャードごとのセグメント数とサイズを確認し、想定より多い場合は refresh_interval の設定や force_merge の実施状況を疑うべきである。

GET /_cat/segments/my-index?v&h=index,shard,segment,size,size.memory

circuit breaker 作動時に起きること

サーキットブレーカーが作動すると、NativeMemoryCacheManager は LRU 方針で使用中のグラフをキャッシュから追い出す。追い出された直後に同じインデックスへの検索が来ると、その場でグラフファイルを再ロードすることになり、レイテンシが跳ね上がる。warmup のベストプラクティスにある通り、メモリ容量に対してグラフサイズが恒常的にオーバーしている状況では「ロード→追い出し→再ロード」を繰り返す キャッシュスラッシング(cache thrashing) が発生し、検索が慢性的に遅くなる。これは一時的な障害ではなく設計上の容量不足のサインであり、ノードのメモリ増強・レプリカ数の見直し・量子化の導入・on_disk モードへの切り替えのいずれかで対処すべきである。

なお、KNNWeight.javaisMissingNativeEngineFiles / isExactSearchRequire のロジックが示す通り、セグメントにそもそもネイティブエンジンファイルが存在しない場合(例えば index.knn.advanced.approximate_threshold-1 にしてグラフ構築を意図的に止めている場合)は exact search(総当たり)にフォールバックする。これはサーキットブレーカー作動時の挙動とは別のケースであり、混同しないよう注意したい。サーキットブレーカーはあくまで「メモリからの追い出しと再ロード」を引き起こすものであり、ANN 検索自体を exact search に切り替えるものではない。

refresh・merge によるレイテンシスパイク

3章で見た通り、refresh はセグメント生成とグラフ構築を伴うため、大量書き込み中に短い refresh_interval を使っていると、インデクシングスレッドと検索スレッドの双方で CPU を奪い合い、書き込みレイテンシと検索レイテンシの両方が悪化する。同様に merge が発生するとマージ後セグメントのグラフが再構築されるため、その間 CPU を消費し、レイテンシスパイクの原因になる。_cat/thread_pool_nodes/stats の merge スレッドプールの状況と、k-NN stats の graph_index_requests の推移を突き合わせて確認するとよい。

JVM ヒープと native memory の奪い合い

knn.memory.circuit_breaker.limit は「JVM ヒープが使っている分を除いた残りメモリ」に対する割合として計算される。したがって、JVM ヒープサイズを大きく設定しすぎるとネイティブメモリに割ける物理メモリが減り、同じ 50% という設定でも実際に使えるグラフ用メモリの絶対量は小さくなる。逆に JVM ヒープを絞りすぎると今度は通常のクエリ処理やフィールドデータキャッシュが圧迫される。ノードのメモリサイジングでは、この2つの「ヒープ」と「ネイティブメモリ」のバランスを常に意識する必要がある。

よくあるアンチパターン

  • refresh_interval を1秒のままにして大量書き込みを行う: 大量の小さなセグメントが生成され、その都度グラフが構築されるためインデクシングが遅くなり、かつ検索時のセグメント探索コストも増える。バルクロード時は refresh_interval: -1 にし、完了後に force_merge してから戻すのが定石である。
  • force_merge をせず大量のセグメントを放置する: 検索が「セグメント数に比例して遅くなる」ため、特に読み取り中心のワークロードでは定期的な force_merge(あるいは十分に長い refresh_interval による自然な統合)が欠かせない。
  • ef_search を過大に設定する: recall はわずかに改善するが、探索ノード数が増えることでレイテンシが目に見えて悪化する。デフォルト(100)から始めて、実際のデータセットで recall とレイテンシを測定しながら調整すべきである。
  • warmup を merge や refresh のたびに再実行しない: warmup は「その時点でディスク上にあるセグメント」を対象にした一度きりの操作であり、その後に merge や refresh、追加のインデクシングが発生すると新しいセグメントは warmup 対象外のまま初回ロードのペナルティを負う。

ここまでの性能診断の観点を踏まえ、次章では本番運用で継続的に見ておくべき観測メトリクスを整理する。

7. OpenSearch k-NNの監視: stats APIとメトリクスの読み方

ANNインデックスは「正しく動いているように見えて実は劣化している」状態になりやすい。索引パラメータやハードウェアが変わらなくても、削除の蓄積・データドリフト・レプリカ間のリバランスによってrecallは静かに下がる。recallが劣化する典型的な原因と、ゴールデンクエリセットによる定期サンプリングという対処法自体は製品に依存しない共通原理であり、 ANN基礎編 第9章で扱った内容を参照してほしい。ここではOpenSearch固有の観測ポイントに絞って整理する。

k-NN stats と _nodes/stats

OpenSearchはGET /_plugins/_knn/statsでk-NNプラグイン固有の統計を、GET /_nodes/statsでクラスタ全体のリソース統計を取得できる( k-NN APINodes stats )。主要フィールドは以下の通りである。

カテゴリフィールド意味
ネイティブメモリgraph_memory_usage, graph_memory_usage_percentage, cache_capacity_reachedHNSW/IVFグラフがオフヒープに占めるメモリ量とサーキットブレーカーへの近接度
キャッシュ効率hit_count, miss_count, eviction_count, load_success_countグラフキャッシュのヒット率と、メモリ逼迫によるエビクション頻度
クエリ負荷graph_query_requests, graph_query_errors, knn_query_requestsネイティブライブラリへのクエリ数とエラー率
サーキットブレーカーcircuit_breaker_triggeredk-NN用メモリブレーカーの発火有無(発火するとクエリが例外で失敗する)

これに加えて_nodes/statsのJVMヒープ使用率とfielddata/parentサーキットブレーカーの状態( Circuit breaker settings )を合わせて見る。OpenSearchのHNSWグラフはJVMヒープ外にロードされるため、「JVMヒープは余裕があるのにグラフがエビクションされ続けている」という状態が起こり得る。これはgraph_memory_usage_percentageの上昇とeviction_countの増加で検知できる。

キャパシティ指標: セグメント数とmerge頻度

メモリ使用量そのものに加えて重要なのが「セグメント数」と「merge頻度」である。OpenSearchでは、mergeが実行されるとセグメントが再構成されグラフキャッシュが無効化されるため、warmup APIでロードした直後にmergeが走ると初回クエリのレイテンシスパイクが再発する。理想は1シャードあたり1セグメントに近づけることであり、セグメント数の推移とmerge頻度を監視対象に含めるべきである( Approximate k-NN search )。

次章では、こうした観測の裏付けとなる、実際に本番投入した組織の公開事例を見ていく。

8. OpenSearchベクトル検索の導入事例: Amplitude・MANZ・Juicebox

理屈だけでなく、実際に本番投入した組織の一次情報から学べることは多い。ここではOpenSearchを採用した実在する公開事例を紹介する。捏造を避けるため、出典が明確に確認できるものに限定した。

Amplitude — 統合アーキテクチャを理由にpgvectorから移行

プロダクト分析プラットフォームのAmplitudeは、自然言語によるアナリティクス機能のために、数億件規模のスキーマエントリ(顧客ごとに5,000〜20,000要素)と約2,000万件のユーザー生成チャート/ダッシュボードを検索対象とするセマンティック検索を実装した。当初はサードパーティの全文検索のみ、次にブルートフォースの類似検索、その後pgvectorという段階を経て、最終的にOpenSearch ServiceとPineconeを比較検討した上でOpenSearchへ移行している。選定理由は、キーワード検索とベクトル検索を単一の基盤に統合できること、PostgreSQLと複数インデックスの間で同期パイプラインを何本も維持する必要がなくなること、検索データと埋め込みを同じ場所に置くことでラウンドトリップを減らしレイテンシを下げられること、複数の同期処理がトランザクションDBに負荷をかける問題を解消できることであった。HNSWのproduct quantization/byte quantizationを活用し、recallの低下を抑えながら数百万ベクトル規模をスケールさせている( How Amplitude implemented natural language-powered analytics using Amazon OpenSearch Service as a vector database )。教訓は、「複数の専用ストアを併存させる運用コスト」がベクトルDB単体の性能より優先される意思決定要因になり得るということである。

MANZ — 自前運用の経験不足を補うパートナー活用

オーストリアの大手法律情報プロバイダであるMANZは、5,000万件規模の法律文書を検索対象とし、将来的には3億件規模までの拡張を見込んだ検索基盤刷新を行った。ベクトルデータベースを自前で構築・運用した経験がほとんどなかったため、AWSパートナーのtecRacerと協業してAmazon OpenSearch Serviceを中心とした構成(Amazon Neptuneも併用)を構築した。Infrastructure as Codeと高速なストレージ構成により、5,000万件の文書検索を200〜300ミリ秒で処理できるようになり、従来の検索ソリューションと比較して中期的に最大30%のコスト削減が見込まれている( Accelerating Semantic and Similar Search Functions for a Leading Legal Publisher | MANZ & tecRacer Consulting Case Study )。教訓は、ベクトル検索基盤の運用経験がない組織にとって、マネージドサービス(OpenSearch Service)とパートナー支援の組み合わせが現実的な選択肢になり得るということである。

Juicebox — BM25とk-NNのハイブリッドによる定量的改善

AI採用ソーシングプラットフォームのJuiceboxは、8億件超の候補者プロフィールを対象に、従来の全文検索とセマンティック検索を組み合わせた検索基盤をAmazon OpenSearch Serviceで構築した。BM25アルゴリズムの活用により平均クエリレイテンシを約700msから250msへ削減し、k-NNによるセマンティック検索の導入により複雑なクエリに対して従来のキーワード検索比で35%多くの関連候補者を提示できるようになった。product quantizationなどの最適化により、億単位のベクトルインデックスでも低レイテンシのk-NN検索を実現している( Juicebox recruits Amazon OpenSearch Service’s vector database for improved talent search )。教訓は、ハイブリッド検索の効果はレイテンシ改善と関連性改善という異なる二つの軸で定量化できるということであり、片方だけを見ていては投資対効果を過小評価しかねない。

事例横断で見える共通点

3つの事例を通じて共通するのは、いずれも既存のAWS/OpenSearch運用体制の延長線上での意思決定だという点である。Amplitudeは複数の専用ストアを併存させる運用コストの高さを理由に統合を選び、事前にOpenSearch ServiceとPineconeを比較検討するPoCを経ている。MANZは自前でベクトル検索基盤を運用した経験の乏しさを、マネージドサービスとパートナー支援の組み合わせで補った。Juiceboxはハイブリッド検索の効果をレイテンシ改善と関連性改善という異なる二つの軸で定量化し、投資対効果を具体的な数値で示した。ベンチマーク上の数値差よりも、こうした組織的・運用的な制約が実際の選定を左右していることが、これらの一次情報から読み取れる。専用のベクトル検索データベースとしてゼロから設計されたシステムがどのような判断基準で選ばれているかを知りたい読者は、 Milvus内部構造編 で紹介したTokopedia・Shopeeの事例も参照してほしい。

9. まとめ

本記事ではOpenSearchのベクトル検索(k-NN)機能を、歴史的経緯からアーキテクチャ、書き込み・検索経路、フィルタとメモリ管理、性能障害の切り分け、観測設計、公開事例まで一貫して見てきた。各章の要点は次の通りである。

要点
1章k-NN プラグインは nmslib(非推奨)→ faiss(主力・圧縮・GPU対応)→ lucene(中小規模・フィルタ性能)という3エンジン体制に収斂し、3.x系では Vector Engine として derived source・remote/GPU build・memory-optimized search・on-disk mode へと拡張を続けている。
2章OpenSearch は クラスタ→ノード→インデックス→シャード(=Luceneインデックス)→セグメントという階層を持ち、ANN グラフは knn_vector フィールド×セグメントごとに独立して構築される(faiss/nmslibはネイティブメモリ、luceneはセグメントファイル)。
3章refresh(デフォルト1秒間隔)のたびに新セグメントとそのHNSWグラフが生まれ、merge のたびにグラフは1から再構築される。セグメント数が増えるほど検索コストが増すため、大量書き込み時は refresh_interval を緩め、force_merge で統合するのが定石である。
4章検索は query phase(各シャード・各セグメントのHNSW探索→shard内マージ→グローバルマージでsize件確定)と fetch phase(該当シャードのみから_source取得)の2段階からなり、warmup API は初回クエリのレイテンシを避けるためにグラフを事前にネイティブメモリへロードする。
5章efficient filtering はフィルタ後件数P・閾値・距離計算回数の3条件のいずれかを満たせばexact searchに直行し、それ以外はANNを実行したうえで結果件数が不足すればexact searchへフォールバックする。native memory circuit breakerはグラフ用メモリをJVMヒープと切り離して保護する。
6章性能障害の切り分けは _plugins/_knn/stats によるメモリ・hit/miss・eviction の確認、_cat/segments によるセグメント数の確認、refresh/merge起因のレイテンシスパイクの識別が基本であり、キャッシュスラッシングは容量不足のサインとして対処すべきである。
7章観測すべき中心はk-NN stats(グラフメモリ・hit/miss・circuit breaker)と_nodes/statsのJVMヒープ、そしてセグメント数とmerge頻度である。recallの定期サンプリングは製品を問わない共通原理であり、放置すれば静かに劣化する。
8章Amplitude・MANZ・Juiceboxいずれの事例も、性能そのものよりも既存運用体制との親和性・マネージドサービスの活用・定量的な投資対効果の提示が実際の意思決定を左右している。

これらすべてを貫くのは、OpenSearchのk-NNが「全文検索エンジンのLuceneセグメントアーキテクチャの上にANNを統合する」という一つの設計方針から論理的に導かれているという事実である。セグメント単位のグラフ構築、refresh/mergeとの結合、ネイティブメモリとJVMヒープの分離、フィルタとexact fallbackの組み合わせ――どれもこの出自を踏まえれば「なぜそうなっているか」が腑に落ちるはずである。

専用のベクトル検索データベースとして一から設計された場合にこれらの設計判断がどう変わるかを比較したい読者は、 Milvus内部構造編 を参照してほしい。同じANNの原理が、出自の異なる2つのシステムでどのように異なる実装に落とし込まれているかが見えてくるはずである。

よくある質問(FAQ)

OpenSearchでベクトル検索(セマンティック検索)はできますか?

できる。OpenSearchはknn_vector型のフィールドとk-NNプラグインにより近似最近傍探索(ANN)によるベクトル検索を提供しており、nmslib(非推奨)・faiss・luceneの3エンジンから選んで利用する。3.x系では単なる検索プラグインの一機能ではなく「Vector Engine」として、derived source・remote/GPUビルド・memory-optimized search・on-diskモードといった拡張が積み重ねられている。詳細は第1章。

faissエンジンとluceneエンジンはどちらを使うべきですか?

本文で整理した役割分担に従えば、faissは大規模データの主力であり、圧縮(量子化)やGPUビルド対応の中心となるエンジンである。一方luceneは中小規模のデータに向き、フィルタ性能とシンプルさが強みになる。自分のワークロードのデータ規模・圧縮の必要性・フィルタ検索の重要度に応じて選ぶとよい。詳細は第1章。

ef_searchはどのくらいの値にすべきですか?

index.knn.algo_param.ef_searchのデフォルト値は100である。ef_searchを過大に設定するとrecallはわずかに改善するが、探索ノード数が増えるためレイテンシが目に見えて悪化する。したがってデフォルト(100)から始め、実際のデータセットでrecallとレイテンシを測定しながら調整するのが本文の立場である。なおLuceneエンジンではef_searchの設定自体が不要である。詳細は第4章・第6章。

インデックスしたベクトルがすぐ検索できるようにならないのはなぜですか?

OpenSearchはドキュメントを書き込んでもすぐには検索対象にならず、in-memory bufferの内容が新しいLuceneセグメントとしてディスクに書き出されるrefreshというトリガーを経て初めて検索可能になる。デフォルトのrefresh_intervalは1秒であり、これは準リアルタイム検索(near real-time search)を実現するための設計である。さらにk-NN特有の点として、このrefreshのタイミングでセグメント内のHNSWグラフも構築される。詳細は第3章。

OpenSearchとMilvusのベクトル検索はどう違いますか?

OpenSearchはElasticsearchからのフォークという出自を持ち、Lucene由来のセグメントアーキテクチャの上にk-NNプラグインとしてANNを後から統合したシステムである。これに対しMilvusはベクトル検索専用に設計された分散システムであり、出自の違いが書き込み経路・検索経路・メモリ管理・フィルタとの組み合わせ方などに影響を及ぼしている。両者の体系的な比較は ANN基礎編 を、Milvus側の内部構造の詳細は Milvus内部構造編 を参照してほしい。

関連書籍

本シリーズの内容を書籍でも体系的に学びたい読者には、日本語で書かれたベクトル検索の実践的な入門書として次の一冊を挙げておく。

ベクトル検索実践入門(真鍋知博、技術評論社)

他の分野の定番書は エンジニアにおすすめの技術書10選 にまとめている。

付録: 用語集(OpenSearch編)

  • refresh: OpenSearch(Lucene)でin-memory bufferの内容を新しいセグメントとしてディスクに書き出し、検索可能にするトリガー。デフォルトのrefresh_intervalは1秒で、このタイミングでセグメント内のHNSWグラフも構築される(3章)。
  • force merge: 複数のセグメントを1つ(または少数)に統合する明示的な操作。OpenSearchでは_forcemerge APIとして提供され、セグメント数削減による検索レイテンシ改善に直結する(3章)。Milvusにも同種のForce Merge Compactionがある( Milvus内部構造編 参照)。
  • warmup: OpenSearchでネイティブライブラリインデックス(グラフファイル)を事前にネイティブメモリへロードしておくAPI。初回クエリのコールドスタートペナルティを避けるため、インデクシング完了後・検索開始前に実行するのが正しい使い方である(4章)。
  • circuit breaker(サーキットブレーカー): OpenSearchのnative memory circuit breakerはknn.memory.circuit_breaker.limitに基づきグラフ用メモリの上限を管理し、作動するとLRU方針でグラフをキャッシュから追い出す(5章、6章)。Milvusのメモリ管理はmmapによる遅延ロードで別のアプローチを取る( Milvus内部構造編 参照)。
  • efficient filtering(効率的フィルタリング): OpenSearchのフィルタ付きk-NN検索方式の一つ。ANNのグラフ探索そのものにフィルタ条件を織り込みながら進める、pre-filtering・post-filteringの中間的な方式(5章)。
  • exact fallback: フィルタ後のドキュメント数がk以下の場合や、推定距離計算回数が閾値を超えない場合などに、ANNを経由せず総当たりのexact searchに切り替えるロジック。OpenSearchのKNNWeight.javaに実装されている(5章)。
  • native memory(ネイティブメモリ): OpenSearchのfaiss/nmslibエンジンがJNI経由で構築するHNSW/IVFグラフが置かれる、JVMヒープ外のメモリ領域。NativeMemoryCacheManagerが管理する(2章、5章)。
  • on_disk mode(disk-based vector search): OpenSearch 2.17で導入された、量子化とリスコアリングを既定構成として有効にする低コスト優先の検索モード。デフォルトのcompression_levelは32x、oversample_factorは2.0(5章)。
  • oversample_factor: 量子化されたインデックスで、第1フェーズの近似検索で取得する候補件数をk件の何倍にするかを指定するパラメータ。第2フェーズでフル精度ベクトルによりリスコアリングして最終的にk件に絞り込む(5章)。

参考文献

公式ドキュメント

ブログ・事例

ソースコード