Hybrid Search and Retrieval#
RAGFlow's retrieval pipeline combines BM25 full-text scoring with KNN vector scoring into a single ranked result set. The central orchestrator is Dealer in rag/nlp/search.py. It holds a reference to the active DocStoreConnection and exposes two key entry points: search() (low-level, one backend call) and retrieval() (full pipeline with reranking and pagination).
Mode Selection: Hybrid vs. Text-Only vs. Vector-Only#
Search mode is selected at query time based on two conditions :
| Condition | Mode |
|---|---|
No emb_mdl provided | Text-only (BM25) — only MatchTextExpr is sent |
emb_mdl present | Hybrid — [MatchTextExpr, MatchDenseExpr, FusionExpr("weighted_sum")] sent together |
The emb_mdl argument is passed by callers when a knowledge base has an embedding model configured; omitting it falls back to keyword search.
For the OpenSearch backend specifically, hybrid mode also requires the normalization pipeline to be active. _init_hybrid_search() runs at startup and provisions a ragflow_hybrid_pipeline on OpenSearch. If the server version is below 2.10 or pipeline creation fails (e.g. insufficient permissions), hybrid_search_enabled is set to False and the backend silently degrades to vector-only KNN queries .
Query Construction: Match Expressions#
The three expression types used in hybrid search are defined in common/doc_store/doc_store_base.py:
MatchTextExpr: fields, query string, topn, andminimum_should_matchoption.MatchDenseExpr: vector column name (e.g.q_1024_vec), cosine distance, topn, and asimilarityfloor. The vector column name encodes the model dimension:q_{dim}_vec.FusionExpr: method"weighted_sum", topn, andweightslike"0.05,0.95"(BM25 weight, KNN weight).
In search(), when all three are present and the OpenSearch pipeline is active, the backend assembles a {"hybrid": {"queries": [keyword_query, {"knn": knn_query}]}} body and attaches search_pipeline as a query parameter .
The BM25 query leg's boost is set to 1.0 - vector_similarity_weight , and KNN boost is set to the configured similarity floor . The pipeline uses min-max normalization + arithmetic mean to merge the two score distributions .
Reranking After Retrieval#
After the backend call returns candidate chunks, retrieval() reranks them locally by blending term similarity and vector similarity:
sim = tkweight * term_sim + vtweight * vector_sim + rank_feature_scores
tkweight = 1 - vector_similarity_weight and vtweight = vector_similarity_weight .
Three reranking paths exist depending on the backend and whether an external reranker is configured :
rerank_by_model(): Used whenrerank_mdlis set. Combines local token similarity with a cross-encoder reranker's scores.rerank_with_knn(): Default ES/OpenSearch path. Issues a second KNN-only call via_knn_scores()to recover the clean cosine score per candidate chunk (without fetching vectors out of the index), then blends it with local term similarity.rerank(): OceanBase path. Fetches chunk vectors directly from the result and computes cosine similarity locally viaqryr.hybrid_similarity().
The Infinity backend skips local rerank entirely and uses the backend's own normalized fusion score .
The similarity_threshold parameter filters out chunks below a composite score. When vector_similarity_weight == 0, the threshold is bypassed (set to 0.0) since BM25 scores are not normalized to the same range .
Configuration Reference#
OpenSearch hybrid pipeline (conf/service_conf.yaml)#
os:
hosts: 'http://localhost:1201'
# hybrid_search_pipeline: 'ragflow_hybrid_pipeline' # default name
# hybrid_search_weights: [0.5, 0.5] # [BM25 leg, KNN leg]
Both keys are optional and commented out by default. The pipeline name can also be overridden via the OS_HYBRID_PIPELINE environment variable . The default weight is [0.5, 0.5] when not specified .
retrieval() parameters that control scoring#
| Parameter | Default | Effect |
|---|---|---|
similarity_threshold | 0.2 | Minimum composite score to include a chunk |
vector_similarity_weight | 0.3 | KNN weight; 1 - this is BM25/term weight |
top | 1024 | Maximum candidates fetched from the backend |
rerank_mdl | None | If set, uses cross-encoder reranker instead of local blend |
Fallback Behavior on Empty Results#
If the initial hybrid search returns zero hits, search() retries with a relaxed min_match=0.1 (down from 0.3) and a slightly higher similarity floor of 0.17 . On a doc_id-scoped search, it falls back to a filter-only query with no match expression at all.
Key Files#
| File | Purpose |
|---|---|
rag/nlp/search.py | Dealer class — full retrieval pipeline, reranking, citation insertion |
rag/utils/opensearch_conn.py | OSConnection — hybrid pipeline provisioning, search() DSL builder |
common/doc_store/doc_store_base.py | MatchTextExpr, MatchDenseExpr, FusionExpr definitions |
conf/service_conf.yaml | OpenSearch hybrid search config keys |