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.
36    fn read_postings(
37        &self,
38        database_id: u64,
39        tid: u64,
40        index: IndexScope<'_>,
41        term: &str,
42    ) -> Result<Vec<Posting>, Self::Error>;
43
44    /// Write/replace the posting list for a term in an index.
45    fn write_postings(
46        &self,
47        database_id: u64,
48        tid: u64,
49        index: IndexScope<'_>,
50        term: &str,
51        postings: &[Posting],
52    ) -> Result<(), Self::Error>;
53
54    /// Remove a term's posting list entirely.
55    fn remove_postings(
56        &self,
57        database_id: u64,
58        tid: u64,
59        index: IndexScope<'_>,
60        term: &str,
61    ) -> Result<(), Self::Error>;
62
63    /// Read the document length (token count) of a document in an index.
64    fn read_doc_length(
65        &self,
66        database_id: u64,
67        tid: u64,
68        index: IndexScope<'_>,
69        doc_id: Surrogate,
70    ) -> Result<Option<u32>, Self::Error>;
71
72    /// Read the document lengths of `doc_ids` in an index, parallel to
73    /// `doc_ids`. A backend with transactions reads them in one.
74    fn read_doc_lengths(
75        &self,
76        database_id: u64,
77        tid: u64,
78        index: IndexScope<'_>,
79        doc_ids: &[Surrogate],
80    ) -> Result<Vec<Option<u32>>, Self::Error> {
81        doc_ids
82            .iter()
83            .map(|doc_id| self.read_doc_length(database_id, tid, index, *doc_id))
84            .collect()
85    }
86
87    /// Write/replace the document length of a document in an index.
88    fn write_doc_length(
89        &self,
90        database_id: u64,
91        tid: u64,
92        index: IndexScope<'_>,
93        doc_id: Surrogate,
94        length: u32,
95    ) -> Result<(), Self::Error>;
96
97    /// Remove a document's length entry from an index.
98    fn remove_doc_length(
99        &self,
100        database_id: u64,
101        tid: u64,
102        index: IndexScope<'_>,
103        doc_id: Surrogate,
104    ) -> Result<(), Self::Error>;
105
106    /// Get all term names in an index (for fuzzy matching).
107    fn collection_terms(
108        &self,
109        database_id: u64,
110        tid: u64,
111        index: IndexScope<'_>,
112    ) -> Result<Vec<String>, Self::Error>;
113
114    /// Get total document count and sum of all document lengths of an index.
115    /// Returns `(doc_count, total_token_sum)`.
116    ///
117    /// Implementations should maintain these incrementally for O(1) lookup.
118    fn collection_stats(
119        &self,
120        database_id: u64,
121        tid: u64,
122        index: IndexScope<'_>,
123    ) -> Result<(u32, u64), Self::Error>;
124
125    /// Increment index stats after indexing a document.
126    /// `doc_len` is the number of tokens in the newly indexed document.
127    fn increment_stats(
128        &self,
129        database_id: u64,
130        tid: u64,
131        index: IndexScope<'_>,
132        doc_len: u32,
133    ) -> Result<(), Self::Error>;
134
135    /// Decrement index stats after removing a document.
136    /// `doc_len` is the token count of the removed document.
137    fn decrement_stats(
138        &self,
139        database_id: u64,
140        tid: u64,
141        index: IndexScope<'_>,
142        doc_len: u32,
143    ) -> Result<(), Self::Error>;
144
145    /// Read a metadata blob by sub-key (e.g., "docmap", "fieldnorms",
146    /// "analyzer", "language").
147    fn read_meta(
148        &self,
149        database_id: u64,
150        tid: u64,
151        index: IndexScope<'_>,
152        subkey: &str,
153    ) -> Result<Option<Vec<u8>>, Self::Error>;
154
155    /// Write a metadata blob by sub-key.
156    fn write_meta(
157        &self,
158        database_id: u64,
159        tid: u64,
160        index: IndexScope<'_>,
161        subkey: &str,
162        value: &[u8],
163    ) -> Result<(), Self::Error>;
164
165    /// Write a segment blob. `segment_id` is a stable per-index
166    /// identifier (e.g., `"L{level}:{id:016x}"`).
167    fn write_segment(
168        &self,
169        database_id: u64,
170        tid: u64,
171        index: IndexScope<'_>,
172        segment_id: &str,
173        data: &[u8],
174    ) -> Result<(), Self::Error>;
175
176    /// Read a segment blob. Returns None if not found.
177    fn read_segment(
178        &self,
179        database_id: u64,
180        tid: u64,
181        index: IndexScope<'_>,
182        segment_id: &str,
183    ) -> Result<Option<Vec<u8>>, Self::Error>;
184
185    /// List all segment ids of an index.
186    fn list_segments(
187        &self,
188        database_id: u64,
189        tid: u64,
190        index: IndexScope<'_>,
191    ) -> Result<Vec<String>, Self::Error>;
192
193    /// Remove a segment blob.
194    fn remove_segment(
195        &self,
196        database_id: u64,
197        tid: u64,
198        index: IndexScope<'_>,
199        segment_id: &str,
200    ) -> Result<(), Self::Error>;
201
202    /// Remove all entries of every index of a collection. Returns count of
203    /// removed entries.
204    fn purge_collection(
205        &self,
206        database_id: u64,
207        tid: u64,
208        collection: &str,
209    ) -> Result<usize, Self::Error>;
210
211    /// Remove all entries for a `(database_id, tid)` across every collection.
212    /// Returns count of removed entries. Implementations MUST use a structural
213    /// drop (e.g., tuple range `(db, tid, ..)..(db, tid+1, ..)`) rather than a
214    /// lexical-prefix scan.
215    fn purge_tenant(&self, database_id: u64, tid: u64) -> Result<usize, Self::Error>;
216}