ベクトルデータベースはどう動くのか――HNSW近似最近傍探索の仕組みからpgvector・Qdrant・Milvusの選び方まで
はじめに――RAG時代に「避けて通れない」ベクトルデータベース
ChatGPTに代表されるLLMの普及以降、RAG(Retrieval-Augmented Generation、検索拡張生成)は社内ナレッジ検索やドキュメントQ&Aといった実務で急速に広がりました。RAGの核心は「関連文書をベクトルの類似度で探し出してLLMのコンテキストに渡す」ことにあり、その検索基盤となるベクトルデータベースは、もはやAIアプリケーション開発においてほぼ必須のインフラになっています。
しかし、いざ導入しようと調べると、すぐに混乱します。PostgreSQLに後付けできるpgvector、Rust製の高速エンジンQdrant、Kubernetesネイティブで数十億ベクトルを捌くMilvus、研究開発の定番ライブラリFAISS、pip一発で試せる軽量なChroma――選択肢が多すぎて、「そもそもどういう基準で比較すればいいのか」「自分のユースケースにはどれが合うのか」が分からない、というのが多くのエンジニアの実感でしょう。
この混乱の背景には、仕組みへの理解が欠けているという問題があります。例として、OpenAIのtext-embedding-3-smallが生成するのは1536次元のベクトルですが、100万件の文書から類似文書を探す単純な全件計算では、たった1回の検索で約15億個の次元要素を比較する計算量に相当します(必要な乗算・加算などの演算回数は距離関数の定義により異なります)。ベクトルDBが中核で採用する近似最近傍探索(ANN)は、この計算量を劇的に減らすための技術であり、リコール・速度・メモリのトレードオフと引き換えに実用速度を実現します。この仕組みを知らなければ、製品を比較する軸すら定まらないのです。
そこで本記事は、その土台を一から築きます。まず埋め込みベクトルとコサイン類似度の基礎を確認し、次に、なぜ全件計算ではスケールしないのかを具体的な数字で示します。続いて、多くの製品が採用するANNの代表アルゴリズムであるHNSW(階層型小世界グラフ)の仕組みを読み解き、そのうえで主要5製品を設計思想から比較します。最後に、ユースケース別の選び方を提示します。基礎から比較、そして選定まで――この記事を通読すれば、比較・検討の土台が固まることが、本記事の約束です。なおデータベース選定の話として、以前本ブログで扱った時系列データベースの比較やSQLiteの耐久性の仕組みとも趣が違う、「意味の近さ」を扱う専用の世界です。
埋め込みベクトルと類似度検索の基礎――「意味の近さ」を数値化する仕組み
ベクトルデータベースを理解する第一歩は、埋め込み(embedding)の把握です。埋め込みとは、テキストや画像といったデータを、その意味的な特徴を反映した高次元の数値ベクトルへ変換する仕組みです。変換を担うのはニューラルネットベースのモデルで、モデルごとに出力ベクトルの次元は固定されます。意味が近いデータほどベクトル空間上で近い位置に配置されるため、コンピュータは「意味の近さ」を数値として計算できるようになります。RAG(検索拡張生成)――類似文書を検索してLLMのコンテキストに渡す構成――もこの仕組みの上に成り立っており、ベクトルDBはその検索を支える土台です。
では「近さ」はどう測るのでしょうか。代表的な尺度は次の3つです。コサイン類似度は2つのベクトルのなす角度に着目した尺度で、値域は [-1, 1] です。向きが同じなら1、真逆なら-1となり、ベクトルの長さの違いに影響されにくい点が特徴です。ユークリッド距離(L2距離)はベクトル空間上の直線距離そのもので、値が小さいほど近いことを示します。内積は向きと大きさの両方を反映したスコアです。なお「コサイン類似度の値域が [-1, 1]」「正規化されたベクトル同士では内積と一致する」という性質は一般論ですが、実務上重要です。事前にベクトルを正規化しておけば、内積の計算1回でコサイン類似度と同じ結果が得られるからです。
続いてベクトルの「次元」です。次元は埋め込みモデルごとに固定されており、代表例は次の表のとおりです。
| モデル | 次元 | 用途例 |
|---|---|---|
| all-MiniLM-L6-v2 | 384 | 軽量・高速なセマンティック検索(一般論) |
| BERT-base系 | 768 | 汎用的な文書処理(一般論) |
| text-embedding-ada-002 | 1536 | OpenAI旧世代モデル |
| text-embedding-3-small | 1536 | 低コストと性能のバランスに優れた現行モデル |
| text-embedding-3-large | 3072 | 高精度志向の現行モデル |
OpenAIの埋め込みガイドによれば、text-embedding-3-small / 3-large は dimensions パラメータで次元を短縮でき、最大入力は8,192トークンです。精度を測るMTEBスコアは3-smallが62.3%、3-largeが64.6%、ada-002は61.0%とされます(いずれも同ガイド掲載の値で、ベンチマーク結果は評価設定により変動します)。用途として同ガイドが挙げるのは、検索(Search)、クラスタリング(Clustering)、推薦(Recommendations)、異常検出(Anomaly detection)などです。次元が大きいほど表現力は高まる傾向がありますが、保存コストも計算コストも膨らみます。この「件数 × 次元」の重荷が、次節で見る近似探索の必要性へと直結します。
なぜ全件計算ではスケールしないのか――近似最近傍探索(ANN)のトレードオフ
埋め込みベクトルの検索で最も単純な方法は、保存済みの全ベクトルとクエリベクトルの距離を1件ずつ計算して上位を取るブルートフォース(全件計算)です。計算量はベクトル数Nと次元Dの積に比例する O(N×D) となり、この数字は規模の増加に対して容赦なく膨らみます。たとえば100万件×1536次元(OpenAIのtext-embedding-3-small相当)なら、1クエリあたり約15億個の次元要素を比較する計算量に達します(必要な乗算・加算の内訳は距離関数の定義に依存します)。同時に複数のクエリが届けばCPUはすぐに飽和し、実際のレイテンシはハードウェアや実装に強く依存しますが、規模の増加とともに実用に耐えない水準になり得ます。
さらに一般論として、高次元空間では「次元の呪い」と呼ばれる現象が知られています。次元Dが大きくなるほど1回の距離計算自体が重くなるうえ、点同士の距離が相対的に均質化してくるため、B木やKD木のような伝統的な索引構造の枝刈りが機能しなくなるのです。低次元の世界で有効だった「ソートして二分探索」といえた戦略は、そのままではベクトル検索に持ち込めません。
そこで登場するのが近似最近傍探索(ANN)です。ANNは「厳密な上位k件」ではなく「ほぼ正しい上位k件」を返すことを諦める代わりに速度を得る手法で、本質的なトレードオフは リコール(正解を取りこぼさない割合) vs 速度 vs メモリ の三点関係に集約されます。Meta FAIRのFAISSが公式ドキュメントで「精度を速度と交換する」と表現しているように、検索精度を何パーセントか差し出して、その見返りに桁違いの高速化と省メモリを得るのがこの分野の基本思想です。
主な手法を整理すると次のとおりです。
| 手法 | 系統 | 仕組み | 強み | 弱み |
|---|---|---|---|---|
| ブルートフォース(FLAT) | 精密検索 | 全ベクトルと距離計算 | リコール100%・実装が単純 | O(N×D)でスケールしない |
| IVF | クラスタ分割 | ボロノイ分割し、クエリ近傍のリストのみ探索 | 構築が速く絞り込み効果が大きい | 境界近くの隣接セル漏れでリコール低下 |
| PQ | 量子化(索引構成と組み合わせて使用) | 部分空間ごとに量子化してメモリ削減 | 組み合わせ次第でメモリを大幅削減(FAISS公式) | 量子化誤差で精度が落ちる |
| HNSW | グラフ | 多層グラフを貪欲探索(次節で詳説) | 高リコールかつ高速(ベクトル追加を漸増反映可能) | メモリ消費が大きく構築も重い |
興味深いのはpgvectorの設計判断です。デフォルトではインデックスを作らず、リコール100%の精密検索(シーケンシャルスキャン)を実行します。件数が少ないうちはこれが最も正確で十分に速い。件数が増えてレイテンシが問題になった時点で、HNSWやIVFFlatのインデックスを作ってANNに切り替える――精度と速度のトレードオフを「いつ引き合いに出すか」まで利用者に委ねているのです。
HNSWの仕組み――多層グラフで対数的に速くする「スキップリストの親戚」
pgvector、Qdrant、Milvusがこぞって採用するANNの代表格がHNSW(Hierarchical Navigable Small World、階層型小世界グラフ)です。原案はMalkovとYashuninの論文(arXiv:1603.09320)で、複数の層を持つ近接グラフを、ベクトルの追加ごとに少しずつ漸増構築していきます。
挿入のたびに各要素には「この要素が存在できる最大レイヤー」が割り当てられます。その確率は指数減衰、つまりレイヤーが1つ上がるごとにそのレイヤーに入れる確率が一定比率で下がる仕組みです。結果として最上位ほど要素は疎になり、最下位のLayer 0には全要素が揃います。上の層は「長距離を一気に飛べる幹線」、下の層は「局所をくまなく歩ける路地」という役割分担です。
検索は最上位レイヤーのエントリ点から始まります。各レイヤーで「今いる頂点の近傍のうち、クエリにより近いものがあれば移動する」という貪欲探索をくり返し、もう改善しなくなったら1つ下のレイヤーへ降りて再開します。上層で荒く目標区域へ近づき、Layer 0で精密に候補を集める二段構えにより、原論文の解析・実験では検索コストの対数的スケーリングが示されています(近似グラフの経験的手法であり、あらゆるデータや最悪ケースに対する保証ではありません)。
graph TD
Start(["クエリ開始"]) --> A2
subgraph L2["Layer 2(最上位・要素はごく少数)"]
A2(("A")) --- E2(("E"))
end
subgraph L1["Layer 1(中間の密度)"]
A1(("A")) --- C1(("C"))
C1 --- E1(("E"))
E1 --- H1(("H"))
end
subgraph L0["Layer 0(すべての要素)"]
A0(("A")) --- B0(("B"))
B0 --- C0(("C"))
C0 --- D0(("D"))
D0 --- E0(("E"))
E0 --- F0(("F"))
F0 --- G0(("G"))
G0 --- H0(("H"))
H0 --- I0(("I"))
end
A2 -.- A1
E2 -.- E1
H1 -.- H0
A2 ==>|"貪欲探索で近傍へ"| E2
E2 ==>|"改善しなくなり降下"| E1
E1 ==>|"貪欲探索で近傍へ"| H1
H1 ==>|"改善しなくなり降下"| H0
H0 ==> G0
G0 ==> F0
F0 --> Goal(["最近傍候補 F を返す"])
図は3層の模式図です。Layer 2にはごく少数(A・E)、Layer 1は中間の密度、Layer 0に全要素(A〜I)が並びます。破線は同じ要素が下の層にも存在することを示し、太い矢印は「Layer 2でAからEへ貪欲探索 → 降下してLayer 1でHまで探索 → 降下してLayer 0でGを経由しFを発見」という探索経路です。
この構造はスキップリストの親戚で、一般には「新幹線で大まかに移動し、地下鉄で途中まで行き、最後は徒歩で細かく探す」という例えで説明されます(この例えは解説上の一般論であり、論文原文の記述ではありません)。
もう一つの要点が近傍選択ヒューリスティックです。単純に「近い順にM本つなぐ」だけだと、クラスタ化されたデータでは同じクラスタ内だけで結合し、クラスタ間をつなぐ橋が失われます。HNSWのヒューリスティックは近い相手だけでなく「別の方向へ伸びる有用な近傍」も残すため、クラスタ境界をまたぐ探索が可能で、実データにありがちなかたまりを持つ分布に対してもロバストに動きます。
実運用で効くのが次の3つのパラメータです(pgvector公式のデフォルト値)。
| パラメータ | 役割 | デフォルト値 | 大きくすると? |
|---|---|---|---|
| m | ノードあたりの接続数の上限 | 16 | リコール・メモリ増、速度への影響は設定依存 |
| ef_construction | インデックス構築時の探索幅 | 64 | リコール増、構築時間とメモリ増 |
| ef_search | 検索時の探索幅(hnsw.ef_search) | 40 | リコール増、レイテンシ増 |
いずれも「大きくするほど精度(リコール)が上がり、速度が下がり、メモリが増える」というANNの本質的トレードオフに沿います。まずデフォルト値でリコールとレイテンシを計測し、精度不足ならef_searchから引き上げるのが定石です。
主要ベクトルDB比較――pgvector・Qdrant・Milvus・FAISS・Chromaの設計思想
5製品を1つの表にまとめます。見るべきは優劣ではなく、どこに軸足を置いた設計かです。
| 製品 | 実装言語 | 形態 | フィルタリング | ハイブリッド検索 | 代表的な使いどころ | ライセンス |
|---|---|---|---|---|---|---|
| pgvector | ―(PostgreSQL拡張) | Postgres拡張(Postgres 13+) | SQLのWHERE・JOINと併用 | 疎ベクトル型(sparsevec)対応 | 既存PostgresにACID・PITR・JOINごと同居。vector型は2,000次元上限 | PostgreSQL License |
| Qdrant | Rust | 専用サーバー(Dockerで即起動) | must/should/must_notの再帰ネストで任意のブール式 | 密+疎+マルチベクトルのハイブリッドクエリ | REST(OpenAPI)/gRPC・公式6言語クライアント。絞り込み中心のRAG | Apache-2.0 |
| Milvus | Go+C++ | 分散サーバー(K8sネイティブ、計算/ストレージ分離) | メタデータフィルタリング | ハイブリッド検索対応(README明記) | 数十億ベクトルの水平スケール。GPU(NVIDIA CAGRA)・DiskANN・量子化。Standalone/Milvus Lite | Apache 2.0 |
| FAISS | C++ | 組み込みライブラリ(サーバーではない) | なし(リレーショナルフィルタ非対応) | 単体では非対応 | GPU(CUDA/ROCm/cuVS、マルチGPU)と圧縮コードでメモリ内数十億件。アプリへの直組み | MIT |
| Chroma | ― | 組み込み型(pip install chromadb) | メタデータフィルタリング | 密・疎・ハイブリッド+全文検索(公式ドキュメント) | ローカル開発・プロトタイピング | Apache 2.0 |
pgvector の思想は「データベースを増やさない」です。PostgreSQL 13以降に拡張として組み込み、ACID・PITR・JOINといった既存資産をベクトル検索と同居させます。
Qdrant はベクトル検索を第一の機能とする専用エンジンです。「point=ベクトル+payload」のデータモデルを土台に、must/should/must_notの再帰フィルタと密・疎・マルチベクトルのハイブリッドクエリを最初から前提に、RAGの「意味検索×条件絞り込み」に直球で応えます。
Milvus はクラウドネイティブな分散を最優先に置きます。計算/ストレージ分離のKubernetesネイティブ構成で数十億ベクトルに水平スケールし、GPU(CAGRA)やDiskANN、量子化、Milvus Liteから本格分散まで一つのファミリーでカバーします。
FAISS はDBではなく、Meta FAIR開発のC++類似検索ライブラリです。リレーショナルフィルタ等の運用機能を持たず、その分をGPUと圧縮コードによるメモリ内数十億件という極限性能に注ぎ込みます。
Chroma は「最初の一歩を最軽量に」する組み込みDBです。pip install chromadb だけでローカルに立ち上がり、ベクトル検索に加えて全文検索も使える、本格製品への差し替え前提のプロトタイピング向け入門です。
※比較表の仕様は、本記事執筆時点(2026年9月28日)に各公式ドキュメント・GitHubリポジトリを直接確認した内容に基づきます。製品の仕様は更新されるため、導入時には最新の公式情報をご確認ください。
実践:ユースケース別の選び方――今日からすぐ試せるところまで
どのベクトルDBを選ぶべきかは、性能表の優劣よりも「何と一緒に・どこまで運用するか」でほぼ決まります。まずは意思決定の早見表から確認しましょう。
| ユースケース | おすすめ | 選ぶ理由 |
|---|---|---|
| 既存Postgresで運用/トランザクション整合が欲しい | pgvector | DBを増やさず、ACID・JOIN・PITRとベクトル検索を同居できる |
| まずローカルでプロトタイプ | Chroma/FAISS | pip install chromadb で即試せる。FAISSはアプリ組み込みライブラリでGPUも利用可 |
| 本格運用で高負荷なフィルタ・ハイブリッド検索 | Qdrant | must/should/must_not を再帰ネストした任意のブールフィルタと、密+疎ベクトルのハイブリッドクエリが強力 |
| 大規模分散・数十億件・マルチテナント | Milvus | K8sネイティブな分散アーキテクチャで水平スケールし、DiskANN・量子化・GPUインデックスにも対応 |
Qdrantをすぐに試す
専用サーバー型のQdrantは、Dockerがあれば起動は数コマンドで済みます(所要時間は環境に依存します)。
docker run -p 6333:6333 qdrant/qdrant
Pythonからは公式クライアントで接続できます。
from qdrant_client import QdrantClient
client = QdrantClient(url="http://localhost:6333")
print(client.get_collections()) # 動作確認
pgvectorクイックスタート
一方、既存のPostgres(13以降)に組み込むpgvectorは、拡張の有効化から始まります。
CREATE EXTENSION vector;
CREATE TABLE docs (
id bigserial PRIMARY KEY,
content text,
embedding vector(1536)
);
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
検索はコサイン距離の演算子 <=> を使います($1 はクエリ文の埋め込みベクトル)。
SELECT id, content
FROM docs
ORDER BY embedding <=> $1
LIMIT 5;
導入前チェックリスト
最後に、製品を確定させる前に整理しておきたい5項目です。
- 埋め込み次元:モデルの次元とDBの型上限は整合していますか。OpenAI 3-large(3072次元)はpgvectorのvector型上限2,000次元を超えるため、halfvecかdimensions短縮での対応が必要です
- 距離関数:埋め込みモデルの学習時と同じ距離を使っていますか。コサインなら演算子
<=>と演算子クラスvector_cosine_opsの組み合わせで揃えます - フィルタ要件:メタデータ絞り込みはSQLのWHEREで十分ですか。複雑なブール条件やセマンティック+全文のハイブリッドが必要ならQdrantが楽です
- データ量:件数×次元でメモリを概算しましょう。IVFFlatを使うなら、pgvector公式READMEの推奨目安(100万行以下はrows/1000、超過は√rows。あくまで経験則の出発点)を起点に調整します
- 運用体制:バックアップ・監視の仕組みと、チームのスキル(Postgres運用経験の有無など)を確認します
迷ったら、まずQdrantを1分起動して小さく試し、要件が固まってから改めて比較し直すのがおすすめです。
よくある質問
HNSWのef_searchを上げると何が変わる?
検索時に探索する候補の広さを決めるパラメータで、pgvectorのデフォルトは40です。値を大きくするとリコール(上位k件の正解を取りこぼさず取得できる割合)が向上する一方、検索レイテンシが増加します。逆に下げれば速くなる代わりに見逃しが増えます。まずデフォルトのままで実データのリコールを測り、許容レイテンシの範囲で段階的に調整するのが定石です。
pgvectorの2000次元制限を超えたい場合は?
pgvectorのvector型は最大2,000次元です。3,072次元のtext-embedding-3-largeを保存したいなら、半精度のhalfvec型(最大4,000次元)を使う手があります。またOpenAIのembeddingsは、公式ガイドによればdimensionsパラメータで概念を表現する性質を保ったまま次元を短縮できるとされるため、1,536次元程度に詰めてvector型に収める選択肢もあります。それでも満たせない要件なら、専用ベクトルDBへの再設計を検討してください。
インデックスなしでも動く?
動きます。pgvectorはデフォルトで完全リコールの精密検索(ブルートフォース)を行い、インデックスを作成するとANNに切り替わります。件数が少ないうちは精密検索でも実用的ですが、件数の増加に伴って線形に遅くなるため、スケールの見込みが立った時点でHNSWかIVFFlatのインデックスを作るのが現実的です。
ハイブリッド検索(キーワード+ベクトル)はどれでできる?
Qdrantが有力です。密ベクトル(セマンティック検索)・疎ベクトル(全文検索)・マルチベクトル(ColBERTなど)に対応し、ハイブリッドクエリ専用のドキュメントも用意されています。pgvectorにも疎ベクトル型(sparsevec)があり、SQLのWHEREやJOINと併用できるため、組み合わせ次第でハイブリッドに近い構成も可能です。
まず何から始めればいい?
規模と既存スタックで判断しましょう。既にPostgresを運用してSQLの資産があるならpgvectorが近道ですし、payloadフィルタや密+疎のハイブリッドが必要ならQdrant、数十億ベクトル規模の分散を見込むならMilvusが候補です。最初から最適解を探すより、小さく始めて要件が変わったときに移行する方が早く着地します。
まとめ
- ベクトルDBの本質は、埋め込みベクトルの近傍検索をHNSWなどのANNで高速化する仕組みです。
- pgvector・Qdrant・Milvusは優劣ではなく、既存スタック・フィルタとハイブリッドの要件・スケール段階に応じた選択肢です。
- 精密検索で正解データを作り、ef_searchなどを調整しながらリコールと速度のバランスを測って本番に進むのが失敗しない進め方です。
次のアクション
- 埋め込みモデルの次元(384/768/1536/3072)を確認する: all-MiniLM-L6-v2なら384次元、BERT-base系なら768次元、OpenAI text-embedding-3系なら1,536/3,072次元。次元が型・メモリ量・制限の設計前提を決めます。
- DockerでQdrantを起動、または既存Postgresへpgvectorを導入:
docker run -p 6333:6333 qdrant/qdrantでREST APIがすぐ試せます。Postgres資産があるなら拡張の追加から始めます。 - リコール vs 速度 vs メモリのトレードオフを測定してから本番投入する: インデックスなしの精密検索結果を正解として、HNSW/IVFFlatのリコールとレイテンシを手元のデータで計測しましょう。
参考文献
- pgvector GitHub — https://github.com/pgvector/pgvector
- Malkov & Yashunin, "Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs" — https://arxiv.org/abs/1603.09320
- Qdrant Documentation — https://qdrant.tech/documentation/
- Milvus GitHub — https://github.com/milvus-io/milvus
- FAISS GitHub Wiki — https://github.com/facebookresearch/faiss/wiki
- Chroma Documentation — https://docs.trychroma.com/
- OpenAI Embeddings Guide — https://developers.openai.com/api/docs/guides/embeddings