Claude Media
Claude CodeでOpenSearchのk-NN検索を立てて運用設定を確かめる

Claude CodeでOpenSearchのk-NN検索を立てて運用設定を確かめる

OpenSearchのk-NN検索で決めるengine・space_type・メモリ見積りを整理し、索引後のウォームアップとサーキットブレーカーをClaude Codeで確認する手順です。

OpenSearchのk-NN検索は、索引を作る前に決める3つの値で運用の姿がほぼ決まります。ANNの実装を選ぶengine、距離の測り方を決めるspace_type、ネイティブメモリに載るサイズです。索引ができたあとは、ウォームアップとサーキットブレーカーの状態を数字で確かめる段階に入ります。

この記事は、その作業をClaude Codeに手伝わせる前提でまとめています。マッピングやクエリの下書き、メモリの計算、_statsの読み取りはClaude Codeが得意な部分です。一方で、engineと距離の選択は埋め込みモデルとデータ量から人が決める部分です。両者の境目を最初に引いておくと、レビューの手間が減ります。検索クラスタをMCP経由で扱う場合はTypesense Cloud MCPの接続手順が、手元で意味検索を動かす別の例はLumenプラグインの入れ方が参考になります。

Claude Codeに渡す前に、CLAUDE.mdと権限を置く

k-NN検索の作業では、クラスタ設定を書き換えるPUTと、ただ状態を読むGETが混ざります。最初に「読むだけの操作は自動で通し、書く操作は毎回確認する」形にしておきます。

Bashの許可ルールは、deny、ask、allowの順に評価され、最初に一致したものが結果になります。広いdenyは、より狭いallowでは覆せません。引数でURLを絞るような書き方は、オプションの位置やリダイレクトで簡単にすり抜けられるため脆い、とも書かれています。そこでcurlを直接許可せず、メソッドを第1引数に取る小さなラッパーを経由させる構成にします。

#!/usr/bin/env bash
# scripts/os.sh  使い方: os.sh GET /_plugins/_knn/stats
set -euo pipefail
method="$1"; path="$2"; shift 2
curl -sS -X "$method" "${OS_URL:-http://localhost:9200}${path}" \
  -H 'Content-Type: application/json' "$@"

認証の設定は環境ごとに違うため、ここでは省いています。接続先は手元の開発クラスタを想定し、本番のURLをOS_URLに入れた状態でClaude Codeを起動しないでください。

権限は.claude/settings.jsonに次のように置きます。

{
  "permissions": {
    "allow": ["Bash(./scripts/os.sh GET *)"],
    "ask": [
      "Bash(./scripts/os.sh PUT *)",
      "Bash(./scripts/os.sh POST *)"
    ],
    "deny": [
      "Bash(./scripts/os.sh DELETE *)",
      "Read(./.env)"
    ]
  }
}

*はサブコマンドに相当する語の後ろに置くと、そのコマンドだけに効きます。ただしBashルールが一致するのは、Claudeが書いたコマンド文字列そのものです。sh -cで包んだ呼び出しや別パスからの呼び出しは一致しないため、拒否ルールの抜けを埋めたい場合はサンドボックスのネットワーク許可リストと組み合わせます。

次に、CLAUDE.mdへ確認の手順を書いておきます。以降の節で決める値と、見るべき数字がそのまま入ります。

## OpenSearch k-NN の作業ルール
- クラスタへの操作は ./scripts/os.sh 経由。DELETE は使わない
- 索引を作る前に、engine / space_type / dimension / m を表で示して承認を取る
- メモリ見積りは 1.1 * (4 * dimension + 8 * m) bytes/vector で計算し、式と結果を併記する
- 投入後は GET /_plugins/_knn/warmup/<index> を実行し、_shards の failed が 0 であることを報告する
- 最後に GET /_plugins/_knn/stats/circuit_breaker_triggered,graph_memory_usage,eviction_count を実行して数値を貼る

engineは規模とフィルタの要件から選ぶ

engineは、HNSWやIVFといった手法を実装するライブラリです。OpenSearchにはLucene、Faiss、NMSLIB、JVectorがあります。NMSLIBは旧実装でdeprecatedの扱いなので、新しい索引では候補から外します。JVectorはopensearch-jvectorプラグインが前提です。

選び方の目安は次のとおりです。

項目Faiss HNSWFaiss IVFLucene HNSWJVector
ベクトル数の目安Faiss HNSW数百億規模Faiss IVF数百億規模Lucene HNSW1,000万未満JVector10億単位
学習ステップFaiss HNSW不要(PQ使用時は必要)Faiss IVF必要Lucene HNSW不要JVector不要
クエリ遅延と精度Faiss HNSW低遅延・高精度Faiss IVF低遅延・低精度Lucene HNSW高遅延・高精度JVector低遅延・高精度
プラグインFaiss HNSW不要Faiss IVF不要Lucene HNSW不要JVector必要

大規模ならFaiss、小規模でフィルタ条件が多いならLuceneが基本線です。Luceneはフィルタを検索中に適用し、状況に応じて事前フィルタ、事後フィルタ、完全探索を自動で切り替えます。Faissは事後フィルタです。

Claude Codeに「どれがよいか」と聞くと、もっともらしい一つの答えが返ります。これでは判断材料になりません。次のように、条件を渡して表にさせるほうが検証しやすくなります。

ベクトル数は 300 万件、次元は 768、検索には category での絞り込みが
ほぼ毎回付く。engine の候補を Faiss HNSW / Lucene HNSW に絞り、
フィルタ方式・メモリ・運用上の制約を表にして。根拠は
docs.opensearch.org の Methods and engines の記述に限る。

Lucene HNSWはef_searchを持ちません。検索リクエストのkの値がそのまま使われるため、Faissでknn.algo_param.ef_searchを調整する感覚でLuceneの索引に同じ設定を入れても効きません。mとef_constructionは両engineで既定が16と100で、mはメモリに大きく効きます。

次元の上限は、Lucene、Faiss、NMSLIBのいずれでも16,000です。SIMDによる高速化は、Linux上のFaissで次元が8の倍数のときだけ効きます。768や1024は問題ありませんが、独自に次元を削った埋め込みを使う場合は確認が必要です。

space_typeは埋め込みモデルの学習時の距離に合わせる

space_typeは、近さをどう測るかを決める値です。k-NN検索では、距離が小さいほど近い結果になります。OpenSearchのスコアは大きいほど良い結果なので、内部で距離をスコアに変換しています。

指定できる値はl1、l2、linf、cosinesimil、innerproduct、hamming、hammingbitです。ただし、engineと手法の組み合わせによって使えるものが変わります。

  • Faiss HNSW: l2、innerproduct(PQ使用時は不可)、hamming、cosinesimil(2.19以降)
  • Faiss IVF: l2、innerproduct、hamming、cosinesimil(2.19以降)
  • Lucene HNSW: l2、cosinesimil、innerproduct(2.13以降)

エンジン選択の推奨表ではLuceneの類似度がl2とcosinesimilだけで、手法ごとの節ではinnerproductも挙がっています。ページ間で食い違うため、innerproductをLuceneで使う場合は、手元のバージョンで索引を作って確かめてください。

埋め込みモデルが正規化済みの出力を前提にしているなら、cosinesimilかinnerproductです。Faissでcosinesimilを使うと、ベクトルは索引時に単位長へ正規化されます。保存される値が入力と変わる点には注意が要ります。すでに正規化済みなら、正規化を自分で管理できるinnerproductのほうが明示的です。

l2はユークリッド距離の2乗で、平方根を取りません。たとえば[0, 0]と[3, 4]の距離は25で、スコアは1/(1+25)の約0.0385です。順位はユークリッド距離と変わらないので、スコアの絶対値を閾値に使うときだけ気にすれば足ります。

cosinesimilには、ゼロベクトルを渡せないという制約があります。大きさが0のベクトルでは分母が0になるため、リクエストごと拒否されます。

メモリ見積りはClaude Codeに式ごと計算させる

Faissの索引は、JVMヒープの外にあるネイティブメモリへ載ります。載せられる量は、ヒープを除いた残りのRAMにknn.memory.circuit_breaker.limitの割合を掛けた値で、既定は50%です。

見積りの式は次のとおりです。

  • HNSW: 1.1 * (4 * dimension + 8 * m) bytes/vector
  • IVF: 1.1 * (((4 * dimension) * num_vectors) + (4 * nlist * d)) bytes

レプリカを置くと、ベクトルの総数は倍になります。計算はClaude Codeに任せられますが、式と入力値を併記させます。

awk 'BEGIN { n=1000000; d=768; m=16;
  printf "%.2f GB\n", 1.1*(4*d+8*m)*n/1e9 }'

次元768、m=16、100万件なら約3.52GBです。500万件なら17.6GBになります。RAM 64GB、ヒープ31GBのノードなら、残り33GBの50%で上限は16.5GBです。500万件を1ノードに載せると、この計算では上限を超えます。シャードを複数のノードへ分けるか、量子化を使うか、ノードを増やす判断が必要になります。これは上の式にこの条件を代入した計算例で、実測値ではありません。

メモリを減らす手段は、マッピングのmodeとcompression_levelです。modeはin_memory(低遅延を優先)とon_disk(低コストを優先)の2択で、compression_levelは1x、2x、4x、8x、16x、32xから選べます。Faissのエンコーダなら、fp16で約半分、byte量子化で4分の1が目安として示されています。圧縮は検索精度とのトレードオフなので、再現率を測ってから採用します。

索引を作って投入し、強制マージで一本化する

マッピングは次のようになります。Claude Codeに書かせた場合も、index.knnがtrueになっているかを最初に見ます。ANN検索の有効化は索引作成時にしか決められず、あとから既存の索引で有効にはできません。やり直すときは、index.knn: trueの新しい索引へreindexします。

{
  "settings": { "index": { "knn": true } },
  "mappings": {
    "properties": {
      "embedding": {
        "type": "knn_vector",
        "dimension": 768,
        "space_type": "cosinesimil",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "parameters": { "ef_construction": 100, "m": 16 }
        }
      }
    }
  }
}

大量投入の前には、次の3点を整えます。

  • refresh_intervalを-1にして小さなセグメントを増やさない(終わったら必ず戻す)
  • レプリカを0にして、プライマリとレプリカでネイティブ索引を二重に作らない
  • 複数コアならknn.algo_param.index_thread_qtyでスレッド数を増やす(CPU使用率を見ながら調整)

レプリカを0にすると、ノード障害でデータを失うおそれがあります。投入元のデータが別の場所にあり、やり直せることが条件です。

最初の一括投入だけで、以降は検索専用という索引なら、ベクトル構造の構築を後回しにする方法もあります。index.knn.advanced.approximate_thresholdを-1にして投入し、終わったら0に戻してからmax_num_segments=1で強制マージします。投入中の検索は完全探索になります。0に戻さずにマージすると、構造が作られずreindexが必要になります。単一セグメントのシャードは強制マージで書き直されないため、その場合もreindexです。この手順は上級者向けと位置づけられています。

落とし穴

強制マージを忘れると、検索が遅いままになる

セグメントがシャードごとに1つのとき、検索遅延は最も小さくなります。投入後にセグメントが多数残っていると、各セグメントを順に検索するため遅くなります。Claude Codeに「投入が終わったらセグメント数を確認する」と指示し、マージ後にウォームアップへ進む順序を守らせてください。

ウォームアップは、マージと書き込みが止まってから実行する

ネイティブ索引は、索引時に作られても、メモリへ載るのは最初の検索のときです。最初のクエリは数秒かかり、以降はミリ秒で返ります。この初回の遅さを避けるために使うのが、ウォームアップAPIです。

./scripts/os.sh GET "/_plugins/_knn/warmup/my-index?pretty"

応答には、試みたシャード数のtotal、成功数のsuccessful、失敗数のfailedが入ります。API呼び出しは、ウォームアップが終わるかタイムアウトするまで戻りません。タイムアウトしても処理はクラスタ上で続くので、GET /_tasksで追えます。同じ索引に何度実行しても、すでにメモリにあるものは再読み込みされません。

守る点は3つです。

  • ウォームアップ中の索引に、マージをかけない。マージで新しいセグメントができると、読み込んだ分が使えなくなる
  • 載せたい索引が、ネイティブメモリに収まることを先に確認する。収まらないと、キャッシュの追い出しと再読み込みが繰り返される
  • ウォームアップしたい文書を、そのあとに書き込まない。書き込みで増えたセグメントは再実行が必要になる

CLAUDE.mdの手順に「failedが0であること」を入れたのは、このためです。0でなければ、続く_statsの確認に進まず、原因の調査に切り替えます。

サーキットブレーカーは設定と統計の両方で見る

ネイティブメモリの上限に達したとき、k-NNプラグインは最近使われていないネイティブ索引をキャッシュから外します。これを制御するのが、次のクラスタ設定です。

設定既定値役割
knn.memory.circuit_breaker.enabled既定値true役割サーキットブレーカーの有効化
knn.memory.circuit_breaker.limit既定値50%役割ネイティブ索引に使えるメモリの上限
knn.circuit_breaker.unset.percentage既定値75役割使用量がこの割合を下回ると解除される閾値
knn.cache.item.expiry.enabled既定値false役割一定時間アクセスのない索引をメモリから外す
knn.cache.item.expiry.minutes既定値3h役割上のアイドル時間(有効時のみ)

上限はノード単位に変えられます。opensearch.ymlにnode.attr.knn_cb_tierを書き、knn.memory.circuit_breaker.limit.<tier名>を設定すると、そのノードだけ別の割合になります。ノードの構成が揃っていないクラスタで役に立ちます。

設定を変えるPUTは、先に挙げたとおりaskに入れてあるため、実行前に必ず確認が入ります。値を決めるのはあくまで人です。

状態は統計APIで読みます。

./scripts/os.sh GET \
  "/_plugins/_knn/stats/circuit_breaker_triggered,graph_memory_usage,eviction_count,cache_capacity_reached?pretty"

読むべき項目は次のとおりです。

  • circuit_breaker_triggered: ブレーカーが作動しているか。trueならメモリの余裕が小さい
  • graph_memory_usage と graph_memory_usage_percentage: ネイティブ索引が使っているメモリ(キロバイト)と、上限に対する割合
  • eviction_count: メモリ制約やアイドルで追い出された索引の数。ウォームアップ後に増え続けるなら、載りきっていない
  • cache_capacity_reached: 上限に達したか
  • hit_count と miss_count: 載っている索引へのクエリか、読み込みが必要だったクエリかの比率

見積りが合っているかの判断は、graph_memory_usageを先ほどの計算結果と突き合わせる形で行います。大きくずれていれば、dimensionやmの入力ミスか、レプリカの数え漏れが疑えます。ここまでの出力をClaude Codeに貼らせ、式の値との差を一行で書かせると、報告が揃います。

統計は項目名で絞ると出力が短く済みます。なお、OpenSearch用のMCPサーバーに切り替える場合は、Claude CodeのMCP出力が標準で25,000トークンまでに制限され、10,000トークンを超えると警告が出る点も頭に置いてください。

まとめ

OpenSearchのk-NN検索で後から直せないのは、index.knn、engine、space_type、dimensionです。ここは人が決め、Claude Codeには表にさせて根拠を添えさせます。運用に入ってからは、メモリ見積り、ウォームアップのfailed、サーキットブレーカーの統計の3つを、CLAUDE.mdの手順どおりに報告させるだけで、見落としはかなり減ります。

メモリが足りないと分かった時点の選択肢は、量子化、on_diskモード、シャードの分散の3つです。どれを取るかは、再現率の測定結果を見てから決めます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.opensearch.org/latest/mappings/supported-field-types/knn-methods-engines
  2. docs.opensearch.org/latest/mappings/supported-field-types/knn-spaces
  3. docs.opensearch.org/latest/vector-search/api/knn
  4. docs.opensearch.org/latest/vector-search/settings
  5. docs.opensearch.org/latest/vector-search/performance-tuning-indexing
  6. docs.opensearch.org/latest/vector-search/performance-tuning-search
  7. code.claude.com/docs/en/permissions
  8. code.claude.com/docs/en/mcp
この記事を共有:XはてブLinkedIn