Skip to main content

nodedb_fts/backend/
traits.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use nodedb_types::Surrogate;
4
5use crate::posting::Posting;
6use crate::scope::IndexScope;
7
8/// Storage backend abstraction for the full-text search engine.
9///
10/// Origin implements this with redb (persistent). Lite implements with
11/// in-memory HashMap. All scoring, BMW, compression, and analysis logic
12/// works identically over any backend.
13///
14/// Every tenant-partitioned method takes `database_id: u64` and `tid: u64`
15/// as first-class parameters. Backends are required to isolate databases
16/// and tenants structurally — no boundary may depend on lexical-prefix
17/// ordering of a composed string key.
18///
19/// Per-index methods take an [`IndexScope`]: a collection's whole-document
20/// index or one of its field indexes. Backends key every per-index entry by
21/// both the collection and the scope's field key, so no two scopes share
22/// postings, lengths, stats, metadata, or segments. Collection-level
23/// configuration (analyzer, language, fuzzy) lives in the metadata of
24/// `IndexScope::document(collection)`.
25///
26/// Write methods take `&self` (not `&mut self`) because:
27/// - Redb provides transactional isolation internally — concurrent writes
28///   are safe through redb's MVCC.
29/// - MemoryBackend uses interior mutability (`RefCell`) to match the same
30///   trait signature, keeping the trait uniform.
31pub trait FtsBackend {
32    /// Error type for backend operations.
33    type Error: std::fmt::Display;
34
35    /// Read the posting list for a term in an index: one posting per
36    /// document.
37    fn read_postings(
38        &self,
39        database_id: u64,
40        tid: u64,
41        index: IndexScope<'_>,
42        term: &str,
43    ) -> Result<Vec<Posting>, Self::Error>;
44
45    /// Write/replace the posting list for a term in an index.
46    fn write_postings(
47        &self,
48        database_id: u64,
49        tid: u64,
50        index: IndexScope<'_>,
51        term: &str,
52        postings: &[Posting],
53    ) -> Result<(), Self::Error>;
54
55    /// Remove a term's posting list entirely.
56    fn remove_postings(
57        &self,
58        database_id: u64,
59        tid: u64,
60        index: IndexScope<'_>,
61        term: &str,
62    ) -> Result<(), Self::Error>;
63
64    /// Insert one document's posting into a term's list, replacing that
65    /// document's prior posting in the list.
66    ///
67    /// The default reads and rewrites the whole list, so it costs the list's
68    /// length. A backend that stores one entry per `(term, document)`
69    /// overrides it with a single write.
70    fn upsert_posting(
71        &self,
72        database_id: u64,
73        tid: u64,
74        index: IndexScope<'_>,
75        term: &str,
76        posting: &Posting,
77    ) -> Result<(), Self::Error> {
78        let mut list = self.read_postings(database_id, tid, index, term)?;
79        match list.iter_mut().find(|p| p.doc_id == posting.doc_id) {
80            Some(slot) => slot.clone_from(posting),
81            None => list.push(posting.clone()),
82        }
83        self.write_postings(database_id, tid, index, term, &list)
84    }
85
86    /// Remove one document's posting from a term's list. The list is removed
87    /// when this empties it. A document absent from the list is a no-op.
88    ///
89    /// The default reads and rewrites the whole list, so it costs the list's
90    /// length. A backend that stores one entry per `(term, document)`
91    /// overrides it with a single remove.
92    fn remove_posting(
93        &self,
94        database_id: u64,
95        tid: u64,
96        index: IndexScope<'_>,
97        term: &str,
98        doc_id: Surrogate,
99    ) -> Result<(), Self::Error> {
100        let mut list = self.read_postings(database_id, tid, index, term)?;
101        let before = list.len();
102        list.retain(|p| p.doc_id != doc_id);
103        if list.len() == before {
104            return Ok(());
105        }
106        if list.is_empty() {
107            self.remove_postings(database_id, tid, index, term)
108        } else {
109            self.write_postings(database_id, tid, index, term, &list)
110        }
111    }
112
113    /// Read the document length (token count) of a document in an index.
114    fn read_doc_length(
115        &self,
116        database_id: u64,
117        tid: u64,
118        index: IndexScope<'_>,
119        doc_id: Surrogate,
120    ) -> Result<Option<u32>, Self::Error>;
121
122    /// Read the document lengths of `doc_ids` in an index, parallel to
123    /// `doc_ids`. A backend with transactions reads them in one.
124    fn read_doc_lengths(
125        &self,
126        database_id: u64,
127        tid: u64,
128        index: IndexScope<'_>,
129        doc_ids: &[Surrogate],
130    ) -> Result<Vec<Option<u32>>, Self::Error> {
131        doc_ids
132            .iter()
133            .map(|doc_id| self.read_doc_length(database_id, tid, index, *doc_id))
134            .collect()
135    }
136
137    /// Write/replace the document length of a document in an index.
138    fn write_doc_length(
139        &self,
140        database_id: u64,
141        tid: u64,
142        index: IndexScope<'_>,
143        doc_id: Surrogate,
144        length: u32,
145    ) -> Result<(), Self::Error>;
146
147    /// Remove a document's length entry from an index.
148    fn remove_doc_length(
149        &self,
150        database_id: u64,
151        tid: u64,
152        index: IndexScope<'_>,
153        doc_id: Surrogate,
154    ) -> Result<(), Self::Error>;
155
156    /// Get all term names in an index (for fuzzy matching).
157    fn collection_terms(
158        &self,
159        database_id: u64,
160        tid: u64,
161        index: IndexScope<'_>,
162    ) -> Result<Vec<String>, Self::Error>;
163
164    /// Get total document count and sum of all document lengths of an index.
165    /// Returns `(doc_count, total_token_sum)`.
166    ///
167    /// Implementations should maintain these incrementally for O(1) lookup.
168    fn collection_stats(
169        &self,
170        database_id: u64,
171        tid: u64,
172        index: IndexScope<'_>,
173    ) -> Result<(u32, u64), Self::Error>;
174
175    /// Increment index stats after indexing a document.
176    /// `doc_len` is the number of tokens in the newly indexed document.
177    fn increment_stats(
178        &self,
179        database_id: u64,
180        tid: u64,
181        index: IndexScope<'_>,
182        doc_len: u32,
183    ) -> Result<(), Self::Error>;
184
185    /// Decrement index stats after removing a document.
186    /// `doc_len` is the token count of the removed document.
187    fn decrement_stats(
188        &self,
189        database_id: u64,
190        tid: u64,
191        index: IndexScope<'_>,
192        doc_len: u32,
193    ) -> Result<(), Self::Error>;
194
195    /// Read a metadata blob by sub-key (e.g., "docmap", "fieldnorms",
196    /// "analyzer", "language").
197    fn read_meta(
198        &self,
199        database_id: u64,
200        tid: u64,
201        index: IndexScope<'_>,
202        subkey: &str,
203    ) -> Result<Option<Vec<u8>>, Self::Error>;
204
205    /// Write a metadata blob by sub-key.
206    fn write_meta(
207        &self,
208        database_id: u64,
209        tid: u64,
210        index: IndexScope<'_>,
211        subkey: &str,
212        value: &[u8],
213    ) -> Result<(), Self::Error>;
214
215    /// Write a segment blob. `segment_id` is a stable per-index
216    /// identifier (e.g., `"L{level}:{id:016x}"`).
217    fn write_segment(
218        &self,
219        database_id: u64,
220        tid: u64,
221        index: IndexScope<'_>,
222        segment_id: &str,
223        data: &[u8],
224    ) -> Result<(), Self::Error>;
225
226    /// Read a segment blob. Returns None if not found.
227    fn read_segment(
228        &self,
229        database_id: u64,
230        tid: u64,
231        index: IndexScope<'_>,
232        segment_id: &str,
233    ) -> Result<Option<Vec<u8>>, Self::Error>;
234
235    /// List all segment ids of an index.
236    fn list_segments(
237        &self,
238        database_id: u64,
239        tid: u64,
240        index: IndexScope<'_>,
241    ) -> Result<Vec<String>, Self::Error>;
242
243    /// Remove a segment blob.
244    fn remove_segment(
245        &self,
246        database_id: u64,
247        tid: u64,
248        index: IndexScope<'_>,
249        segment_id: &str,
250    ) -> Result<(), Self::Error>;
251
252    /// Remove all entries of every index of a collection. Returns count of
253    /// removed entries.
254    fn purge_collection(
255        &self,
256        database_id: u64,
257        tid: u64,
258        collection: &str,
259    ) -> Result<usize, Self::Error>;
260
261    /// Remove all entries for a `(database_id, tid)` across every collection.
262    /// Returns count of removed entries. Implementations MUST use a structural
263    /// drop (e.g., tuple range `(db, tid, ..)..(db, tid+1, ..)`) rather than a
264    /// lexical-prefix scan.
265    fn purge_tenant(&self, database_id: u64, tid: u64) -> Result<usize, Self::Error>;
266}