-- Interogare hibridă folosind Reciprocal Rank Fusion (RRF)
WITH semantic_search AS (
SELECT id, text,
ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank_sem
FROM documents
ORDER BY embedding <=> $1
LIMIT 40
),
keyword_search AS (
SELECT id, text,
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(fts_vector, plainto_tsquery('romanian', $2)) DESC) AS rank_key
FROM documents
WHERE fts_vector @@ plainto_tsquery('romanian', $2)
LIMIT 40
)
SELECT
COALESCE(s.id, k.id) AS id,
COALESCE(s.text, k.text) AS text,
-- Scor RRF cu constanta standard 60
COALESCE(1.0 / (60 + s.rank_sem), 0.0) + COALESCE(1.0 / (60 + k.rank_key), 0.0) AS rrf_score
FROM semantic_search s
FULL OUTER JOIN keyword_search k ON s.id = k.id
ORDER BY rrf_score DESC
LIMIT 10;Toată lumea sare pe Pinecone, Milvus sau Qdrant când aude de RAG. Eu am preferat să rămân în Postgres cu pgvector pentru un proiect cu 50.000 de documente medicale și nu regret nicio secundă alegerea făcută. Economisești timp cu infrastructura și ții toate datele într-un singur loc, protejate de aceleași tranzacții ACID cu care te-ai obișnuit.
De ce pgvector și nu o bază dedicată?
Am avut cazul acum câteva luni la un startup. Echipa de AI insista pentru o bază de date vectorială separată. Le-am zis direct: „Băi, avem deja Postgres pe RDS. De ce să mai administrăm o infrastructură, să facem sync-uri dubioase prin CDC și să ne batem capul cu permisiunile?”.
pgvector este incredibil de simplu de utilizat. Adaugi extensia, pui o coloană de tip vector(1536) (dacă folosești embeddings de la OpenAI) și gata.
Dar haideți să fim sinceri cu trade-off-urile. Dacă ai peste 10 milioane de vectori și vrei latență sub 5ms la throughput masiv, Postgres o să înceapă să gâfâie. Indexul HNSW (Hierarchical Navigable Small World) mănâncă foarte mult RAM. Însă pentru 90% din proiectele de pe piață, unde ai sub un milion de documente, Postgres este mai mult decât suficient. Am eliminat complet latența de rețea dintre baza de date operațională și cea vectorială.
Problema cu căutarea pur semantică
Căutarea semantică (pe bază de cosine similarity pe embeddings) este excelentă pentru a înțelege contextul și sinonimele. Dar este incredibil de proastă când userul caută un ID de produs exact, un cod de eroare sau un acronim specific (de exemplu, „RO-442”). Vectorii tind să „șteargă” aceste detalii exacte în favoarea sensului general.
Aici intervine căutarea hibridă (Hybrid Search). Combinăm Full Text Search-ul nativ din Postgres (care folosește BM25 sub capotă prin tsvector) cu căutarea semantică din pgvector. Pentru a le combina elegant, folosim Reciprocal Rank Fusion (RRF), un algoritm simplu care acordă scoruri în funcție de poziția rezultatului în ambele liste.
Optimizarea indexului: HNSW vs IVFFlat
La început, pe un set mic de date, căutările secvențiale mergeau strună fără niciun index. Când am trecut de 20.000 de rânduri, interogările au început să facă spike-uri de CPU.
Am testat ambele tipuri de index:
- IVFFlat: Se construiește rapid, dar recall-ul scade destul de mult dacă nu reîmprospătezi listele des.
- HNSW: Are un recall excelent și performanță foarte bună la citire, dar build-ul durează mult și consumă memorie.
Am mers pe HNSW. Am setat m = 16 și ef_construction = 64. Prin reglarea fină a acestor parametri, am economisit cam 30% din timpul de build al indexului pe pipeline-ul de CI/CD, păstrând în același timp o acuratețe a căutării de peste 95%.
Voi cum abordați problema? Ați spart deja datele în baze dedicate sau preferați să stoarceți tot ce se poate din Postgres?