WITH vector_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
FROM document_chunks
ORDER BY embedding <=> $1
LIMIT 40
),
text_search AS (
SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(fts_tokens, query) DESC) AS rank
FROM document_chunks, plainto_tsquery('romanian', $2) query
WHERE fts_tokens @@ query
ORDER BY rank
LIMIT 40
)
SELECT
c.id,
c.content,
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
JOIN document_chunks c ON c.id = COALESCE(v.id, t.id)
ORDER BY rrf_score DESC
LIMIT 10;Am mutat anul trecut un pipeline de RAG de pe Qdrant înapoi în Postgres pentru un client cu ~400.000 de documente tehnice. Am economisit vreo 350$ pe lună pe infrastructură și am scăzut latența medie a sistemului cu 40ms pentru că am eliminat un network hop extern. Dacă ai deja Postgres în stack, aruncarea unei baze de date vectoriale dedicate pe cluster e adesea o complicare inutilă de arhitectură.
Când e pgvector suficient și unde suferă
Marea problemă cu vector embeddings e că sunt geniali pe semantică, dar catastrofali pe exact matches. Dacă un utilizator caută un cod de eroare specific precum "ERR-80070005" sau un SKU de produs gen "SKU-9942", căutarea vectorială pura o să-ți întoarcă rezultate despre erori similare de permisiuni, dar s-ar putea să rateze fix documentul care conține exact acel string.
Să folosești doar căutare după vectori în producție e o rețetă sigură pentru tichete de suport de la useri frustrați. Aici intervine indexarea HNSW (Hierarchical Navigable Small World) introdusă recent în pgvector. Spre deosebire de IVFFlat, care necesita reindexări periodice când se schimba volumul de date, HNSW e rapid și precis. Trade-off-ul e că mănâncă memorie RAM la greu în momentul construirii și îți crește timpul de build al indexului considerabil.
Hybrid Search nativ via RRF (Reciprocal Rank Fusion)
Ca să rezolvi problema exact match-urilor fără să adaugi ElasticSearch sau Pinecone, folosești tsvector (full-text search nativ, echivalentul BM25) combinat cu distanța cosinus (<=>) din pgvector.
Cea mai curată metodă de a îmbina scorurile din cele două lumi diferite (unde vectorii dau scoruri între 0 și 1, iar FTS dă scoruri nelimitate prin ts_rank) este RRF. Treci o dată cu vectorul, o dată cu textul, le calculezi rank-ul relativ, și le aduni cu o constantă de smoothing (de regulă k=60).
Codul de mai jos rulează totul într-un singur query cu CTE-uri, fără să fie nevoie să faci orchestration din backend în Node.js sau Python.
Setări de producție pe care mulți le ignoră
La 400k chunk-uri cu vectori de 1536 de dimensiuni (OpenAI text-embedding-3-small), o să lovești repede limita implicită de memorie din Postgres. Dacă nu setezi maintenance_work_mem la cel puțin 1GB-2GB înainte să creezi indexul HNSW, procesul o să dureze ore întregi sau va crape cu Out of Memory.
La query-time, mai e un parametru critic: hnsw.ef_search. Valoarea implicită (40) e gândită pentru resurse puține, dar în producție am urcat-o la 100. Am crescut recall-ul de la 88% la 97%, adăugând doar vreo 8ms pe interogare.
Atât timp cât nu ai zeci de milioane de vectori și sute de scrieri pe secundă, un Postgres bine tunat cu hibrid search mătură pe jos cu configurațiile complexe multi-database.
Voi ce folosiți în prod pentru RAG? Ați rămas pe Postgres sau a fost nevoie să migrați pe soluții dedicate când a crescut volumul?