nodedb_lite/nodedb/core/sparse_ops.rs
1// SPDX-License-Identifier: Apache-2.0
2
3//! Sparse-vector index maintenance and the public sparse search API.
4//!
5//! Write-path hooks (`index_document_sparse` / `remove_document_sparse`) are
6//! invoked from the same points as their full-text counterparts, so every
7//! document write keeps the sparse inverted index in step with document state.
8
9use nodedb_types::SparseVector;
10use nodedb_types::error::{NodeDbError, NodeDbResult};
11
12use crate::nodedb::lock_ext::LockExt;
13use crate::storage::engine::StorageEngine;
14
15use super::types::NodeDbLite;
16
17impl<S: StorageEngine> NodeDbLite<S> {
18 /// Update the sparse-vector inverted index after a document write.
19 ///
20 /// Every string field is offered to the sparse-vector literal parser; the
21 /// ones that parse (`'{12: 0.5, 88: 0.3}'`) are indexed under their own
22 /// field name. Fields that do not parse are simply not sparse columns and
23 /// are skipped silently.
24 pub(crate) fn index_document_sparse(
25 &self,
26 collection: &str,
27 doc_id: &str,
28 fields: &std::collections::HashMap<String, nodedb_types::Value>,
29 ) {
30 self.sparse_state
31 .manager
32 .lock_or_recover()
33 .index_document_fields(collection, doc_id, fields);
34 }
35
36 /// Remove a document from every sparse index of `collection`.
37 pub(crate) fn remove_document_sparse(&self, collection: &str, doc_id: &str) {
38 self.sparse_state
39 .manager
40 .lock_or_recover()
41 .remove_document_all_fields(collection, doc_id);
42 }
43
44 /// Index a sparse vector under `(collection, field)`.
45 ///
46 /// `entries` are `(dimension, weight)` pairs; they are sorted, deduplicated
47 /// and validated by [`SparseVector`]. Re-indexing the same `doc_id`
48 /// replaces its postings rather than adding to them.
49 pub fn sparse_insert(
50 &self,
51 collection: &str,
52 field: &str,
53 doc_id: &str,
54 entries: &[(u32, f32)],
55 ) -> NodeDbResult<()> {
56 let vector =
57 SparseVector::from_entries(entries.to_vec()).map_err(NodeDbError::bad_request)?;
58 self.sparse_state
59 .manager
60 .lock_or_recover()
61 .index_document(collection, field, doc_id, &vector);
62 Ok(())
63 }
64
65 /// Remove a document from one `(collection, field)` sparse index.
66 ///
67 /// Returns `true` when the document was indexed.
68 pub fn sparse_delete(&self, collection: &str, field: &str, doc_id: &str) -> bool {
69 self.sparse_state
70 .manager
71 .lock_or_recover()
72 .remove_document(collection, field, doc_id)
73 }
74
75 /// Top-`k` documents in `(collection, field)` by sparse dot product.
76 ///
77 /// Results are `(doc_id, score)` ordered by score descending, ties broken
78 /// by ascending `doc_id`. Documents sharing no dimension with the query
79 /// score zero and are excluded. A collection with no sparse index yields
80 /// no hits rather than an error.
81 pub fn sparse_search(
82 &self,
83 collection: &str,
84 field: &str,
85 query_entries: &[(u32, f32)],
86 top_k: usize,
87 ) -> NodeDbResult<Vec<(String, f32)>> {
88 let query =
89 SparseVector::from_entries(query_entries.to_vec()).map_err(NodeDbError::bad_request)?;
90 let hits = self
91 .sparse_state
92 .manager
93 .lock_or_recover()
94 .search(collection, field, &query, top_k);
95 Ok(hits.into_iter().map(|h| (h.doc_id, h.score)).collect())
96 }
97}