WITH semantic_search AS (
SELECT id, 1 - (embedding <=> $1) AS semantic_score
FROM documents
ORDER BY embedding <=> $1
LIMIT 50
),
text_search AS (
SELECT id, ts_rank_cd(fts_document, query) AS text_score
FROM documents, to_tsquery('romanian', $2) query
WHERE fts_document @@ query
ORDER BY text_score DESC
LIMIT 50
)
SELECT
coalesce(s.id, t.id) AS document_id,
(coalesce(s.semantic_score, 0) * 0.6) + (coalesce(t.text_score, 0) * 0.4) AS hybrid_score
FROM semantic_search s
FULL OUTER JOIN text_search t ON s.id = t.id
ORDER BY hybrid_score DESC
LIMIT 10;Am mutat recent tot pipeline-ul de RAG de pe o soluție cloud dedicată direct în Postgres-ul nostru principal folosind pgvector. Chestia asta ne-a salvat de la o grămadă de sincronizări de date obositoare și a redus latența de rețea la zero. Dacă ai deja Postgres în stack, n-are sens să te complici cu alte baze de date vectoriale până nu atingi volume cu adevărat masive.
Setup-ul de care te lovești la început
Când am pornit la drum pe un proiect cu vreo 120.000 de documente tehnice, prima tentație a fost să arunc totul în Pinecone sau Qdrant. Dar sincronizarea între baza de date tranzacțională și baza vectorială devenise rapid un coșmar: ID-uri orfane la ștergeri, lag la update-uri și costuri aiurea pe infrastructură. Am activat extensia pgvector în RDS și viața s-a simplificat enorm.
Pentru embeddings generate cu text-embedding-3-small de la OpenAI, ai nevoie de o coloană de tip vector(1536). Procesul e simplu, dar m-am prins repede că o căutare secvențială (exactă) devine inutilizabilă odată ce treci de câteva zeci de mii de rânduri.
Indexarea HNSW vs IVFFlat (Trade-off-ul pe RAM)
Aici e prima capcană unde te poți arde dacă nu ești atent la resurse. IVFFlat e rapid de construit și consumă puțin RAM, dar dacă datele tale se schimbă des, acuratețea căutării scade dramatic în timp fără un re-index periodic. Am mers direct pe HNSW (Hierarchical Navigable Small World).
Trade-off-ul cu HNSW e dureros la resurse: indexul pentru cele 120k de rânduri a mâncat aproape 2GB de RAM instant, iar procesul de build a durat vreo 8 minute pe o instanță db.m6g.xlarge. În schimb, timpul de răspuns la interogări a scăzut de la 150ms la sub 12ms. Dacă nu ai destul RAM liber pe instanță pentru a ține indexul complet în memorie, Postgres o să facă swap pe disc și performanța se va prăbuși.
De ce doar vectorii nu sunt suficienți: Hybrid Search cu BM25
După prima săptămână în producție, userii au început să se plângă. Căutarea semantică e genială pentru concepte generale, dar e praf când cineva caută un cod de eroare exact (ex: "ERR-403-DB") sau un ID de produs. Modelele de embedding tind să ignore token-urile foarte specifice dacă nu au fost antrenate fix pe ele.
Soluția? Hybrid search. Combinăm căutarea clasică Full-Text Search din Postgres (care folosește o variantă de BM25 sub capotă) cu distanța de cosinus din pgvector. Am folosit o interogare cu un scor ponderat: 60% relevanță semantică și 40% potrivire pe text exact. Rezultatele au fost spectaculoase, iar acuratețea răspunsurilor LLM-ului a crescut vizibil.
Postgres se descurcă excelent ca motor de căutare hibrid până pe la câteva milioane de înregistrări, fără să mai ai grija sincronizării datelor între sisteme diferite. Voi ce folosiți în producție pentru RAG? Mergeți pe Postgres sau ați fost nevoiți să treceți la soluții dedicate?