高次元ベクトル索引:HNSW と IVF-PQ の内部
数百万件の埋め込みから類似ベクトルを数ミリ秒で引ける近似最近傍探索の中身が分かります。HNSW のグラフ探索と IVF-PQ の量子化を、距離計算量とメモリ消費の観点から原理で押さえられます。
- 1.高次元では全件との距離計算(厳密最近傍)が現実的でなく、わずかな取りこぼしを許して高速化する近似最近傍探索(ANN)を使う。評価軸は recall(取りこぼしの少なさ)・QPS(スループット)・メモリの三つ巴。
- 2.HNSW は多層の近傍グラフを貪欲に辿る方式。探索は概ね O(log N) のノード訪問で済み高 recall・高速だが、グラフの隣接リストを全ベクトルぶん持つためメモリ消費が大きい。
- 3.IVF-PQ はベクトルを粗いクラスタに割り当て(転置)、各ベクトルを直積量子化で数バイトのコードに圧縮する。メモリを1〜2桁削れる代わりに距離が近似化し、探索する nprobe を増やすほど recall は上がるが遅くなる。
なぜ高次元では「全件距離計算」が破綻するのか
埋め込み(embedding)は、文章や画像を 768 次元や 1536 次元といった実数ベクトルへ写したものです。「意味が近い」を「ベクトルが近い」に対応させるので、検索とは「クエリベクトルに最も近い上位 k 件を返す」操作、すなわち 最近傍探索(nearest neighbor search) になります。
素朴にやるなら全 N 件と距離を計算して上位 k を取ります。これが 厳密最近傍(exact kNN) で、計算量は次元 d・件数 N に対して O(N * d)。N が数百万、d が四桁になると 1 クエリで数十億回の積和になり、実時間では返せません。
ではツリーで枝刈りすればよいかというと、ここで 次元の呪い(curse of dimensionality) が立ちはだかります。高次元では任意の2点間の距離が互いに似通い、空間分割(k-d 木など)の枝刈りがほとんど効きません。結局ほぼ全葉を見ることになり、線形走査と大差なくなります。
そこで実務では 近似最近傍探索(ANN: Approximate Nearest Neighbor) を使います。「ごく稀に真の最近傍を取りこぼす」ことを許す代わりに、桁違いの高速化を得る方針です。品質は次の三つの綱引きで測ります。
| 評価軸 | 意味 | 上げると犠牲になるもの |
|---|---|---|
| recall@k | 真の上位 k 件のうち実際に返せた割合(取りこぼしの少なさ) | QPS(探索を広げるほど遅い) |
| QPS / レイテンシ | 1秒あたり処理クエリ数・1件あたり応答時間 | recall(探索を絞ると取りこぼす) |
| メモリ消費 | 索引が常駐する RAM 量 | recall・精度(圧縮するほど距離が荒い) |
ANN の二大方式が、グラフを辿る HNSW と、クラスタ+圧縮の IVF-PQ です。前者は速度と recall、後者はメモリを軸に設計されています。
HNSW:階層型近傍グラフを貪欲に降りる
HNSW(Hierarchical Navigable Small World) は、ベクトルをノードとし「近いもの同士」を辺で結んだ 近傍グラフ を探索する方式です。鍵は、その近傍グラフを 多層 に積むことにあります。
- 最下層(layer 0)は全ベクトルを含む密なグラフ。
- 上の層ほど確率的に間引かれ、ノードが疎になる。各ノードが属する最上層は指数分布で決まる。
- 上層の辺は遠くまで一気に飛べる「高速道路」、下層の辺は近場を細かく結ぶ「一般道」に相当する。
探索はスキップリストのアイデアを多次元へ拡張したものです。最上層の入口から始め、各層で 貪欲法(greedy) によりクエリに最も近づく隣へ移動し、それ以上近づけなくなったら一つ下の層へ降ります。最下層では、訪問候補を保持する優先度付きキューの幅 efSearch を使ったビーム探索で上位 k を絞り込みます。
HNSW 探索(概念)
entry ← 最上層の入口ノード
for layer = Lmax down to 1:
entry ← layer 上で entry から貪欲に「クエリへ最も近いノード」へ移動
layer 0 で efSearch 幅のビーム探索 → 上位 k を返す
上層で大きく近づき、下層で精密化するため、訪問ノード数は概ね O(log N) に収まります。内訳は、上層の貪欲降下が層数ぶんで概ね O(log N) ノード、最下層のビーム探索が概ね O(efSearch) ノードで、両者は足し算(積ではない)になります。1ノードを評価するたびに d 次元の距離を計算するので、1クエリの計算量はおおよそ (log N + efSearch) * d 相当。efSearch を上げるとビームが広がり recall は上がりますが、距離計算が増えて遅くなります。これが HNSW における recall と QPS のつまみです。
| パラメータ | 効果 | 上げ過ぎの副作用 |
|---|---|---|
| M(1ノードの最大次数) | 辺を増やしグラフ接続性と recall 向上 | メモリ増・構築遅延(隣接リストが太る) |
| efConstruction(構築時の探索幅) | 良い隣を選びグラフ品質を上げる | 構築時間の増大 |
| efSearch(探索時のビーム幅) | recall 向上 | 1クエリの距離計算が増えレイテンシ増 |
HNSW は元ベクトルに加えて 各ノードの隣接リスト(ノードあたり最大 M 本、層ぶん) を保持します。これがベクトル本体に上乗せされるため、メモリ消費が大きいのが最大の弱点です。1536 次元 float32(約 6KB/件)に M=16 規模のグラフを足すと、数百万件で容易に数十 GB に達します。また辺の整合性を保ちながらの大量削除・更新は不得手で、削除はトゥームストーン+定期再構築で扱うのが一般的です。
IVF:粗い量子化で探索範囲を切り出す
メモリと探索範囲を抑える別系統が、転置(IVF: Inverted File) です。発想は 全文検索 の転置インデックスや、データを区画へ割り当てる パーティショニング と同じく「全部見ない」ことにあります。
まず学習段階で全ベクトルを k-means で nlist 個のクラスタ(セルと呼ぶ)に分け、各セルの重心(セントロイド)を求めます。各ベクトルは最も近い重心のリスト(転置リスト)に登録されます。これが 粗い量子化(coarse quantization) です。
検索時はクエリと nlist 個の重心だけ距離を取り、近いほうから nprobe 個のセルを選んで、そのセル内のベクトルとだけ距離を計算します。走査対象が N * (nprobe / nlist) に縮むのが効きどころです。
nprobeを増やす → 多くのセルを見るので recall ↑、ただし走査件数が増えて遅い。- セル境界をまたぐ近傍を取りこぼすのが IVF の典型的な誤りで、nprobe はその救済つまみ。
ただし IVF 単体ではセル内を厳密距離で走査するため、元ベクトルをメモリに持つ必要が残ります。メモリを本気で削るには、次の直積量子化と組み合わせます。
PQ:直積量子化でベクトルを数バイトに圧縮する
直積量子化(PQ: Product Quantization) は、ベクトルを少数のバイトへ非可逆圧縮しつつ距離を近似計算できるようにする手法です。手順はこうです。
- d 次元ベクトルを m 個の サブベクトル(各 d/m 次元)に分割する。
- 各サブ空間ごとに、学習データから k-means で
2^nbits個(通常 nbits=8 なので 256 個)の代表点からなる コードブック を作る。 - 各サブベクトルを最も近い代表点の番号(1 バイト)に置き換える。ベクトル全体は m バイトのコードになる。
1536 次元 float32 は約 6144 バイトですが、m=64・nbits=8 なら 64 バイト に圧縮されます。約 96 倍の削減です。これがメモリを1〜2桁減らせる理由です。
距離は 非対称距離計算(ADC: Asymmetric Distance Computation) で近似します。クエリは圧縮せず、クエリの各サブベクトルと各コードブック代表点との距離をあらかじめ表(ルックアップテーブル)に焼いておき、各データ点についてはコード番号で表を引いて m 個を足すだけにします。
ADC(クエリ q と PQ コード c の距離近似)
前処理: 各サブ空間 j について
tbl[j][i] = dist( q_j , codebook[j][i] ) ← 256 通りを先に計算
各データ点 c=(c_1,...,c_m) について
dist ≈ Σ_j tbl[j][c_j] ← 表引き m 回の足し算のみ
1点の距離評価が「d 回の積和」から「m 回の表引き加算」に変わり、m は d よりずっと小さい(例 64 対 1536)ので、距離計算そのものも軽くなります。代償は、コードブックの代表点で量子化したぶん 距離が近似値になり recall の上限が下がる ことです。m や nbits を増やせば精度は戻りますが、コードが太りメモリと計算が増えます。なお重心からの残差を量子化する IVFADC(IVF と PQ の標準的な組み合わせ)にすると、同じ m バイトでも精度が上がります。
両者は組み合わせられます。代表が IVF を HNSW で引く 構成で、nlist が大きいときに「クエリに近い重心」をグラフ探索で高速に見つけます。さらに各セル内を PQ コードで走査すれば、メモリ削減(PQ)・探索範囲の限定(IVF)・重心探索の高速化(HNSW)を一度に得られます。Faiss の IVF65536_HNSW32,PQ64 のような索引文字列はこの三段重ねを表します。
二方式の使い分け
| 観点 | HNSW | IVF-PQ |
|---|---|---|
| 探索の仕組み | 多層近傍グラフを貪欲+ビームで辿る | 近い nprobe 個のセル内を量子化距離で走査 |
| 1クエリの距離計算 | 概ね (log N + efSearch) 回(全次元) | 重心 nlist 回+セル内件数×表引き(軽い) |
| recall を上げるつまみ | efSearch(探索幅) | nprobe(探索セル数)・m/nbits(量子化精度) |
| メモリ | 大(元ベクトル+隣接リスト) | 小(PQ コードのみ常駐も可) |
| 得意 | 高 recall・低レイテンシを最優先 | 巨大データを限られた RAM で扱う |
| 苦手 | メモリ大・大量更新 | 高 recall を出すと走査が増え遅くなりやすい |
判断の軸は単純です。RAM に元ベクトルを載せきれて、とにかく速く高 recall が欲しいなら HNSW。件数が RAM を圧迫し、多少の精度低下を許してでもメモリを削りたいなら IVF-PQ。中間として、HNSW のグラフ上のベクトルを PQ で圧縮する HNSWPQ もあり、グラフの隣接リストは残しつつベクトル本体だけ縮めます。
コサイン類似度で検索したいなら、ベクトルを L2 正規化したうえで内積(あるいはユークリッド距離)で索引を作ります。正規化済みベクトル同士なら内積の大小がコサインの大小と一致するためです。索引の距離計量とクエリ前処理がずれると、recall が静かに落ちるので注意します。
厳密 kNN が O(N×d) で高次元では枝刈りも効かないため ANN を使うこと、評価が recall・QPS・メモリの三つ巴であること。HNSW は 多層近傍グラフを貪欲探索し概ね O(log N) 訪問で速いが 隣接リストでメモリを食う。IVF-PQ は 粗い量子化で探索範囲を絞り(nprobe)、直積量子化でベクトルを m バイトに圧縮(ADC で表引き距離)してメモリを削るが距離が近似化する、という対比を説明できるように。
まとめ
高次元の類似検索では厳密最近傍が O(N*d) で破綻し、枝刈りも次元の呪いで効かないため、取りこぼしを許す近似最近傍探索(ANN)を使います。評価は recall・QPS・メモリの三つ巴。HNSW は多層の近傍グラフを貪欲+ビーム探索で辿り、概ね O(log N) 訪問で高速・高 recall を出す代わりに、各ノードの隣接リストぶんメモリを食います。IVF-PQ は粗い量子化でクエリ近傍の nprobe 個のセルだけを走査し、直積量子化 で各ベクトルを数バイトのコードに圧縮(ADC の表引きで距離近似)してメモリを1〜2桁削る代わりに、距離が近似化します。RAM に余裕があり高 recall を最優先なら HNSW、巨大データをメモリ節約で捌くなら IVF-PQ、というのが基本の使い分けです。同じ「全部は見ない」発想は 全文検索 の転置や パーティショニング、近似で省メモリを得る ブルームフィルタ とも地続きです。
データベース Article
高次元ベクトル索引:HNSW と IVF-PQ の内部を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ベクトル検索
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
HNSW は多層の近傍グラフを貪欲に辿る方式。探索は概ね O(log N) のノード訪問で済み高 recall・高速だが、グラフの隣接リストを全ベクトルぶん持つためメモリ消費が大きい。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ベクトル検索 / 近似最近傍」に近いか確認する。
- 強みである「高次元では全件との距離計算(厳密最近傍)が現実的でなく、わずかな取りこぼしを許して高速化する近似最近傍探索(ANN)を使う。評価軸は recall(取りこぼしの少なさ)・QPS(スループット)・メモリの三つ巴。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。