1use std::collections::{BinaryHeap, HashSet};
2use std::sync::atomic::Ordering;
3use std::sync::{Arc, Mutex};
4
5use ndarray::{Array1, Array2};
6use ordered_float::NotNan;
7
8use super::{Index, persistence};
9use crate::error::{EcpError, Result};
10use crate::metric::{Metric, calculate_distances};
11
12pub type ScoredItems = Vec<(NotNan<f32>, u32)>;
14
15pub(super) struct QueryState {
17 pub(super) query: Array1<f32>,
18 pub(super) tree_pq: BinaryHeap<HeapEntry>,
20 pub(super) items: ScoredItems,
22 pub(super) exclude: HashSet<u32>,
24}
25
26impl QueryState {
27 fn add_exclude(&mut self, exclude: &HashSet<u32>) {
29 let before = self.exclude.len();
30 self.exclude.extend(exclude);
31 if self.exclude.len() > before {
32 let excluded = &self.exclude;
33 self.items.retain(|(_, id)| !excluded.contains(id));
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub(super) struct HeapEntry {
41 pub(super) score: NotNan<f32>,
43 pub(super) is_leaf: i32,
45 pub(super) level: u32,
47 pub(super) node_id: u32,
48}
49
50impl PartialEq for HeapEntry {
52 fn eq(&self, other: &Self) -> bool {
53 self.score == other.score
54 }
55}
56impl Eq for HeapEntry {}
57
58impl PartialOrd for HeapEntry {
59 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
60 Some(self.cmp(other))
61 }
62}
63impl Ord for HeapEntry {
64 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
65 self.score.cmp(&other.score)
66 }
67}
68
69const QUERY_NEVER_PERSISTED: &str = "query was never persisted";
70
71impl Index {
72 fn get_or_load_query(&self, query_id: usize) -> Result<Option<Arc<Mutex<QueryState>>>> {
75 match self.queries.try_get_with(query_id, || {
76 persistence::load_query(&self.store, query_id)?
79 .map(|state| Arc::new(Mutex::new(state)))
80 .ok_or_else(|| EcpError::NotFound(QUERY_NEVER_PERSISTED.to_string()))
81 }) {
82 Ok(state_arc) => Ok(Some(state_arc)),
83 Err(e) if matches!(&*e, EcpError::NotFound(msg) if msg == QUERY_NEVER_PERSISTED) => {
84 Ok(None)
85 }
86 Err(e) => Err((*e).clone()),
87 }
88 }
89
90 fn drain_items(&self, query_id: usize, k: usize) -> Result<ScoredItems> {
93 match self.get_or_load_query(query_id)? {
94 Some(state_arc) => {
95 let mut state = state_arc.lock().unwrap();
96 let cnt = state.items.len().min(k);
97 Ok(state.items.drain(0..cnt).collect())
98 }
99 None => Ok(Vec::new()),
100 }
101 }
102
103 pub fn new_search(
107 &self,
108 query: Array1<f32>,
109 k: usize,
110 search_exp: u32,
111 max_increments: i32,
112 exclude: &HashSet<u32>,
113 ) -> Result<(ScoredItems, usize)> {
114 let query_id = self.next_query_id.fetch_add(1, Ordering::Relaxed);
115 if !self.accepting.load(Ordering::SeqCst) {
116 return Ok((Vec::new(), query_id));
117 }
118 log::debug!(
119 "new_search: query_id={query_id} k={k} search_exp={search_exp} \
120 max_increments={max_increments} exclude={exclude:?}"
121 );
122 self.queries.insert(
123 query_id,
124 Arc::new(Mutex::new(QueryState {
125 query,
126 tree_pq: BinaryHeap::new(),
127 items: Vec::new(),
128 exclude: exclude.clone(),
129 })),
130 );
131 let leaf_nodes_scanned =
132 self.incremental_search(query_id, k, search_exp, max_increments)?;
133 let items = self.drain_items(query_id, k)?;
134 log::info!(
135 "search: query_id={query_id} k={k} leaf_nodes_scanned={leaf_nodes_scanned} items_returned={}",
136 items.len()
137 );
138 Ok((items, query_id))
139 }
140
141 pub fn incremental_search(
146 &self,
147 query_id: usize,
148 k: usize,
149 search_exp: u32,
150 max_increments: i32,
151 ) -> Result<u32> {
152 if !self.accepting.load(Ordering::SeqCst) {
154 return Ok(0);
155 }
156 let Some(state_arc) = self.get_or_load_query(query_id)? else {
157 return Ok(0);
158 };
159 let mut leaf_cnt = 0;
160 {
161 let mut state = state_arc.lock().unwrap();
162 let QueryState {
163 query,
164 tree_pq,
165 items,
166 exclude,
167 } = &mut *state;
168
169 let sign = match self.metric {
172 Metric::L2 => -1.0,
173 Metric::IP => 1.0,
174 };
175 let mut search_exp = search_exp;
176
177 let mut increments = 0;
178
179 if tree_pq.is_empty() {
181 let root_distances: Array1<f32> =
182 calculate_distances(&self.root, query, &self.metric, self.is_normalized)?;
183 let is_root_leaf = self.levels == 1;
185 for i in 0..root_distances.len() {
186 tree_pq.push(HeapEntry {
187 score: NotNan::new(sign * root_distances[i]).unwrap(),
188 is_leaf: is_root_leaf as i32,
189 level: 0,
190 node_id: i as u32,
191 });
192 }
193 }
194
195 while !tree_pq.is_empty() {
196 let HeapEntry {
197 score: _,
198 is_leaf,
199 level,
200 node_id,
201 } = tree_pq.pop().unwrap();
202 let lvl = level as usize;
203 log::trace!("visiting node lvl={lvl} node={node_id} is_leaf={is_leaf}");
204 let node = self.node_at(lvl, node_id);
205 let embeddings_f32: &Array2<f32> = match node.embeddings()? {
207 Some(embs) => embs,
208 None => {
209 log::warn!("lvl={lvl} node={node_id} has no embeddings on disk, skipping");
210 continue;
211 }
212 };
213
214 let distances: Array1<f32> =
215 calculate_distances(embeddings_f32, query, &self.metric, self.is_normalized)?;
216 let children = node.children()?.ok_or_else(|| {
217 EcpError::Corrupt(format!(
218 "{} has embeddings but no children, likely an interrupted write",
219 node.group_path
220 ))
221 })?;
222 if is_leaf == 1 {
223 let before = items.len();
226 for i in 0..distances.len() {
227 if !exclude.contains(&children[i]) {
228 items.push((NotNan::new(-sign * distances[i]).unwrap(), children[i]));
229 }
230 }
231 log::trace!(
232 "lvl={lvl} node={node_id}: collected {} of {} items",
233 items.len() - before,
234 distances.len()
235 );
236 leaf_cnt += 1;
237 } else {
238 let children_are_leaves = level + 1 == self.levels - 1;
240 for i in 0..distances.len() {
241 tree_pq.push(HeapEntry {
242 score: NotNan::new(sign * distances[i]).unwrap(),
243 is_leaf: children_are_leaves as i32,
244 level: level + 1,
245 node_id: children[i],
246 });
247 }
248 }
249
250 if self.memory_limit_bytes.is_some() {
252 self.nodes.insert((lvl, node_id), node.clone());
253 }
254
255 if leaf_cnt == search_exp {
257 if items.len() >= k {
258 break;
259 }
260 if increments < max_increments || max_increments == -1 {
261 increments += 1;
262 search_exp *= 2;
263 log::debug!(
264 "query_id={query_id}: only {} of {k} items after {leaf_cnt} leaves, \
265 doubling search_exp to {search_exp}",
266 items.len()
267 );
268 } else {
269 break;
270 }
271 }
272 }
273
274 items.sort_unstable_by_key(|&(first, _)| first);
276 }
277
278 if self.memory_limit_bytes.is_some() {
280 self.queries.insert(query_id, state_arc);
281 }
282 Ok(leaf_cnt)
283 }
284
285 pub fn get_next_k_items(
289 &self,
290 query_id: usize,
291 k: usize,
292 search_exp: u32,
293 max_increments: i32,
294 exclude: &HashSet<u32>,
295 ) -> Result<ScoredItems> {
296 if !self.accepting.load(Ordering::SeqCst) {
297 return Ok(Vec::new());
298 }
299 let Some(state_arc) = self.get_or_load_query(query_id)? else {
300 log::warn!(
301 "get_next_k_items: query_id={query_id} not found (evicted, invalid, \
302 or never persisted), returning no items"
303 );
304 return Ok(Vec::new());
305 };
306 let needs_more_search = {
307 let mut state = state_arc.lock().unwrap();
308 state.add_exclude(exclude);
309 log::debug!(
310 "get_next_k_items: query_id={query_id} k={k} search_exp={search_exp} \
311 max_increments={max_increments} exclude={exclude:?}"
312 );
313 state.items.len() < k && !state.tree_pq.is_empty()
314 };
315 let leaf_nodes_scanned = if needs_more_search {
316 self.incremental_search(query_id, k, search_exp, max_increments)?
317 } else {
318 0
319 };
320 let items = self.drain_items(query_id, k)?;
321 log::info!(
322 "search: query_id={query_id} k={k} leaf_nodes_scanned={leaf_nodes_scanned} items_returned={}",
323 items.len()
324 );
325 Ok(items)
326 }
327}
328
329#[cfg(test)]
330#[path = "utests/query.rs"]
331mod tests;