Skip to main content

iqdb_quantize/
lib.rs

1//! # iqdb-quantize
2//!
3//! Vector quantization for the **iqdb** embedded vector-database spine. The
4//! crate compresses `f32` embedding vectors into compact codes that preserve
5//! similarity-search quality. It ships three schemes behind one trait:
6//!
7//! - [`ScalarQuantizer`] — scalar quantization (SQ8, 4× compression).
8//!   Per-dimension affine calibration learned from a training sample; codes
9//!   are `u8`. Asymmetric distance dequantizes the candidate to a temporary
10//!   buffer and routes through [`iqdb_distance::compute`] for every metric.
11//! - [`BinaryQuantizer`] — binary quantization (BQ, 32× compression). One
12//!   bit per dimension thresholded against a trained per-dimension mean;
13//!   codes are packed into [`u64`] words. Hamming distance is computed
14//!   directly on the packed codes via XOR + popcount. **BQ supports
15//!   [`DistanceMetric::Hamming`] only**; other metrics return
16//!   [`IqdbError::InvalidMetric`].
17//! - [`ProductQuantizer`] — product quantization (PQ, configurable
18//!   compression — `M` bytes per code, e.g. `M = 16` shrinks a 768-dim
19//!   `f32` vector from 3072 bytes to 16). Splits each vector into `M`
20//!   subvectors and learns a `K`-centroid codebook per position via
21//!   deterministic k-means (k-means++ seeding, Lloyd's iterations, seeded
22//!   by [`ProductQuantizer::seed`]). Asymmetric distance computation (ADC)
23//!   precomputes per-subvector distance tables and scores codes by table
24//!   lookup + summation. **PQ supports [`DistanceMetric::Euclidean`],
25//!   [`DistanceMetric::DotProduct`], and [`DistanceMetric::Manhattan`]**;
26//!   [`DistanceMetric::Cosine`] (no global norm) and
27//!   [`DistanceMetric::Hamming`] (wrong code space) return
28//!   [`IqdbError::InvalidMetric`].
29//!
30//! Every method of the [`Quantizer`] trait is fallible and returns
31//! [`iqdb_types::Result`]. The library never panics on bad input.
32//!
33//! ## How to use quantization correctly
34//!
35//! Quantization is lossy by design. Two rules:
36//!
37//! 1. **Train on representative data.** Per-dimension calibration is only
38//!    as good as the sample it was learned from. Train on the embeddings
39//!    you intend to index, not a synthetic placeholder.
40//! 2. **Search quantized, rerank with full `f32`.** Quantized distance
41//!    narrows the candidate set cheaply; the final ranking should use the
42//!    original `f32` vectors. Skipping the rerank step is the most common
43//!    cause of "quantization broke recall" reports.
44//!
45//! ## Example
46//!
47//! ```
48//! use iqdb_quantize::{Quantizer, ScalarQuantizer};
49//! use iqdb_types::DistanceMetric;
50//!
51//! let training = [
52//!     vec![0.10_f32, 0.20, 0.30],
53//!     vec![0.15, 0.18, 0.32],
54//!     vec![0.12, 0.22, 0.28],
55//! ];
56//! let refs: Vec<&[f32]> = training.iter().map(Vec::as_slice).collect();
57//!
58//! let mut sq = ScalarQuantizer::new();
59//! sq.train(&refs).expect("non-empty, consistent dims, finite values");
60//!
61//! let code = sq.quantize(&[0.11, 0.21, 0.29]).expect("dim matches training");
62//! let d = sq
63//!     .distance(&[0.10, 0.20, 0.30], &code, DistanceMetric::Cosine)
64//!     .expect("dim matches");
65//! assert!(d.is_finite());
66//! ```
67//!
68//! ## Errors
69//!
70//! Every fallible call returns [`iqdb_types::Result`]. Empty or non-finite
71//! inputs surface as [`IqdbError::InvalidVector`]; dimension drift as
72//! [`IqdbError::DimensionMismatch`]; calling a hot method before
73//! [`Quantizer::train`] returns [`IqdbError::InvalidConfig`]; a non-Hamming
74//! metric against [`BinaryQuantizer`] or an unsupported metric
75//! ([`DistanceMetric::Cosine`], [`DistanceMetric::Hamming`]) against
76//! [`ProductQuantizer`] returns [`IqdbError::InvalidMetric`].
77//!
78//! [`DistanceMetric::Cosine`]: iqdb_types::DistanceMetric::Cosine
79//! [`DistanceMetric::DotProduct`]: iqdb_types::DistanceMetric::DotProduct
80//! [`DistanceMetric::Euclidean`]: iqdb_types::DistanceMetric::Euclidean
81//! [`DistanceMetric::Hamming`]: iqdb_types::DistanceMetric::Hamming
82//! [`DistanceMetric::Manhattan`]: iqdb_types::DistanceMetric::Manhattan
83//! [`IqdbError`]: iqdb_types::IqdbError
84//! [`IqdbError::InvalidConfig`]: iqdb_types::IqdbError::InvalidConfig
85//! [`IqdbError::InvalidMetric`]: iqdb_types::IqdbError::InvalidMetric
86//! [`IqdbError::InvalidVector`]: iqdb_types::IqdbError::InvalidVector
87//! [`IqdbError::DimensionMismatch`]: iqdb_types::IqdbError::DimensionMismatch
88
89#![cfg_attr(docsrs, feature(doc_cfg))]
90#![deny(warnings)]
91#![deny(missing_docs)]
92#![deny(unsafe_op_in_unsafe_fn)]
93#![deny(unused_must_use)]
94#![deny(unused_results)]
95#![deny(clippy::unwrap_used)]
96#![deny(clippy::expect_used)]
97#![deny(clippy::todo)]
98#![deny(clippy::unimplemented)]
99#![deny(clippy::print_stdout)]
100#![deny(clippy::print_stderr)]
101#![deny(clippy::dbg_macro)]
102#![deny(clippy::unreachable)]
103#![deny(clippy::undocumented_unsafe_blocks)]
104
105mod binary;
106mod code;
107mod product;
108mod rng;
109mod scalar;
110mod train;
111mod traits;
112mod validate;
113
114pub use crate::binary::BinaryQuantizer;
115pub use crate::code::{BqCode, PqCode, Sq8Code};
116pub use crate::product::{PqAdcTables, ProductQuantizer};
117pub use crate::scalar::ScalarQuantizer;
118pub use crate::traits::Quantizer;
119
120/// The version of this crate, taken from `Cargo.toml` at compile time.
121///
122/// Exposed so a consumer can report the exact `iqdb-quantize` build it links
123/// against — useful in diagnostics and version-skew checks across the iqdb
124/// crate family.
125///
126/// # Examples
127///
128/// ```
129/// // Carries a `major.minor.patch` SemVer core.
130/// let version = iqdb_quantize::VERSION;
131/// assert_eq!(version.split('.').count(), 3);
132/// assert!(version.split('.').all(|part| !part.is_empty()));
133/// ```
134pub const VERSION: &str = env!("CARGO_PKG_VERSION");