eduardweb.
RAG & EmbeddingsAvansat#rag#postgres#pgvector#sql#hybrid-search

RAG avansat cu pgvector: de ce căutarea semantică simplă dă chix și cum o repari cu hybrid search

De Adrian Voicu, 7 iun. 2026 · 1 vizualizări · 2 like-uri

Postat acum 2 zile
sql
-- Exemplu de căutare hibridă folosind RRF (Reciprocal Rank Fusion) în Postgres
WITH vector_search AS (
  SELECT id, title, 
         row_number() OVER (ORDER BY embedding <=> $1) AS rank
  FROM documents
  ORDER BY embedding <=> $1
  LIMIT 50
),
text_search AS (
  SELECT id, title, 
         row_number() OVER (ORDER BY ts_rank_cd(fts_vector, plainto_tsquery('romanian', $2)) DESC) AS rank
  FROM documents
  WHERE fts_vector @@ plainto_tsquery('romanian', $2)
  LIMIT 50
)
SELECT
  COALESCE(v.id, t.id) AS doc_id,
  COALESCE(v.title, t.title) AS title,
  -- k=60 este standardul recomandat pentru RRF
  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;

Dacă faci RAG, probabil te-ai lovit deja de faza în care căutarea semantică pură dă chix pe termeni preciși, cum ar fi coduri de produse, ID-uri sau denumiri exacte de branduri. Am pățit asta pe un proiect cu peste 120.000 de documente tehnice, unde userii căutau chestii ultra-specifice gen „eroare 404 pe modulul X”. Embeddings-urile de la OpenAI pur și simplu ignorau specificitatea codurilor și returnau rezultate generale despre erori HTTP.

Soluția nu a fost să mai adaug o bază de date de vectori dedicată (încă o chestie de sincronizat și mentenanță în plus), ci să rămân în Postgres și să configurez un hybrid search solid folosind pgvector și Full-Text Search-ul nativ.

De ce eșuează embeddings-urile singure

Embeddings-urile (cum sunt cele generate de text-embedding-3-small de la OpenAI, cu 1536 de dimensiuni) sunt excelente pentru a înțelege contextul conceptual. Dacă cauți „cum repar scurgerea de apă”, vor returna documente despre instalații sanitare chiar dacă cuvântul „repar” nu apare explicit.

Dar au o problemă majoră de granularitate. Când userul caută exact piesa „supapă-34B”, distanța cosinus (cosine similarity) s-ar putea să prioritizeze „supapă-34A” doar pentru că sunt extrem de apropiate în spațiul vectorial. Pentru un sistem de producție, asta e rețeta perfectă ca userul să primească informații greșite.

Setup-ul de pgvector cu HNSW

Pentru a asigura performanța la cele peste 120k de documente, indexul implicit ivfflat nu a fost de ajuns deoarece acuratețea scădea destul de mult la modificări frecvente de date. Am mers pe un index HNSW (Hierarchical Navigable Small World).

Un trade-off sincer pe care trebuie să-l știi: indexul HNSW mănâncă foarte mult RAM. La 120k de vectori de dimensiune 1536, indexul a ocupat instant în jur de 1.4 GB RAM doar ca să poată face căutarea rapid. Dacă nu ai destul RAM alocat pentru shared_buffers în Postgres, o să înceapă să scrie pe disc și performanța se prăbușește.

Magia hibridă: Reciprocal Rank Fusion (RRF)

Ca să combinăm cele mai bune lumi (căutarea semantică din pgvector și căutarea exactă din keyword search - BM25 style), folosim un algoritm numit Reciprocal Rank Fusion (RRF).

Ideea din spatele RRF este simplă: rulăm ambele căutări separat, le clasificăm rezultatele de la 1 la N, iar apoi le combinăm scorurile folosind o formulă matematică simplă: 1 / (k + rank). Valoarea standard pentru constanta k este 60. Această abordare normalizează scorurile, indiferent de scalele complet diferite folosite de distanța cosinus și de algoritmul de text-search din Postgres (ts_rank_cd).

În codul de mai jos, am implementat o interogare SQL curată care face exact acest lucru printr-un query hibrid de tip CTE. Pe proiectul nostru, asta a crescut relevanța primelor 3 rezultate returnate cu peste 40%.

Compromisul de care nu scrie în tutoriale

Postgres este genial pentru că ai ACID, ai datele relaționale și vectorii în același loc, iar consistența e garantată. Dar nu e glonțul de argint.

Dacă ai de gând să scalezi la peste 5-10 milioane de documente cu update-uri în timp real la vectori, Postgres va începe să gâfâie la rebuild-ul indecșilor HNSW. Generarea indexului blochează resurse masive. În plus, parserul de text search nativ din Postgres (tsvector) nu este la fel de flexibil pe limba română sau pe corectarea automată a greșelilor de tipar cum este Elasticsearch.

Pentru proiecte medii și mari (până în câteva milioane de rânduri), însă, simplitatea de a avea totul într-o singură bază de date bate orice alt stack complex.

Voi cum gestionați căutarea hibridă în producție? Mergeți pe all-in-one în Postgres sau preferați să mențineți o infrastructură separată cu Qdrant/Pinecone și Elasticsearch?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.