iqdb_index/lib.rs
1//! # iqdb-index
2//!
3//! The index-trait layer for the HiveDB **iqdb** vector-database spine. It
4//! defines the shape every concrete index (flat, HNSW, IVF, …) implements so
5//! the engine can hold them polymorphically, and exposes [`IndexStats`] for
6//! runtime introspection. Its only dependency is
7//! [`iqdb-types`](iqdb_types) — the shared vocabulary.
8//!
9//! ## Why the trait is split
10//!
11//! [`Index`] carries an associated `type Config: Default + Clone` and a
12//! `Self`-returning `fn new(...) -> Result<Self>`. That combination makes a
13//! single trait non-object-safe — `Box<dyn Index>` would not compile. The
14//! engine needs to hold a heterogeneous set of indexes, so the trait is
15//! split:
16//!
17//! - [`IndexCore`] — the object-safe operational surface: `insert`,
18//! `delete`, `search`, `len`, `dim`, `metric`, `flush`, `stats` (plus the
19//! default batch shims). The engine stores `Box<dyn IndexCore>` through
20//! this trait.
21//! - [`Index`] — adds the associated `Config` and `new`. Used where the
22//! concrete index type is known.
23//!
24//! Every concrete index implements **both**.
25//!
26//! ## Ordering contract on `Hit.distance`
27//!
28//! [`iqdb_types::Hit::distance`] is documented as **smaller is nearer**.
29//! Four of the five metrics (Cosine, Euclidean, Manhattan, Hamming) already
30//! satisfy that contract under [`iqdb_types::DistanceMetric`]. For
31//! `DotProduct` the raw value is a similarity (larger is more similar), so
32//! every index MUST negate it at the boundary — store `-dot` in
33//! `Hit.distance` — to keep one ordering invariant across the index family.
34//!
35//! ## Synchronous by design
36//!
37//! The trait is **synchronous**: `search`, `insert`, and the rest return
38//! [`Result`](iqdb_types::Result), not futures. This is a deliberate, frozen
39//! decision, for three reasons:
40//!
41//! 1. **Object safety on the hot path.** [`IndexCore`] must be
42//! `dyn`-compatible so the engine can hold `Box<dyn IndexCore>`. An
43//! `async fn` in the trait is not `dyn`-compatible without boxing the
44//! returned future, which would put a heap allocation on every `search`
45//! call — unacceptable for the query hot path.
46//! 2. **The work is CPU-bound.** A nearest-neighbour search is an in-memory
47//! scan or graph walk, not I/O. Wrapping CPU-bound work in a future buys
48//! nothing and costs a state machine.
49//! 3. **Async belongs at the engine boundary, not the index.** The engine
50//! already guards each index with an `RwLock`; if it wants an async API
51//! edge, it offloads the blocking call (for example via
52//! `spawn_blocking`). That keeps the index contract simple and leaves the
53//! runtime choice to the engine.
54//!
55//! There is therefore no async trait, no `async` feature, and no `futures`
56//! dependency. Async is optional *wrapping* by a consumer, not part of this
57//! crate's surface.
58//!
59//! ## Example
60//!
61//! ```
62//! use std::sync::Arc;
63//!
64//! use iqdb_index::{Index, IndexCore, IndexStats};
65//! use iqdb_types::{
66//! DistanceMetric, Hit, IqdbError, Metadata, Result, SearchParams, VectorId,
67//! };
68//!
69//! /// A toy in-memory index used only to document the trait shape.
70//! struct Toy {
71//! dim: usize,
72//! metric: DistanceMetric,
73//! ids: Vec<VectorId>,
74//! }
75//!
76//! #[derive(Default, Clone)]
77//! struct ToyConfig;
78//!
79//! impl IndexCore for Toy {
80//! fn insert(&mut self, id: VectorId, _v: Arc<[f32]>, _m: Option<Metadata>) -> Result<()> {
81//! self.ids.push(id);
82//! Ok(())
83//! }
84//! fn delete(&mut self, id: &VectorId) -> Result<()> {
85//! match self.ids.iter().position(|x| x == id) {
86//! Some(pos) => {
87//! let _removed = self.ids.remove(pos);
88//! Ok(())
89//! }
90//! None => Err(IqdbError::NotFound),
91//! }
92//! }
93//! fn search(&self, _q: &[f32], _p: &SearchParams) -> Result<Vec<Hit>> {
94//! Ok(Vec::new())
95//! }
96//! fn len(&self) -> usize { self.ids.len() }
97//! fn dim(&self) -> usize { self.dim }
98//! fn metric(&self) -> DistanceMetric { self.metric }
99//! fn flush(&mut self) -> Result<()> { Ok(()) }
100//! fn stats(&self) -> IndexStats {
101//! IndexStats {
102//! n_vectors: self.ids.len(),
103//! index_type: "toy",
104//! ..IndexStats::default()
105//! }
106//! }
107//! }
108//!
109//! impl Index for Toy {
110//! type Config = ToyConfig;
111//! fn new(dim: usize, metric: DistanceMetric, _c: Self::Config) -> Result<Self> {
112//! Ok(Toy { dim, metric, ids: Vec::new() })
113//! }
114//! }
115//!
116//! # fn main() -> Result<()> {
117//! let mut idx = Toy::new(3, DistanceMetric::Cosine, ToyConfig)?;
118//! assert!(idx.is_empty());
119//! idx.insert(VectorId::from(1u64), Arc::<[f32]>::from(&[1.0, 0.0, 0.0][..]), None)?;
120//! assert_eq!(idx.len(), 1);
121//! idx.delete(&VectorId::from(1u64))?;
122//! assert!(idx.is_empty());
123//! # Ok(())
124//! # }
125//! ```
126
127#![deny(warnings)]
128#![deny(missing_docs)]
129#![deny(unsafe_op_in_unsafe_fn)]
130#![deny(unused_must_use)]
131#![deny(unused_results)]
132#![deny(clippy::unwrap_used)]
133#![deny(clippy::expect_used)]
134#![deny(clippy::todo)]
135#![deny(clippy::unimplemented)]
136#![deny(clippy::print_stdout)]
137#![deny(clippy::print_stderr)]
138#![deny(clippy::dbg_macro)]
139#![deny(clippy::unreachable)]
140#![deny(clippy::undocumented_unsafe_blocks)]
141#![forbid(unsafe_code)]
142#![cfg_attr(docsrs, feature(doc_cfg))]
143
144mod index;
145mod stats;
146
147pub use crate::index::{Index, IndexCore};
148pub use crate::stats::IndexStats;
149
150/// The version of this crate, taken from `Cargo.toml` at compile time.
151///
152/// Exposed so a consumer can report the exact `iqdb-index` build it links
153/// against — useful in diagnostics and version-skew checks across the iqdb
154/// crate family.
155///
156/// # Examples
157///
158/// ```
159/// // Carries a `major.minor.patch` SemVer core.
160/// let version = iqdb_index::VERSION;
161/// assert_eq!(version.split('.').count(), 3);
162/// assert!(version.split('.').all(|part| !part.is_empty()));
163/// ```
164pub const VERSION: &str = env!("CARGO_PKG_VERSION");