Skip to main content

iqdb_ivf/
lib.rs

1//! # iqdb-ivf
2//!
3//! Inverted-file (IVF) approximate nearest-neighbour index for the iQDB
4//! vector database. [`IvfIndex`] partitions the vector space with
5//! deterministic k-means and answers queries by exhaustively scanning only
6//! the [`IvfConfig::n_probes`] clusters whose centroids are nearest to the
7//! query. It is the complement to a graph index: more memory-efficient at
8//! very large scale and with more predictable latency, because every query
9//! probes the same fixed number of clusters.
10//!
11//! Two variants share one surface. **IVF-Flat** (the default) stores
12//! vectors verbatim and scores probed clusters exactly. **IVF-PQ**
13//! (`use_pq = true`) compresses each vector to a Product-Quantization code
14//! and scores by asymmetric distance (ADC), optionally exact-reranking a
15//! shortlist against the retained vectors. Both honour metadata filters and
16//! the same `smaller-is-nearer` ordering contract as every other iQDB index.
17//!
18//! ## Tiered API
19//!
20//! - **Tier 1 — the lazy path.** [`IvfConfig::default`] plus
21//!   [`IvfIndex::new`](iqdb_index::Index::new), [`IvfIndex::train`], and
22//!   the [`iqdb_index::IndexCore`] insert/search calls cover the whole
23//!   common case with no generics to name.
24//! - **Tier 2 — the configured path.** The builder-style
25//!   [`IvfConfig::with_n_clusters`] / [`IvfConfig::with_n_probes`] /
26//!   [`IvfConfig::with_use_pq`] (and friends) tune partitioning, recall,
27//!   and compression, while [`IvfIndex::set_n_probes`],
28//!   [`IvfIndex::set_pq_refine_factor`], [`IvfIndex::suggest_n_probes`],
29//!   [`IvfIndex::retrain`], and [`IvfIndex::cluster_stats`] tune and
30//!   inspect a live index.
31//! - **Tier 3 — the trait seam.** [`IvfIndex`] implements
32//!   [`iqdb_index::Index`] and [`iqdb_index::IndexCore`], so it is
33//!   interchangeable with any other backend behind those traits.
34//!
35//! ## Design
36//!
37//! - Storage is `n_clusters` inverted lists, each owning the
38//!   `(VectorId, Arc<[f32]>, Option<Metadata>, seq)` tuples for the
39//!   vectors assigned to its centroid. The payload is wrapped in
40//!   [`Arc<[f32]>`](std::sync::Arc) so the engine shares one allocation
41//!   between this index and its record store — the same zero-copy
42//!   contract `iqdb-flat` and `iqdb-hnsw` use.
43//! - **Training is mandatory.** [`IvfIndex::new`](iqdb_index::Index::new)
44//!   returns an untrained index; the four entry points that depend on
45//!   centroids ([`insert`](iqdb_index::IndexCore::insert),
46//!   [`insert_batch`](iqdb_index::IndexCore::insert_batch),
47//!   [`search`](iqdb_index::IndexCore::search), and
48//!   [`search_batch`](iqdb_index::IndexCore::search_batch))
49//!   short-circuit with [`iqdb_types::IqdbError::InvalidConfig`] until the
50//!   caller invokes [`IvfIndex::train`]. This UX cliff is intentional and
51//!   documented loudly.
52//! - K-means runs through a seeded SplitMix64 PRNG, accumulating centroid
53//!   sums in `f64` and reducing in fixed order, so identical `seed` plus
54//!   identical sample produce byte-identical centroids on every platform.
55//! - All distance math is delegated to [`iqdb_distance::compute_batch`];
56//!   IVF never reimplements a metric. For
57//!   [`iqdb_types::DistanceMetric::DotProduct`] the raw inner product is
58//!   negated at the boundary so [`iqdb_types::Hit::distance`] is always
59//!   *smaller-is-nearer*, identical across all five metrics.
60//! - Top-`k` selection uses a bounded max-heap keyed on `(distance, seq)`
61//!   via [`f32::total_cmp`] — `O(n log k)`, NaN-safe, and deterministic:
62//!   ties break on insertion order (lower sequence number wins).
63//! - Metadata filtering goes through [`iqdb_filter::FilterEvaluator`];
64//!   pathological filters surface as
65//!   [`iqdb_types::IqdbError::InvalidFilter`] at query time, before any
66//!   cluster data is touched.
67//!
68//! ## Example
69//!
70//! ```
71//! use std::sync::Arc;
72//!
73//! use iqdb_index::{Index, IndexCore};
74//! use iqdb_ivf::{IvfConfig, IvfIndex};
75//! use iqdb_types::{DistanceMetric, SearchParams, VectorId};
76//!
77//! # fn main() -> iqdb_types::Result<()> {
78//! let cfg = IvfConfig::default()
79//!     .with_n_clusters(2)
80//!     .with_n_probes(2)
81//!     .with_training_sample_size(16)
82//!     .with_seed(7);
83//! let mut idx = IvfIndex::new(2, DistanceMetric::Euclidean, cfg)?;
84//!
85//! // Train on a representative sample before any insert / search.
86//! let sample: Vec<Vec<f32>> = vec![
87//!     vec![0.0, 0.0], vec![0.1, -0.1], vec![-0.1, 0.1],
88//!     vec![10.0, 10.0], vec![10.1, 9.9], vec![9.9, 10.1],
89//! ];
90//! let sample_refs: Vec<&[f32]> = sample.iter().map(|v| v.as_slice()).collect();
91//! idx.train(&sample_refs)?;
92//!
93//! idx.insert(VectorId::from(1u64), Arc::<[f32]>::from(&[0.0, 0.0][..]), None)?;
94//! idx.insert(VectorId::from(2u64), Arc::<[f32]>::from(&[10.0, 10.0][..]), None)?;
95//!
96//! let hits = idx.search(&[0.0, 0.0], &SearchParams::new(1, DistanceMetric::Euclidean))?;
97//! assert_eq!(hits.len(), 1);
98//! assert_eq!(hits[0].id, VectorId::U64(1));
99//! # Ok(())
100//! # }
101//! ```
102
103#![cfg_attr(docsrs, feature(doc_cfg))]
104#![deny(warnings)]
105#![deny(missing_docs)]
106#![deny(unsafe_op_in_unsafe_fn)]
107#![deny(unused_must_use)]
108#![deny(unused_results)]
109#![deny(clippy::unwrap_used)]
110#![deny(clippy::expect_used)]
111#![deny(clippy::todo)]
112#![deny(clippy::unimplemented)]
113#![deny(clippy::print_stdout)]
114#![deny(clippy::print_stderr)]
115#![deny(clippy::dbg_macro)]
116#![deny(clippy::unreachable)]
117#![deny(clippy::undocumented_unsafe_blocks)]
118#![forbid(unsafe_code)]
119
120mod assign;
121mod config;
122mod index;
123mod pq_variant;
124mod rng;
125mod search;
126mod stats;
127mod topk;
128mod train;
129
130pub use crate::config::IvfConfig;
131pub use crate::index::IvfIndex;
132pub use crate::stats::IvfClusterStats;
133
134// Re-export the `Hit` type that searches return so callers can drive
135// `IvfIndex` without a second `use` line for the result type.
136pub use iqdb_types::Hit;
137
138/// The version of this crate, taken from `Cargo.toml` at compile time.
139///
140/// # Examples
141///
142/// ```
143/// let version = iqdb_ivf::VERSION;
144/// assert_eq!(version.split('.').count(), 3);
145/// ```
146pub const VERSION: &str = env!("CARGO_PKG_VERSION");