Skip to main content

vecdb_core/index/
mod.rs

1pub mod backend;
2pub mod binary;
3pub mod distance;
4pub mod ivf;
5pub mod scalar;
6
7pub use backend::{HnswConfig, HnswIndex, IndexBackend};
8pub use binary::{BinaryQuantizedIndex, BinaryQuantizer};
9pub use distance::{
10    compute_distance, cosine_similarity, cosine_similarity_simd, dot_product, dot_product_simd,
11    euclidean_distance, euclidean_distance_simd, normalize, to_score,
12};
13pub use ivf::IvfIndex;
14pub use scalar::{ScalarQuantizedIndex, ScalarQuantizer};
15
16use crate::errors::Result;
17use crate::types::{CollectionConfig, IndexType, Quantization, Vector, VectorId};
18
19// ──────────────────────────────────────────────
20// AnyIndex — dispatch enum over all backends
21// ──────────────────────────────────────────────
22
23pub enum AnyIndex {
24    Hnsw(HnswIndex),
25    Ivf(IvfIndex),
26    ScalarQuantized(ScalarQuantizedIndex),
27    BinaryQuantized(BinaryQuantizedIndex),
28}
29
30impl IndexBackend for AnyIndex {
31    fn build(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
32        match self {
33            AnyIndex::Hnsw(h) => h.build(vectors),
34            AnyIndex::Ivf(i) => i.build(vectors),
35            AnyIndex::ScalarQuantized(s) => s.build(vectors),
36            AnyIndex::BinaryQuantized(b) => b.build(vectors),
37        }
38    }
39
40    fn insert(&mut self, id: VectorId, vector: Vector) -> Result<()> {
41        match self {
42            AnyIndex::Hnsw(h) => h.insert(id, vector),
43            AnyIndex::Ivf(i) => i.insert(id, vector),
44            AnyIndex::ScalarQuantized(s) => s.insert(id, vector),
45            AnyIndex::BinaryQuantized(b) => b.insert(id, vector),
46        }
47    }
48
49    fn search(&self, query: &Vector, k: usize) -> Result<Vec<(VectorId, f32)>> {
50        match self {
51            AnyIndex::Hnsw(h) => h.search(query, k),
52            AnyIndex::Ivf(i) => i.search(query, k),
53            AnyIndex::ScalarQuantized(s) => s.search(query, k),
54            AnyIndex::BinaryQuantized(b) => b.search(query, k),
55        }
56    }
57
58    fn delete(&mut self, id: &VectorId) -> Result<()> {
59        match self {
60            AnyIndex::Hnsw(h) => h.delete(id),
61            AnyIndex::Ivf(i) => i.delete(id),
62            AnyIndex::ScalarQuantized(s) => s.delete(id),
63            AnyIndex::BinaryQuantized(b) => b.delete(id),
64        }
65    }
66
67    fn save(&self, path: &std::path::Path) -> Result<()> {
68        match self {
69            AnyIndex::Hnsw(h) => h.save(path),
70            AnyIndex::Ivf(i) => i.save(path),
71            AnyIndex::ScalarQuantized(s) => s.save(path),
72            AnyIndex::BinaryQuantized(b) => b.save(path),
73        }
74    }
75
76    fn load_from(path: &std::path::Path, config: &CollectionConfig) -> Result<Box<dyn IndexBackend>>
77    where
78        Self: Sized,
79    {
80        // Dispatch based on file extension convention
81        if path.extension().map(|e| e == "json").unwrap_or(false) {
82            let stem = path
83                .file_name()
84                .and_then(|s| s.to_str())
85                .unwrap_or_default();
86            if stem.ends_with(".ivf.json") {
87                return IvfIndex::load_from(path, config);
88            }
89            if stem.ends_with(".sq.json") {
90                return ScalarQuantizedIndex::load_from(path, config);
91            }
92            if stem.ends_with(".bq.json") {
93                return BinaryQuantizedIndex::load_from(path, config);
94            }
95        }
96        HnswIndex::load_from(path, config)
97    }
98
99    fn len(&self) -> usize {
100        match self {
101            AnyIndex::Hnsw(h) => h.len(),
102            AnyIndex::Ivf(i) => i.len(),
103            AnyIndex::ScalarQuantized(s) => s.len(),
104            AnyIndex::BinaryQuantized(b) => b.len(),
105        }
106    }
107
108    fn is_empty(&self) -> bool {
109        match self {
110            AnyIndex::Hnsw(h) => h.is_empty(),
111            AnyIndex::Ivf(i) => i.is_empty(),
112            AnyIndex::ScalarQuantized(s) => s.is_empty(),
113            AnyIndex::BinaryQuantized(b) => b.is_empty(),
114        }
115    }
116
117    fn index_type(&self) -> IndexType {
118        match self {
119            AnyIndex::Hnsw(h) => h.index_type(),
120            AnyIndex::Ivf(i) => i.index_type(),
121            AnyIndex::ScalarQuantized(s) => s.index_type(),
122            AnyIndex::BinaryQuantized(b) => b.index_type(),
123        }
124    }
125
126    fn rebuild(&mut self, vectors: Vec<(VectorId, Vector)>) -> Result<()> {
127        match self {
128            AnyIndex::Hnsw(h) => h.rebuild(vectors),
129            AnyIndex::Ivf(i) => i.rebuild(vectors),
130            AnyIndex::ScalarQuantized(s) => s.rebuild(vectors),
131            AnyIndex::BinaryQuantized(b) => b.rebuild(vectors),
132        }
133    }
134}
135
136// Safety: HnswIndex and IvfIndex are both Send+Sync, so AnyIndex is too.
137unsafe impl Send for AnyIndex {}
138unsafe impl Sync for AnyIndex {}
139
140impl AnyIndex {
141    /// Construct the correct variant for the given collection config.
142    ///
143    /// `Quantization::ScalarInt8` selects the int8 scalar-quantized flat index
144    /// regardless of `index_type` (HNSW-over-int8 is not yet implemented); the
145    /// full-precision on-disk vectors remain authoritative.
146    pub fn from_config(config: &CollectionConfig) -> Self {
147        match config.quantization {
148            Quantization::ScalarInt8 => {
149                return AnyIndex::ScalarQuantized(ScalarQuantizedIndex::new(config))
150            }
151            Quantization::Binary => {
152                return AnyIndex::BinaryQuantized(BinaryQuantizedIndex::new(config))
153            }
154            Quantization::None => {}
155        }
156        match config.index_type {
157            IndexType::IVF => AnyIndex::Ivf(IvfIndex::new(config)),
158            IndexType::HNSW => AnyIndex::Hnsw(HnswIndex::from_collection_config(config)),
159        }
160    }
161
162    /// Load a saved index from `data_dir` or construct a fresh one.
163    /// Returns `(index, was_loaded_from_disk)`.
164    pub fn load_or_create(
165        data_dir: &std::path::Path,
166        name: &str,
167        config: &CollectionConfig,
168    ) -> (Self, bool) {
169        if config.quantization == Quantization::ScalarInt8 {
170            let path = data_dir.join(format!("{name}.sq.json"));
171            if path.exists() {
172                match ScalarQuantizedIndex::load_file(&path) {
173                    Ok(idx) => return (AnyIndex::ScalarQuantized(idx), true),
174                    Err(_) => {
175                        return (
176                            AnyIndex::ScalarQuantized(ScalarQuantizedIndex::new(config)),
177                            false,
178                        )
179                    }
180                }
181            }
182            return (
183                AnyIndex::ScalarQuantized(ScalarQuantizedIndex::new(config)),
184                false,
185            );
186        }
187        if config.quantization == Quantization::Binary {
188            let path = data_dir.join(format!("{name}.bq.json"));
189            if path.exists() {
190                match BinaryQuantizedIndex::load_file(&path) {
191                    Ok(idx) => return (AnyIndex::BinaryQuantized(idx), true),
192                    Err(_) => {
193                        return (
194                            AnyIndex::BinaryQuantized(BinaryQuantizedIndex::new(config)),
195                            false,
196                        )
197                    }
198                }
199            }
200            return (
201                AnyIndex::BinaryQuantized(BinaryQuantizedIndex::new(config)),
202                false,
203            );
204        }
205        match config.index_type {
206            IndexType::IVF => {
207                let path = data_dir.join(format!("{name}.ivf.json"));
208                if path.exists() {
209                    match IvfIndex::load_file(&path) {
210                        Ok(idx) => (AnyIndex::Ivf(idx), true),
211                        Err(_) => (AnyIndex::Ivf(IvfIndex::new(config)), false),
212                    }
213                } else {
214                    (AnyIndex::Ivf(IvfIndex::new(config)), false)
215                }
216            }
217            IndexType::HNSW => {
218                let path = data_dir.join(format!("{name}.hnsw.json"));
219                if path.exists() {
220                    match HnswIndex::load_file(&path, config) {
221                        Ok(idx) => (AnyIndex::Hnsw(idx), true),
222                        Err(_) => (
223                            AnyIndex::Hnsw(HnswIndex::from_collection_config(config)),
224                            false,
225                        ),
226                    }
227                } else {
228                    (
229                        AnyIndex::Hnsw(HnswIndex::from_collection_config(config)),
230                        false,
231                    )
232                }
233            }
234        }
235    }
236
237    /// Persist the index to the appropriate file under `data_dir`.
238    pub fn save_for_collection(
239        &self,
240        data_dir: &std::path::Path,
241        name: &str,
242    ) -> crate::errors::Result<()> {
243        match self {
244            AnyIndex::Hnsw(h) => h.save(&data_dir.join(format!("{name}.hnsw.json"))),
245            AnyIndex::Ivf(i) => i.save(&data_dir.join(format!("{name}.ivf.json"))),
246            AnyIndex::ScalarQuantized(s) => s.save(&data_dir.join(format!("{name}.sq.json"))),
247            AnyIndex::BinaryQuantized(b) => b.save(&data_dir.join(format!("{name}.bq.json"))),
248        }
249    }
250}
251
252#[cfg(test)]
253mod ivf_tests;
254
255#[cfg(test)]
256mod perf_tests;