1use crate::bm25;
6use crate::error::RuntimeError;
7use crate::mmap_file::MmapUrnaFile;
8use crate::rerank::RerankSource;
9use crate::{RerankSourceKind, SearchExplain, SearchHit, SearchResult};
10
11impl MmapUrnaFile {
12 fn rerank_source_kind(&self) -> RerankSourceKind {
15 let dtype = self
16 .embeddings_fp_slab()
17 .map(|(_, d)| d)
18 .unwrap_or(self.dtype);
19 RerankSourceKind::from_dtype(dtype)
20 }
21
22 fn validate_query(&self, query: &[f32], k: i32) -> Result<Vec<f32>, RuntimeError> {
24 if k <= 0 {
25 return Err(RuntimeError::InvalidK(k));
26 }
27 if query.is_empty() {
28 return Err(RuntimeError::EmptyQuery);
29 }
30 if query.len() != self.embedding_dim {
31 return Err(RuntimeError::DimensionMismatch {
32 expected: self.embedding_dim,
33 got: query.len(),
34 });
35 }
36 for &v in query {
37 if v.is_nan() || v.is_infinite() {
38 return Err(RuntimeError::InvalidQueryValue);
39 }
40 }
41 let mut qnorm = query.to_vec();
42 let norm: f32 = qnorm.iter().map(|x| x * x).sum::<f32>().sqrt();
43 if norm == 0.0 {
44 return Err(RuntimeError::ZeroNormQuery);
45 }
46 for x in &mut qnorm {
47 *x /= norm;
48 }
49 Ok(qnorm)
50 }
51
52 fn rerank_source(&self) -> Result<RerankSource<'_>, RuntimeError> {
58 let (dtype, bytes) = match self.embeddings_fp_slab() {
59 Some((bytes, dtype)) => (dtype, bytes),
60 None => (self.dtype, self.embeddings_bytes()),
61 };
62 RerankSource::new(dtype, bytes, self.n_embeddings, self.embedding_dim)
63 }
64
65 fn score_all(&self, qnorm: &[f32]) -> Result<Vec<(usize, f32)>, RuntimeError> {
68 let n = self.n_embeddings;
69 let mut src = self.rerank_source()?;
70 let mut scores: Vec<(usize, f32)> = Vec::with_capacity(n);
71 for i in 0..n {
72 scores.push((i, src.score(qnorm, i)));
73 }
74 Ok(scores)
75 }
76
77 fn score_subset(
81 &self,
82 qnorm: &[f32],
83 idxs: &[usize],
84 ) -> Result<Vec<(usize, f32)>, RuntimeError> {
85 let mut src = self.rerank_source()?;
86 let mut out: Vec<(usize, f32)> = Vec::with_capacity(idxs.len());
87 for &i in idxs {
88 out.push((i, src.score(qnorm, i)));
89 }
90 Ok(out)
91 }
92
93 pub fn search(&self, query: &[f32], k: i32) -> Result<SearchResult, RuntimeError> {
95 let t0 = std::time::Instant::now();
96 let qnorm = self.validate_query(query, k)?;
97 let mut scores = self.score_all(&qnorm)?;
98 sort_scores_desc(&mut scores);
99 let k_usize = k as usize;
100 let truncated = k_usize < self.n_embeddings;
101 let top = &scores[..k_usize.min(self.n_embeddings)];
102 let hits = self.materialize_hits(top, "exact", false);
103 Ok(SearchResult {
104 hits: hits.clone(),
105 query_time_ms: t0.elapsed().as_secs_f64() * 1000.0,
106 index_type: "exact",
107 recall: 1.0,
108 truncated,
109 k_requested: k,
110 k_returned: hits.len(),
111 explain: SearchExplain::exact(self.n_embeddings, self.rerank_source_kind()),
112 })
113 }
114
115 pub fn search_ann(
120 &self,
121 query: &[f32],
122 k: i32,
123 ef_search: usize,
124 ) -> Result<SearchResult, RuntimeError> {
125 let t0 = std::time::Instant::now();
126 let Some(idx) = self.ann_index.as_ref() else {
127 return self.search(query, k);
128 };
129 let qnorm = self.validate_query(query, k)?;
130 let candidates = idx.search(&qnorm, ef_search.max(k as usize));
131 let ann_candidates = candidates.len();
132 let mut reranked = self.score_subset(&qnorm, &candidates)?;
133 sort_scores_desc(&mut reranked);
134 let k_usize = k as usize;
135 let truncated = k_usize < self.n_embeddings;
136 let top = &reranked[..k_usize.min(reranked.len())];
137 let hits = self.materialize_hits(top, "hnsw", true);
138 Ok(SearchResult {
139 hits: hits.clone(),
140 query_time_ms: t0.elapsed().as_secs_f64() * 1000.0,
141 index_type: "hnsw",
142 recall: f32::NAN,
145 truncated,
146 k_requested: k,
147 k_returned: hits.len(),
148 explain: SearchExplain::hnsw(ann_candidates, self.rerank_source_kind()),
149 })
150 }
151
152 pub fn search_graph(
159 &self,
160 query: &[f32],
161 k: i32,
162 hops: usize,
163 ef: usize,
164 ) -> Result<SearchResult, RuntimeError> {
165 let t0 = std::time::Instant::now();
166 let Some(graph) = self.graph_index.as_ref() else {
167 return self.search(query, k);
168 };
169 let qnorm = self.validate_query(query, k)?;
170
171 let mut seed_scores = self.score_all(&qnorm)?;
174 sort_scores_desc(&mut seed_scores);
175 let seed_budget = ef.max(k as usize).min(seed_scores.len());
176 let seeds: Vec<usize> = seed_scores[..seed_budget].iter().map(|p| p.0).collect();
177
178 let max_frontier = (seed_budget.saturating_mul(8)).max(seed_budget);
182 let mut traversal = crate::graph::Traversal::new(graph.n_nodes());
183 let union = traversal.bounded_bfs(graph, &seeds, hops, max_frontier);
184 let graph_candidates = union.len();
185
186 let mut reranked = self.score_subset(&qnorm, &union)?;
187 sort_scores_desc(&mut reranked);
188 let k_usize = k as usize;
189 let truncated = k_usize < self.n_embeddings;
190 let top = &reranked[..k_usize.min(reranked.len())];
191 let hits = self.materialize_hits(top, "graph", true);
192 Ok(SearchResult {
193 hits: hits.clone(),
194 query_time_ms: t0.elapsed().as_secs_f64() * 1000.0,
195 index_type: "graph",
196 recall: f32::NAN,
197 truncated,
198 k_requested: k,
199 k_returned: hits.len(),
200 explain: SearchExplain::graph(seed_budget, graph_candidates, self.rerank_source_kind()),
201 })
202 }
203
204 pub fn search_hybrid(
208 &self,
209 query_vec: &[f32],
210 query_text: &str,
211 k: i32,
212 candidates_per_path: usize,
213 ) -> Result<SearchResult, RuntimeError> {
214 let t0 = std::time::Instant::now();
215 let qnorm = self.validate_query(query_vec, k)?;
216 let src = self.rerank_source_kind();
217
218 let has_ann = self.ann_index.is_some();
220 let vec_candidates: Vec<usize> = if let Some(idx) = self.ann_index.as_ref() {
221 idx.search(&qnorm, candidates_per_path.max(k as usize))
222 } else {
223 let mut all = self.score_all(&qnorm)?;
224 sort_scores_desc(&mut all);
225 all.iter().take(candidates_per_path).map(|p| p.0).collect()
226 };
227
228 let lex_candidates: Vec<usize> = if let Some(bm) = self.bm25_index.as_ref() {
230 bm.search(query_text, candidates_per_path)
231 .into_iter()
232 .map(|(idx, _score)| idx)
233 .collect()
234 } else {
235 Vec::new()
236 };
237
238 let nvec = vec_candidates.len();
241 let (ann_candidates, exact_candidates) = if has_ann { (nvec, 0) } else { (0, nvec) };
242 let bm25_candidates = lex_candidates.len();
243
244 let union = bm25::rrf_union(&vec_candidates, &lex_candidates);
246 let mut reranked = self.score_subset(&qnorm, &union)?;
247 sort_scores_desc(&mut reranked);
248 let k_usize = k as usize;
249 let truncated = k_usize < self.n_embeddings;
250 let top = &reranked[..k_usize.min(reranked.len())];
251 let hits = self.materialize_hits(top, "hybrid", true);
252 Ok(SearchResult {
253 hits: hits.clone(),
254 query_time_ms: t0.elapsed().as_secs_f64() * 1000.0,
255 index_type: "hybrid",
256 recall: f32::NAN,
257 truncated,
258 k_requested: k,
259 k_returned: hits.len(),
260 explain: SearchExplain::hybrid(ann_candidates, exact_candidates, bm25_candidates, src),
261 })
262 }
263
264 pub fn search_routed(
274 &self,
275 query: &[f32],
276 query_text: Option<&str>,
277 k: i32,
278 candidates: usize,
279 ) -> Result<SearchResult, RuntimeError> {
280 match query_text {
281 Some(text) if self.bm25_index.is_some() && !text.trim().is_empty() => {
282 self.search_hybrid(query, text, k, candidates)
283 }
284 _ if self.ann_index.is_some() => self.search_ann(query, k, candidates),
285 _ => self.search(query, k),
286 }
287 }
288
289 pub(crate) fn materialize_hits(
290 &self,
291 scored: &[(usize, f32)],
292 index_type: &'static str,
293 reranked: bool,
294 ) -> Vec<SearchHit> {
295 scored
296 .iter()
297 .map(|(idx, score)| {
298 let span = &self.spans[*idx];
299 let id = &self.chunk_ids[*idx];
300 SearchHit {
301 chunk_id: id.clone(),
302 score: *score,
303 score_type: "cosine",
304 source_uri: span.source_uri.clone(),
305 offset_start: span.byte_start,
306 offset_end: span.byte_end,
307 embedding_model: self.embedding_model.clone(),
308 index_type,
309 reranked,
310 file_hash: self.file_hash.clone(),
311 content_hash: self.content_hash.clone(),
312 citation_id: format!("urna://{}/{}", self.content_hash, id),
313 }
314 })
315 .collect()
316 }
317}
318
319#[inline]
320pub(crate) fn sort_scores_desc(scores: &mut [(usize, f32)]) {
321 scores.sort_by(|a, b| crate::order::cmp_score_desc(*a, *b));
322}