Skip to main content

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}