Skip to content

Hybrid search explained

Combine keyword and vector retrieval without inventing a universal accuracy gain. Includes PostgreSQL queries, reciprocal rank fusion, and an evaluation plan.

Chatsy TeamEngineering
Updated
Reading time
7 min read

Vector search and keyword search fail in different ways. A vector retriever can connect "end my plan" with "cancel a subscription," but it may miss an exact error code. Keyword search finds the code and misses the paraphrase.

Hybrid search runs both, then combines their ranked results. That is the whole idea. Whether it improves your agent depends on the corpus, questions, filters, and fusion method. There is no honest percentage that applies to every support system.

In brief

  • Keyword retrieval protects exact terms such as product names, invoice fields, and error codes.
  • Vector retrieval finds related wording and concepts.
  • Reciprocal Rank Fusion combines positions without pretending the two score scales are equal.
  • A labeled query set decides whether hybrid search is better for your content.

Where each retriever helps

Consider a help center with these passages:

Cancel a subscription before its renewal date
Resolve checkout error E-1234
Update SAML single sign-on settings

A customer might ask:

How do I stop my plan from renewing?

Vector search should connect that paraphrase to the cancellation article. Keyword search may not.

Now consider:

What does E-1234 mean?

The exact code is the best clue. Keyword search should preserve it, while a dense embedding may place general checkout errors nearby.

Hybrid search keeps both candidates available for ranking.

Keyword retrieval in PostgreSQL

PostgreSQL full-text search can store a tsvector next to each chunk.

sql
ALTER TABLE knowledge_chunks
ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', coalesce(title, '') || ' ' || content)) STORED;

CREATE INDEX knowledge_chunks_search_vector_idx
ON knowledge_chunks USING GIN (search_vector);

Query it with the same workspace boundary used by vector search:

sql
SELECT id,
 ts_rank_cd(search_vector, websearch_to_tsquery('english', $2)) AS score
FROM knowledge_chunks
WHERE workspace_id = $1
 AND search_vector @@ websearch_to_tsquery('english', $2)
ORDER BY score DESC, id
LIMIT $3;

Choose the text-search configuration for the language you support. If the corpus is multilingual, test a language-aware strategy rather than applying English stemming to every document.

Vector retrieval with the same boundary

With pgvector, a cosine-distance query can look like this:

sql
SELECT id, embedding <=> $2 AS distance
FROM knowledge_chunks
WHERE workspace_id = $1
ORDER BY embedding <=> $2, id
LIMIT $3;

The embedding model determines the vector dimension and quality. Keep that model fixed while evaluating the addition of keyword search. Changing both at once makes the result hard to diagnose.

Run both searches in parallel

Both branches must apply the same authorization and content filters.

ts
const [keywordResults, vectorResults] = await Promise.all([
 keywordSearch({ workspaceId, query, limit: 20 }),
 vectorSearch({ workspaceId, queryEmbedding, limit: 20 }),
])

const fused = reciprocalRankFusion([keywordResults, vectorResults])
const context = fused.slice(0, 8)

The limits are examples. Increase them only when the evaluation set shows that relevant passages are being cut off before fusion.

Combine ranks with RRF

Keyword scores and vector distances use different scales. Adding them directly creates a weighting problem. Reciprocal Rank Fusion, or RRF, uses each result's position.

For a document at rank (r), its contribution is:

1 / (k + r)

If the document appears in both lists, add both contributions.

ts
type RankedResult = {
 id: string
 content: string
}

function reciprocalRankFusion(lists: RankedResult[][], k = 60) {
 const byId = new Map<string, { result: RankedResult; score: number }>()

 for (const list of lists) {
 list.forEach((result, index) => {
 const current = byId.get(result.id)
 byId.set(result.id, {
 result,
 score: (current?.score? 0) + 1 / (k + index + 1),
 })
 })
 }

 return [...byId.values()]
.sort((a, b) => b.score - a.score)
.map(item => item.result)
}

The constant 60 is a common starting value, not a promised optimum. Evaluate it alongside other settings.

Add weights only for a measured reason

You can weight a branch when the query contains a reliable signal. An error code, quoted phrase, SKU, or proper product name may justify more keyword influence. A conversational question without exact identifiers may benefit from semantic retrieval.

Keep the rule inspectable:

ts
const hasExactIdentifier = /\b[A-Z]{1,5}-\d{2,}\b/.test(query)
const keywordWeight = hasExactIdentifier? 1.5: 1

Do not ask a second model to choose weights until a simpler rule fails on a labeled set. An opaque router adds cost and makes retrieval incidents harder to explain.

Rerank a small candidate set

Fusion ranks documents from retrieval signals. An optional reranker can examine the question and a small candidate set together.

Use reranking when the initial lists regularly contain the right passage but place it too low. Do not use it to hide poor workspace filters, broken chunking, or missing content.

Measure the extra model or cross-encoder time. Voice and other tight-latency channels may not have room for it.

Build the evaluation set

Collect representative questions and label at least one supporting passage for each answerable question. Include:

  • Paraphrases with no shared keywords
  • Exact codes, plan names, and API fields
  • Short ambiguous questions
  • Queries with filters, such as product version or country
  • Questions with no approved answer

Evaluate keyword-only, vector-only, and hybrid retrieval on the same set. Useful measures include:

  • Recall at a fixed result limit
  • Reciprocal rank of the first supporting passage
  • No-answer precision
  • Median and tail latency
  • Retrieved tokens passed to the answer model

Then run an answer evaluation for citation support and handoff behavior. Better retrieval does not excuse an answer that invents details outside the selected passages.

Diagnose common failures

Exact term still missing. Check tokenization, stemming, punctuation, and the text-search language. Codes may need a dedicated normalized field.

General pages dominate. Reduce oversized chunks, add title or section fields, and inspect duplicates before changing fusion weights.

One language performs poorly. Verify the text-search configuration and embedding coverage for that language. Test each supported language separately.

Latency doubles. Confirm the searches run in parallel, filters use indexes, and candidate lists are bounded. Remove reranking while isolating the slow stage.

Wrong workspace appears. Stop. Fix authorization and data scoping before any relevance tuning.

When hybrid search is unnecessary

Keyword-only retrieval may be enough for a narrow catalog dominated by exact identifiers. Vector-only retrieval may be enough for a small, conversational corpus with no codes or names and a strict latency budget.

Do not add a second index because hybrid search is fashionable. Add it when one retriever has a repeatable failure that the other one fixes on your evaluation set.

For broader retrieval design, see vector search explained and query expansion for better retrieval.

Frequently asked questions

Hybrid search combines results from keyword and semantic retrieval. A fusion method, often RRF, ranks the combined candidates before they reach the answer model.

Is hybrid search always more accurate?

No. It helps when the corpus contains both exact terms and conceptual language. It can add noise or latency in a narrow corpus where one retriever already works.

Does hybrid search require a separate search service?

No. PostgreSQL can provide full-text search and pgvector in one database. Separate services may still make sense for scale, operations, or language features.

How should I choose fusion weights?

Start with unweighted RRF. Add a clear rule only when labeled queries show a repeatable failure, then test the rule against the full set.

Share this article

Written byChatsy Team · EngineeringAbout Chatsy
All articles
  • AI

    Future of Customer Support: AI That Understands

    How contextual AI agents are revolutionizing customer support with human-like understanding. Trends shaping the next decade.

    Dec 5, 2024 · 11 min read

  • AI

    GPT-5 for customer support: what to test

    A practical evaluation plan for using GPT-5 in customer support, covering grounded answers, tool calls, multi-turn cases, cost, latency, rollout, and rollback.

    Feb 7, 2026 · 7 min read

  • Technical

    RAG vs Fine-Tuning for AI Chatbots: How to Choose

    Should you use Retrieval-Augmented Generation or fine-tune a model for your chatbot? We break down the pros, cons, and best use cases for each approach.

    Jan 14, 2026 · 16 min read

Build an agent from your support content

Add approved help content, test its answers, and decide when a person takes over.

No credit card required