Skip to main content

hermes_core/structures/vector/index/
rabitq.rs

1//! Standalone RaBitQ index (without IVF)
2//!
3//! For small datasets where IVF overhead isn't worth it.
4//! Uses brute-force search over all quantized vectors.
5
6use std::io;
7
8use serde::{Deserialize, Serialize};
9
10use crate::structures::vector::ivf::QuantizedCode;
11use crate::structures::vector::quantization::{
12    QuantizedQuery, QuantizedVector, RaBitQCodebook, RaBitQConfig,
13};
14
15/// Standalone RaBitQ index for small datasets
16///
17/// Uses brute-force search over all quantized vectors.
18/// For larger datasets, use `IVFRaBitQIndex` instead.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RaBitQIndex {
21    /// RaBitQ codebook (random transform parameters)
22    pub codebook: RaBitQCodebook,
23    /// Centroid of all indexed vectors
24    pub centroid: Vec<f32>,
25    /// Document IDs
26    pub doc_ids: Vec<u32>,
27    /// Element ordinals for multi-valued fields (0 for single-valued)
28    pub ordinals: Vec<u16>,
29    /// Quantized vectors
30    pub vectors: Vec<QuantizedVector>,
31}
32
33impl RaBitQIndex {
34    pub(crate) fn validate(&self) -> Result<(), String> {
35        self.codebook.validate()?;
36        let dim = self.codebook.config.dim;
37        if self.centroid.len() != dim || self.centroid.iter().any(|value| !value.is_finite()) {
38            return Err(format!("RaBitQ centroid is invalid for dimension {dim}"));
39        }
40        if self.doc_ids.len() != self.vectors.len() || self.ordinals.len() != self.vectors.len() {
41            return Err(format!(
42                "RaBitQ column lengths differ: docs={}, ordinals={}, vectors={}",
43                self.doc_ids.len(),
44                self.ordinals.len(),
45                self.vectors.len()
46            ));
47        }
48        for vector in &self.vectors {
49            self.codebook.validate_vector(vector)?;
50        }
51        Ok(())
52    }
53
54    /// Create a new empty RaBitQ index
55    pub fn new(config: RaBitQConfig) -> Self {
56        let dim = config.dim;
57        let codebook = RaBitQCodebook::new(config);
58
59        Self {
60            codebook,
61            centroid: vec![0.0; dim],
62            doc_ids: Vec::new(),
63            ordinals: Vec::new(),
64            vectors: Vec::new(),
65        }
66    }
67
68    /// Build index from vectors with doc IDs and ordinals
69    pub fn build_with_ids(
70        config: RaBitQConfig,
71        vectors: &[(u32, u16, Vec<f32>)], // (doc_id, ordinal, vector)
72    ) -> Self {
73        let n = vectors.len();
74        let dim = config.dim;
75
76        assert!(n > 0, "Cannot build index from empty vector set");
77        assert!(vectors[0].2.len() == dim, "Vector dimension mismatch");
78
79        let mut index = Self::new(config);
80
81        // Compute centroid
82        index.centroid = vec![0.0; dim];
83        for (_, _, v) in vectors {
84            for (i, &val) in v.iter().enumerate() {
85                index.centroid[i] += val;
86            }
87        }
88        for c in &mut index.centroid {
89            *c /= n as f32;
90        }
91
92        // Store doc_ids, ordinals and quantize vectors
93        index.doc_ids = vectors.iter().map(|(doc_id, _, _)| *doc_id).collect();
94        index.ordinals = vectors.iter().map(|(_, ordinal, _)| *ordinal).collect();
95        index.vectors = vectors
96            .iter()
97            .map(|(_, _, v)| index.codebook.encode(v, Some(&index.centroid)))
98            .collect();
99
100        index
101    }
102
103    /// Build index from a set of vectors (legacy, uses doc_id = index, ordinal = 0)
104    pub fn build(config: RaBitQConfig, vectors: &[Vec<f32>]) -> Self {
105        let with_ids: Vec<(u32, u16, Vec<f32>)> = vectors
106            .iter()
107            .enumerate()
108            .map(|(i, v)| (i as u32, 0u16, v.clone()))
109            .collect();
110        Self::build_with_ids(config, &with_ids)
111    }
112
113    /// Add a single vector to the index
114    pub fn add_vector(&mut self, doc_id: u32, ordinal: u16, vector: &[f32]) {
115        self.doc_ids.push(doc_id);
116        self.ordinals.push(ordinal);
117        self.vectors
118            .push(self.codebook.encode(vector, Some(&self.centroid)));
119    }
120
121    /// Prepare a query for fast distance estimation
122    pub fn prepare_query(&self, query: &[f32]) -> QuantizedQuery {
123        self.codebook.prepare_query(query, Some(&self.centroid))
124    }
125
126    /// Estimate squared distance between query and a quantized vector
127    pub fn estimate_distance(&self, query: &QuantizedQuery, vec_idx: usize) -> f32 {
128        self.codebook
129            .estimate_distance(query, &self.vectors[vec_idx])
130    }
131
132    /// Search for k nearest neighbors, returns (doc_id, ordinal, distance)
133    pub fn search(&self, query: &[f32], k: usize) -> Vec<(u32, u16, f32)> {
134        let prepared = self.prepare_query(query);
135
136        let mut candidates = super::BoundedDistanceCollector::new(k);
137        for idx in 0..self.vectors.len() {
138            candidates.insert(
139                self.doc_ids[idx],
140                self.ordinals[idx],
141                self.estimate_distance(&prepared, idx),
142            );
143        }
144        candidates.into_sorted_results()
145    }
146
147    /// Number of indexed vectors
148    pub fn len(&self) -> usize {
149        self.vectors.len()
150    }
151
152    pub fn is_empty(&self) -> bool {
153        self.vectors.is_empty()
154    }
155
156    /// Memory usage in bytes
157    pub fn size_bytes(&self) -> usize {
158        use std::mem::size_of;
159
160        let vectors_size: usize = self.vectors.iter().map(|v| v.size_bytes()).sum();
161        let centroid_size = self.centroid.len() * size_of::<f32>();
162        let doc_ids_size = self.doc_ids.len() * size_of::<u32>();
163        let ordinals_size = self.ordinals.len() * size_of::<u16>();
164        let codebook_size = self.codebook.size_bytes();
165        vectors_size + centroid_size + doc_ids_size + ordinals_size + codebook_size
166    }
167
168    /// Estimated memory usage in bytes (alias for size_bytes)
169    pub fn estimated_memory_bytes(&self) -> usize {
170        self.size_bytes()
171    }
172
173    /// Compression ratio compared to raw float32 vectors
174    pub fn compression_ratio(&self) -> f32 {
175        if self.vectors.is_empty() {
176            return 1.0;
177        }
178
179        let dim = self.codebook.config.dim;
180        let raw_size = self.vectors.len() * dim * 4;
181        let compressed_size: usize = self.vectors.iter().map(|v| v.size_bytes()).sum();
182
183        raw_size as f32 / compressed_size as f32
184    }
185
186    /// Serialize to bytes
187    pub fn to_bytes(&self) -> io::Result<Vec<u8>> {
188        serde_json::to_vec(self).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
189    }
190
191    /// Deserialize from bytes
192    pub fn from_bytes(data: &[u8]) -> io::Result<Self> {
193        let index: Self = serde_json::from_slice(data)
194            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
195        index
196            .validate()
197            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
198        Ok(index)
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use rand::prelude::*;
206
207    #[test]
208    fn test_rabitq_basic() {
209        let dim = 128;
210        let n = 100;
211
212        let mut rng = rand::rngs::StdRng::seed_from_u64(12345);
213        let vectors: Vec<Vec<f32>> = (0..n)
214            .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
215            .collect();
216
217        let config = RaBitQConfig::new(dim);
218        let index = RaBitQIndex::build(config, &vectors);
219
220        assert_eq!(index.len(), n);
221        println!("Compression ratio: {:.1}x", index.compression_ratio());
222    }
223
224    #[test]
225    fn test_rabitq_search() {
226        let dim = 64;
227        let n = 1000;
228        let k = 10;
229
230        let mut rng = rand::rngs::StdRng::seed_from_u64(42);
231        let vectors: Vec<Vec<f32>> = (0..n)
232            .map(|_| (0..dim).map(|_| rng.random::<f32>() - 0.5).collect())
233            .collect();
234
235        let config = RaBitQConfig::new(dim);
236        let index = RaBitQIndex::build(config, &vectors);
237
238        let query: Vec<f32> = (0..dim).map(|_| rng.random::<f32>() - 0.5).collect();
239        let results = index.search(&query, k);
240
241        assert_eq!(results.len(), k);
242
243        // Verify results are sorted by distance
244        for i in 1..results.len() {
245            assert!(results[i].2 >= results[i - 1].2);
246        }
247    }
248}