Skip to main content

ringdb/
engine.rs

1use std::path::Path;
2use std::time::Instant;
3
4use crate::BackendPreference;
5use crate::backend::{CpuBackend, RingComputeBackend};
6use crate::config::RingDbConfig;
7use crate::error::{Result, RingDbError};
8use crate::payload::{OwnedPayloadStore, Payload, PayloadBuilderOps, RefPayloadStore};
9use crate::persist::{read_f32_file, read_meta, write_f32_file, write_meta};
10use crate::query::{DiskQuery, QueryResult, RangeQuery, RingQuery};
11
12// ─── RingDb (builder) ────────────────────────────────────────────────────────
13
14/// Builder for a ring-query vector database.
15///
16/// Insert vectors with their associated payloads via
17/// [`add_vector()`](Self::add_vector), then call [`build()`](Self::build)
18/// to obtain a [`SealedRingDb`].
19///
20/// `T` must implement [`Payload`], which is derived with `#[derive(Payload)]`.
21/// Use `T = ()` when no payload is needed.
22///
23/// # Example — no payload
24///
25/// ```
26/// use ringdb::{RingDb, RingDbConfig, RingQuery};
27///
28/// let mut db = RingDb::new(RingDbConfig::new(4)).unwrap();
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/// ```
36pub struct RingDb<T: Payload = ()> {
37    config: RingDbConfig,
38    backend: Box<dyn RingComputeBackend>,
39    n_vectors: usize,
40
41    /// Staging buffer: f32 vectors, row-major, `n_vectors × dims`.
42    vectors: Vec<f32>,
43
44    /// Staging buffer: per-vector squared L2 norm.
45    norms_sq: Vec<f32>,
46
47    /// Concrete builder — `SerdeStoreBuilder<T>` or `PodStoreBuilder<T>`,
48    /// determined at construction time by `T::make_builder()`.
49    /// No heap indirection; lives directly in the struct.
50    payload_builder: T::Builder,
51}
52
53impl<T: Payload> RingDb<T> {
54    /// Create a new empty `RingDb`.
55    ///
56    /// The storage strategy (Serde or Pod) is determined entirely by `T`'s
57    /// `#[derive(Payload)]` — no second constructor needed.
58    ///
59    /// # Example — with Serde payload
60    ///
61    /// ```
62    /// use ringdb::{RingDb, RingDbConfig, RingQuery, Payload};
63    /// use serde::{Serialize, Deserialize};
64    ///
65    /// #[derive(Serialize, Deserialize, Payload)]
66    /// struct Meta { label: String }
67    ///
68    /// let mut db: RingDb<Meta> = RingDb::new(RingDbConfig::new(2)).unwrap();
69    /// db.add_vector(&[1.0, 0.0], Meta { label: "dog".into() }).unwrap();
70    /// db.add_vector(&[0.0, 1.0], Meta { label: "cat".into() }).unwrap();
71    ///
72    /// let db = db.build().unwrap();
73    /// let result = db.query(&RingQuery { query: &[1.0f32, 0.0], d: 1.0, lambda: 0.1 }).unwrap();
74    /// let payloads = db.fetch_payloads(&result.ids).unwrap();
75    /// ```
76    pub fn new(config: RingDbConfig) -> Result<Self> {
77        let backend = match config.backend_preference {
78            BackendPreference::Cpu => Box::new(CpuBackend::new()),
79        };
80        Ok(Self {
81            config,
82            backend,
83            n_vectors: 0,
84            vectors: Vec::new(),
85            norms_sq: Vec::new(),
86            payload_builder: T::make_builder()?,
87        })
88    }
89
90    /// Insert a single vector and its associated payload.
91    ///
92    /// Vectors are assigned sequential IDs starting from 0.
93    /// The slice length must equal `dims`.
94    pub fn add_vector(&mut self, vector: &[f32], payload: T) -> Result<()> {
95        let dims = self.config.dims;
96        if vector.len() != dims {
97            return Err(RingDbError::DimensionMismatch {
98                expected: dims,
99                got: vector.len(),
100            });
101        }
102        let norm_sq: f32 = vector.iter().map(|x| x * x).sum();
103        self.norms_sq.push(norm_sq);
104        self.vectors.extend_from_slice(vector);
105        self.payload_builder.push(payload)?;
106        self.n_vectors += 1;
107        Ok(())
108    }
109
110    /// Seal the database.
111    ///
112    /// Transfers vectors to the compute backend and flushes the payload builder
113    /// to its mmap. If [`RingDbConfig::persist_dir`] is set, all data is also
114    /// written to disk (reload with [`RingDb::load`]).
115    pub fn build(self) -> Result<SealedRingDb<T>> {
116        let RingDb {
117            config,
118            mut backend,
119            vectors,
120            norms_sq,
121            payload_builder,
122            n_vectors,
123        } = self;
124        let dims = config.dims;
125
126        if let Some(dir) = config.persist_dir.clone() {
127            std::fs::create_dir_all(&dir)?;
128            write_meta(&dir.join("meta.bin"), dims, n_vectors)?;
129            write_f32_file(&dir.join("vectors.bin"), &vectors)?;
130            write_f32_file(&dir.join("norms_sq.bin"), &norms_sq)?;
131            let payload_store = payload_builder
132                .finish_persisted(&dir.join("payloads.bin"), &dir.join("offsets.bin"))?;
133            backend.upload_f32_dataset(dims, vectors, norms_sq)?;
134            return Ok(SealedRingDb {
135                config,
136                backend,
137                n_vectors,
138                payload_store,
139            });
140        }
141
142        backend.upload_f32_dataset(dims, vectors, norms_sq)?;
143        let payload_store = payload_builder.finish()?;
144        Ok(SealedRingDb {
145            config,
146            backend,
147            n_vectors,
148            payload_store,
149        })
150    }
151
152    /// Reconstruct a [`SealedRingDb`] from a directory previously written by
153    /// [`build()`](Self::build) with a persist dir configured.
154    ///
155    /// The correct store variant is selected automatically based on `T`'s
156    /// `Payload` impl — no separate `load_pod` method needed.
157    ///
158    /// # Example
159    ///
160    /// ```no_run
161    /// use ringdb::{RingDb, RingDbConfig, BackendPreference};
162    /// use std::path::Path;
163    ///
164    /// // --- save ---
165    /// let mut db = RingDb::<()>::new(RingDbConfig::new(4).with_persist_dir("/tmp/mydb")).unwrap();
166    /// db.add_vector(&[1.0, 0.0, 0.0, 0.0], ()).unwrap();
167    /// let _sealed = db.build().unwrap();
168    ///
169    /// // --- load ---
170    /// let loaded = RingDb::<()>::load(Path::new("/tmp/mydb"), BackendPreference::Cpu).unwrap();
171    /// ```
172    pub fn load(
173        dir: &Path,
174        backend_preference: crate::config::BackendPreference,
175    ) -> Result<SealedRingDb<T>> {
176        let (dims, n_vectors) = read_meta(&dir.join("meta.bin"))?;
177        let vectors = read_f32_file(&dir.join("vectors.bin"))?;
178        let norms_sq = read_f32_file(&dir.join("norms_sq.bin"))?;
179
180        let expected = n_vectors * dims;
181        if vectors.len() != expected {
182            return Err(RingDbError::Corrupt(format!(
183                "vectors.bin has {} f32 values, expected {}",
184                vectors.len(),
185                expected,
186            )));
187        }
188        if norms_sq.len() != n_vectors {
189            return Err(RingDbError::Corrupt(format!(
190                "norms_sq.bin has {} f32 values, expected {}",
191                norms_sq.len(),
192                n_vectors,
193            )));
194        }
195
196        let mut backend: Box<dyn RingComputeBackend> = match backend_preference {
197            crate::config::BackendPreference::Cpu => Box::new(CpuBackend::new()),
198        };
199        backend.upload_f32_dataset(dims, vectors, norms_sq)?;
200
201        let payload_store = T::load_store(dir)?;
202
203        Ok(SealedRingDb {
204            config: RingDbConfig::new(dims)
205                .with_persist_dir(dir)
206                .with_backend_preference(backend_preference),
207            backend,
208            n_vectors,
209            payload_store,
210        })
211    }
212
213    /// Number of vectors currently staged.
214    pub fn len(&self) -> usize {
215        self.n_vectors
216    }
217
218    /// Returns `true` if no vectors have been inserted.
219    pub fn is_empty(&self) -> bool {
220        self.n_vectors == 0
221    }
222
223    /// Number of dimensions per vector.
224    pub fn dims(&self) -> usize {
225        self.config.dims
226    }
227
228    /// Name of the backend currently in use.
229    pub fn backend_name(&self) -> &str {
230        self.backend.name()
231    }
232}
233
234// ─── SealedRingDb ────────────────────────────────────────────────────────────
235
236/// Sealed (immutable) ring-query database.
237///
238/// Obtained by calling [`RingDb::build()`] or [`RingDb::load()`].
239///
240/// The hot side (vectors + norms) is owned by the compute backend.
241/// The cold side (payloads) lives in a file-backed mmap via `T::Store`.
242pub struct SealedRingDb<T: Payload = ()> {
243    config: RingDbConfig,
244    backend: Box<dyn RingComputeBackend>,
245    n_vectors: usize,
246    payload_store: T::Store,
247}
248
249impl<T: Payload> SealedRingDb<T> {
250    // ── Query methods ─────────────────────────────────────────────────────────
251
252    /// Execute a ring query and return matching vector IDs.
253    pub fn query(&self, q: &RingQuery<'_>) -> Result<QueryResult> {
254        let dims = self.config.dims;
255        if q.query.len() != dims {
256            return Err(RingDbError::DimensionMismatch {
257                expected: dims,
258                got: q.query.len(),
259            });
260        }
261        let t = Instant::now();
262        let ids = self.backend.ring_query_f32(
263            dims,
264            q.query,
265            (q.d - q.lambda).max(0.0),
266            q.d + q.lambda,
267        )?;
268        Ok(QueryResult {
269            ids,
270            backend_used: self.backend.name(),
271            elapsed: t.elapsed(),
272        })
273    }
274
275    /// Execute a range query: all vectors with distance in `[d_min, d_max]`.
276    pub fn query_range(&self, q: &RangeQuery<'_>) -> Result<QueryResult> {
277        let dims = self.config.dims;
278        if q.query.len() != dims {
279            return Err(RingDbError::DimensionMismatch {
280                expected: dims,
281                got: q.query.len(),
282            });
283        }
284        let t = Instant::now();
285        let ids = self
286            .backend
287            .ring_query_f32(dims, q.query, q.d_min, q.d_max)?;
288        Ok(QueryResult {
289            ids,
290            backend_used: self.backend.name(),
291            elapsed: t.elapsed(),
292        })
293    }
294
295    /// Execute a disk query: all vectors within radius `d_max`.
296    pub fn query_disk(&self, q: &DiskQuery<'_>) -> Result<QueryResult> {
297        let dims = self.config.dims;
298        if q.query.len() != dims {
299            return Err(RingDbError::DimensionMismatch {
300                expected: dims,
301                got: q.query.len(),
302            });
303        }
304        let t = Instant::now();
305        let ids = self.backend.ring_query_f32(dims, q.query, 0.0, q.d_max)?;
306        Ok(QueryResult {
307            ids,
308            backend_used: self.backend.name(),
309            elapsed: t.elapsed(),
310        })
311    }
312
313    // ── Serde payload fetch ───────────────────────────────────────────────────
314
315    /// Fetch and deserialize the payload for a single vector ID.
316    pub fn fetch_payload(&self, id: u32) -> Result<T> {
317        self.payload_store.fetch_owned(id)
318    }
319
320    /// Fetch and deserialize payloads for a slice of IDs, in order.
321    pub fn fetch_payloads(&self, ids: &[u32]) -> Result<Vec<T>> {
322        self.payload_store.fetch_many_owned(ids)
323    }
324
325    // ── Accessors ─────────────────────────────────────────────────────────────
326
327    /// Number of vectors stored.
328    pub fn len(&self) -> usize {
329        self.n_vectors
330    }
331
332    /// Returns `true` if the database contains no vectors.
333    pub fn is_empty(&self) -> bool {
334        self.n_vectors == 0
335    }
336
337    /// Number of dimensions per vector.
338    pub fn dims(&self) -> usize {
339        self.config.dims
340    }
341
342    /// Name of the backend currently in use.
343    pub fn backend_name(&self) -> &str {
344        self.backend.name()
345    }
346}
347
348// ── Pod fetch — only when T::Store: RefPayloadStore<T> ───────────────────────
349//
350// This impl block is only available for types whose `#[derive(Payload)]`
351// chose `storage = "pod"`. For Serde types, `T::Store = SerdeStore<T>` which
352// does NOT implement `RefPayloadStore<T>`, so these methods simply don't exist.
353// The compiler enforces this statically — no runtime error possible.
354
355impl<T: Payload> SealedRingDb<T>
356where
357    T::Store: RefPayloadStore<T>,
358{
359    /// Fetch a zero-copy reference to the payload for a single vector ID.
360    ///
361    /// Returns a `&T` pointing directly into the mmap — O(1), no allocation,
362    /// no deserialization. Only available for `#[payload(storage = "pod")]` types.
363    pub fn fetch_pod(&self, id: u32) -> &T {
364        self.payload_store.fetch_ref(id)
365    }
366
367    /// Fetch zero-copy references to payloads for a slice of IDs, in order.
368    pub fn fetch_pods<'a>(&'a self, ids: &[u32]) -> Vec<&'a T> {
369        self.payload_store.fetch_many_ref(ids)
370    }
371}