@@ -70,6 +70,9 @@ def __init__(
7070 self ._corpus_size = 0
7171 self ._collisions = 0
7272 self ._search_traces : list [dict [str , Any ]] = []
73+ self ._relevant_docs_by_context : dict [
74+ tuple [str , str , str ], dict [str , set [str ]]
75+ ] = {}
7376
7477 safe_name = client .name .replace (" " , "-" )
7578 self ._mteb_model_meta = ModelMeta (
@@ -99,6 +102,31 @@ def __init__(
99102 def mteb_model_meta (self ) -> ModelMeta :
100103 return self ._mteb_model_meta
101104
105+ def set_relevant_docs (
106+ self ,
107+ task_name : str ,
108+ hf_split : str ,
109+ hf_subset : str ,
110+ relevant_docs : dict [str , Any ],
111+ ) -> None :
112+ """Attach qrels for richer debug traces.
113+
114+ MTEB's SearchProtocol does not pass qrels into ``search()``, but Dingo's
115+ detailed traces are easier to inspect when mapped hits are annotated as
116+ relevant or not.
117+ """
118+ normalized : dict [str , set [str ]] = {}
119+ for qid , docs in (relevant_docs or {}).items ():
120+ if isinstance (docs , dict ):
121+ normalized [str (qid )] = {
122+ str (doc_id ) for doc_id , score in docs .items () if score
123+ }
124+ else :
125+ normalized [str (qid )] = {str (doc_id ) for doc_id in docs }
126+ self ._relevant_docs_by_context [
127+ (task_name , hf_split , hf_subset )
128+ ] = normalized
129+
102130 def index (
103131 self ,
104132 corpus : "CorpusDatasetType" ,
@@ -158,6 +186,9 @@ def search(
158186 errors = 0
159187 total_matched = 0
160188 query_details : list [dict [str , Any ]] = []
189+ relevant_docs_by_qid = self ._relevant_docs_by_context .get (
190+ (task_metadata .name , hf_split , hf_subset )
191+ )
161192
162193 def _process_query (idx_qid_text ):
163194 idx , qid , q_text = idx_qid_text
@@ -168,13 +199,18 @@ def _process_query(idx_qid_text):
168199 query = q_text , results = [], response_time_ms = 0.0 ,
169200 status_code = 0 , error = str (e ),
170201 )
171- return idx , qid , q_text , error_resp , None , None , None
202+ return idx , qid , q_text , error_resp , None , None , None , None
172203
173204 if response .error :
174- return idx , qid , q_text , response , None , None , None
205+ return idx , qid , q_text , response , None , None , None , None
175206
176207 doc_scores : dict [str , float ] = {}
177208 top_api_results : list [dict [str , Any ]] = []
209+ relevant_doc_ids = (
210+ relevant_docs_by_qid .get (str (qid ))
211+ if relevant_docs_by_qid is not None
212+ else None
213+ )
178214 mapping_stats : dict [str , int ] = {
179215 "doc_id_exact" : 0 ,
180216 "title_fallback" : 0 ,
@@ -195,13 +231,33 @@ def _process_query(idx_qid_text):
195231 "score" : paper .score ,
196232 "resolved_corpus_id" : resolved_id ,
197233 "mapping_source" : src ,
234+ "is_relevant" : (
235+ bool (resolved_id and resolved_id in relevant_doc_ids )
236+ if relevant_doc_ids is not None
237+ else None
238+ ),
198239 }
199240 )
200241 if not resolved_id or resolved_id in doc_scores :
201242 continue
202243 doc_scores [resolved_id ] = 1.0 / (rank + 1 )
203244
204- return idx , qid , q_text , response , doc_scores , top_api_results , mapping_stats
245+ relevant_matched_count = (
246+ sum (1 for doc_id in doc_scores if doc_id in relevant_doc_ids )
247+ if relevant_doc_ids is not None
248+ else None
249+ )
250+
251+ return (
252+ idx ,
253+ qid ,
254+ q_text ,
255+ response ,
256+ doc_scores ,
257+ top_api_results ,
258+ mapping_stats ,
259+ relevant_matched_count ,
260+ )
205261
206262 items = [(i , qid , qt ) for i , (qid , qt ) in enumerate (zip (query_ids , query_texts ))]
207263
@@ -215,7 +271,16 @@ def _process_query(idx_qid_text):
215271 unit = "query" ,
216272 )
217273 for future in concurrent .futures .as_completed (futures ):
218- idx , qid , q_text , response , doc_scores , top_api_results , mapping_stats = future .result ()
274+ (
275+ idx ,
276+ qid ,
277+ q_text ,
278+ response ,
279+ doc_scores ,
280+ top_api_results ,
281+ mapping_stats ,
282+ relevant_matched_count ,
283+ ) = future .result ()
219284
220285 if doc_scores is None :
221286 errors += 1
@@ -231,19 +296,44 @@ def _process_query(idx_qid_text):
231296 "response_time_ms" : response .response_time_ms ,
232297 "api_results_count" : 0 ,
233298 "matched_count" : 0 ,
299+ "mapped_count" : 0 ,
300+ "relevant_matched_count" : 0 ,
301+ "relevant_total" : 0 ,
234302 }
235303 )
236304 else :
237305 results [qid ] = doc_scores
238306 total_matched += len (doc_scores )
307+ matched_count = (
308+ relevant_matched_count
309+ if relevant_matched_count is not None
310+ else len (doc_scores )
311+ )
312+ relevant_doc_ids = (
313+ relevant_docs_by_qid .get (str (qid ))
314+ if relevant_docs_by_qid is not None
315+ else None
316+ )
239317 query_details .append (
240318 {
241319 "qid" : qid ,
242320 "query_text" : q_text ,
243321 "error" : "" ,
244322 "response_time_ms" : response .response_time_ms ,
245323 "api_results_count" : len (response .results ),
246- "matched_count" : len (doc_scores ),
324+ "matched_count" : matched_count ,
325+ "mapped_count" : len (doc_scores ),
326+ "relevant_matched_count" : relevant_matched_count ,
327+ "relevant_total" : (
328+ len (relevant_doc_ids )
329+ if relevant_doc_ids is not None
330+ else None
331+ ),
332+ "gold_doc_ids" : (
333+ sorted (relevant_doc_ids )
334+ if relevant_doc_ids is not None
335+ else None
336+ ),
247337 "top_api_results" : top_api_results ,
248338 "retrieved_doc_ids" : list (doc_scores .keys ()),
249339 "mapping_stats" : mapping_stats ,
0 commit comments