Furkan Turkoglu
TR EN
All posts

Why vector similarity alone falls short: PostgreSQL FTS, pgvector, and RRF

Furkan Turkoglu 2 min read

The Blind Spot of Dense Embeddings

Relying exclusively on dense vector embeddings works fine in demos, then degrades quietly the moment queries include exact technical identifiers. When an engineer searches for RFC 8785, SQLx, or a specific statutory clause like Article 41/A, cosine similarity over dense embeddings routinely dilutes that exact token into a broad conceptual neighborhood—ranking semantically adjacent documents above the exact match.

Rather than introducing a separate vector store and dealing with dual-write consistency bugs, combining PostgreSQL full-text search (tsvector) and pgvector HNSW indexes inside a single query via Reciprocal Rank Fusion (RRF) keeps the infrastructure footprint minimal and the retrieval pipeline deterministic.

Why Rank Fusion Beats Score Normalization

ts_rank_cd produces unbounded term-density scores, whereas pgvector cosine distance lives in [0, 2]. Linearly blending raw scores (0.7 * dense + 0.3 * lexical) forces brittle min-max calibration that shifts whenever corpus size grows. RRF sidesteps score distributions altogether and fuses candidates strictly by ordinal rank.

Fusing Lexical and Vector Arms in SQL

Bounding each retrieval arm inside a CTE and merging ranks over a FULL OUTER JOIN:

WITH semantic AS (
    SELECT id,
           ROW_NUMBER() OVER (ORDER BY embedding <=> $1::vector) AS r_sem
    FROM repositories
    ORDER BY embedding <=> $1::vector
    LIMIT 60
),
lexical AS (
    SELECT id,
           ROW_NUMBER() OVER (
               ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC
           ) AS r_lex
    FROM repositories
    WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
    LIMIT 60
)
SELECT COALESCE(s.id, l.id) AS repo_id,
       COALESCE(1.0 / (60 + s.r_sem), 0.0) +
       COALESCE(1.0 / (60 + l.r_lex), 0.0) AS rrf_score
FROM semantic s
FULL OUTER JOIN lexical l ON s.id = l.id
ORDER BY rrf_score DESC
LIMIT 20;

Explicit Trade-Offs

The smoothing constant k = 60 prevents an outlier ranked #1 in only one retrieval arm from overpowering candidates corroborated across both arms. Documents scoring near the top of both lexical and semantic indexes surface consistently at the top of the fused candidate set.

Two operational constraints matter in production:

  1. HNSW Memory Residency: Latency spikes sharply once the HNSW graph outgrows available RAM; tuning m and ef_construction alongside partial predicates keeps index size predictable.
  2. Bounded Reranking: RRF excels at high-recall candidate generation (top-50), making it an ideal first stage before running a heavier cross-encoder reranker over a strictly bounded candidate window.