Skip to main content

velesdb_core/quantization/
mod.rs

1//! Scalar Quantization (SQ8) and Binary Quantization for memory-efficient vector storage.
2//!
3//! This module implements quantization strategies to reduce memory usage:
4//!
5//! ## Benefits
6//!
7//! | Metric | f32 | SQ8 | Binary |
8//! |--------|-----|-----|--------|
9//! | RAM/vector (768d) | 3 KB | 770 bytes | 96 bytes |
10//! | Cache efficiency | Baseline | ~4x better | ~32x better |
11//! | Recall loss | 0% | ~0.5-1% | ~5-10% |
12//!
13//! ## Engine integration status
14//!
15//! The figures above describe the quantization primitives themselves. In the
16//! collection query path: `RaBitQ` (binary traversal backend) and PQ (ADC
17//! rescoring) are wired end-to-end. Persistence across reopens covers
18//! TRAIN-QUANTIZER-produced artifacts (`rabitq.idx`, `codebook.pq`); a PQ
19//! quantizer trained lazily from inserts (no TRAIN statement) is in-memory
20//! only and retrains after a restart. SQ8/Binary collection modes currently
21//! maintain caches that no search path consumes — collection search stays
22//! full-precision f32 for those modes. See `docs/guides/QUANTIZATION.md`.
23
24use std::io;
25
26use serde::{Deserialize, Serialize};
27
28/// Validate that a flat row-major rotation matrix has exactly `dimension^2`
29/// elements, returning [`crate::error::Error::IndexCorrupted`] otherwise.
30///
31/// Shared by the PQ (OPQ) and `RaBitQ` load-time validators so the unchecked
32/// `matrix[i * d + j]` indexing in their rotation kernels stays in bounds.
33pub(crate) fn validate_rotation_len(
34    len: usize,
35    dimension: usize,
36    label: &str,
37) -> Result<(), crate::error::Error> {
38    // `checked_mul`: `dimension` is attacker-controlled post-deserialize; a wrapping
39    // `dimension * dimension` (esp. on 32-bit targets) could yield a small `expected`
40    // that a tampered `len` matches, false-passing the shape check that the unchecked
41    // `matrix[i * d + j]` indexing relies on.
42    let Some(expected) = dimension.checked_mul(dimension) else {
43        return Err(crate::error::Error::IndexCorrupted(format!(
44            "{label} rotation dimension {dimension} squared overflows usize"
45        )));
46    };
47    if len != expected {
48        return Err(crate::error::Error::IndexCorrupted(format!(
49            "{label} rotation has {len} elements, expected dimension^2 = {expected}"
50        )));
51    }
52    Ok(())
53}
54
55mod binary;
56pub(crate) mod codec_helpers;
57mod pq;
58pub(crate) mod pq_kmeans;
59pub(crate) mod pq_opq;
60#[cfg(feature = "persistence")]
61mod pq_persistence;
62mod rabitq;
63pub(crate) mod rabitq_store;
64mod scalar;
65
66// Re-export binary quantization
67pub use binary::BinaryQuantizedVector;
68#[allow(unused_imports)] // Called from vector.rs search path (persistence-gated).
69pub(crate) use pq::distance_pq_l2;
70#[allow(unused_imports)] // Called from vector.rs search path (persistence-gated).
71pub(crate) use pq::pq_adc_batch_rescore;
72pub use pq::{PQCodebook, PQVector, ProductQuantizer};
73#[cfg(feature = "persistence")]
74pub use pq_opq::train_opq;
75
76// Re-export RaBitQ quantization
77#[cfg(feature = "persistence")]
78pub(crate) use rabitq::PreparedQuery;
79pub use rabitq::{RaBitQCorrection, RaBitQIndex, RaBitQVector};
80#[cfg(feature = "persistence")]
81pub(crate) use rabitq_store::RaBitQVectorStore;
82
83// Re-export scalar quantization
84pub use scalar::{
85    cosine_similarity_quantized, cosine_similarity_quantized_simd, dot_product_quantized,
86    dot_product_quantized_simd, euclidean_squared_quantized, euclidean_squared_quantized_simd,
87    QuantizedVector,
88};
89
90/// Trait for serializing and deserializing quantized vectors to/from bytes.
91///
92/// Provides a uniform interface for byte-level serialization across
93/// different quantization strategies (SQ8, Binary).
94pub trait QuantizationCodec: Sized {
95    /// Serializes the quantized vector to a byte representation.
96    fn to_bytes(&self) -> Vec<u8>;
97
98    /// Deserializes a quantized vector from bytes.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the byte slice is too short or contains invalid data.
103    fn from_bytes(bytes: &[u8]) -> io::Result<Self>;
104}
105
106/// Storage mode for vectors.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
108#[serde(rename_all = "lowercase")]
109#[non_exhaustive]
110pub enum StorageMode {
111    /// Full precision f32 storage (default).
112    #[default]
113    Full,
114    /// 8-bit scalar quantization for 4x memory reduction.
115    SQ8,
116    /// 1-bit binary quantization for 32x memory reduction.
117    /// Best for edge/IoT devices with limited RAM.
118    Binary,
119    /// Product Quantization (PQ) for aggressive lossy compression (8x-16x typical).
120    ProductQuantization,
121    /// `RaBitQ` binary quantization for 32x compression with scalar correction.
122    RaBitQ,
123}
124
125impl StorageMode {
126    /// Returns the canonical lowercase name for this storage mode.
127    ///
128    /// This is the single source of truth for string representations,
129    /// used by [`std::fmt::Display`], [`std::str::FromStr`], and downstream crates.
130    #[must_use]
131    pub const fn canonical_name(self) -> &'static str {
132        match self {
133            Self::Full => "full",
134            Self::SQ8 => "sq8",
135            Self::Binary => "binary",
136            Self::ProductQuantization => "pq",
137            Self::RaBitQ => "rabitq",
138        }
139    }
140
141    /// Parses a storage mode string with alias support.
142    ///
143    /// Accepted aliases (case-insensitive):
144    /// - `full`, `f32` -> `Full`
145    /// - `sq8`, `int8` -> `SQ8`
146    /// - `binary`, `bit` -> `Binary`
147    /// - `pq`, `product_quantization` -> `ProductQuantization`
148    /// - `rabitq` -> `RaBitQ`
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use velesdb_core::StorageMode;
154    ///
155    /// assert_eq!(StorageMode::parse_alias("sq8"), Some(StorageMode::SQ8));
156    /// assert_eq!(StorageMode::parse_alias("INT8"), Some(StorageMode::SQ8));
157    /// assert_eq!(StorageMode::parse_alias("unknown"), None);
158    /// ```
159    #[must_use]
160    pub fn parse_alias(value: &str) -> Option<Self> {
161        match value.trim().to_lowercase().as_str() {
162            "full" | "f32" => Some(Self::Full),
163            "sq8" | "int8" => Some(Self::SQ8),
164            "binary" | "bit" => Some(Self::Binary),
165            "pq" | "product_quantization" => Some(Self::ProductQuantization),
166            "rabitq" => Some(Self::RaBitQ),
167            _ => None,
168        }
169    }
170}
171
172impl std::fmt::Display for StorageMode {
173    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174        f.write_str(self.canonical_name())
175    }
176}
177
178impl std::str::FromStr for StorageMode {
179    type Err = String;
180
181    fn from_str(s: &str) -> Result<Self, Self::Err> {
182        Self::parse_alias(s).ok_or_else(|| {
183            format!(
184                "Unknown storage mode '{s}'. Valid options: full, f32, sq8, int8, binary, bit, pq, product_quantization, rabitq"
185            )
186        })
187    }
188}
189
190#[cfg(test)]
191mod storage_mode_parsing_tests {
192    use super::StorageMode;
193
194    #[test]
195    fn test_parse_all_canonical_names() {
196        assert_eq!("full".parse::<StorageMode>().unwrap(), StorageMode::Full);
197        assert_eq!("sq8".parse::<StorageMode>().unwrap(), StorageMode::SQ8);
198        assert_eq!(
199            "binary".parse::<StorageMode>().unwrap(),
200            StorageMode::Binary
201        );
202        assert_eq!(
203            "pq".parse::<StorageMode>().unwrap(),
204            StorageMode::ProductQuantization
205        );
206        assert_eq!(
207            "rabitq".parse::<StorageMode>().unwrap(),
208            StorageMode::RaBitQ
209        );
210    }
211
212    #[test]
213    fn test_parse_aliases() {
214        assert_eq!("f32".parse::<StorageMode>().unwrap(), StorageMode::Full);
215        assert_eq!("int8".parse::<StorageMode>().unwrap(), StorageMode::SQ8);
216        assert_eq!("bit".parse::<StorageMode>().unwrap(), StorageMode::Binary);
217        assert_eq!(
218            "product_quantization".parse::<StorageMode>().unwrap(),
219            StorageMode::ProductQuantization
220        );
221    }
222
223    #[test]
224    fn test_parse_case_insensitive() {
225        assert_eq!("SQ8".parse::<StorageMode>().unwrap(), StorageMode::SQ8);
226        assert_eq!("FULL".parse::<StorageMode>().unwrap(), StorageMode::Full);
227        assert_eq!(
228            "RaBitQ".parse::<StorageMode>().unwrap(),
229            StorageMode::RaBitQ
230        );
231    }
232
233    #[test]
234    fn test_parse_unknown_returns_error() {
235        assert!("unknown".parse::<StorageMode>().is_err());
236        assert!("".parse::<StorageMode>().is_err());
237    }
238
239    #[test]
240    fn test_canonical_name_roundtrip() {
241        for mode in [
242            StorageMode::Full,
243            StorageMode::SQ8,
244            StorageMode::Binary,
245            StorageMode::ProductQuantization,
246            StorageMode::RaBitQ,
247        ] {
248            let name = mode.canonical_name();
249            assert_eq!(name.parse::<StorageMode>().unwrap(), mode);
250        }
251    }
252
253    #[test]
254    fn test_display_uses_canonical_name() {
255        assert_eq!(format!("{}", StorageMode::Full), "full");
256        assert_eq!(format!("{}", StorageMode::SQ8), "sq8");
257        assert_eq!(format!("{}", StorageMode::Binary), "binary");
258        assert_eq!(format!("{}", StorageMode::ProductQuantization), "pq");
259        assert_eq!(format!("{}", StorageMode::RaBitQ), "rabitq");
260    }
261}