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