Vektorová databáze: Výběr a optimalizace pro inženýrství AI
Při stavbě RAG potrubí ve výrobě není výběr vektorové databáze detailem implementace: je to architektonické rozhodnutí, které ovlivňuje latenci, provozní náklady, přesnost vyvolání a škálovatelnost systému. V roce 2025 bude mít trh s vektorovými databázemi větší hodnotu 2,65 miliardy dolarů a počet dostupných řešení vzrostl drasticky, takže výběr je stále složitější.
Tento článek není marketingovým přehledem komerčních funkcí. Je to technický hluboký ponor jak vektorové databáze interně fungují, jaké indexační algoritmy používají, jak jsou nakonfigurovány a optimalizovány pro skutečné pracovní zatížení. Budeme analyzovat Qdrant, Pinecone, Milvus a Weaviate je porovnává v konkrétních rozměrech: architektura HNSW, kvantizační strategie, filtrované vyhledávání, DiskANN vs in-memory a ladění parametrů k dosažení cíle odvolání/latence definovaná vaší aplikací.
Pokud budujete systém RAG, který potřebuje zpracovat miliony dokumentů s latencí pod 50 ms a vyvolání více než 95 %, nebo pokud optimalizujete stávající systém, který spotřebovává příliš mnoho paměti, Tento článek vám poskytuje koncepční a praktické nástroje pro informovaná rozhodnutí.
Co se naučíte
- Vnitřní architektura vektorových databází: jak HNSW funguje na algoritmické úrovni
- Srovnání IVF vs HNSW vs DiskANN: kdy použít které a proč
- Skalární, součinová a binární kvantizace: kompromis mezi pamětí a přesností
- Filtrované vektorové vyhledávání: předfiltrování, následné filtrování a problém úzkých filtrů
- Praktická konfigurace Qdrant, Milvus a Pinecone s příklady kódu
- Benchmarking a ladění ve výrobě: jak měřit a zlepšovat QPS a odvolání
Vnitřní architektura: Jak funguje vektorová databáze
Vektorová databáze se liší od relační databáze nejen v datech, která spravuje, ale pro typ základní operace je třeba optimalizovat: místo přesného vyhledávání na diskrétních klíčích, provede Hledání přibližného nejbližšího souseda (ANN). na velkorozměrných prostorech, typicky 768-4096 rozměrů pro moderní LLM vložení.
Přesné hledání k nejbližších sousedů (kNN) má složitost O(n*d), kde n je číslo vektorů a d rozměrů. S 10 miliony vektorů v 1536 rozměrech (standardní velikost OpenAI ada-002), přesný dotaz by vyžadoval ~ 15 miliard operací s pohyblivou řádovou čárkou: zcela nepřijatelné pro systém v reálném čase. Všechny moderní vektorové databáze používají proto algoritmy ANN, které obětují určité vybavování, aby získaly řády v rychlosti.
Vnitřní zásobník vektorové databáze je rozdělen do několika úrovní:
- Úložné vrstvy: správa komprimovaných vektorů na disku nebo v paměti s podporou mmap pro efektivní přístup
- Indexové vrstvy: Datová struktura ANN (HNSW, IVF, DiskANN) pro navigaci ve vektorovém prostoru
- Vrstva dat/metadat: skalární atributy spojené s vektory pro filtrování
- Plánovač dotazů: rozhoduje o optimální strategii kombinací vyhledávání vektorů a filtrování užitečného zatížení
- Vrstva replikace/sharding: pro distribuované systémy jako Milvus nebo Pinecone
HNSW: Algorithmic Deep Dive
Hierarchický plavební malý svět (HNSW) je dominantním algoritmem ANN v roce 2025, používá standardně Qdrant, Weaviate je k dispozici v Milvus. Pochopte, jak to funguje interní je nezbytný pro jeho správnou konfiguraci.
HNSW konstruuje víceúrovňový hierarchický graf. Na nejvyšší úrovni je několik uzlů vzájemně pevně spojeny ("huby"), při sestupu po úrovni se hustota zvyšuje na úrovni 0, která obsahuje všechny vektory. Při vyhledávání začíná algoritmus shora a sestupuje přes úrovně a postupně zdokonaluje kandidaturu nejpodobnějších sousedů. Tento přístup je inspirován fenoménem „small world“ sociálních grafů: z jakéhokoli uzlu, k jakémukoli jinému se dostanete během pár skoků díky dálkovým spojům.
Tři základní parametry HNSW jsou:
- M (výchozí 16): maximální počet obousměrných hran na uzel. Typické hodnoty: 8-64. Zvýšení M zlepšuje zapamatování, ale zvyšuje paměť a dobu budování. Pro vysokorozměrné datové soubory (1536+) poskytuje dobré výsledky M=32-64.
- efConstruction (výchozí 100-200): velikost kandidátní listiny při stavbě indexu. Nemá to vliv na konečnou velikost indexu, ale určuje kvalitu spojení. Vyšší hodnoty = lepší index, ale pomalejší sestavení. Doporučený rozsah: 200-400 pro vysokou kvalitu.
- ef (nebo efSearch, konfigurovatelné za běhu): velikost seznamu kandidátů během dotazu. Musí být >= k (počet požadovaných výsledků). Zvýšením ef se zlepšuje vybavit, ale zvyšuje latenci. Obvykle 50-500.
Základní kompromis: M a efConstruction určit kvalitu indexu (nákladný jednorázový provoz), přičemž ef vyvážení odvolání vs latence v době dotazu (lze dynamicky měnit).
# 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 vs HNSW vs DiskANN: Který algoritmus zvolit
HNSW není jediný dostupný indexovací algoritmus. Výběr velmi závisí podle paměťových omezení, velikosti datové sady a vzoru aktualizace.
IVF (Inverted File Index)
IVF rozděluje vektorový prostor na nlist shlukování pomocí k-means.
Při dotazování pouze hledejte nprobe shluky nejblíže vektoru dotazu.
Klíčové parametry jsou nlist (číslo shluku) e nprobe
(kolik shluků zkontrolovat). Doporučený empirický vzorec: nlist = 4 * sqrt(n_vectors).
Pro IVF: Rychlé sestavení, střední paměť, dobré pro statické datové sady. Proti IVF: vyžaduje re-clustering, pokud se data hodně změní, vyvolat nižší než HNSW za stejný výpočetní rozpočet, studený start u nových kolekcí.
HNSW (Hierarchical Navigable Small World)
Jak je popsáno výše, sestrojí vícevrstvý graf. Je to nejuniverzálnější algoritmus a používá se standardně ve většině vektorových databází.
Pro HNSW: vynikající kompromis v rychlosti vyvolání, nativní podpora aktualizací přírůstkové, parametry konfigurovatelné v době dotazu. Nevýhody HNSW: vyžaduje, aby se celý index vešel do paměti RAM, stává se nepřístupným nad 50-100 milionů nosičů na standardním hardwaru.
DiskANN
DiskANN, vyvinutý společností Microsoft Research, je navržen pro datové sady, které se nevejdou do paměti RAM. Uchovává v paměti pouze kompaktní grafovou strukturu (komprimovaný graf), zatímco vektory kompletní jsou na NVMe SSD. S hardwarem PCIe Gen5 udržuje vyvolání > 95 % a latenci 10 ms na miliardách nosičů, s náklady DRAM 10-20x nižšími než ekvivalentní HNSW.
Pro DiskANN: škálovat na miliardy dopravců na komoditní hardware, náklady snížený provoz. Nevýhody DiskANN: vyžaduje rychlé NVMe SSD, vyšší latenci než HNSW in-memory, základní implementace je neměnná (FreshDiskANN obstarává aktualizace). Dostupné v Milvus, Azure PostgreSQL s rozšířeným pgvectorem a ve verzi Preview na jiných systémech.
Dejte si pozor na Anti-Pattern „HNSW pro všechno“.
Mnoho týmů konfiguruje HNSW v paměti pro více než 50 milionů vektorových datových sad a pak se ocitnou s instancemi 256 GB RAM za neudržitelné náklady. Základní pravidlo: pokud vaše datová sada překračuje 10–20 milionů nosičů a nemáte požadavky na ultra nízkou latenci (<5 ms), vyhodnoťte vážně DiskANN nebo agresivní kvantování před škálováním hardwaru.
# 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')}")
Vektorová kvantizace: Komprimujte bez ztráty vyvolání
Kvantování je nejúčinnější technika pro snížení využití paměti vektorovými databázemi, s kontrolovatelným dopadem na kvalitu výzkumu. 1536rozměrný vektor float32 zabírá 6144 bajtů (6KB). Pomocí kvantizace jej můžeme zmenšit na 384 bajtů nebo méně.
Skalární kvantizace (SQ)
Mapuje každou hodnotu float32 (4 bajty) na int8 (1 bajt), což má za následek čtyřnásobnou kompresi. Algoritmus analyzuje rozložení hodnot v každé dimenzi a určí rozsah optimální pro kvantování. Vzdálenosti se počítají přímo na int8, výpočetně efektivnější. Typická ztráta stažení je 1-3% ve srovnání s float32.
Kdy jej použít: doporučený výchozí bod pro jakékoli nasazení, 4x snížení s minimálním dopadem na kvalitu. Podporováno Qdrant, Milvus (IVF_SQ8), Weaviate (PQ se skalárním výpadkem).
Kvantifikace produktu (PQ)
Rozdělí vektor na m subvektory a každý z nich kvantizujte v číselníku z 2^nbity vstup. Typická komprese: 16-64x. S PQ každý dopravce je reprezentován jako sekvence indexů v číselníku. Přibližné vzdálenosti vypočítávají se pomocí předvýpočtu z vyhledávací tabulky (ADC - Asymetric Distance Computation).
PQ kompromisy: agresivní komprese (10-50 MB místo 10 GB) ale významná ztráta paměti (5-15 %). Vyžaduje školení kódové knihy na datové sadě. Dobré pro velké datové sady, kde je primárním omezením paměť.
Binární kvantizace (BQ)
Sníží každý rozměr na 1 bit: nejkomprimovanější možná hodnota. Přepravce na 1536 velikost se stane 192 bajtů (32x komprese ve srovnání s float32). Vypočítá se vzdálenost s Hammingovou vzdáleností (XOR + popcount), extrémně rychlý provoz na moderních CPU. Qdrant hlásí zrychlení až 40x o operacích výpočtu vzdálenosti.
Binární kvantování však funguje dobře pouze na vloženích se specifickými vlastnostmi: hodnoty musí být rozloženy symetricky kolem nuly (vlastnost splněna od OpenAI ada-002, Cohere embed-v3, modely e-5). Pro vložení s asymetrickým rozdělením, pokles paměti může být závažný (15–30 %).
Qdrant také zavedl v roce 2025 mezilehlé kvantizace 1,5bitové a 2bitové, nabízí rovnovážný bod mezi skalárním (4x) a binárním (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)
Rule of Thumb: Zvolte Kvantování
- Soubor dat <10 milionů vektorů, kritické vyvolání: nativní float32 (bez kvantování)
- Datová sada 10–100 milionů vektorů: Skalární kvantizace INT8, kvalita/paměť sweet spot
- Sada dat > 100 milionů vektorů, omezená paměť: Kvantizace produktu s hodnocením
- Mimořádně nízká latence s vestavěním OpenAI/Cohere: Binární kvantizace + skóre
Filtrované vyhledávání vektorů: Problém úzkých filtrů
V praxi většina dotazů RAG není čistě vektorové vyhledávání: chcete najít sémanticky podobné dokumenty e patřící k určitému uživatele, datový rozsah, kategorii nebo tenanta. Filtrované vyhledávání vektorů je jedním z problémů algoritmicky obtížnější ve vektorových databázích.
Základní problém: s velmi selektivními filtry (např. „pouze dokumenty za poslední měsíc“ které odpovídají 0,1 % datové sady), k nejbližších sousedů ve vektorovém prostoru by mohlo být všechny vyloučeny z filtru, což přinutí vyhledávání prozkoumat velmi velkou část grafu HNSW před nalezením k platných výsledků. To může zvýšit latenci 10-100x ve srovnání s nefiltrovaným vyhledáváním.
Strategie filtrování
Následné filtrování: spusťte vyhledávání ANN jako obvykle a poté výsledky filtrujte. To funguje, pokud filtr není příliš selektivní (vylučuje méně než 50 % výsledků). Problém: Pokud filtr vyloučí 99 % vektorů, musíte získat 100x více kandidátů.
Předfiltrování: nejprve identifikujte body, které vyhovují filtru, pak proveďte vyhledávání ANN pouze na tomto zařízení. Vyžaduje efektivní skalární index na filtrovaném poli. Funguje dobře s vysoce selektivními filtry, ale vyžaduje indexování užitečného zatížení.
Filtrovatelný HNSW (Qdrant): Qdrant implementuje sofistikované rozšíření HNSW, který přidá další hrany do grafu na základě hodnot indexovaného užitečného zatížení. Plánovač dotazů odhadne mohutnost filtru a dynamicky zvolí strategii: pokud je filtr velmi selektivní, použijte index užitečného zatížení, jinak použijte filtrovatelný HNSW.
Pro případy s vícenásobnými a úzkými kombinacemi filtrů Qdrant doporučuje použití algoritmu ACORN (Adaptive Component-Overlap Routing Network), který si lépe poradí s odpojenými grafy způsobenými agresivním filtrováním.
# 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
)
Srovnání databáze: Qdrant vs Pinecone vs Milvus vs Weaviate
Každá databáze má jiný profil síly. Neexistuje žádná univerzálně optimální volba: rozhodnutí závisí na omezeních nasazení, schopnostech týmu a konkrétních požadavcích.
Qdrant
Napsáno v Rustu, je to databáze nejlepší hodnoty provozní výkon/složitost in 2025. Supports sophisticated filtering with payload indexes and filterable HNSW, scalar/product/binary quantization, multi-vector for named vectors, sparse vectors for native hybrid search. The simplest deployment: single binary, Docker, or cloud managed. Skvělé pro týmy they want control without massive operational overhead.
Ideální pro: RAG enterprise, multi-tenant systémy, on-premise nasazení, tým se zkušenostmi s Pythonem, ale bez složité infrastruktury Kubernetes.
Borová šiška
Plně spravované, bez serveru, nulové operace. Cena je vyšší než u alternativ s vlastním hostitelem ale zcela eliminuje náklady na provoz infrastruktury. Skvělé pro týmy, které raději se zaměřují na produkt bez správy klastrů. Podporuje moduly bez serveru s transparentní automatické škálování a multiregionální replikace. Latence je trvale nízká díky optimalizované infrastruktuře.
Ideální pro: startupy v rané fázi, malé týmy, variabilní pracovní vytížení, důkazy konceptu, které se stávají výrobou bez přepracování.
Milvus / Zilliz Cloud
Nejvyspělejší distribuovaný systém s kompletními funkcemi. Podporuje všechny typy indexů (HNSW, IVF, DiskANN, ScanN, GPU-accelerated), automatické sharding na Kubernetes, oddělení výpočet/paměť. Cloudová verze (Zilliz) vítězí v testech propustnosti na datových sadách >100 milionů vektorů. Značná provozní režie na Kubernetes.
Ideální pro: datová sada >50 milionů vektorů, tým s infrastrukturou Kubernetes stávající, požadavky na maximální propustnost, akcelerace GPU.
Weaviate
Umístěný mezi čistě vektorovou databází a znalostním grafem. Podporuje vestavěné moduly pro automatické generování vkládání (text2vec-openai, text2vec-cohere), dotaz GraphQL rozhraní a hybridizace s nativním BM25. Vyžaduje více paměti než ostatní pro stejnou datovou sadu. Skvělé pro týmy, které chtějí integrovat získávání a znalostní graf.
Ideální pro: sémantické vyhledávání s grafem znalostí, týmy využívající GraphQL, přímou integraci s poskytovateli modelů bez správy kanálů vkládání.
Rozhodovací matice: Jak si vybrat
- Malý tým, rychlost vývoje: Šiška (nula ops) nebo Qdrant (jednoduchost)
- Datová sada >50 milionů vektorů, vysoká propustnost: Milvus s indexem DiskANN nebo GPU
- RAG pro více nájemců se složitými filtry: Qdrant (filtrovatelný HNSW)
- Graf znalostí + sémantické vyhledávání: Weaviate
- Již na PostgreSQL, mírná hlasitost: pgvector (vyhýbá se další infrastruktuře)
- Nativní hybridní vyhledávání bez režie: Qdrant řídké vektory nebo Weaviate BM25
Šiška: Konfigurace a optimalizace
Pinecone v letech 2024–2025 dále zjednodušil své SDK s architekturou bez serveru. Již nemusíte explicitně konfigurovat indexovací algoritmus: Pinecone to zvládá interně výběr indexu na základě velikosti datové sady.
# 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"
)
Optimalizace paměti a ladění výroby
Vektorová databáze ve výrobě vyžaduje pozornost věnovanou mnoha dimenzím: ne pouze vyvolání a latence, ale také využití paměti, propustnost, chování při zatížení, a dlouhodobé provozní náklady.
Odhad paměťové stopy
Základní vzorec pro odhad potřebné paměti (float32, žádné kvantování):
- Nezpracované vektory: n_vectors * dim * 4 bajty
- HNSW graf: n_vectors * M * 2 * 8 bajtů (přibližně, závisí na implementaci)
- Obsah/metadata: proměnná, typicky 100-500 bajtů na vektor
- Režie systému: ~20-30% z celkového počtu
Příklad: 5 milionů vektorů při 1536 dim s HNSW M=32: Vektory: 5M * 1536 * 4 = ~29 GB. HNSW graf: 5M * 32 * 2 * 8 = ~2,5 GB. Odhadovaná celková režie: ~38 GB RAM. S SQ8: ~ 11 GB. S binárním: ~ 1,5 GB.
Ladění výkonu Qdrant ve výrobě
# 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
Benchmarking a zpětné měření
Žádná optimalizace není platná bez přísného měření. Standardní rámec hodnocení vektorových databází je založeno na třech základních metrikách:
- Recall@k: procento skutečných k nejbližších sousedů nalezených mezi k výsledky se vrátil. Je to nejdůležitější ukazatel kvality. Vzorec: |získáno ∩ true| /k
- QPS (dotazy za sekundu): propustnost systému při zatížení. Obvykle se měří s pevným cílem vyvolání (např. "QPS @ reminiscence=0,95").
- Percentily latence (p50, p95, p99): průměrná latence je zavádějící. V produkci se počítá p99: 99 % dotazů musí být dokončeno v rámci SLA.
Referenčním benchmarkem pro vektorové databáze je ann-benchmarks.com, který měří všechny hlavní systémy na standardizovaných souborech dat (SIFT1M, GIST1M, Rukavice-100-úhlová). Výsledky 2024-2025 ukazují, že Qdrant a Milvus patří mezi lídry pro kompromis mezi propustností vyvolání, s Pinecone vynikající pro konzistenci latence 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}")
Hybridní vyhledávání: Vector + BM25 ve vektorové databázi
Moderní vektorové databáze již nejsou čistě vektorové systémy: mnoho jich podporuje nyní hybridní vyhledávání, které kombinuje husté vektory s řídkými vektory (BM25/TF-IDF). Toto téma je podrobně prozkoumáno v článku věnovaném Hybrid Retrieval, ale je důležité pochopit, jak se integruje na úrovni vektorové databáze.
Qdrant nativně podporuje řídké vektory: můžete uložit jak hustý vektor (sémantické vkládání), že řídký vektor (váhy BM25) pro každý dokument a spusťte hybridní dotazy v jediném požadavku s RRF (Reciprocal Rank Fusion) nebo vlastní fúzí skóre.
Weaviate má hybridní vyhledávání integrované do schématu GraphQL: určuje alfa (0 = čistý BM25, 1 = čistý vektor) pro řízení relativní hmotnosti. Milvus 2,4+ zavedena řídká-hustá fúze. Borová šiška podporuje sparse-dense s kodérem Pinecone Sparse nebo vlastním modelem BM25.
Chcete-li se dozvědět více o implementacích hybridního vyhledávání a metodách fúze (RRF, vážený součet, naučená fúze), viz článek Hybridní vyhledávání: BM25 + vektorové vyhledávání této série.
Cross-Link: Související články
- RAG: Retrieval-Augmented Generation Explained - Základy RAG pro kontextualizaci role vektorové databáze
- Vkládání a vyhledávání vektorů: BERT vs Sentence Transformers - Jak vybrat model zapuštění pro vaše potrubí
- Hybridní vyhledávání: BM25 + vektorové vyhledávání - Zkombinujte vektorové vyhledávání s vyhledáváním klíčových slov pro lepší zapamatování
- PostgreSQL s pgvector - Vektorové vyhledávání na PostgreSQL bez další infrastruktury
Kontrolní seznam výroby
Před uvedením vektorové databáze do výroby zkontrolujte tyto kritické body:
- Srovnání vaší skutečné datové sady: obecné výsledky se nepřenášejí automaticky na váš případ použití. Měřte vybavování a latenci pomocí skutečných dotazů.
- Konfigurované indexy užitečného zatížení: každé pole, které filtrujete, musí mít index, jinak filtrování prohledá všechny body.
- Vhodné kvantování: vyhodnotit SQ8 jako výchozí, změřit ztrátu vyvolání. Pokud je to přijatelné, požádejte nyní – úspora paměti je značná.
- Zálohy a snímky: konfigurovat automatické snímky. Vektorové databáze ne vždy mají ACID transakce; selhání během zpracování může poškodit index.
- Sledování: plot indexed_vectors_count vs vectors_count for detekovat zpoždění v indexování, které snižuje výkon dotazů.
- Velikost paměti: vypočítat skutečnou stopu před nasazením. Server s nedostatečnou pamětí způsobuje swapování, které ničí latenci.
- Test s úzkými filtry: pokud vaše aplikace používá velmi selektivní filtry, explicitně otestujte tyto scénáře. Latence pod úzkými filtry je velmi odlišná z toho na nefiltrované vyhledávání.
Běžné anti-vzorce, kterým je třeba se vyhnout
- Příliš nízký práh indexování: s indexing_threshold=0 nebo velmi nízkým, každé vložení spustí reindexaci, takže příjem je velmi pomalý. Použijte prahové hodnoty 10 000–100 000 pro hromadné vkládání a poté optimalizujte.
- M příliš vysoké bez měření: M=128 není vždy lepší než M=32. Nad určitým bodem se paměť mírně zlepšuje, ale paměť roste lineárně. Změřte pomocí své datové sady.
- Žádný index užitečného zatížení ve filtrovaných polích: bez indexu, jakákoli podmínka filtru and O(n). With 10M vectors, an unindexed filter makes the difference between 5ms and 5000ms.
- Dimenze nenormalizovaných vektorů s kosinovou podobností: pokud používáte kosinus podobnosti, musí být vektory normalizovány. Některé modely je nenormalizují standardně. Vektory nenormalizované s kosiny dávají sémanticky nesprávné výsledky.
Závěry a další kroky
Volba je, že optimalizace vektorové databáze je jedním z nejvíce technických a nejpůsobivějších aspektů inženýrství AI. Neexistují žádné univerzální odpovědi: každý systém má profil síly různé a optimální konfigurace závisí na vaší konkrétní pracovní zátěži.
Doporučená cesta pro nový projekt: Začněte s Qdrant s SQ8 pro provozní jednoduchost a dobrý výkon pak změřte vyvolání a latenci na vaší skutečné datové sadě. Pokud výkon nestačí, prozkoumejte ladění M a ef. Pokud je problém s pamětí, zvažte kvantování produktu nebo DiskANN. Pokud již máte PostgreSQL a střední objem (<5 milionů vektorů), před přidáním nové infrastruktury zvažte pgvector.
Další články této série staví na těchto základech: v článku na Hybridní vyhledávání uvidíme, jak zkombinovat vektorové vyhledávání s BM25, abychom zlepšili vyvolání přesných dotazů, zatímco v článku o RAG ve výrobě uvidíme, jak měřit celkový dopad výběru vektorové databáze na kvalitu odpovědí RAG.
Zde prezentované koncepty vkládání a sémantické modely se přímo propojují do série Moderní NLP a k seriálu PostgreSQL AI pro ty, kteří chtějí implementovat vektorové vyhledávání na stávající infrastruktuře.







