Vector Database: AI エンジニアリングのための選択と最適化
実稼働環境で RAG パイプラインを構築する場合、ベクター データベースの選択は詳細ではありません 実装: レイテンシ、運用コスト、リコール精度に影響を与えるアーキテクチャ上の決定です。 そしてシステムの拡張性。 2025 年にはベクトル データベース市場の価値はさらに高まる 26.5億ドル 利用可能なソリューションの数も増えています 大幅に選択が複雑になります。
この記事は、商用機能のマーケティング概要を説明するものではありません。技術的な詳細な説明です ベクトル データベースが内部でどのように動作するか、どのようなインデックス作成アルゴリズムが使用されるか、 実際のワークロードに合わせてそれらがどのように構成され、最適化されるか。 Qdrant、Pinecone、Milvus を分析します そして Weaviate は、HNSW アーキテクチャ、量子化戦略、 フィルタリングされた検索、DiskANN とインメモリ、およびターゲットに到達するためのパラメータ調整 アプリケーションによって定義されたリコール/レイテンシ。
50 ミリ秒未満の遅延で数百万のドキュメントを処理する必要がある RAG システムを構築している場合 再現率が 95% 以上である場合、またはメモリを大量に消費する既存のシステムを最適化している場合は、 この記事では、情報に基づいた意思決定を行うための概念的かつ実践的なツールを提供します。
何を学ぶか
- ベクトル データベースの内部アーキテクチャ: HNSW がアルゴリズム レベルでどのように機能するか
- IVF、HNSW、DiskANN の比較: いつどれを使用するか、そしてなぜ使用するか
- スカラー量子化、積量子化、およびバイナリ量子化: メモリと精度のトレードオフ
- フィルタリングされたベクトル検索: プレフィルタリング、ポストフィルタリング、および狭いフィルタの問題
- Qdrant、Milvus、Pinecone の実践的な構成とコード例
- 本番環境でのベンチマークとチューニング: QPS とリコールを測定および改善する方法
内部アーキテクチャ: ベクトル データベースの仕組み
ベクトル データベースは、管理するデータがリレーショナル データベースと異なるだけでなく、 ただし、基本的な操作のタイプについては、正確な検索の代わりに最適化する必要があります。 個別のキーで実行します 近似最近傍 (ANN) 検索 高次元空間上で、最新の LLM 埋め込みでは通常 768 ~ 4096 次元です。
k 個の最近傍 (kNN) の正確な検索の複雑さは O(n*d) です。ここで、n は数値です ベクトルと d の次元。 1,536 次元の 1,000 万個のベクトル (標準サイズ) OpenAI ada-002)、正確なクエリには最大 150 億の浮動小数点演算が必要になります。 リアルタイム システムではまったく受け入れられません。最新のベクトル データベースはすべて、 したがって、数桁の規模を得るために一部の再現率を犠牲にするANNアルゴリズム スピードで。
ベクトル データベースの内部スタックは、いくつかのレベルに分割されています。
- ストレージ層: 効率的なアクセスのための mmap サポートによる、ディスクまたはメモリ内の圧縮ベクトルの管理
- インデックスレイヤー: ベクトル空間をナビゲートするための ANN データ構造 (HNSW、IVF、DiskANN)
- ペイロード/メタデータ層: フィルタリング用のベクトルに関連付けられたスカラー属性
- クエリプランナー: ベクトル検索とペイロードフィルタリングを組み合わせて最適な戦略を決定します
- レプリケーション/シャーディング層: Milvus や Pinecone などの分散システム用
HNSW: アルゴリズムの詳細
階層型ナビゲート可能なスモールワールド (HNSW) は 2025 年の支配的な ANN アルゴリズムです。 Qdrant によってデフォルトで使用されますが、Weaviate は Milvus で利用できます。仕組みを理解する 正しく設定するには、internal が不可欠です。
HNSW は、複数レベルの階層グラフを構築します。最上位レベルではノードがほとんどありません 相互に強く接続されており (「ハブ」)、レベルが下がるにつれて密度が増加します。 すべてのベクトルを含むレベル 0。検索する場合、アルゴリズムは上から開始されます そしてレベルを下りていき、最も類似した近隣の候補を徐々に絞り込んでいきます。 このアプローチは、ソーシャル グラフの「スモールワールド」現象からインスピレーションを得ています。どのノードからでも、 長距離接続のおかげで、数ホップで他のどこにでもアクセスできます。
HNSW の 3 つの基本パラメータは次のとおりです。
- M (デフォルトは 16): ノードごとの双方向エッジの最大数。 一般的な値: 8 ~ 64。 M を増やすとリコールが向上しますが、メモリとビルド時間が増加します。 高次元データセット (1536 以上) の場合、M=32 ~ 64 で良好な結果が得られます。
- ef建設 (デフォルト 100-200): 候補リストのサイズ インデックス構築中。インデックスの最終的なサイズには影響しません。 ただし、それによって接続の品質が決まります。値が大きいほど、インデックスは向上しますが、ビルドは遅くなります。 推奨範囲: 高品質の場合は 200 ~ 400。
- ef (または efSearch、実行時に構成可能): 候補リストのサイズ クエリ中に。 >= k (要求された結果の数) である必要があります。 efを上げると改善される リコールしますが、レイテンシーが増加します。通常は 50 ~ 500 です。
基本的なトレードオフ: MとefConstruction インデックスの品質を決定する (高価な 1 回限りの操作)、一方 ef クエリ時のリコールとレイテンシのバランスを取る (動的に変更できます)。
# Configurazione HNSW in Qdrant - esempio pratico con tradeoff espliciti
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance,
HnswConfigDiff, OptimizersConfigDiff,
CollectionConfig
)
client = QdrantClient(url="http://localhost:6333")
# --- Configurazione HIGH-RECALL per RAG critico ---
# Target: recall >= 0.98, latency accettabile fino a 50ms
# Costo: ~4x memoria rispetto a configurazione base
client.recreate_collection(
collection_name="rag_high_recall",
vectors_config=VectorParams(
size=1536, # OpenAI text-embedding-3-small
distance=Distance.COSINE
),
hnsw_config=HnswConfigDiff(
m=64, # Alta connettivita: migliore recall ma +memoria
ef_construct=400, # Build lento ma indice di alta qualità
full_scan_threshold=10000, # Sotto 10k vettori usa brute force
on_disk=False # In-memory per latenza minima
),
optimizers_config=OptimizersConfigDiff(
default_segment_number=4,
indexing_threshold=20000
)
)
# --- Configurazione BALANCED per produzione tipica ---
# Target: recall >= 0.95, latency < 20ms, memoria ottimizzata
client.recreate_collection(
collection_name="rag_balanced",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE
),
hnsw_config=HnswConfigDiff(
m=32, # Buon bilanciamento recall/memoria
ef_construct=200, # Build ragionevole
full_scan_threshold=5000,
on_disk=False
)
)
# --- Configurazione LOW-LATENCY per real-time ---
# Target: latency < 5ms, recall accettabile >= 0.90
client.recreate_collection(
collection_name="rag_fast",
vectors_config=VectorParams(
size=768, # Embeddings compatti (all-MiniLM-L6-v2)
distance=Distance.COSINE
),
hnsw_config=HnswConfigDiff(
m=16, # Meno connessioni = query più veloci
ef_construct=128,
full_scan_threshold=1000,
on_disk=False
)
)
# Configurare ef a query time (più flessibile)
results = client.search(
collection_name="rag_balanced",
query_vector=query_embedding,
limit=10,
search_params={
"hnsw_ef": 128, # Aumenta recall senza rebuild indice
"exact": False
}
)
print(f"Trovati {len(results)} risultati")
for hit in results:
print(f"Score: {hit.score:.4f} | ID: {hit.id}")
IVF、HNSW、DiskANN: どのアルゴリズムを選択するか
利用可能なインデックス作成アルゴリズムは HNSW だけではありません。選択は大きく左右されます メモリの制約、データセットのサイズ、更新パターンによって異なります。
IVF (逆ファイルインデックス)
IVF はベクトル空間を次のように分割します。 nlist K 平均法によるクラスタリング。
クエリ時は検索のみ ンプローブ クエリ ベクトルに最も近いクラスター。
主要なパラメータは次のとおりです。 nlist (クラスター番号) e nprobe
(検査するクラスターの数)。推奨される実験式: nlist = 4 * sqrt(n_vectors)。
プロの体外受精: 高速ビルド、中程度のメモリ、静的データセットに適しています。 体外受精に対して: データが大幅に変更される場合は再クラスタリングが必要です。思い出してください。 同じ計算予算では HNSW よりも低く、新しいコレクションでコールド スタートします。
HNSW (階層型ナビゲート可能なスモールワールド)
前述したように、多層グラフを構築します。最も汎用性の高いアルゴリズムです ほとんどのベクトル データベースでデフォルトで使用されます。
プロHNSW: 優れたリコール速度のトレードオフ、ネイティブアップデートのサポート インクリメンタル、パラメータはクエリ時に構成可能。 短所 ニューサウスウェールズ州: インデックス全体を RAM に収める必要があるため、法外な量になります 標準ハードウェアでは 50 ~ 100M キャリア以上。
ディスクANN
Microsoft Research によって開発された DiskANN は、RAM に収まらないデータセット用に設計されています。 メモリにはコンパクトなグラフ構造 (圧縮グラフ) のみが保持されますが、ベクトルは すべてはNVMe SSD上にあります。 PCIe Gen5 ハードウェアにより、リコール >95% とレイテンシ 10ms を維持 数十億の通信事業者で利用でき、DRAM のコストは同等の HNSW より 10 ~ 20 分の 1 です。
プロディスクANN: コモディティハードウェア上で数十億の通信事業者に拡張可能、コスト 操作を減らします。 短所 DiskANN: 高速な NVMe SSD が必要で、HNSW よりも遅延が長い メモリ内では、基本的な実装は不変です (FreshDiskANN は更新を処理します)。 Milvus、pgvector が拡張された Azure PostgreSQL で利用可能で、他のシステムでもプレビュー中です。
「HNSW for everything」アンチパターンに注意してください
多くのチームは、5,000 万以上のベクトル データセットに対して HNSW をメモリ内で構成し、その後、 持続不可能なコストで 256GB RAM インスタンスを使用します。経験則: データセットが キャリア数が 10 ~ 20M を超えており、超低遅延要件 (<5ms) がない場合は、評価してください。 ハードウェアをスケールアップする前に、真剣に DiskANN または積極的な量子化を行ってください。
# Configurazione IVF_FLAT e HNSW in Milvus - confronto pratico
from pymilvus import (
connections, Collection, CollectionSchema,
FieldSchema, DataType, utility
)
connections.connect("default", host="localhost", port="19530")
# Schema comune per entrambi i test
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="content", dtype=DataType.VARCHAR, max_length=65535),
FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=100),
FieldSchema(name="timestamp", dtype=DataType.INT64),
FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536)
]
schema = CollectionSchema(fields, description="RAG document collection")
# Crea collezione
collection = Collection("rag_docs", schema)
# --- Indice IVF_FLAT: per dataset statici o quasi-statici ---
# nlist = 4 * sqrt(n_vectors) regola empirica
# Per 1M vettori: nlist = 4000
ivf_index_params = {
"metric_type": "COSINE",
"index_type": "IVF_FLAT",
"params": {
"nlist": 4096 # Numero di cluster Voronoi
}
}
# --- Indice HNSW: per dataset con update frequenti ---
hnsw_index_params = {
"metric_type": "COSINE",
"index_type": "HNSW",
"params": {
"M": 32,
"efConstruction": 256
}
}
# --- Indice DiskANN: per dataset >50M vettori ---
diskann_index_params = {
"metric_type": "L2",
"index_type": "DISKANN",
"params": {
"search_list": 100 # Candidati durante la ricerca
}
}
# Build dell'indice prescelto
collection.create_index(
field_name="embedding",
index_params=hnsw_index_params # Scegliere in base al caso d'uso
)
collection.load()
# Query con parametri specifici per HNSW
search_params_hnsw = {
"metric_type": "COSINE",
"params": {
"ef": 256 # Aumentare per più recall, diminuire per più velocità
}
}
# Query con parametri per IVF
search_params_ivf = {
"metric_type": "COSINE",
"params": {
"nprobe": 64 # nprobe/nlist = fraction di cluster ispezionati
} # 64/4096 = 1.5% - bilancio recall/speed
}
results = collection.search(
data=[query_embedding],
anns_field="embedding",
param=search_params_hnsw,
limit=10,
output_fields=["content", "category", "timestamp"]
)
for hit in results[0]:
print(f"Distance: {hit.distance:.4f} | Category: {hit.entity.get('category')}")
ベクトル量子化: リコールを失わずに圧縮
量子化は、ベクトル データベースのメモリ使用量を削減するための最も強力な手法です。 研究の質に与える影響は制御可能です。 1536 次元の float32 ベクトル 6144 バイト (6KB) を占めます。量子化を使用すると、384 バイト以下に減らすことができます。
スカラー量子化 (SQ)
各 float32 値 (4 バイト) を int8 (1 バイト) にマップし、結果として 4 倍の圧縮が行われます。 アルゴリズムは各次元の値の分布を分析し、範囲を決定します。 量子化に最適です。距離は int8 で直接計算されます。 計算効率が向上します。一般的なリコール損失は、float32 と比較して 1 ~ 3% です。
いつ使用するか: あらゆる導入において推奨される開始点、 品質への影響を最小限に抑えながら 4 倍の削減。 Qdrant、Milvus (IVF_SQ8) によるサポート Weaviate (スカラー フォールバックを備えた PQ)。
積量子化 (PQ)
ベクトルを次のように分割します。 m コードブック内でサブベクトルを作成し、それぞれを量子化します の 2^nビット エントリ。一般的な圧縮: 16 ~ 64 倍。各キャリアPQ付き コードブックではインデックスのシーケンスとして表されます。おおよその距離 これらは、ルックアップ テーブルの事前計算 (ADC - 非対称距離計算) によって計算されます。
PQ のトレードオフ: 積極的な圧縮 (10 GB ではなく 10 ~ 50 MB) ただし、大幅なリコール損失 (5 ~ 15%)。データセットでのコードブック トレーニングが必要です。 メモリが主な制約となる巨大なデータセットに適しています。
バイナリ量子化 (BQ)
各次元を 1 ビット (可能な限り最も圧縮された値) に削減します。 1536の運送業者 サイズは 192 バイトになります (float32 と比較して 32 倍の圧縮)。距離が計算されます ハミング距離 (XOR + ポップカウント) を使用すると、最新の CPU で非常に高速に動作します。 Qdrant レポートの速度が向上 40倍 距離計算の操作について。
ただし、バイナリ量子化は、特定のプロパティを持つ埋め込みに対してのみ適切に機能します。 値はゼロの周りに対称的に分布する必要があります (プロパティが満たされている) OpenAI ada-002、Cohere embed-v3、e-5 モデルから)。非対称分布による埋め込みの場合、 リコールの低下が深刻になる可能性があります (15 ~ 30%)。
Qdrant は 2025 年に中間量子化も導入しました 1.5ビットと2ビット、 スカラー (4x) とバイナリ (32x) の間のバランス ポイントを提供します。
# Configurazione quantizzazione in Qdrant - tutti i tipi
from qdrant_client import QdrantClient
from qdrant_client.models import (
VectorParams, Distance,
ScalarQuantizationConfig, ScalarType,
ProductQuantizationConfig, CompressionRatio,
BinaryQuantizationConfig,
QuantizationConfig
)
client = QdrantClient(url="http://localhost:6333")
# --- 1. Scalar Quantization (SQ8) ---
# Compressione 4x, recall loss ~1-3%
# CONSIGLIATO: miglior punto di partenza
client.recreate_collection(
collection_name="rag_sq8",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE
),
quantization_config=ScalarQuantizationConfig(
scalar=QuantizationConfig(
type=ScalarType.INT8,
quantile=0.99, # Usa il 99° percentile per definire il range
always_ram=True # Tieni quantized vectors in RAM (+ velocità)
)
)
)
# --- 2. Product Quantization (PQ) ---
# Compressione 16-64x, recall loss 5-15%
# PER dataset enormi (>100M vettori) con vincoli di memoria severi
client.recreate_collection(
collection_name="rag_pq",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE
),
quantization_config=ProductQuantizationConfig(
product=QuantizationConfig(
compression=CompressionRatio.X16, # 16x compressione
always_ram=True
)
)
)
# --- 3. Binary Quantization (BQ) ---
# Compressione 32x, speedup 40x, recall loss variabile
# SOLO per embedding con distribuzione simmetrica (OpenAI, Cohere)
client.recreate_collection(
collection_name="rag_binary",
vectors_config=VectorParams(
size=1536,
distance=Distance.COSINE
),
quantization_config=BinaryQuantizationConfig(
binary=QuantizationConfig(
always_ram=True
)
)
)
# Verifica dell'impatto sulla qualità a query time
# Con rescore=True, i candidati BQ vengono rirankinati con float32
def search_with_rescore(client, collection_name, query_vector, limit=10):
"""
rescore=True: usa BQ per candidate generation, poi
ricalcola distanze esatte con float32 sui top-k candidati.
Bilancia la velocità di BQ con la precisione di float32.
"""
return client.search(
collection_name=collection_name,
query_vector=query_vector,
limit=limit,
search_params={
"quantization": {
"ignore": False, # Usa quantizzazione
"rescore": True, # Rescore finale con float32
"oversampling": 3.0 # Preleva 3x candidati per il rescore
}
}
)
# Benchmark comparativo: misura recall vs latency
import time
import numpy as np
def benchmark_collection(client, collection_name, test_queries, ground_truth, k=10):
recalls = []
latencies = []
for query, gt in zip(test_queries, ground_truth):
start = time.perf_counter()
results = client.search(
collection_name=collection_name,
query_vector=query.tolist(),
limit=k
)
elapsed_ms = (time.perf_counter() - start) * 1000
latencies.append(elapsed_ms)
# Calcola recall@k
retrieved_ids = {hit.id for hit in results}
true_ids = set(gt[:k])
recall = len(retrieved_ids & true_ids) / k
recalls.append(recall)
return {
"mean_recall": np.mean(recalls),
"p95_latency_ms": np.percentile(latencies, 95),
"p99_latency_ms": np.percentile(latencies, 99)
}
# Confronto risultati tipici (su hardware commodity, 1M vettori 1536-dim)
# float32: recall=1.00, p95_latency=45ms, memory=6.1GB
# SQ8: recall=0.98, p95_latency=18ms, memory=1.6GB ← sweet spot
# PQ16: recall=0.91, p95_latency=8ms, memory=0.4GB
# Binary: recall=0.93, p95_latency=3ms, memory=0.2GB (con rescore)
経験則: 量子化を選択する
- データセット <1,000 万ベクトル、クリティカル リコール: ネイティブ float32 (量子化なし)
- データセット 10 ~ 100M ベクトル: スカラー量子化 INT8、品質/メモリ スイート スポット
- データセット > 100M ベクトル、限られたメモリ: リスコアによる積量子化
- OpenAI/Cohere 埋め込みによる超低レイテンシ: バイナリ量子化 + リスコア
フィルタリングされたベクトル検索: 狭いフィルタの問題
実際には、ほとんどの RAG クエリは純粋なベクトル検索ではありません。 意味的に類似した文書を見つけたい e あるものに属する ユーザー、データ範囲、カテゴリ、またはテナント。フィルタリングされたベクトル検索は問題の 1 つです ベクトルデータベースではアルゴリズム的により困難です。
根本的な問題: 非常に選択的なフィルター (例: 「先月のドキュメントのみ」) これはデータセットの 0.1% に一致します)、ベクトル空間内の k 個の最近傍は次のようになります。 すべてがフィルタから除外されるため、検索では非常に大部分を探索する必要があります。 k 個の有効な結果を見つける前に、HNSW グラフを解析します。これにより、レイテンシが 10 ~ 100 倍に増加する可能性があります フィルタなしの検索と比較します。
フィルタリング戦略
ポストフィルタリング: 通常どおり ANN 検索を実行し、結果をフィルタリングします。 これは、フィルターの選択性が低い場合 (結果の 50% 未満を除外する場合) に機能します。 問題: フィルターがベクトルの 99% を除外する場合、100 倍以上の候補を取得する必要があります。
事前フィルタリング: まずフィルターを満たす点を特定します。 次に、そのセットに対してのみ ANN 検索を実行します。効率的なスカラーインデックスが必要です フィルターされたフィールド上で。選択性の高いフィルターとうまく連携しますが、ペイロードのインデックス作成が必要です。
フィルタリング可能な HNSW (Qdrant): Qdrant は洗練された拡張機能を実装しています HNSW のインデックス付きペイロードの値に基づいてグラフにエッジを追加します。 クエリ プランナーはフィルターのカーディナリティを推定し、戦略を動的に選択します。 フィルターが非常に選択的である場合はペイロード インデックスを使用し、それ以外の場合はフィルター可能な HNSW を使用します。
複数の狭いフィルターの組み合わせがある場合、Qdrant は使用することを推奨します。 アルゴリズムの ACORN (アダプティブ コンポーネント オーバーラップ ルーティング ネットワーク)、 これにより、積極的なフィルタリングによって発生した切断されたグラフをより適切に処理できます。
# Filtered vector search in Qdrant - best practices
from qdrant_client import QdrantClient
from qdrant_client.models import (
Filter, FieldCondition, MatchValue, Range,
MatchAny, SearchRequest
)
import datetime
client = QdrantClient(url="http://localhost:6333")
# STEP 1: Crea indici payload per i campi filtrati frequentemente
# CRITICO: senza payload index, il filtering scansiona tutti i punti
# Indice per filtri di uguaglianza (tenant_id, category)
client.create_payload_index(
collection_name="rag_docs",
field_name="tenant_id",
field_schema="keyword" # Per valori categorici
)
client.create_payload_index(
collection_name="rag_docs",
field_name="category",
field_schema="keyword"
)
# Indice per filtri di range (timestamp, score)
client.create_payload_index(
collection_name="rag_docs",
field_name="created_at",
field_schema="integer" # UNIX timestamp per range queries
)
client.create_payload_index(
collection_name="rag_docs",
field_name="relevance_score",
field_schema="float"
)
# STEP 2: Query con filtri - da semplice a complesso
# Filtro singolo (alta cardinalita): efficiente con keyword index
def search_by_tenant(query_vector, tenant_id, limit=10):
return client.search(
collection_name="rag_docs",
query_vector=query_vector,
query_filter=Filter(
must=[
FieldCondition(
key="tenant_id",
match=MatchValue(value=tenant_id)
)
]
),
limit=limit
)
# Filtro combinato (filtro stretto): usa HNSW filterable
def search_recent_high_quality(query_vector, tenant_id, days_back=7, limit=10):
cutoff = int((datetime.datetime.now() -
datetime.timedelta(days=days_back)).timestamp())
return client.search(
collection_name="rag_docs",
query_vector=query_vector,
query_filter=Filter(
must=[
FieldCondition(
key="tenant_id",
match=MatchValue(value=tenant_id)
),
FieldCondition(
key="created_at",
range=Range(gte=cutoff) # >= cutoff timestamp
),
FieldCondition(
key="relevance_score",
range=Range(gte=0.7) # Solo documenti di qualità
)
]
),
limit=limit,
search_params={
"hnsw_ef": 256, # Aumenta ef per filtri stretti
"exact": False
}
)
# Filtro con OR (MatchAny): utile per multi-category search
def search_multi_category(query_vector, categories, limit=10):
return client.search(
collection_name="rag_docs",
query_vector=query_vector,
query_filter=Filter(
must=[
FieldCondition(
key="category",
match=MatchAny(any=categories) # OR sui valori
)
]
),
limit=limit
)
# STEP 3: Batch search per performance (evita N query singole)
def batch_search(query_vectors, tenant_id, limit=10):
"""
Usa search_batch per ridurre overhead di N query indipendenti.
Throughput tipico: 3-5x rispetto a query sequenziali.
"""
requests = [
SearchRequest(
vector=qv,
filter=Filter(
must=[FieldCondition(
key="tenant_id",
match=MatchValue(value=tenant_id)
)]
),
limit=limit
)
for qv in query_vectors
]
return client.search_batch(
collection_name="rag_docs",
requests=requests
)
データベースの比較: Qdrant vs Pinecone vs Milvus vs Weaviate
各データベースには異なる強度プロファイルがあります。普遍的に最適な選択肢はありません。 決定は、展開の制約、チームの能力、および特定の要件によって異なります。
クドラント
Rust で書かれた、最高の価値のあるデータベースです 運用パフォーマンス/複雑さ ペイロード インデックスとフィルタリング可能な HNSW、スカラー/プロダクト/バイナリによる高度なフィルタリングをサポート 量子化、名前付きベクトルのマルチベクトル、ネイティブ ハイブリッド検索のスパース ベクトル。 最も単純なデプロイメント: 単一バイナリ、Docker、またはクラウド管理。次のようなチームに最適です 彼らは、膨大な運用上のオーバーヘッドを発生させずに制御できることを望んでいます。
以下に最適です: RAG エンタープライズ、マルチテナント システム、オンプレミス展開、 Python の経験はあるものの、複雑な Kubernetes インフラストラクチャを持たないチーム。
松ぼっくり
フルマネージド、サーバーレス、運用ゼロ。価格は自己ホスト型の代替手段よりも高い ただし、インフラストラクチャの運用コストは完全に排除されます。次のようなチームに最適です。 彼らはクラスターを管理せずに製品に集中することを好みます。サーバーレスポッドをサポート 透過的な自動スケーリングとマルチリージョンのレプリケーション。レイテンシーは一貫して低い 最適化されたインフラストラクチャのおかげで。
以下に最適です: 初期段階のスタートアップ、小規模チーム、変動するワークロード、 概念実証が、手戻りなしで製品化されます。
ミルバス / ジリズ・クラウド
最も成熟し、機能が完全に揃った分散システム。すべてのインデックスタイプをサポート (HNSW、IVF、DiskANN、ScaNN、GPU アクセラレーション)、Kubernetes での自動シャーディング、 コンピューティングとストレージの分離。クラウド版(Zilliz)がスループットベンチマークで勝利 データセット > 100M ベクトル。 Kubernetes での運用上の大幅なオーバーヘッド。
以下に最適です: データセット > 5,000 万ベクトル、Kubernetes インフラストラクチャとチーム 既存の最大スループット要件、GPU アクセラレーション。
ウィアビエイト
純粋なベクトルデータベースとナレッジグラフの間に位置します。組み込みモジュールをサポート 自動埋め込み生成 (text2vec-openai、text2vec-cohere)、GraphQL クエリ インターフェイス、およびネイティブ BM25 とのハイブリッド化。他のものよりも多くのメモリが必要です 同じデータセットの場合。検索とナレッジ グラフを統合したいチームに最適です。
以下に最適です: ナレッジグラフによるセマンティック検索、GraphQL を使用するチーム、 埋め込みパイプラインを管理することなく、モデルプロバイダーと直接統合します。
意思決定マトリックス: 選択方法
- 少人数のチーム、開発スピード: Pinecone (ゼロ操作) または Qdrant (シンプルさ)
- データセット > 50M ベクトル、高スループット: DiskANN または GPU インデックスを備えた Milvus
- 複雑なフィルターを備えたマルチテナント RAG: Qdrant (フィルタリング可能な HNSW)
- ナレッジグラフ + セマンティック検索: ウィアビエイト
- すでに PostgreSQL を使用しており、ボリュームは中程度です。 pgvector (追加のインフラストラクチャを回避)
- オーバーヘッドのないネイティブ ハイブリッド検索: Qdrant スパース ベクトルまたは Weaviate BM25
松ぼっくり: 構成と最適化
Pinecone は、2024 年から 2025 年にかけてサーバーレス アーキテクチャで SDK をさらに簡素化しました。 インデックス作成アルゴリズムを明示的に設定する必要はなくなり、Pinecone が処理します。 内部的には、データセットのサイズに基づいてインデックスが選択されます。
# Pinecone - setup e ottimizzazione con SDK v3+
from pinecone import Pinecone, ServerlessSpec, PodSpec
import os
pc = Pinecone(api_key=os.environ["PINECONE_API_KEY"])
# --- Serverless Index (consigliato per la maggior parte dei casi) ---
# Autoscaling trasparente, pay-per-query
pc.create_index(
name="rag-serverless",
dimension=1536, # Deve matchare il modello di embedding
metric="cosine", # oppure "dotproduct" per modelli ottimizzati per IP
spec=ServerlessSpec(
cloud="aws",
region="us-east-1"
)
)
# --- Pod Index (per latency garantita e throughput alto) ---
# Scegliere il tipo di pod in base al profilo workload
pc.create_index(
name="rag-pod-optimized",
dimension=1536,
metric="cosine",
spec=PodSpec(
environment="us-east1-gcp",
pod_type="p2.x1", # p1=storage, p2=speed, s1=storage-optimized
pods=1,
replicas=2, # 2 replicas per HA
shards=1
)
)
index = pc.Index("rag-serverless")
# Upsert con metadata ricchi per filtering
def upsert_documents(documents, embeddings):
vectors = [
{
"id": doc["id"],
"values": emb.tolist(),
"metadata": {
"text": doc["text"][:1000], # Pinecone limit: 40KB per vector
"source": doc["source"],
"tenant_id": doc["tenant_id"],
"created_at": doc["created_at"], # ISO string o epoch int
"category": doc["category"],
"language": doc.get("language", "it")
}
}
for doc, emb in zip(documents, embeddings)
]
# Batch upsert: max 100 vectors per request, max 2MB
batch_size = 100
for i in range(0, len(vectors), batch_size):
batch = vectors[i:i + batch_size]
index.upsert(vectors=batch)
# Query con metadata filtering
def query_pinecone(query_embedding, tenant_id, limit=10, category=None):
filter_dict = {"tenant_id": {"$eq": tenant_id}}
if category:
filter_dict["category"] = {"$in": category if isinstance(category, list) else [category]}
return index.query(
vector=query_embedding.tolist(),
top_k=limit,
filter=filter_dict,
include_metadata=True
)
# Fetch statistiche indice per monitoring
stats = index.describe_index_stats()
print(f"Totale vettori: {stats['total_vector_count']}")
print(f"Dimensione: {stats['dimension']}")
print(f"Namespace breakdown: {stats.get('namespaces', {})}")
# Pinecone Namespaces: isolamento logico multi-tenant senza costi extra
# Inserimento in namespace specifico
index.upsert(
vectors=[{"id": "doc1", "values": embedding}],
namespace="tenant-acme-corp"
)
# Query nel namespace
index.query(
vector=query_embedding,
top_k=10,
namespace="tenant-acme-corp"
)
メモリの最適化とプロダクションチューニング
運用中のベクトル データベースでは、リコールだけでなく、複数の側面に注意を払う必要があります。 レイテンシだけでなく、メモリ使用量、スループット、負荷時の動作、 そして長期的な運営コスト。
メモリ使用量の推定
必要なメモリを見積もるための基本的な式 (float32、量子化なし):
- 生のベクトル: n_vectors * ディム * 4 バイト
- ニューサウスウェールズ州のグラフ: n_vectors * M * 2 * 8 バイト (おおよそ、実装によって異なります)
- ペイロード/メタデータ: 変数、通常はベクトルあたり 100 ~ 500 バイト
- システムのオーバーヘッド: 全体の約 20 ~ 30%
例: HNSW M=32、1536 ディムでの 500 万のベクトル: ベクター: 5M * 1536 * 4 = ~29GB。 HNSW グラフ: 5M * 32 * 2 * 8 = ~2.5GB。 オーバーヘッドを含む推定合計: ~38GB RAM。 SQ8 の場合: ~11GB。バイナリの場合: ~1.5GB。
本番環境での Qdrant パフォーマンスのチューニング
# Qdrant - ottimizzazioni avanzate per produzione
from qdrant_client import QdrantClient
from qdrant_client.models import (
OptimizersConfigDiff, WalConfigDiff,
HnswConfigDiff, QuantizationConfig,
ScalarQuantizationConfig, ScalarType
)
client = QdrantClient(
url="http://localhost:6333",
# Connection pool per high-throughput
timeout=30
)
# Configurazione ottimizers per bulk ingestion
# Durante l'ingestion massiva, disabilita temporaneamente il reindexing
client.update_collection(
collection_name="rag_docs",
optimizers_config=OptimizersConfigDiff(
# Aumenta la soglia per ritardare il reindexing
# durante bulk insert (es. 200k invece di default 20k)
indexing_threshold=200000,
# Numero di segmenti ottimali per la collection
# più segmenti = parallelismo migliore in lettura
default_segment_number=8,
# Max dimensione segmento prima di merge
max_segment_size=500000,
# Delay prima di ottimizzare (evita ottimizzazioni inutili su dati transienti)
flush_interval_sec=5,
)
)
# Dopo il bulk insert, forza l'ottimizzazione
# e ripristina configurazione normale
client.update_collection(
collection_name="rag_docs",
optimizers_config=OptimizersConfigDiff(
indexing_threshold=20000, # Ripristina default
default_segment_number=4
)
)
# Monitoring della collection: verifica stato di ottimizzazione
collection_info = client.get_collection("rag_docs")
print(f"Stato: {collection_info.status}")
print(f"Vettori totali: {collection_info.vectors_count}")
print(f"Segmenti: {collection_info.segments_count}")
print(f"Dimensione disco: {collection_info.disk_data_size} bytes")
print(f"Dimensione RAM: {collection_info.ram_data_size} bytes")
# Check se l'indice è aggiornato (indexed_vectors_count == vectors_count)
if collection_info.indexed_vectors_count < collection_info.vectors_count:
not_indexed = collection_info.vectors_count - collection_info.indexed_vectors_count
print(f"ATTENZIONE: {not_indexed} vettori non ancora indicizzati (query meno efficienti)")
# Configurazione WAL per durability vs performance
# Per ambienti in cui un crash è accettabile (risincronizzazione possibile)
client.update_collection(
collection_name="rag_docs",
wal_config=WalConfigDiff(
wal_capacity_mb=256,
wal_segments_ahead=0 # 0 = massima velocità, meno durability
)
)
# Snapshot per backup
snapshot_info = client.create_snapshot(collection_name="rag_docs")
print(f"Snapshot creato: {snapshot_info.name}")
# Scroll per esportare o processare tutti i vettori
# (evita di usare search per questo scopo)
def export_all_vectors(client, collection_name, batch_size=1000):
offset = None
all_points = []
while True:
batch, next_offset = client.scroll(
collection_name=collection_name,
offset=offset,
limit=batch_size,
with_vectors=True,
with_payload=True
)
all_points.extend(batch)
if next_offset is None:
break
offset = next_offset
return all_points
ベンチマークとリコール測定
厳密な測定なしには有効な最適化はありません。標準フレームワーク ベクトル データベースの評価は、次の 3 つの基本的な指標に基づいて行われます。
- @k を思い出してください: k 個の結果の中で見つかった真の k 個の最近傍の割合 戻ってきました。これは品質にとって最も重要な指標です。式: |取得済み ∩ true| /k
- QPS (1 秒あたりのクエリ数): 負荷時のシステム スループット。 通常、固定の再現目標(例:「QPS @ remember=0.95」)を使用して測定されます。
- レイテンシーのパーセンタイル (p50、p95、p99): 平均レイテンシーは誤解を招きます。 運用環境では、p99 が重要です。クエリの 99% が SLA 内で完了する必要があります。
ベクトル データベースの参照ベンチマークは次のとおりです。 ann-benchmarks.com、 標準化されたデータセット (SIFT1M、GIST1M、 GloVe-100-angular)。 2024 ~ 2025 年の結果では、Qdrant と Milvus がリーダーに名を連ねていることが示されています リコールとスループットのトレードオフについては、Pinecon がレイテンシ p99 の一貫性に優れています。
# Framework di benchmarking per vector database
import time
import numpy as np
from typing import List, Tuple, Dict
from dataclasses import dataclass
@dataclass
class BenchmarkResult:
mean_recall: float
p50_latency_ms: float
p95_latency_ms: float
p99_latency_ms: float
qps: float
total_queries: int
class VectorDBBenchmark:
"""
Framework per benchmarkare un vector database.
Genera ground truth con brute force e confronta con ANN.
"""
def __init__(self, collection_size: int, dim: int, n_test_queries: int = 1000):
self.collection_size = collection_size
self.dim = dim
self.n_test_queries = n_test_queries
def generate_test_data(self) -> Tuple[np.ndarray, np.ndarray]:
"""Genera dataset e query vectors normalizzati."""
# Simula embedding realistici (distribuzione gaussiana normalizzata)
data = np.random.randn(self.collection_size, self.dim).astype(np.float32)
data = data / np.linalg.norm(data, axis=1, keepdims=True)
queries = np.random.randn(self.n_test_queries, self.dim).astype(np.float32)
queries = queries / np.linalg.norm(queries, axis=1, keepdims=True)
return data, queries
def compute_ground_truth(self, data: np.ndarray, queries: np.ndarray, k: int = 10) -> np.ndarray:
"""
Calcola i veri k nearest neighbors con brute force.
LENTO ma necessario come reference per calcolare il recall.
"""
ground_truth = np.zeros((len(queries), k), dtype=np.int64)
for i, query in enumerate(queries):
# Cosine similarity = dot product su vettori normalizzati
similarities = data @ query
top_k_indices = np.argsort(similarities)[::-1][:k]
ground_truth[i] = top_k_indices
return ground_truth
def run_benchmark(
self,
search_fn, # Funzione di ricerca: (query_vector, k) -> List[int]
queries: np.ndarray,
ground_truth: np.ndarray,
k: int = 10
) -> BenchmarkResult:
"""Esegue il benchmark completo."""
recalls = []
latencies = []
# Warmup (i primi risultati possono essere penalizzati da cold start)
for _ in range(10):
search_fn(queries[0], k)
# Benchmark effettivo
for i, query in enumerate(queries):
start = time.perf_counter()
results = search_fn(query, k)
elapsed_ms = (time.perf_counter() - start) * 1000
latencies.append(elapsed_ms)
# Calcola recall@k
retrieved = set(results[:k])
true_set = set(ground_truth[i].tolist())
recall = len(retrieved & true_set) / k
recalls.append(recall)
total_time = sum(latencies) / 1000 # in secondi
qps = len(queries) / total_time
return BenchmarkResult(
mean_recall=float(np.mean(recalls)),
p50_latency_ms=float(np.percentile(latencies, 50)),
p95_latency_ms=float(np.percentile(latencies, 95)),
p99_latency_ms=float(np.percentile(latencies, 99)),
qps=qps,
total_queries=len(queries)
)
# Esempio di utilizzo con Qdrant
def qdrant_search_fn(client, collection_name, ef=128):
def search(query_vector: np.ndarray, k: int) -> List[int]:
results = client.search(
collection_name=collection_name,
query_vector=query_vector.tolist(),
limit=k,
search_params={"hnsw_ef": ef}
)
return [hit.id for hit in results]
return search
# Esegui il benchmark per diversi valori di ef
benchmark = VectorDBBenchmark(collection_size=1_000_000, dim=1536)
data, queries = benchmark.generate_test_data()
gt = benchmark.compute_ground_truth(data, queries[:100], k=10) # Subset per ground truth
for ef_value in [32, 64, 128, 256, 512]:
search_fn = qdrant_search_fn(client, "rag_docs", ef=ef_value)
result = benchmark.run_benchmark(search_fn, queries[:100], gt, k=10)
print(f"ef={ef_value:3d} | "
f"Recall: {result.mean_recall:.3f} | "
f"P95: {result.p95_latency_ms:.1f}ms | "
f"QPS: {result.qps:.0f}")
ハイブリッド検索: Vector データベース内の Vector + BM25
最新のベクトル データベースはもはや純粋なベクトル システムではなく、多くのサポートが行われています。 密ベクトルと疎ベクトルを組み合わせたハイブリッド検索 (BM25/TF-IDF) が追加されました。 このトピックは、ハイブリッド検索に特化した記事で詳しく説明されていますが、重要です。 ベクトルデータベースレベルでどのように統合されるかを理解します。
クドラント スパース ベクトルをネイティブにサポート: 密なベクトルの両方を保存できます (セマンティック埋め込み) 各ドキュメントのスパース ベクトル (BM25 重み付け) を実行し、 RRF (Reciprocal Rank Fusion) またはカスタム スコア フュージョンを使用した単一リクエストでのハイブリッド クエリ。
ウィアビエイト ハイブリッド検索が GraphQL スキーマに統合されています。 相対的な重みを制御するアルファ (0=純粋な BM25、1=純粋なベクトル) を指定します。 ミルバス 2.4+ 疎密融合を導入しました。 松ぼっくり Pinecone Sparse エンコーダまたは BM25 カスタム モデルによるスパース - デンスをサポートします。
ハイブリッド検索の実装と融合方法について詳しく知るには (RRF、加重和、学習された融合)、記事を参照してください。 ハイブリッド検索: BM25 + ベクトル検索 このシリーズの。
相互リンク: 関連記事
- RAG: 検索拡張生成の説明 - ベクトル データベースの役割を文脈化するための RAG の基礎
- 埋め込みとベクトル検索: BERT と文変換器 - パイプラインの埋め込みモデルを選択する方法
- ハイブリッド検索: BM25 + ベクトル検索 - ベクトル検索とキーワード検索を組み合わせて、再現率を高めます
- pgvector を使用した PostgreSQL - 追加のインフラストラクチャを必要としない PostgreSQL でのベクトル検索
生産チェックリスト
ベクター データベースを運用環境に導入する前に、次の重要な点を確認してください。
- 実際のデータセットのベンチマーク: 一般的な結果は引き継がれません ユースケースに自動的に適用されます。実際のクエリでリコールとレイテンシを測定します。
- 構成されたペイロード インデックス: フィルタリングする各フィールドにはインデックスが必要です。 それ以外の場合、フィルタリングはすべてのポイントをスキャンします。
- 適切な量子化: SQ8 をデフォルトとして評価し、リコールロスを測定します。 許容できる場合は、今すぐ適用してください。メモリが大幅に節約されます。
- バックアップとスナップショット: 自動スナップショットを構成します。ベクトルデータベース 常に ACID トランザクションがあるとは限りません。取り込み中にクラッシュが発生すると、インデックスが破損する可能性があります。
- 監視: のindexed_vectors_countとvectors_countのプロット クエリのパフォーマンスを低下させるインデックス作成の遅れを検出します。
- メモリのサイジング: 導入前に実際のフットプリントを計算します。 サーバーのメモリが不十分だとスワップが発生し、遅延が発生します。
- 狭いフィルターを使用してテストします。 アプリケーションが非常に選択的なフィルターを使用している場合、 これらのシナリオを明示的にテストします。狭いフィルターの下でのレイテンシーは大きく異なります そこからフィルタリングされていない検索が行われます。
避けるべき一般的なアンチパターン
- インデックス作成のしきい値が低すぎます: Indexing_threshold=0 または非常に低い場合、 挿入のたびにインデックスの再作成がトリガーされるため、取り込みが非常に遅くなります。 一括挿入には 10k ~ 100k のしきい値を使用して、最適化します。
- 測定せずに M が高すぎる: M=128 が M=32 よりも優れているとは限りません。 ある点を超えると、記憶力はわずかに向上しますが、記憶力は直線的に増加します。 データセットを使用して測定します。
- フィルタリングされたフィールドにペイロード インデックスがありません: インデックスなし、フィルター条件なし そしてO(n)。 10M ベクトルの場合、インデックスのないフィルターでは 5 ミリ秒と 5000 ミリ秒の差が生じます。
- コサイン類似度を持つ非正規化ベクトルの次元: 使用する場合 コサイン類似度を考慮するには、ベクトルを正規化する必要があります。一部のモデルでは正規化されません デフォルトでは。コサインで正規化されていないベクトルは、意味的に正しくない結果をもたらします。
結論と次のステップ
ベクトル データベースの最適化は、最も技術的で影響力のある側面の 1 つです。 AIエンジニアリングの。普遍的な答えはありません。すべてのシステムには強度プロファイルがあります。 最適な構成は特定のワークロードによって異なります。
新しいプロジェクトの推奨パス: 以下から開始します。 SQ8 を使用した Qdrant 操作の簡素化と優れたパフォーマンスを実現するには、実際のデータセットでリコールとレイテンシーを測定します。 パフォーマンスが十分でない場合は、M と ef のチューニングを検討してください。メモリに問題がある場合は、次の点を考慮してください。 積量子化または DiskANN。すでに PostgreSQL を使用しており、中程度のボリューム (ベクトルが 500 万未満) がある場合は、 新しいインフラストラクチャを追加する前に、pgvector を検討してください。
このシリーズの次の記事は、次の基礎に基づいて作成されます。 に ハイブリッド検索 ベクトル検索と BM25 を組み合わせて、正確なクエリの再現率を向上させる方法を見ていきます。 の記事の中で 本番環境の RAG ベクトルデータベースの選択が品質に与えるエンドツーエンドの影響を測定する方法を見ていきます。 RAG 応答の数。
ここで紹介する埋め込みモデルとセマンティック モデルの概念は直接結びついています。 シリーズに 最新の NLP そしてシリーズへ PostgreSQL AI 既存のインフラストラクチャ上にベクトル検索を実装したい人向け。







