Hybrid Search for RAG: BM25 and Vectors Setup

Vladimir Lomtev•
Hybrid Search for RAG: BM25 and Vectors Setup

Hybrid search in RAG means running two searches against a knowledge base at the same time: full-text search (BM25 or similar), which accurately finds words, codes, and numbers, and vector search, which finds paraphrased questions by meaning. The two result lists are then merged into one, most often using Reciprocal Rank Fusion (RRF). The setup looks like this: collect a set of real questions, measure each search separately, add Russian morphology support, index the same chunks in both indexes, merge the results with RRF, and tune the weights on a held-out set of questions.

Prepared by AIrassvet October 24, 2026 for IT leaders, analysts, and developers who are launching an enterprise assistant or knowledge base search and seeing that it cannot find a contract number, SKU, or error code. The instructions are based on the documentation for Qdrant, Elasticsearch, Weaviate, PostgreSQL, and pgvector, as well as the original article on the RRF method. We did not perform our own measurements for this article.

Contents

At a glance

  • Two searches cover each other’s weak spots. Vector search understands paraphrases, while full-text search matches exact words. Qdrant documentation describes this combination as a way to get both semantic understanding and exact word matching at the same time.
  • Combine by ranking position, not raw scores. BM25 scores and cosine similarity are measured on different scales. RRF adds values of the form 1/(k + rank), so chunks that rank highly in both lists rise to the top.
  • The k constant differs across systems. In Elasticsearch, the default is 60, as in the original RRF article; in Qdrant, it is 2. Keep this in mind when transferring settings.
  • Tune weights only on a test set. Qdrant recommends leaving weights equal if you do not have a question set with known answers, and tuning on one half and validating on the other if you do.
  • Russian text needs morphology support. In PostgreSQL, there is a full-text search configuration russian with the Snowball stemmer for this.

What the guide includes and excludes

This guide covers the first stage of search in a RAG system: how to find candidates using two methods and combine them. We do not cover document chunking here — that is covered in a separate article “Document Chunking for RAG”, and we do not repeat what a vector database is — see “What Is a Vector Database”. The second stage, reranking candidates with a separate model, is described in the article “Reranker in RAG: When You Need One”.

How hybrid search works

The user query is sent to two searches in parallel:

  1. Full-text (lexical). It finds chunks where the query words appear. The classic ranking algorithm is BM25. It is strongest where exact values matter: “contract No. 47-K,” “error E-102,” a surname, or the name of a policy clause.
  2. Vector (semantic). It turns the query into a vector using an embedding model and finds the nearest chunks by meaning. It can find the answer to “how to refund money to a customer” even if the policy says “payment refund procedure.”

Each search returns its own ranked list. The lists are then merged. The most common method is Reciprocal Rank Fusion (RRF)introduced by Cormack, Clarke, and Büttcher in the paper presented at SIGIR 2009. The score for a chunk is the sum of 1/(k + rank) across all lists where it appears. In the paper, k = 60 was chosen in a pilot experiment; the authors note that this value was close to optimal, but the exact choice is not critical.

Calculation example. Suppose k = 60, and ranks start at 1. Chunk A is first in the full-text list and third in the vector list: 1/61 + 1/63 ≈ 0.0164 + 0.0159 = 0.0323. Chunk B is first only in the vector list: 1/61 ≈ 0.0164. In the final results, A will rank above B because both searches supported it.

There are also alternatives to RRF. Weaviate blends scores with the alphaparameter: according to the documentation, alpha = 1 means pure vector search, and alpha = 0 means pure full-text search. In addition to RRF, Qdrant offers the DBSF method, which normalizes each search’s scores based on their distribution before adding them together.

An embedding model can provide both dense and sparse vectors (similar to keywords) on its own. BGE-M3, for example, is designed this way: BGE-M3According to the model card, it supports dense, sparse, and multi-vector search, works with more than 100 languages, and the authors recommend the combination of “hybrid search + reranking” for RAG.

What you need before you start

  • A working vector search over the knowledge base with the chosen embedding model. If you have not chosen a model yet, the embedding model selection checklistcan help.
  • A full-text search engine: PostgreSQL, Elasticsearch, or OpenSearch, and a vector database that supports text or sparse queries (for example, Qdrant or Weaviate).
  • Unified chunk IDs. The same chunk must have the same ID in both indexes, otherwise the lists cannot be merged.
  • A set of questions with known answers: real questions from employees or customers and the IDs of the chunks that contain the answer. You need both types of questions: ones with exact values (numbers, codes, surnames) and ones phrased in plain language.
  • Metric: the share of questions for which the needed chunk appeared in the first k results (recall@k or Hit@k). How to measure RAG quality metrics is described in the article “RAG Accuracy and Hallucinations”.
  • A business-side owner for the question set: they confirm which chunk counts as the correct answer.

Step-by-step hybrid search setup

Step 1. Label questions by type

Split the set into groups: questions with identifiers (number, SKU, code), questions with company terms and abbreviations, and questions in plain language. Hybrid search often helps unevenly: strongly for the identifier group, less so for paraphrases. Without labeling, this effect gets lost in the overall average.

Step 2. Measure each search separately

Run the full set through pure vector search and then through pure full-text search. Record recall@k for each question group. This is your baseline: if full-text search does not find anything that vector search misses, hybrid search will not add value, and you should look for the problem in document chunking or in the embedding model.

Step 3. Tune full-text search for Russian

Without morphology, the query “supply contracts” won’t find a fragment with the words “supply contract.” In PostgreSQL, use the configuration russian: per the documentation Snowball dictionaries reduce different word forms to a common root and remove stop words. In other engines, enable the analyzer for Russian.

Also check queries with identifiers: make sure codes like “47-K” or “E-102” are searched after text parsing the same way users enter them. If not, store those values in a separate metadata field and search for them with an exact match.

Step 4. Index the same fragments in both indexes

The full-text and vector indexes are built from the same document chunks with the same IDs. Metadata (document, section, effective date, access level) should also match: access-rights filters are applied in both searches before merging, otherwise a fragment the employee is not allowed to see will end up in the results.

Step 5. Fetch extra candidates

Each search should return more results than you need in the end. In the example from the Qdrant documentation each of the two searches returns 20 results, and after merging 10 remain. In the Elasticsearch example, the rank_window_size window is 50; the documentation notes that a larger window improves relevance at the cost of performance.

Step 6. Merge the lists with RRF

Start with RRF using equal weights. Note the default values:

System Default k constant What else is configurable
Elasticsearch, retriever rrf 60 (rank_constant, not less than 1) rank_window_size, defaults to size
Qdrant, query rrf 2 (positions are counted from zero) k — available since version 1.16.0, list weights — since version 1.17.0
Original article on RRF 60 —

The smaller the k, the more the final result depends on the top positions in each list. That’s why settings tuned in one system can’t just be copied into another.

Step 7. Tune the weights on a holdout set of questions

If one search is clearly stronger on your data, give it a higher weight. In Qdrant, a weight of 3.0 for the first list and 1.0 for the second means that third place in the first list is valued the same as first place in the second. Tune the weights on half of your question set, then validate the final setup on the other half: the Qdrant documentation warns that measuring on the same questions you used to tune the weights inflates the result. Without such a validation set, Qdrant recommends leaving the default weights.

Step 8. Decide whether you need a reranker

Hybrid search expands the candidate pool, and the reranker orders them more precisely. If the needed fragment already appears in the top 20–50 candidates but not in the top 5, the next step is a reranker. If it is not even among the candidates, reranking will not help: go back to steps 3–5.

Step 9. Lock in the configuration and recheck it

Save the following with the index: the embedding model, the full-text search language configuration, candidate window sizes, the merging method, k, and weights. Qdrant recommends retuning the weights when you change the embedding model or chunking, when the document corpus changes significantly, or on a regular basis using fresh questions.

Example for PostgreSQL with pgvector

The pgvector README recommends combining vector search with PostgreSQL’s built-in full-text search and merging the results with RRF or a reranker. Below is a sample query for learning purposes; adapt the table names, window sizes, and k to your system and test it.

-- Full-text index for Russian morphology
ALTER TABLE chunks
  ADD COLUMN tsv tsvector
  GENERATED ALWAYS AS (to_tsvector('russian', content)) STORED;
CREATE INDEX chunks_tsv_idx ON chunks USING GIN (tsv);

-- Hybrid search: two lists of 40 candidates each and RRF with k = 60
WITH semantic AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
  FROM chunks
  ORDER BY embedding <=> $1
  LIMIT 40
),
keyword AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY ts_rank_cd(tsv, query) DESC) AS rank
  FROM chunks, websearch_to_tsquery('russian', $2) AS query
  WHERE tsv @@ query
  ORDER BY ts_rank_cd(tsv, query) DESC
  LIMIT 40
)
SELECT COALESCE(s.id, k.id) AS id,
       COALESCE(1.0 / (60 + s.rank), 0) + COALESCE(1.0 / (60 + k.rank), 0) AS rrf_score
FROM semantic s
FULL OUTER JOIN keyword k ON s.id = k.id
ORDER BY rrf_score DESC
LIMIT 10;

Here $1 is the query vector, $2 is the query text. Note: PostgreSQL’s built-in ranking functions are not BM25. According to the documentation, ts_rank takes into account the frequency of matched lexemes, while ts_rank_cd measures the density of their coverage. For RRF, only the order matters, so this is enough to start; if testing shows the full-text side is weak, compare it with an engine that has BM25.

Common mistakes

  • Adding raw scores from two searches. BM25 scores and cosine similarity are on different scales, and a simple sum gives the win to whichever search produces larger numbers. Use RRF, a normalization method like DBSF, or the mixing parameter provided by the engine.
  • Different fragment IDs in the two indexes. Merging silently stops working: each fragment appears in only one list.
  • Access filter in only one search. A fragment filtered out in vector search comes back through full-text search.
  • No morphology. Full-text search over Russian text without stemming misses word forms and looks useless.
  • Carrying k and weights over between systems. k = 60 in Elasticsearch and k = 2 in Qdrant produce different behavior with the same weights.
  • Tuning weights and measuring on the same questions. The result looks better than what users will actually get.
  • A small candidate window. If each search returns only 5 results, there is almost nothing to merge.
  • Expecting a miracle from hybrid search. If the answer is split across chunks or the document is poorly OCRed, hybrid search will not fix that.

A reusable configuration and test protocol template

HYBRID SEARCH CONFIGURATION — version [__]
Date: [__]   Owner: [__]

Index and data
☐ Document chunking version: [__]
☐ Embedding model and version: [__]
☐ Full-text engine: [PostgreSQL / Elasticsearch / OpenSearch / Qdrant / other]
☐ Language configuration: [Russian / other]
☐ Fields for exact ID lookup: [__]
☐ Same chunk IDs in both indexes: [yes / no]
☐ Access filters applied in both searches: [yes / no]

Fusion
☐ Candidate window, full-text: [__]
☐ Candidate window, vector: [__]
☐ Method: [RRF / DBSF / alpha / other]
☐ k: [__]   Weights: full-text [__], vector [__]
☐ Final number of chunks for the model: [__]
☐ Reranker: [none / model __]

TEST PROTOCOL
Question set: [__] items, including [__] with IDs, [__] in plain language
Split: weight tuning [__] items / validation [__] items

| Variant          | recall@k (IDs) | recall@k (plain language) | recall@k (total) |
|------------------|----------------|----------------------------|------------------|
| Vector only      | [__]           | [__]                       | [__]             |
| Text only        | [__]           | [__]                       | [__]             |
| Hybrid, RRF      | [__]           | [__]                       | [__]             |
| Hybrid + weights | [__]           | [__]                       | [__]             |

Validation failures (question → reason): [__]
Decision and rationale: [__]
When to retest: when the model or chunking changes, or when the corpus changes significantly

Frequently Asked Questions

What is hybrid search in RAG?

Hybrid search combines full-text and vector search over a single knowledge base. Full-text search finds exact words, codes, and numbers, while vector search finds passages that are semantically similar. The two result lists are merged into one, usually with RRF, and the best chunks are passed to the language model for an answer.

When do you need hybrid search, and when do you not?

You need it when users search for exact values: contract numbers, SKUs, error codes, surnames, or section titles. If, in your question set, full-text search finds nothing that vector search misses, hybrid search will not add value. Make the decision based on recall@k measured on your own questions, not in advance.

What k value should you choose for RRF?

Start with your system’s default: in Elasticsearch, it is 60; in Qdrant, it is 2. In the original RRF paper, k = 60 was close to optimal, and the exact choice was not critical. Change k only if testing on held-out questions shows an improvement, and do not carry it over between systems without validation.

Can you do hybrid search in PostgreSQL?

Yes. pgvector handles vector search, PostgreSQL’s built-in full-text search with the russian configuration handles word-based search, and the results are merged with an SQL query using RRF. PostgreSQL’s built-in ranking is not BM25, but for RRF the result order matters, so for a starting point this is usually enough. Check the quality on your own questions.

How is hybrid search different from a reranker?

Hybrid search is the first stage: it gathers candidates in two ways and merges the lists. A reranker is the second stage: a separate model reorders the already retrieved candidates. Hybrid search helps when the needed chunk is not among the candidates; a reranker helps when it is there but ranked too low. They are often used together.

Primary sources and method

All sources were checked on October 24, 2026. The sequence of steps is assembled from the recommendations in this documentation; the RRF calculation example was done by us using the formula from the paper. The SQL query is an instructional example and should be tested on your PostgreSQL version and data. We do not provide quality gain figures for hybrid search: they depend on the documents and questions of a specific company.

How AIrassvet helps set up hybrid search in a knowledge base

Hybrid search is part of a RAG system, and its setup depends on the documents and questions of a specific company. Among the directions listed on the AIrassvet website, there are those that cover this work:

  • RAG and enterprise knowledge bases: full-text and vector indexes with shared IDs, Russian morphology, and access filters;
  • data preparation: collecting a set of real questions with verified answers and labeling them by type;
  • testing and deployment: comparing vector, full-text, and hybrid options on your questions and documenting the chosen configuration.

The first step is to choose one process where employees search for answers in documents, and document the current result, data sources, constraints, and acceptance criteria.

Discuss the project

Bottom line: hybrid search is tuned through testing

Hybrid search combines the precision of full-text search with the semantic understanding of vector search. For it to work, both indexes must be built from the same chunks with shared IDs and access filters, full-text search must be configured for Russian morphology, and the lists must be merged with RRF.

Start with equal weights and your system’s default k value, compare the options on a set of real questions, and tune weights only on a holdout set. If the needed chunk makes it into the candidates but ranks too low, the next step is a reranker.

Request an audit

Share your contact details and we will follow up.

← All articles

Comments (0)

Loading comments…

Leave a comment
No registration required

Book a strategy call
for agentic operations

Tell us which workflow you want to improve. We will map feasibility, risks, and the fastest MVP path.

By submitting, you agree to our privacy policy

Contacts

Global Operations

Serving U.S. clients remotely
with private cloud and on-prem options

Strategy calls by request

We respond after reviewing your workflow context.

lamooof@gmail.com

For partnership inquiries

Have a proposal?

Write to us in messengers

© 2026 AgentSunrise