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