Skip to main content

velesdb_core/quantization/
binary.rs

1//! Binary quantization (1-bit per dimension) for extreme memory reduction.
2//!
3//! Each f32 value is converted to 1 bit: >= 0.0 becomes 1, < 0.0 becomes 0.
4//! This provides **32x memory reduction** compared to f32 storage.
5
6use std::io;
7
8/// A binary quantized vector using 1-bit per dimension.
9///
10/// Each f32 value is converted to 1 bit: >= 0.0 becomes 1, < 0.0 becomes 0.
11/// This provides **32x memory reduction** compared to f32 storage.
12///
13/// # Memory Usage
14///
15/// | Dimension | f32 | Binary |
16/// |-----------|-----|--------|
17/// | 768 | 3072 bytes | 96 bytes |
18/// | 1536 | 6144 bytes | 192 bytes |
19///
20/// # Use with Rescoring
21///
22/// For best accuracy, use binary search for candidate selection,
23/// then rescore top candidates with full-precision vectors.
24#[derive(Debug, Clone)]
25pub struct BinaryQuantizedVector {
26    /// Binary data (1 bit per dimension, packed into bytes).
27    pub data: Vec<u8>,
28    /// Original dimension of the vector.
29    dimension: usize,
30}
31
32impl BinaryQuantizedVector {
33    /// Creates a new binary quantized vector from f32 data.
34    ///
35    /// Values >= 0.0 become 1, values < 0.0 become 0.
36    ///
37    /// # Arguments
38    ///
39    /// * `vector` - The original f32 vector to quantize
40    ///
41    /// # Panics
42    ///
43    /// Panics if the vector is empty.
44    #[must_use]
45    pub fn from_f32(vector: &[f32]) -> Self {
46        assert!(!vector.is_empty(), "Cannot quantize empty vector");
47
48        let dimension = vector.len();
49        // Calculate number of bytes needed: ceil(dimension / 8)
50        let num_bytes = dimension.div_ceil(8);
51        let mut data = vec![0u8; num_bytes];
52
53        for (i, &value) in vector.iter().enumerate() {
54            if value >= 0.0 {
55                // Set bit i in the packed byte array
56                let byte_idx = i / 8;
57                let bit_idx = i % 8;
58                data[byte_idx] |= 1 << bit_idx;
59            }
60        }
61
62        Self { data, dimension }
63    }
64
65    /// Returns the dimension of the original vector.
66    #[must_use]
67    pub fn dimension(&self) -> usize {
68        self.dimension
69    }
70
71    /// Returns the memory size in bytes.
72    #[must_use]
73    pub fn memory_size(&self) -> usize {
74        self.data.len()
75    }
76
77    /// Returns the individual bits as a boolean vector.
78    ///
79    /// Useful for debugging and testing.
80    #[must_use]
81    pub fn get_bits(&self) -> Vec<bool> {
82        (0..self.dimension)
83            .map(|i| {
84                let byte_idx = i / 8;
85                let bit_idx = i % 8;
86                (self.data[byte_idx] >> bit_idx) & 1 == 1
87            })
88            .collect()
89    }
90
91    /// Computes the Hamming distance to another binary vector.
92    ///
93    /// Hamming distance counts the number of bits that differ.
94    /// Uses POPCNT for fast bit counting.
95    ///
96    /// # Panics
97    ///
98    /// Panics if the vectors have different dimensions.
99    #[must_use]
100    pub fn hamming_distance(&self, other: &Self) -> u32 {
101        debug_assert_eq!(
102            self.dimension, other.dimension,
103            "Dimension mismatch in hamming_distance"
104        );
105
106        // XOR bytes and count differing bits using POPCNT
107        self.data
108            .iter()
109            .zip(other.data.iter())
110            .map(|(&a, &b)| (a ^ b).count_ones())
111            .sum()
112    }
113
114    /// Computes normalized Hamming similarity (0.0 to 1.0).
115    ///
116    /// Returns 1.0 for identical vectors, 0.0 for completely different.
117    #[must_use]
118    #[allow(clippy::cast_precision_loss)]
119    pub fn hamming_similarity(&self, other: &Self) -> f32 {
120        let distance = self.hamming_distance(other);
121        1.0 - (distance as f32 / self.dimension as f32)
122    }
123
124    /// Serializes the binary quantized vector to bytes.
125    ///
126    /// # Panics
127    ///
128    /// Panics if dimension exceeds `u32::MAX` (4,294,967,295).
129    /// This is a theoretical limit as vectors of this size would require
130    /// ~512MB of binary data alone.
131    #[must_use]
132    pub fn to_bytes(&self) -> Vec<u8> {
133        assert!(
134            u32::try_from(self.dimension).is_ok(),
135            "BinaryQuantizedVector dimension {} exceeds u32::MAX for serialization",
136            self.dimension
137        );
138
139        let mut bytes = Vec::with_capacity(4 + self.data.len());
140        // Store dimension as u32 (4 bytes)
141        // SAFETY: dimension validated above to fit in u32
142        #[allow(clippy::cast_possible_truncation)]
143        bytes.extend_from_slice(&(self.dimension as u32).to_le_bytes());
144        bytes.extend_from_slice(&self.data);
145        bytes
146    }
147
148    /// Deserializes a binary quantized vector from bytes.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if the bytes are invalid.
153    pub fn from_bytes(bytes: &[u8]) -> io::Result<Self> {
154        if bytes.len() < 4 {
155            return Err(io::Error::new(
156                io::ErrorKind::InvalidData,
157                "Not enough bytes for BinaryQuantizedVector header",
158            ));
159        }
160
161        let dimension = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
162        let expected_data_len = dimension.div_ceil(8);
163
164        if bytes.len() < 4 + expected_data_len {
165            return Err(io::Error::new(
166                io::ErrorKind::InvalidData,
167                format!(
168                    "Not enough bytes for BinaryQuantizedVector data: expected {}, got {}",
169                    4 + expected_data_len,
170                    bytes.len()
171                ),
172            ));
173        }
174
175        let data = bytes[4..4 + expected_data_len].to_vec();
176
177        Ok(Self { data, dimension })
178    }
179}