データベース

Elasticsearchのベクトル検索|dense_vectorの設計とkNN実装、RAG基盤の採用条件

Elasticsearchのベクトル検索|dense_vectorの設計とkNN実装、RAG基盤の採用条件

Elasticsearchでベクトル検索を動かすだけなら、フィールドを一つ足してkNNクエリを投げれば済みます。検証データ千件なら半日で着きます。詰まるのはその先です。本番のデータ量でヒープが足りなくなる、8系の記事を写した設定が9系の既定と食い違う、量子化した索引の精度が戻せない。いずれもフィールド定義と探索範囲の値を決めないまま書き始めたことが原因です。9.5系(2026年8月時点)を前提に、決めるべき値と、その値がどこに効くかを順に見ていきます。

まとめ:ベクトル検索を載せる前に決める6つの値

ベクトル検索の設計とは、次の6点を決める作業です。ここが決まれば残りは公式リファレンスの引き写しで足ります。決めずに動かすと、データ量が増えた段階で索引ごと作り直しになります。

決める値 指定する場所 決め方の基準
次元数 dims 埋め込みモデルの出力に一致
数値の持ち方 element_type 既定はfloat・4バイト
距離の測り方 similarity 既定はcosine
量子化の方式 index_options 版で既定が変わる
探索の広さ num_candidates 既定はkの1.5倍
埋め込みの生成元 semantic_textか外部 言語と更新頻度で決める

このうち後から変えられないのがdimsとelement_typeで、変更にはリインデックスが要ります。num_candidatesはクエリ側の値なので運用中に振れる。設計の重心は前半3つです。ベクトル検索とセマンティック検索という語の関係はベクトル検索とセマンティック検索の違いで整理しているため、本記事は製品の設定だけを扱います。

フィールド設計|dense_vectorの四つの値で検索の性質が決まる

ベクトルを入れる器がdense_vectorです。指定できる値は多くありませんが、それぞれが精度・速度・メモリのどれかに直結します。Elasticsearchの索引構造とシャード設計を前提に、ベクトル固有の項目だけを見ます。

element_typeとdims|4096の上限と埋め込みモデル側の制約

element_typeは1次元あたりの持ち方です。既定のfloatが4バイト、bfloat16が2バイト、byteが1バイト、bitが1ビット。単純に、選んだ型がそのまま索引サイズの係数になります。

dimsの上限は4096です。text-embedding-3-largeの3072次元やCohereの1024次元は収まりますが、上限に近い次元数を選ぶ意味は薄いと考えます。公式のチューニング指針は「kNN検索の速度は次元数に線形で比例する」と述べていて、次元を半分にすれば距離計算も半分になるからです。モデル側で切り詰められるなら先に検討します。

similarityの選び方|cosineが既定でbitはl2_norm固定

距離の測り方はl2_norm、dot_product、cosine、max_inner_productの4種類です。既定はcosineで、element_typeがbitのときだけl2_normが既定になり、しかもbitはl2_normしか受け付けません。

選び方の基準は単純で、埋め込みモデルの提供元が推奨する指標に合わせます。多くの文章埋め込みモデルはコサイン類似度を前提に学習されているため、既定のままで外れません。ベクトルを事前に正規化してあるならdot_productのほうが計算は軽くなります。max_inner_productは正規化していないベクトルで内積の大小そのものを見たい場合の選択肢です。

ここで迷って時間を使う価値はありません。モデルカードの記載に従い、評価セットで差が出たときだけ振り直します。dimsやelement_typeを含むフィールド定義そのものの決め方はElasticsearchのマッピング設計で扱っています。

index_optionsの既定|9.4系でbbq_diskへ移った量子化

索引の作り方を決めるのがindex_optionsで、指定できるtypeはhnsw、flat、int8_hnsw、int8_flat、int4_hnsw、int4_flat、bbq_hnsw、bbq_flat、bbq_diskです。末尾がhnswなら近似グラフを張り、flatなら総当たりになります。

問題は既定が版で三度変わっていることです。9.0はint8_hnsw。9.1から9.3は384次元以上でbbq_hnsw、384次元未満でint8_hnsw。9.4以降とServerlessは、現行のライセンスで使える場合にfloatとbfloat16でbbq_diskが既定になりました。byteとbitだけは全版で量子化なしのhnswのままです。

8系の記事を写してint8_hnswを明示した設定例は今も多く流通しています。それ自体は動きますが、同じ次元数のフィールドが索引ごとに違う量子化で作られる状態を生む。方式を揃えるなら明示指定、版の推奨に追従するなら省略と、方針を先に決めておきます。

PUT /docs
{
  "mappings": {
    "properties": {
      "content_vector": {
        "type": "dense_vector",
        "dims": 1024,
        "similarity": "cosine",
        "index_options": { "type": "bbq_hnsw" }
      }
    }
  }
}

kNN検索の実装|kとnum_candidatesが再現率を決める仕組み

フィールドができたら検索側です。指定する値は3つか4つですが、既定のまま投げると「取りこぼしているのに気づかない」状態になりやすい箇所です。クエリ本体の書き方や結果集合の制御はElasticsearchのクエリDSLで扱うため、ここではkNN固有の値に絞ります。

近似kNNと厳密kNN|script_scoreを選んでよいデータ量

Elasticsearchのベクトル検索には二つの経路があります。近似kNNはHNSWまたはDiskBBQのグラフを辿って候補を絞る方式で、本番の既定路線です。厳密kNNはscript_scoreでベクトル関数を評価する方式で、公式は「正確な結果を保証するが大規模データにはスケールしない」と明記しています。一致した全ドキュメントを1件ずつ計算するためです。

使い分けの目安はフィルタ後の候補件数です。テナントIDや日付で数千件まで絞った先の検索なら、script_scoreのほうが構成は単純で、グラフのメモリも要りません。数十万件以上を素で走査させる用途では選びません。

num_candidatesの既定と上限|1.5倍と1万件で切れる探索

kは各シャードから返す近傍数で、既定は検索リクエストのsizeです。num_candidatesはシャードごとに集める候補数で、既定はkを指定していれば1.5倍、指定していなければsizeの1.5倍になります。上限は10,000です。

この既定が曲者です。sizeを10にしただけのクエリは、シャードあたり15件しか候補を見ずに近傍を決めています。近似探索は候補を広げるほど正解に近づくため、再現率が要る用途では明示的に引き上げます。実務ではkの5倍から10倍あたりから始めて、評価セットのHit Rateが頭打ちになる点で止めるのが手早い決め方です。

kはnum_candidates以下でなければエラーになります。上限の10,000は設定で緩められる値ではありません。

filter付きkNN|事前フィルタがk件を保証する仕組みと代償

knnクエリのfilterは、近似探索の実行中に適用されます。公式は「k件の一致文書が返ることを保証するために探索中に適用される」と説明し、探索後に絞る後置フィルタは「k件未満になりうる」と対比しています。件数が足りなくなる事故を避けたいならfilterに置く、が基本です。

代償は速度です。フィルタで弾かれるノードが多いほどグラフを深く辿る必要が出て、探索コストが上がります。極端に選択率の低い条件、たとえば全体の0.1%しか通らないフィルタをkNNに掛けるくらいなら、その条件で先に絞ってからscript_scoreの厳密検索に切り替えたほうが速く終わります。

similarityパラメータも併用できます。これは一致とみなす生の類似度の下限で、「近いものが無ければ0件でよい」という要件に効く指定です。件数を埋めた検索結果は、RAGでは雑音をそのままLLMへ渡します。

POST /docs/_search
{
  "knn": {
    "field": "content_vector",
    "query_vector": [0.031, -0.114, 0.402],
    "k": 10,
    "num_candidates": 100,
    "filter": { "term": { "tenant_id": "t-0012" } },
    "rescore_vector": { "oversample": 2.0 }
  },
  "size": 10
}

rescore_vector|量子化で落ちた精度をoversampleで戻す

量子化は容量と速度のために精度を削る仕組みです。bbqなら1次元あたり1ビットまで潰すため、そのままでは順位が荒くなります。これを戻すのがrescore_vectorで、oversampleに指定した倍率だけ多く候補を取り、元のベクトルで計算し直してから上位k件を返します。

指定できる値は1.0以上、または0です。量子化したフィールドにだけ効き、非量子化のフィールドでは無視されます。num_candidatesとの関係はmax(num_candidates, k * oversample)で、実際に集める候補数は大きいほうが採られます。

9.4以降の既定がbbq_diskになったということは、明示指定していない索引はすでに量子化されているということです。「9系にしたら精度が落ちた」という相談の多くは、この再スコアリングを入れていないだけで説明が付きます。まずoversampleを2.0前後で入れ、それでも足りなければ量子化方式のほうをint8_hnswへ戻す順で調べます。

埋め込みの生成経路|semantic_textと外部モデルの二択

ベクトルを誰が作るかは、索引設計と同じ重みを持つ判断です。Elasticsearchの中で完結させるか、アプリケーション側で作って投げ込むか。運用の手間と、モデル差し替えのしやすさが逆向きに動きます。

semantic_textに寄せる範囲|チャンク分割まで任せる条件

semantic_textはテキスト専用のフィールド型で、索引マッピングの設定、投入時の埋め込み生成、チャンク分割の3つを自動で引き受けます。dense_vectorを自分で定義して取り込みパイプラインを組む構成に比べ、書く量が目に見えて減ります。

分割の粒度はchunking_settingsで変えられます。公式の例に挙がるのはstrategyがword、max_chunk_sizeが120、overlapが40という値です。日本語では語数基準の分割が段落の切れ目と合わないことがあるため、実データで分割結果を見てから決めます。

寄せてよいのは、文書の更新経路がElasticsearchへの投入に一本化できている場合です。複数のシステムが同じベクトルを共有するなら、生成はアプリ側に置きます。

日本語で既定エンドポイントを選ぶ基準|ELSERが英語限定である理由

inference_idを省略すると既定の推論エンドポイントが適用されます。ここに落とし穴がある。.elser-2-elasticsearchはELSERを展開するエンドポイントで、公式は「英語テキストに推奨」と明記しています。日本語のコーパスに当てる前提の既定ではありません。

非英語テキスト向けとして案内されているのは.multilingual-e5-small-elasticsearchです。日本語の社内文書を対象にするなら、既定に任せずinference_idを明示します。公式は、版を上げたときに新しく作った索引だけ別の埋め込みモデルになりうるとも注意しています。省略は「今の推奨に追従する」という選択であって、「何も決めない」ではありません。

検索時だけ別のモデルを使う場合はsearch_inference_idを指定します。省略時はinference_idにフォールバックします。

外部APIで生成する場合|inference経由とアプリ側の分岐

外部モデルを使う経路も二つに分かれます。ひとつはInference APIにエンドポイントを登録し、Elasticsearchから呼ばせる形で、Amazon Bedrock、Azure AI Studio、Cohere、Google AI、Mistral、OpenAI、Hugging Faceなどが接続先に並びます。もうひとつはアプリケーション側で埋め込みを作り、配列として投入する形です。

前者は投入処理が短くなる代わり、検索クラスタが外部APIの停止やレート制限の影響を直接受けます。後者は再試行やキャッシュを自前で設計できるぶん、大量の初期ロードで制御が効く。数百万件の初回投入を伴う案件では、投入ジョブ側に流量制御を持たせる構成を採ります。

どちらの経路でも、検索時のクエリベクトルは索引時と同じモデルで作る必要があります。モデルを変えるときは索引とクエリ生成を同時に切り替えます。片側だけ差し替えると、エラーは出ないまま検索結果だけが崩れる。

BM25との併用|rrf retrieverで束ねる範囲と委譲する境界

ベクトル検索だけの構成は、型番や社内用語の検索で穴が出ます。Elasticsearchは全文検索エンジンなので、同じ索引でBM25と併走させられるのが強みです。ただし束ね方を誤ると単独より落ちます。

rrf retrieverの構成|rank_window_sizeが既定10で切れる

Elasticsearchではretriever構文のrrfで、通常のクエリとkNNの結果を順位ベースで統合します。定数rank_constantの既定は60で、変更できます。

実装で最初に外すのがrank_window_sizeです。既定は10で、各サブクエリの上位10件しかRRFの対象になりません。sizeを10にしていると、統合の材料が10件ずつしか無い状態で並べ替えていることになります。実運用では50から100へ広げます。

スコア統合の方式そのもの、つまりRRFと正規化+加重平均のどちらを選ぶか、定数をいくつにするかは製品をまたぐ論点です。BM25とベクトル検索を統合するハイブリッド検索の設計に計算式と他製品の実装まで整理してあるため、そちらへ委譲します。

BM25を併用しない判断|日本語の解析器を作り込めない場合の帰結

併用は常に得ではありません。前掲の記事に載せた公開実験では、ベクトル単独のHit Rate@10が0.8355だったのに対し、形態素解析だけ入れた既定設定のBM25を足すと0.7819へ下がっています。ランク統合は弱いほうのランキングもそのまま混ぜるからです。

だから判断は明快です。kuromojiやSudachiの解析器、同義語辞書、正規化を誰かが保守できないなら、ハイブリッド化は見送ります。ベクトル単独のまま、フィルタと再スコアリングで精度を詰めるほうが工数に見合う。型番検索の要件が仕様に入っているなら、全文検索側の作り込みを工数として先に積んでおきます。

メモリ設計|ベクトルがページキャッシュに載るかで決まる検索速度

ベクトル検索の速度は、索引の作りよりも「ベクトルデータがファイルシステムキャッシュに載っているか」で決まります。ここは設計時に計算できるので、動かしてから驚く必要はありません。

見積り式|量子化の方式別バイト数と100万件・1024次元の例

公式のチューニング指針が方式ごとの必要バイト数を示しています。件数と次元数を入れるだけで、必要なメモリは着工前に出ます。

型と量子化 必要バイト数
float(量子化なし) 件数 * 次元数 * 4
float + int8 件数 * (次元数 + 4)
float + int4 件数 * (次元数/2 + 4)
float + bbq 件数 * (次元数/8 + 14)
byte 件数 * 次元数
bit 件数 * (次元数/8)

公式が挙げる具体例は、100万件・1024次元の非量子化floatをHNSWで持つ場合で、グラフ分と合わせて4.16GBという計算です。同じ条件をbbqにすると1件あたり142バイト、合計142MB前後まで落ちます。量子化の既定が版を追うごとに強い方式へ寄っているのは、この差が効くからです。

この容量はJVMヒープではなくOSのページキャッシュに載せる前提の数字です。ヒープとキャッシュの配分そのものはElasticsearchのメモリ配分とシャード設計で扱っています。

_sourceからの除外|9.2以降のexclude_source_vectors設定

ベクトルは_sourceにも生の配列として入ります。1024次元の配列がJSONで文書ごとに保存されるため、検索応答の組み立てで無視できない負荷になります。

9.2以降で案内されているのがindex.mapping.exclude_source_vectorsという索引設定です。汎用のexcludes指定と違い、必要なときに自動で復元される仕組みが付きます。合成_sourceを使う手もありますが、まずはこの設定を見ます。

RAG検索基盤として採用する条件と、専用ベクトルDBへ寄せる場面

Elasticsearchをベクトル検索の基盤に選ぶべきかどうかは、ベクトルの性能比較ではなく、周辺の要件がどれだけ同居するかで決まります。

採用条件|既存の全文検索と同居させてよい三つの前提と確認手順

次の3点が揃うなら、専用のベクトルDBを別に立てるより有利だと判断します。

  • すでにElasticsearchが動いており、運用と監視の体制がある
  • キーワード一致での検索要件が仕様に含まれている
  • テナントや権限、日付での絞り込みが検索条件として必須

3点目が効きます。事前フィルタ付きのkNNを同じクエリで書けることは、権限境界を持つ社内文書検索では設計の単純さに直結する。検索基盤を分けると、フィルタ条件の同期という別問題を抱え込みます。着工前に、フィルタ後の想定件数と前掲の式で出した必要メモリを、既存ノードの空き容量と突き合わせておきます。RAGの検索工程をどこまで自前で持つかの設計は、RAG構築支援で引き受けている範囲です。

見送る場面|ベクトルだけを大量に持つ用途で割に合わなくなる条件

逆に選ばないのは、全文検索の要件が一切なく、億単位のベクトルを持ち、更新頻度が高い用途です。この構成では、Elasticsearchが持つ転置索引や集計の機能が丸ごと遊びます。ノードのメモリは専用エンジンに割り当てたほうが素直に効きます。

もうひとつ見送るのは、埋め込みモデルを頻繁に差し替える計画がある場合です。dimsとelement_typeの変更にはリインデックスが要るため、モデル選定が固まらない探索段階では、切り替えの軽い構成で試してから移すほうが早く終わります。モデルが決まるまで本番索引は作らない、と決めておきます。

よくある質問

dense_vectorのdimsは後から変更できますか?

できません。dimsとelement_typeはフィールドの物理的な持ち方を決める値で、変更するには新しいマッピングで索引を作り直し、リインデックスする必要があります。埋め込みモデルの差し替えは次元数の変更を伴うことが多いため、モデル選定が固まってから本番索引を作る順番が安全です。

9系にしたらベクトル検索の精度が落ちたのはなぜですか?

量子化の既定が版で変わっているためです。9.0はint8_hnsw、9.1から9.3は384次元以上でbbq_hnsw、9.4以降はライセンス条件下でbbq_diskが既定になります。index_optionsを明示していない索引は、作り直したタイミングで別の方式になります。knnクエリにrescore_vectorのoversampleを2.0前後で加え、元のベクトルで並べ直してから差を測り直してください。

num_candidatesはどのくらいの値にすればよいですか?

既定はkの1.5倍、上限は10,000です。既定のままだとシャードあたり十数件しか候補を見ないため、再現率が要る用途ではkの5倍から10倍あたりを起点にします。値を上げるほど探索は遅くなるため、評価セットのHit Rateが頭打ちになる点で止めます。

semantic_textを使えば埋め込みモデルの用意は不要ですか?

inference_idを省略すれば既定のエンドポイントが適用されるため、用意なしでも動きます。ただし既定の.elser-2-elasticsearchは公式が英語テキスト向けと明記しているモデルです。日本語の文書が対象なら、非英語向けと案内されている.multilingual-e5-small-elasticsearchなどをinference_idに明示してください。

ベクトル検索とBM25は必ず併用したほうがよいですか?

いいえ。日本語の公開実験では、ベクトル単独のHit Rate@10が0.8355だったのに対し、既定設定のBM25を足した構成は0.7819へ下がっています。ランク統合は弱いほうのランキングも混ぜるためです。解析器や同義語辞書を保守できる体制が無いなら、ベクトル単独で運用するほうが精度は保てます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  3. 2026.07.15 コラム LINEミニアプリとは?未認証と認証済みの違い・LIFFとの使い分け・審査と開発費用【2026年版】
  4. 2026.07.23 コラム クラウド販売管理システムとは?オンプレとの違い・料金相場と5年総額の比べ方【2026年】
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次