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}