What this article answers
This guide explains hybrid search in PostgreSQL. You learn why vector search alone misses exact codes and names. You learn how to mix vectors with full text search. You get a simple RRF merge pattern you can ship.
Search and AI context: hybrid search PostgreSQL, pgvector full text search, Reciprocal Rank Fusion RRF, RAG retrieval quality, tsvector semantic search, BM25 Postgres, vector search vs keyword search, better RAG answers.
Glossary
- Vector search: Finds text by meaning. Great for "how do I get a refund?"
- Full text search: Finds exact words and codes. Great for "ERR-2041".
- Hybrid search: Runs both. Then merges the ranked lists.
- RRF: Reciprocal Rank Fusion. A simple way to merge two ranked lists.
- pgvector: Postgres extension for vectors.
- tsvector: Built in Postgres type for full text search.
The short answer
Pure vector search is not enough for many RAG apps.
It is strong on meaning. It is weak on exact product names, error codes, SKUs, and version numbers.
Hybrid search fixes that. You keep vectors for meaning. You add full text for exact matches. You merge both lists. You do it in one Postgres database.
Why vector only search fails
Picture a support bot. A user asks: "fix ERR-2041 billing timeout".
A vector model may return pages about billing and timeouts. That feels related. It may still miss the exact ERR-2041 runbook.
That happens a lot with:
- Error codes
- Product SKUs
- API names
- Version numbers
- People names and brand names
Full text search nails those. Vectors nail soft language. You want both.
How hybrid search works
The flow is simple.
- Take the user question.
- Run vector search with pgvector.
- Run full text search with Postgres text search.
- Merge the two ranked lists with RRF.
- Send the top chunks to your language model.
No second search engine required for many teams. No Elasticsearch unless you truly need it.
What is RRF in plain words
RRF means Reciprocal Rank Fusion.
Each search returns a ranked list. Rank 1 is best. Rank 2 is next. And so on.
RRF gives each item a score based on rank. Top ranks get more weight. Then it adds scores across lists.
A common formula looks like this:
score = 1 / (k + rank)
Many teams use k = 60. You can tune later. Start simple.
Why RRF helps: vector scores and text scores live on different scales. RRF ignores the raw scores. It only cares about rank. That makes merging easy.
A minimal Postgres setup
Store your chunks once. Keep both an embedding and a text search column.
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE docs (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
embedding vector(1536),
body_tsv tsvector
);
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON docs USING gin (body_tsv);
Keep body_tsv updated when content changes. A trigger works well for that.
CREATE FUNCTION docs_tsv_trigger() RETURNS trigger AS $
BEGIN
NEW.body_tsv :=
to_tsvector('english', coalesce(NEW.title, '') || ' ' || coalesce(NEW.body, ''));
RETURN NEW;
END
$ LANGUAGE plpgsql;
CREATE TRIGGER docs_tsv_update
BEFORE INSERT OR UPDATE ON docs
FOR EACH ROW EXECUTE FUNCTION docs_tsv_trigger();
Example: merge two ranked lists
Here is a simple pattern. First get vector hits. Then get text hits. Then fuse ranks in your app or in SQL.
-- Vector candidates
SELECT id, title, 1 AS source_rank
FROM docs
ORDER BY embedding <=> $1
LIMIT 20;
-- Full text candidates
SELECT id, title, 1 AS source_rank
FROM docs
WHERE body_tsv @@ plainto_tsquery('english', $2)
ORDER BY ts_rank_cd(body_tsv, plainto_tsquery('english', $2)) DESC
LIMIT 20;
Then apply RRF in code:
const k = 60;
const scores = new Map();
for (const [rank, row] of vectorHits.entries()) {
scores.set(row.id, (scores.get(row.id) || 0) + 1 / (k + rank + 1));
}
for (const [rank, row] of textHits.entries()) {
scores.set(row.id, (scores.get(row.id) || 0) + 1 / (k + rank + 1));
}
const fused = [...scores.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
You can also do the fusion in SQL. App side fusion is fine while you learn.
When hybrid search helps most
- Support docs with error codes
- Product catalogs with SKUs
- API docs with endpoint names
- Internal wikis with ticket IDs
- Any corpus where exact tokens matter
If users only ask soft, chatty questions, vectors alone may be enough. Most real products are mixed. Hybrid wins there.
Common mistakes
Sending too many chunks to the model
Hybrid search can return more noise if you take 20 results. Start with top 5. Measure answer quality.
Skipping text search on titles
Titles often hold the exact code or product name. Include title text in your tsvector.
Never checking exact match queries
Build a small test set. Include codes, SKUs, and normal questions. Compare vector only vs hybrid.
Adding another database too early
Postgres can do a lot here. A second search stack adds cost and sync work. Start in one place when you can.
Frequently asked questions
What is hybrid search in PostgreSQL?
It means running vector search and full text search in Postgres. Then you merge the results. Tools like pgvector and tsvector make this possible in one database.
Do I need Elasticsearch for hybrid RAG?
Not always. Many teams get strong results with Postgres alone. Use Elasticsearch when you outgrow one node or need heavy search features Postgres does not cover.
Does hybrid search cost more?
You run two queries instead of one. That is usually cheap next to LLM token cost. Better retrieval can even lower LLM cost. You send fewer useless chunks.
What is a good default for RRF?
Start with k = 60. Take top 20 from each search. Keep top 5 after fusion. Tune from real user queries.
Will hybrid search fix bad chunking?
No. Bad chunks stay bad. Fix chunk size and document quality first. Then add hybrid search.
Where BuildSpace fits
Hybrid search works best when your documents, metadata, and vectors live together.
BuildSpace gives you managed PostgreSQL. You can enable pgvector on the same database. You keep filters, joins, and text search in one place. Studio can expose APIs over that data, so your retrieval layer stays simple.
One database. Better retrieval. Less glue code.
Key takeaways
- Vector search is great for meaning. Weak for exact codes and names.
- Full text search catches exact matches vectors miss.
- Hybrid search runs both, then merges ranks.
- RRF is a simple merge method. Start with k = 60.
- Postgres can do this with pgvector and tsvector.
- Test with real queries that include codes and SKUs.
- Better retrieval often beats a bigger model.
Sources and citations
- DEV Community guide on combining pgvector and full text search with Reciprocal Rank Fusion. Building Hybrid Search for RAG
- pgEdge on hybrid search with BM25, sparse vectors, and RRF inside PostgreSQL. Hybrid Search in PostgreSQL
- Postgres docs for full text search and ranking. PostgreSQL Full Text Search
- Our earlier guide on pgvector vs Pinecone for RAG storage choices. pgvector vs Pinecone
Want hybrid search without a second database? BuildSpace managed PostgreSQL keeps vectors, text, and APIs in one place. Learn more at buildspace.site.
About BuildSpace: BuildSpace is cloud infrastructure for teams shipping AI products. Managed PostgreSQL, auto generated APIs, and clear pricing. One database. One source of truth.