From d70657171f9d005076e0bb302a829557d519fd3f Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 26 May 2026 08:11:17 -0400 Subject: [PATCH] fix(multi-search): use configured over_fetch_factor instead of hardcoded 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multi-search route was hardcoding over_fetch_factor to 1 instead of using the configured vector_search.over_fetch_factor value. This meant vector searches in multi-query batches didn't benefit from over-fetching, leading to incorrect global ranking on sparse semantic matches. Changes: - Added HeaderMap parameter to multi_search handler - Extract X-Miroir-Over-Fetch header for per-request override (plan §13.12) - Pass over_fetch_factor into the executor closure - Use over_fetch_factor when building SearchRequest Closes: bf-5204 Co-Authored-By: Claude Opus 4.7 --- crates/miroir-proxy/src/routes/multi_search.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/miroir-proxy/src/routes/multi_search.rs b/crates/miroir-proxy/src/routes/multi_search.rs index 6aaa629..37ae120 100644 --- a/crates/miroir-proxy/src/routes/multi_search.rs +++ b/crates/miroir-proxy/src/routes/multi_search.rs @@ -3,7 +3,7 @@ use axum::{ extract::{FromRef, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, Json, }; use miroir_core::{ @@ -154,6 +154,7 @@ fn error_label(e: &miroir_core::scatter::NodeError) -> &'static str { #[instrument(skip_all, fields(query_count = body.queries.len()))] pub async fn multi_search( State(state): State, + headers: HeaderMap, Json(body): Json, ) -> Result, StatusCode> where @@ -179,6 +180,15 @@ where .observe_multisearch_queries_per_batch(query_count); state.metrics.inc_multisearch_batches_total(); + // Extract X-Miroir-Over-Fetch header (plan §13.12) + // Per-request override of vector_search.over_fetch_factor + let over_fetch_factor = headers + .get("X-Miroir-Over-Fetch") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()) + .filter(|&f| f >= 1) + .unwrap_or_else(|| state.config.vector_search.over_fetch_factor); + let executor = MultiSearchExecutor::new(state.config.multi_search.clone()); // Get topology and policy once for all queries @@ -290,6 +300,7 @@ where let replica_selector_for_executor = state.replica_selector.clone(); let query_planner_for_executor = state.query_planner.clone(); let metrics_for_executor = state.metrics.clone(); + let over_fetch_factor_for_executor = over_fetch_factor; let response = executor .execute(core_request, move |query| { @@ -301,6 +312,7 @@ where let replica_selector = replica_selector_for_executor.clone(); let query_planner = query_planner_for_executor.clone(); let metrics = metrics_for_executor.clone(); + let over_fetch_factor = over_fetch_factor_for_executor; async move { let start = Instant::now(); @@ -374,7 +386,7 @@ where .unwrap_or(false), body: serde_json::json!(query.other), global_idf: None, - over_fetch_factor: 1, // TODO: support over-fetch in multi-search + over_fetch_factor, vector_mode, vector_config: None, };