WITH vector_search AS (
SELECT id, row_number() OVER (ORDER BY embedding <=> $1) as rank
FROM documents
ORDER BY embedding <=> $1
LIMIT 50
),
text_search AS (
SELECT id, row_number() OVER (ORDER BY ts_rank_cd(fts_tokens, query) DESC) as rank
FROM documents, to_tsquery('romanian', $2) query
WHERE fts_tokens @@ query
LIMIT 50
)
SELECT
coalesce(v.id, t.id) AS document_id,
coalesce(1.0 / (60 + v.rank), 0.0) + coalesce(1.0 / (60 + t.rank), 0.0) AS rrf_score
FROM vector_search v
FULL OUTER JOIN text_search t ON v.id = t.id
ORDER BY rrf_score DESC
LIMIT 10;Am trecut prin febra bazelor de date vectoriale dedicate acum un an. După ce m-am chinuit să sincronizez Postgres-ul cu Pinecone pentru un proiect cu vreo 2 milioane de chunk-uri, mi-am băgat picioarele și am mutat totul în pgvector. Când ai datele tranzacționale într-o parte și vectorii în alta, consistența devine rapid un coșmar.
Să ții două baze de date sincronizate în timp real înseamnă cozi de mesaje, mecanisme de retry și inevitabilele momente când un update eșuează și rămâi cu date incoerente. Mutând totul în Postgres, am rezolvat problema asta printr-o simplă tranzacție ACID SQL.
Setup-ul și indexarea: HNSW vs IVFFlat
Pentru embeddings de 1536 de dimensiuni (text-embedding-3-small de la OpenAI), ai în principal două opțiuni de indexare în pgvector: IVFFlat și HNSW.
La început am mers pe IVFFlat pentru că se construiește repede. Mare greșeală. Pe măsură ce baza de date a crescut, am început să avem query-uri care durau și 800ms sub sarcină. Am trecut la HNSW (Hierarchical Navigable Small World). Diferența a fost uriașă: query-urile au scăzut la 15ms.
Dar există un trade-off destul de dureros: HNSW mănâncă RAM de rupe și build-ul indexului durează o veșnicie. Pentru cele 2 milioane de înregistrări, indexul HNSW a ocupat în jur de 3 GB de RAM și a durat cam 40 de minute să se construiască pe o instanță de producție. Dacă nu ai destul shared_buffers configurat în Postgres, baza de date va începe să facă swap pe disc și performanța se va prăbuși.
Hybrid Search: De ce doar vectorii nu sunt suficienți
Căutarea semantică (embeddings) e genială pentru a înțelege intenția utilizatorului, dar e praf când vine vorba de potriviri exacte. Dacă un user caută un cod de produs specific, gen "XJS-900-A", căutarea vectorială s-ar putea să-i returneze alte produse similare ca descriere, dar nu pe cel exact.
Soluția este hybrid search: combinăm căutarea semantică (pgvector) cu căutarea clasică pe text (Full-Text Search-ul nativ din Postgres, care folosește BM25 sub capotă).
Pentru a combina cele două scoruri (care au scale complet diferite), se folosește un algoritm numit Reciprocal Rank Fusion (RRF). Ideea e simplă: nu adunăm scorurile brute, ci penalizăm rezultatele în funcție de poziția lor în fiecare listă. Un document care e pe locul 2 la vectori și pe locul 3 la text va fi mai relevant global decât unul care e pe locul 1 la vectori, dar pe locul 100 la text.
Implementarea SQL pentru Hybrid Search
Mai jos ai query-ul pe care îl folosim în producție. Folosește două CTE-uri (Common Table Expressions) pentru a rula ambele căutări în paralel, apoi le combină folosind formula RRF standard cu o constantă de penalizare de 60.
Ce am observat în producție este că valoarea acelei constante (60) este destul de robustă, dar poți să te joci cu ea în funcție de cât de mult vrei să prioritizezi rezultatele de top.
În final, am economisit cam 30% la costurile de infrastructură eliminând serviciul SaaS vectorial și am scăpat de un întreg pipeline de sincronizare. Postgres nu e doar o bază de date relațională; cu extensiile potrivite și puțin tuning, bate la fundul gol soluțiile nișate pentru 95% din cazurile de utilizare.
Voi ați făcut trecerea de la baze de date vectoriale dedicate înapoi la Postgres, sau ați rămas pe soluții dedicate?