IAnswerable

Build RAG Without a Vector Database

16 Dec 2025

If your document set fits in a few hundred MB, you don’t need a vector database. Postgres full-text search gets you most of the retrieval quality with none of the new infrastructure.

When this applies

This is for small-to-medium corpora — internal docs, a product’s help center, a few thousand support tickets. Not for “search across the entire web” or tens of millions of chunks, where approximate nearest-neighbor search actually earns its complexity. If you’re not sure which camp you’re in, you’re probably in this one.

The setup

Postgres has had full-text search since forever, via tsvector and tsquery. It does stemming, ranking, and phrase matching out of the box, no extension required.

ALTER TABLE documents ADD COLUMN search_vector tsvector
  GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;

CREATE INDEX documents_search_idx ON documents USING GIN (search_vector);

That generated column keeps itself in sync on every insert and update — no separate embedding pipeline, no background job that can fall behind.

Query it with ts_rank for relevance ordering:

SELECT id, title, ts_rank(search_vector, query) AS rank
FROM documents, to_tsquery('english', 'deploy & rollback') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 5;

Why this beats a vector DB for small sets

Where it falls short

Full-text search is lexical, not semantic. “How do I cancel a subscription” won’t match a document that says “terminate recurring billing” unless you’ve added synonyms. Two mitigations that cover most of the gap:

  1. websearch_to_tsquery instead of to_tsquery for user-typed queries — it handles quotes, OR, and -exclude the way people actually type search boxes.
  2. A synonym dictionary. Postgres text search config supports custom thesaurus files. Map your domain’s common rewordings once — “cancel” → “cancel terminate stop” — and a lot of the semantic gap closes for free.

If after that you’re still missing real paraphrase matches, that’s the actual signal you need embeddings — not “vector databases are the modern way to do search.”

Here’s the decision path end to end:

flowchart TD
    A["Document set size?"] -->|"Small-medium, fits in a few hundred MB"| B["Use Postgres full-text search"]
    A -->|"Tens of millions of chunks"| C["Use a vector database"]
    B --> D{"Missing paraphrase matches?"}
    D -->|No| E["Done - FTS is enough"]
    D -->|Yes| F["Add synonyms / websearch_to_tsquery"]
    F --> G{"Still missing real paraphrases?"}
    G -->|No| E
    G -->|Yes| H["Add pgvector, combine with RRF"]
  

The migration path

If you do outgrow this, you don’t have to throw it away. Add pgvector to the same Postgres instance and run both: full-text for exact-term recall, vector similarity for paraphrase recall, and combine the two result sets with reciprocal rank fusion. You get hybrid search without introducing a second database, and you can make that jump exactly when you have evidence you need it, not before.

The actual failure mode to watch for

Full-text ranking (ts_rank) is not tuned out of the box for short documents — it can rank a page that mentions your term once above a page that’s actually about the topic, if the shorter page has a higher term-to-length ratio. If ranking looks off, try ts_rank_cd (cover density) instead, which rewards terms appearing close together, or add a manual boost for title matches over body matches.