Skip to main content

ipfrs_semantic/quantization/
stores.rs

1//! Memory-efficient vector stores: INT8 (`QuantizedVectorStore`) and binary (`BinaryVectorStore`).
2
3use super::{dequantize_i8_to_f32, quantize_f32_to_i8};
4
5/// Stores f32 vectors quantized to INT8, reducing memory ~4× vs raw f32.
6///
7/// Each vector is independently scaled with per-vector `scale` and `zero_point`
8/// so that dequantization error is bounded to roughly `scale / 2` per element.
9///
10/// # Memory layout
11/// Internally uses a single flat `Vec<i8>` of length `count * dim`.
12pub struct QuantizedVectorStore {
13    /// Flattened quantized data: `[v0_d0, v0_d1, …, v1_d0, …]`
14    data: Vec<i8>,
15    /// Vector dimension
16    dim: usize,
17    /// Number of stored vectors
18    count: usize,
19    /// Per-vector scale factors (index → scale)
20    scales: Vec<f32>,
21    /// Per-vector zero-points (index → zero_point)
22    zero_points: Vec<f32>,
23}
24
25impl QuantizedVectorStore {
26    /// Create an empty store for vectors of `dim` dimensions.
27    pub fn new(dim: usize) -> Self {
28        Self {
29            data: Vec::new(),
30            dim,
31            count: 0,
32            scales: Vec::new(),
33            zero_points: Vec::new(),
34        }
35    }
36
37    /// Quantize `vector` to INT8 and append it to the store.
38    ///
39    /// Returns the assigned integer ID (0-based).
40    pub fn push(&mut self, vector: &[f32]) -> usize {
41        debug_assert_eq!(
42            vector.len(),
43            self.dim,
44            "push: vector length {} != dim {}",
45            vector.len(),
46            self.dim
47        );
48
49        let (q, scale, zero_point) = quantize_f32_to_i8(vector);
50        self.data.extend_from_slice(&q);
51        self.scales.push(scale);
52        self.zero_points.push(zero_point);
53
54        let id = self.count;
55        self.count += 1;
56        id
57    }
58
59    /// Dequantize the vector stored at `id` back to f32.
60    ///
61    /// Returns `None` if `id >= self.len()`.
62    pub fn get(&self, id: usize) -> Option<Vec<f32>> {
63        if id >= self.count {
64            return None;
65        }
66        let start = id * self.dim;
67        let end = start + self.dim;
68        let q = &self.data[start..end];
69        Some(dequantize_i8_to_f32(
70            q,
71            self.scales[id],
72            self.zero_points[id],
73        ))
74    }
75
76    /// Compute approximate cosine similarity between two stored vectors.
77    ///
78    /// Dequantizes both vectors then computes exact cosine similarity in f32.
79    /// Returns 0.0 if either id is out of range or a vector has zero norm.
80    pub fn cosine_similarity_q(&self, a_id: usize, b_id: usize) -> f32 {
81        let a = match self.get(a_id) {
82            Some(v) => v,
83            None => return 0.0,
84        };
85        let b = match self.get(b_id) {
86            Some(v) => v,
87            None => return 0.0,
88        };
89
90        let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
91        let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
92        let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
93
94        if norm_a < 1e-9 || norm_b < 1e-9 {
95            return 0.0;
96        }
97        dot / (norm_a * norm_b)
98    }
99
100    /// Approximate memory usage per stored vector (bytes).
101    ///
102    /// Counts the `i8` storage only (not scale/zero_point metadata):
103    /// `dim * 1 byte` per vector.
104    pub fn bytes_per_vector(&self) -> f64 {
105        self.dim as f64
106    }
107
108    /// Number of stored vectors.
109    pub fn len(&self) -> usize {
110        self.count
111    }
112
113    /// Returns `true` if no vectors have been stored.
114    pub fn is_empty(&self) -> bool {
115        self.count == 0
116    }
117
118    /// Vector dimension.
119    pub fn dim(&self) -> usize {
120        self.dim
121    }
122}
123
124/// Stores f32 vectors binarized to 1 bit per dimension.
125///
126/// Each dimension is thresholded at the per-vector mean:
127/// `bit = 1 if v[i] >= mean(v) else 0`.
128///
129/// Bits are packed into `u64` words (64 dims per word), so a 384-dim vector
130/// occupies `ceil(384/64) = 6` words = 48 bytes, vs 1536 bytes for f32.
131///
132/// Similarity is measured with Hamming distance (popcount of XOR).
133pub struct BinaryVectorStore {
134    /// Packed bit data: `dim_words` u64 words per vector.
135    data: Vec<u64>,
136    /// Original vector dimension (number of bits per vector).
137    dim: usize,
138    /// Number of stored vectors.
139    count: usize,
140    /// Number of u64 words per vector: `(dim + 63) / 64`.
141    dim_words: usize,
142}
143
144impl BinaryVectorStore {
145    /// Create an empty store for vectors of `dim` dimensions.
146    pub fn new(dim: usize) -> Self {
147        let dim_words = dim.div_ceil(64);
148        Self {
149            data: Vec::new(),
150            dim,
151            count: 0,
152            dim_words,
153        }
154    }
155
156    /// Binarize `vector` (threshold at mean) and store it.
157    ///
158    /// Returns the assigned ID.
159    pub fn push(&mut self, vector: &[f32]) -> usize {
160        debug_assert_eq!(
161            vector.len(),
162            self.dim,
163            "push: vector length {} != dim {}",
164            vector.len(),
165            self.dim
166        );
167
168        // Compute per-vector mean for threshold
169        let mean = if vector.is_empty() {
170            0.0_f32
171        } else {
172            vector.iter().sum::<f32>() / vector.len() as f32
173        };
174
175        // Pack bits into u64 words
176        let mut words = vec![0u64; self.dim_words];
177        for (i, &val) in vector.iter().enumerate() {
178            if val >= mean {
179                let word_idx = i / 64;
180                let bit_idx = i % 64;
181                words[word_idx] |= 1u64 << bit_idx;
182            }
183        }
184
185        self.data.extend_from_slice(&words);
186        let id = self.count;
187        self.count += 1;
188        id
189    }
190
191    /// Compute the Hamming distance between two stored vectors.
192    ///
193    /// Hamming distance = number of bit positions where the two vectors differ.
194    /// Returns `u32::MAX` if either id is out of range.
195    pub fn hamming_distance(&self, a_id: usize, b_id: usize) -> u32 {
196        if a_id >= self.count || b_id >= self.count {
197            return u32::MAX;
198        }
199
200        let a_start = a_id * self.dim_words;
201        let b_start = b_id * self.dim_words;
202        let a_words = &self.data[a_start..a_start + self.dim_words];
203        let b_words = &self.data[b_start..b_start + self.dim_words];
204
205        a_words
206            .iter()
207            .zip(b_words.iter())
208            .map(|(a, b)| (a ^ b).count_ones())
209            .sum()
210    }
211
212    /// Approximate memory per stored vector (bytes).
213    ///
214    /// `dim_words * 8` bytes per vector, e.g. dim=384 → 6 × 8 = 48 bytes.
215    pub fn bytes_per_vector(&self) -> f64 {
216        (self.dim_words * 8) as f64
217    }
218
219    /// Number of stored vectors.
220    pub fn len(&self) -> usize {
221        self.count
222    }
223
224    /// Returns `true` if no vectors have been stored.
225    pub fn is_empty(&self) -> bool {
226        self.count == 0
227    }
228
229    /// Vector dimension.
230    pub fn dim(&self) -> usize {
231        self.dim
232    }
233}