eduardweb.
RAG & EmbeddingsAvansat#rag#postgres#pgvector#sql#embeddings

De ce am renunțat la Pinecone pentru pgvector și cum faci hybrid search curat în Postgres

De Mihai Popescu, 5 aug. 2026 · 11 vizualizări · 3 like-uri

Postat 5 aug. 2026
sql
WITH semantic_search AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
  FROM document_chunks
  ORDER BY embedding <=> $1
  LIMIT 40
),
lexical_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 ts_rank_cd(fts_tokens, query) DESC
  LIMIT 40
)
SELECT 
  COALESCE(s.id, l.id) AS id,
  d.content,
  COALESCE(1.0 / (60 + s.rank), 0.0) + COALESCE(1.0 / (60 + l.rank), 0.0) AS rrf_score
FROM semantic_search s
FULL OUTER JOIN lexical_search l ON s.id = l.id
JOIN document_chunks d ON d.id = COALESCE(s.id, l.id)
ORDER BY rrf_score DESC
LIMIT 10;

Dacă faci RAG în producție, probabil ai realizat deja că vector search-ul pur te lasă la greu exact când ai mai mare nevoie. Se întâmplă mereu când userul caută un cod de piesă, o eroare din loguri sau un nume propriu scurt. M-am lovit tare de chestia asta anul trecut la o platformă de e-learning cu peste 180k chunk-uri de text, unde OpenAI text-embedding-3-small genera rezultate halucinante pe termeni tehnici specifici.

Soluția n-a fost să mai adaug un SaaS extern scump gen Pinecone sau Qdrant. Am adus totul în Postgres folosind pgvector pentru partea semantică și tsvector nativ pentru BM25. Câștigul? Zero dureri de cap cu sincronizarea datelor și o latență medie sub 45ms per query.

Problema căutării pur semantice

Vectorii înțeleg contextul și intenția, dar sunt proști la potriviri exacte. Dacă un user caută ERR_502_TIMEOUT, un model de embeddings o să returneze paragrafe despre timeouts în general, erori HTTP sau retries. Poate că articolul exact care conține stringul ERR_502_TIMEOUT ajunge pe locul 15 în rezultatele de cosine similarity.

Aici intervine BM25 (sau full-text search-ul clasic din Postgres). El nu înțelege că "mașină" e înrudit cu "automobil", dar găsește garantat stringul exact. Când le pui împreună, obții ce e mai bun din ambele lumi.

Arhitectura: pgvector + tsvector în același tabel

Pentru un volum de până la 2-3 milioane de vectori, Postgres duce lejer dacă e configurat cum trebuie. Secretul e să creezi indexul potrivit pentru fiecare tip de căutare:

  1. Index HNSW (Hierarchical Navigable Small World) pe coloana de vectori pentru căutare semantică rapidă.
  2. Index GIN pe coloana tsvector pentru căutare lexicală (BM25 / text search).

Cea mai curată metodă de a combina rezultatele este RRF (Reciprocal Rank Fusion). În loc să aduni scorurile brute (care au scări complet diferite), le dai un rank în fiecare listă de rezultate și aplici formula: score = 1 / (60 + rank_vector) + 1 / (60 + rank_text).

Trade-off-uri reale și de ce să ai grijă

Nu totul e lapte și miere. Am învățat pe pielea mea câteva lucruri pe care nu ți le spune nimeni în tutoriale scurte:

  • Consumul de RAM la indexare: HNSW pe vectori de 1536 de dimensiuni papă memorie serios. Când am făcut indexarea pe cele 180k rânduri, Postgres mi-a crapat pentru că maintenance_work_mem era rămas pe valoarea default. A trebuit să-l urc temporar la 2GB.
  • Latența la scrieri: Dacă ai un sistem cu scrieri foarte frecvente (mii de insert-uri pe minut), indexul HNSW o să-ți încetinească tranzacțiile. În cazul ăsta, e mai bine să inserezi direct și să faci re-indexare asincronă sau să folosești IVFFlat (deși IVFFlat necesită antrenare pe date existente).
  • Limita de scalare: Dacă treci de 5-10 milioane de documente și ai sute de căutări pe secundă, Postgres va începe să gâfâie pe RAM. Abia în punctul ăla merită să migrezi spre un vector DB dedicat ca Qdrant sau Milvus.

Pentru 90% din aplicațiile de pe piață, arhitectura asta hibridă simplifică infrastructura enorm. Ai ACID, ai backup-uri normale cu pg_dump, ai un singur baze de date de gestionat.

Voi ce folosiți pentru RAG în producție? Ați rămas pe Postgres sau ați mutat partea de vectori într-un engine separat?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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