ringdb/engine.rs
1use std::time::Instant;
2
3use serde::{Serialize, de::DeserializeOwned};
4
5use crate::backend::{CpuBackend, RingComputeBackend};
6use crate::config::RingDbConfig;
7use crate::error::Result;
8use crate::payload::{PayloadStore, PayloadStoreBuilder};
9use crate::query::{DiskQuery, QueryResult, RangeQuery, RingQuery};
10
11/// Builder for a ring-query vector database.
12///
13/// Insert vectors (and their associated payloads) with
14/// [`add_vector()`](Self::add_vector), then call [`build()`](Self::build) to
15/// transfer ownership to the compute backend and obtain a [`SealedRingDb`]
16/// that can be queried.
17///
18/// `T` is the payload type stored alongside each vector. Use `T = ()` when
19/// no payload is needed.
20///
21/// # Example — no payload
22///
23/// ```
24/// use ringdb::{RingDb, RingDbConfig, RingQuery};
25///
26/// let config = RingDbConfig::new(4);
27/// let mut db = RingDb::new(config).unwrap();
28///
29/// db.add_vector(&[1.0, 0.0, 0.0, 0.0], ()).unwrap();
30/// db.add_vector(&[0.0, 1.0, 0.0, 0.0], ()).unwrap();
31///
32/// let db = db.build().unwrap();
33/// let result = db.query(&RingQuery { query: &[1.0f32, 0.0, 0.0, 0.0], d: 1.0, lambda: 0.1 }).unwrap();
34/// println!("hits: {:?}", result.ids);
35/// ```
36///
37/// # Example — with payload
38///
39/// ```
40/// use ringdb::{RingDb, RingDbConfig, RingQuery};
41/// use serde::{Serialize, Deserialize};
42///
43/// #[derive(Serialize, Deserialize)]
44/// struct Meta { label: String }
45///
46/// let mut db: RingDb<Meta> = RingDb::new(RingDbConfig::new(2)).unwrap();
47/// db.add_vector(&[1.0, 0.0], Meta { label: "dog".into() }).unwrap();
48/// db.add_vector(&[0.0, 1.0], Meta { label: "cat".into() }).unwrap();
49///
50/// let db = db.build().unwrap();
51/// let result = db.query(&RingQuery { query: &[1.0f32, 0.0], d: 1.0, lambda: 0.1 }).unwrap();
52/// let payloads = db.fetch_payloads(&result.ids).unwrap();
53/// ```
54pub struct RingDb<T = ()> {
55 config: RingDbConfig,
56 backend: Box<dyn RingComputeBackend>,
57 n_vectors: usize,
58
59 /// Staging buffer: f32 vectors, row-major, `n_vectors × dims`.
60 vectors: Vec<f32>,
61
62 /// Staging buffer: per-vector squared L2 norm.
63 norms_sq: Vec<f32>,
64
65 /// Streams payloads to a temp file as they arrive; never accumulates in RAM.
66 payload_builder: PayloadStoreBuilder<T>,
67}
68
69impl<T: Serialize + DeserializeOwned> RingDb<T> {
70 /// Create a new empty `RingDb` with the given configuration.
71 pub fn new(config: RingDbConfig) -> Result<Self> {
72 Ok(Self {
73 config,
74 backend: Box::new(CpuBackend::new()),
75 n_vectors: 0,
76 vectors: Vec::new(),
77 norms_sq: Vec::new(),
78 payload_builder: PayloadStoreBuilder::new()?,
79 })
80 }
81
82 /// Insert a single vector and its associated payload.
83 ///
84 /// Vectors are assigned sequential IDs starting from 0.
85 /// The slice length must equal `dims`.
86 pub fn add_vector(&mut self, vector: &[f32], payload: T) -> Result<()> {
87 let dims = self.config.dims;
88 if vector.len() != dims {
89 return Err(crate::error::RingDbError::DimensionMismatch {
90 expected: dims,
91 got: vector.len(),
92 });
93 }
94
95 let norm_sq: f32 = vector.iter().map(|x| x * x).sum();
96 self.norms_sq.push(norm_sq);
97 self.vectors.extend_from_slice(vector);
98 self.payload_builder.push(payload)?;
99 self.n_vectors += 1;
100 Ok(())
101 }
102
103 /// Transfer ownership of the accumulated data to the compute backend and
104 /// seal the database.
105 ///
106 /// Vector data is moved into the backend (zero-cost for the CPU backend).
107 /// Payloads are serialized and moved into a cold anonymous mmap — the
108 /// staging `Vec<T>` is dropped immediately after.
109 pub fn build(mut self) -> Result<SealedRingDb<T>> {
110 let dims = self.config.dims;
111 let n_vectors = self.n_vectors;
112 self.backend
113 .upload_f32_dataset(dims, self.vectors, self.norms_sq)?;
114 let payload_store = self.payload_builder.finish()?;
115 Ok(SealedRingDb {
116 config: self.config,
117 backend: self.backend,
118 n_vectors,
119 payload_store,
120 })
121 }
122
123 /// Number of vectors currently staged.
124 pub fn len(&self) -> usize {
125 self.n_vectors
126 }
127
128 /// Returns `true` if no vectors have been inserted.
129 pub fn is_empty(&self) -> bool {
130 self.n_vectors == 0
131 }
132
133 /// Number of dimensions per vector.
134 pub fn dims(&self) -> usize {
135 self.config.dims
136 }
137
138 /// Name of the backend currently in use.
139 pub fn backend_name(&self) -> &str {
140 self.backend.name()
141 }
142}
143
144/// Sealed (immutable) ring-query database.
145///
146/// Obtained by calling [`RingDb::build()`]. Vectors can no longer be
147/// inserted — only queries and payload fetches are allowed.
148///
149/// The hot side (vectors + norms) is owned by the compute backend.
150/// The cold side (payloads) lives in an anonymous mmap managed by
151/// [`PayloadStore`].
152pub struct SealedRingDb<T = ()> {
153 config: RingDbConfig,
154 backend: Box<dyn RingComputeBackend>,
155 n_vectors: usize,
156 payload_store: PayloadStore<T>,
157}
158
159impl<T: Serialize + DeserializeOwned> SealedRingDb<T> {
160 /// Execute a ring query and return matching vector IDs.
161 ///
162 /// The ring `[d - lambda, d + lambda]` is converted to `[d_min, d_max]`
163 /// internally; negative lower bounds are clamped to 0.
164 pub fn query(&self, q: &RingQuery<'_>) -> Result<QueryResult> {
165 let dims = self.config.dims;
166 if q.query.len() != dims {
167 return Err(crate::error::RingDbError::DimensionMismatch {
168 expected: dims,
169 got: q.query.len(),
170 });
171 }
172
173 let d_min = (q.d - q.lambda).max(0.0);
174 let d_max = q.d + q.lambda;
175
176 let t = Instant::now();
177 let ids = self.backend.ring_query_f32(dims, q.query, d_min, d_max)?;
178 let elapsed = t.elapsed();
179
180 Ok(QueryResult {
181 ids,
182 backend_used: self.backend.name(),
183 elapsed,
184 })
185 }
186
187 /// Execute a range query and return matching vector IDs.
188 ///
189 /// Returns all vectors whose Euclidean distance to the query lies in
190 /// `[d_min, d_max]`.
191 pub fn query_range(&self, q: &RangeQuery<'_>) -> Result<QueryResult> {
192 let dims = self.config.dims;
193 if q.query.len() != dims {
194 return Err(crate::error::RingDbError::DimensionMismatch {
195 expected: dims,
196 got: q.query.len(),
197 });
198 }
199
200 let t = Instant::now();
201 let ids = self
202 .backend
203 .ring_query_f32(dims, q.query, q.d_min, q.d_max)?;
204 let elapsed = t.elapsed();
205
206 Ok(QueryResult {
207 ids,
208 backend_used: self.backend.name(),
209 elapsed,
210 })
211 }
212
213 /// Execute a disk query and return matching vector IDs.
214 ///
215 /// Returns all vectors within Euclidean distance `d_max` of the query
216 /// (i.e. the full ball of radius `d_max`, equivalent to `d_min = 0`).
217 pub fn query_disk(&self, q: &DiskQuery<'_>) -> Result<QueryResult> {
218 let dims = self.config.dims;
219 if q.query.len() != dims {
220 return Err(crate::error::RingDbError::DimensionMismatch {
221 expected: dims,
222 got: q.query.len(),
223 });
224 }
225
226 let t = Instant::now();
227 let ids = self.backend.ring_query_f32(dims, q.query, 0.0, q.d_max)?;
228 let elapsed = t.elapsed();
229
230 Ok(QueryResult {
231 ids,
232 backend_used: self.backend.name(),
233 elapsed,
234 })
235 }
236
237 /// Fetch the payload for a single vector ID.
238 ///
239 /// Reads and deserializes from the cold mmap. Call this after
240 /// [`query`](Self::query) to retrieve metadata for the matching vectors.
241 pub fn fetch_payload(&self, id: u32) -> Result<T> {
242 self.payload_store.fetch(id)
243 }
244
245 /// Fetch payloads for a slice of vector IDs, in order.
246 pub fn fetch_payloads(&self, ids: &[u32]) -> Result<Vec<T>> {
247 self.payload_store.fetch_many(ids)
248 }
249
250 /// Number of vectors stored.
251 pub fn len(&self) -> usize {
252 self.n_vectors
253 }
254
255 /// Returns `true` if the database contains no vectors.
256 pub fn is_empty(&self) -> bool {
257 self.n_vectors == 0
258 }
259
260 /// Number of dimensions per vector.
261 pub fn dims(&self) -> usize {
262 self.config.dims
263 }
264
265 /// Name of the backend currently in use.
266 pub fn backend_name(&self) -> &str {
267 self.backend.name()
268 }
269}