Skip to main content

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}