@@ -72,6 +72,41 @@ def _accumulate(results: List[Dict[str, Any]]) -> None:
7272 return merged
7373
7474
75+ def rrf_merge_multiple (
76+ ranked_lists : List [List [Dict [str , Any ]]],
77+ k : int = 60 ,
78+ ) -> List [Dict [str , Any ]]:
79+ """Merge multiple ranked lists using Reciprocal Rank Fusion."""
80+ rrf_scores : Dict [str , float ] = {}
81+ chunk_store : Dict [str , Dict [str , Any ]] = {}
82+
83+ def _key (chunk : Dict [str , Any ]) -> str :
84+ for field in ("id" , "chunk_id" ):
85+ if chunk .get (field ):
86+ return str (chunk [field ])
87+ text = str (chunk .get ("text" , "" ))
88+ return "|" .join ([
89+ str (chunk .get ("document_id" , "" )),
90+ str (chunk .get ("page" , "" )),
91+ text [:200 ],
92+ ])
93+
94+ for lst in ranked_lists :
95+ for rank , chunk in enumerate (lst , start = 1 ):
96+ key = _key (chunk )
97+ rrf_scores [key ] = rrf_scores .get (key , 0.0 ) + 1.0 / (k + rank )
98+ if key not in chunk_store or chunk .get ("score" , 0 ) > chunk_store [key ].get ("score" , 0 ):
99+ chunk_store [key ] = chunk
100+
101+ merged = []
102+ for key , rrf_score in sorted (rrf_scores .items (), key = lambda t : t [1 ], reverse = True ):
103+ chunk = chunk_store [key ].copy ()
104+ chunk ["rrf_score" ] = round (rrf_score , 6 )
105+ merged .append (chunk )
106+
107+ return merged
108+
109+
75110# ── Query helpers ─────────────────────────────────────────────────────────────
76111
77112def transform_query (query : str ) -> List [str ]:
@@ -98,15 +133,18 @@ def _generate_query_variants(query: str) -> List[str]:
98133
99134 client = InferenceClient (token = settings .HF_TOKEN )
100135 prompt = (
101- "Rewrite the user question into concise semantic search queries for document retrieval. "
102- "Split independent topics into separate queries. Return a JSON array of strings only. "
136+ "Decompose the user's complex multi-part question into simple, distinct semantic sub-queries. "
137+ "Each sub-query should focus on a single question, topic, or comparison. "
138+ "Return a JSON array of strings only. "
139+ "Example question: 'Compare treatment A and treatment B for diabetes'\n "
140+ "Example output: [\" treatment A for diabetes\" , \" treatment B for diabetes\" , \" diabetes treatments comparison\" ]\n "
103141 f"User question: { query } "
104142 )
105143 response = client .chat_completion (
106144 messages = [
107145 {
108146 "role" : "system" ,
109- "content" : "You create optimized search queries for a RAG retriever." ,
147+ "content" : "You decompose complex search queries into list of search sub- queries for a RAG retriever." ,
110148 },
111149 {"role" : "user" , "content" : prompt },
112150 ],
@@ -221,10 +259,11 @@ def retrieve(
221259 """
222260 effective_top_k = top_k if top_k is not None else settings .TOP_K_RETRIEVAL
223261
224- # ── Stage 1: Hybrid retrieval with query transformation ───────────────────
225- all_candidates : List [Dict [str , Any ]] = []
262+ # ── Stage 1: Parallel retrieval of sub-queries and RRF merging ───────────
263+ sub_queries = transform_query (query )
264+ sub_query_results : List [List [Dict [str , Any ]]] = []
226265
227- for search_query in transform_query ( query ) :
266+ def retrieve_single_query ( search_query : str ) -> List [ Dict [ str , Any ]] :
228267 query_vector = embed_query (search_query )
229268
230269 # Vector results (always)
@@ -257,14 +296,28 @@ def retrieve(
257296 for chunk in merged :
258297 chunk ["score" ] = chunk .pop ("rrf_score" )
259298
260- all_candidates . extend ( merged )
299+ return merged
261300 else :
262- all_candidates .extend (vector_results )
301+ return vector_results
302+
303+ import concurrent .futures
304+ with concurrent .futures .ThreadPoolExecutor (max_workers = len (sub_queries ) or 1 ) as executor :
305+ future_to_query = {executor .submit (retrieve_single_query , sq ): sq for sq in sub_queries }
306+ for future in concurrent .futures .as_completed (future_to_query ):
307+ try :
308+ results = future .result ()
309+ sub_query_results .append (results )
310+ except Exception as e :
311+ sq = future_to_query [future ]
312+ logger .error ("Failed retrieval for sub-query '%s': %s" , sq , e )
263313
264- if not all_candidates :
314+ if not sub_query_results :
265315 return []
266316
267- candidates = _merge_candidates (all_candidates )
317+ # Merge all sub-query candidate lists using generalized RRF
318+ candidates = rrf_merge_multiple (sub_query_results , k = settings .RRF_K )
319+ for chunk in candidates :
320+ chunk ["score" ] = chunk .pop ("rrf_score" )
268321
269322 # ── Stage 2: Cross-encoder reranking ─────────────────────────────────────
270323 reranker = get_reranker ()
0 commit comments