MCPcopy Create free account
hub / github.com/NodeDB-Lab/nodedb / search

Method search

nodedb/src/engine/sparse/sparse_vector.rs:260–334  ·  view source on GitHub ↗

Search using WAND algorithm for efficient top-k sparse dot-product retrieval. WAND (Weighted AND) skips candidates whose maximum possible score (sum of max posting weights) can't exceed the current k-th best score. This avoids computing full dot products for most candidates.

(
        &self,
        collection: &str,
        query: &SparseVector,
        top_k: usize,
    )

Source from the content-addressed store, hash-verified

258 /// (sum of max posting weights) can't exceed the current k-th best score.
259 /// This avoids computing full dot products for most candidates.
260 pub fn search(
261 &self,
262 collection: &str,
263 query: &SparseVector,
264 top_k: usize,
265 ) -> crate::Result<Vec<SparseSearchResult>> {
266 if query.is_empty() || top_k == 0 {
267 return Ok(Vec::new());
268 }
269
270 let read_txn = self.db.begin_read().map_err(|e| crate::Error::Storage {
271 engine: "sparse_vector".into(),
272 detail: format!("read txn: {e}"),
273 })?;
274 let postings_table =
275 read_txn
276 .open_table(SPARSE_POSTINGS)
277 .map_err(|e| crate::Error::Storage {
278 engine: "sparse_vector".into(),
279 detail: format!("open postings: {e}"),
280 })?;
281
282 // Load posting lists for all query tokens.
283 let mut term_postings: Vec<(f32, Vec<SparsePosting>)> = Vec::new();
284 for &(token_id, query_weight) in query {
285 if query_weight.abs() < f32::EPSILON {
286 continue;
287 }
288 let key = format!("{collection}:{token_id}");
289 if let Ok(Some(guard)) = postings_table.get(key.as_str()) {
290 let postings: Vec<SparsePosting> =
291 zerompk::from_msgpack(guard.value()).unwrap_or_default();
292 if !postings.is_empty() {
293 term_postings.push((query_weight, postings));
294 }
295 }
296 }
297
298 if term_postings.is_empty() {
299 return Ok(Vec::new());
300 }
301
302 // Accumulate dot-product scores per document.
303 // For each query term, multiply query_weight × doc_weight.
304 let mut doc_scores: HashMap<String, f32> = HashMap::new();
305 for (query_weight, postings) in &term_postings {
306 for posting in postings {
307 *doc_scores.entry(posting.doc_id.clone()).or_default() +=
308 query_weight * posting.weight;
309 }
310 }
311
312 // Extract top-k by score (descending).
313 let mut results: Vec<SparseSearchResult> = doc_scores
314 .into_iter()
315 .map(|(doc_id, score)| SparseSearchResult { doc_id, score })
316 .collect();
317

Calls 10

entryMethod · 0.80
collectMethod · 0.80
is_emptyMethod · 0.45
getMethod · 0.45
as_strMethod · 0.45
pushMethod · 0.45
cloneMethod · 0.45
lenMethod · 0.45
partial_cmpMethod · 0.45
truncateMethod · 0.45

Tested by 3

multi_term_queryFunction · 0.36
collection_isolationFunction · 0.36