Skip to main content

shodh_memory/vector_db/
mod.rs

1//! Vector database module with pluggable index backends
2//!
3//! High-performance vector similarity search for the memory system.
4//! Supports two backends:
5//!
6//! - **Vamana** (default): Graph-based ANN, optimal for <100k vectors, lowest latency
7//! - **SPANN**: Disk-based IVF+PQ, optimal for >100k vectors, billion-scale
8//!
9//! # Distance Metrics
10//!
11//! Both backends support three distance metrics via [`DistanceMetric`]:
12//!
13//! - **NormalizedDotProduct** (default): Best for normalized embeddings (MiniLM, etc.)
14//! - **Euclidean**: L2 squared distance for general use
15//! - **Cosine**: Cosine distance (1 - similarity) for unnormalized vectors
16//!
17//! # Auto-Selection
18//!
19//! Use `VectorIndexBackend::auto()` to automatically select the best backend
20//! based on expected dataset size.
21//!
22//! # Example
23//!
24//! ```ignore
25//! use shodh_memory::vector_db::{VectorIndexBackend, BackendConfig};
26//!
27//! // Auto-select based on expected size
28//! let backend = VectorIndexBackend::auto(BackendConfig::default(), 50_000)?;
29//!
30//! // Add vectors
31//! backend.add_vector(embedding)?;
32//!
33//! // Search
34//! let results = backend.search(&query, 10)?;
35//! ```
36
37pub mod distance_inline;
38pub mod pq;
39pub mod spann;
40pub mod vamana;
41pub mod vamana_persist;
42
43// Re-export key types for convenient access
44pub use pq::{CompressedVectorStore, PQConfig, ProductQuantizer};
45pub use spann::{SpannConfig, SpannIndex};
46pub use vamana::{DistanceMetric, VamanaConfig, VamanaIndex, REBUILD_THRESHOLD};
47
48use anyhow::Result;
49use std::path::Path;
50
51/// Threshold for auto-selecting SPANN over Vamana
52/// SPANN is better for large datasets due to disk-based storage
53pub const SPANN_AUTO_THRESHOLD: usize = 100_000;
54
55/// Configuration for vector index backend
56#[derive(Debug, Clone)]
57pub struct BackendConfig {
58    /// Vector dimension (must match embedding model)
59    pub dimension: usize,
60    /// Distance metric
61    pub distance_metric: DistanceMetric,
62    /// Force specific backend (None = auto-select)
63    pub force_backend: Option<BackendType>,
64    /// Enable PQ compression for SPANN (saves 32x storage)
65    pub use_pq: bool,
66    /// Number of partitions to probe in SPANN search
67    pub spann_probes: usize,
68    /// Max degree for Vamana graph
69    pub vamana_max_degree: usize,
70    /// Search list size for Vamana
71    pub vamana_search_list_size: usize,
72}
73
74impl Default for BackendConfig {
75    fn default() -> Self {
76        Self {
77            dimension: 384, // MiniLM
78            distance_metric: DistanceMetric::NormalizedDotProduct,
79            force_backend: None,
80            use_pq: true,
81            spann_probes: 20,
82            vamana_max_degree: 32,
83            vamana_search_list_size: 100,
84        }
85    }
86}
87
88/// Backend type for vector index
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum BackendType {
91    /// Graph-based ANN - fast, in-memory, best for <100k vectors
92    Vamana,
93    /// Disk-based IVF+PQ - scalable, best for >100k vectors
94    Spann,
95}
96
97/// Unified vector index backend supporting Vamana and SPANN
98pub enum VectorIndexBackend {
99    Vamana(VamanaIndex),
100    Spann(SpannIndex),
101}
102
103impl VectorIndexBackend {
104    /// Create backend with auto-selection based on expected vector count
105    pub fn auto(config: BackendConfig, expected_vectors: usize) -> Result<Self> {
106        let backend_type = config.force_backend.unwrap_or_else(|| {
107            if expected_vectors >= SPANN_AUTO_THRESHOLD {
108                BackendType::Spann
109            } else {
110                BackendType::Vamana
111            }
112        });
113
114        match backend_type {
115            BackendType::Vamana => Self::new_vamana(config),
116            BackendType::Spann => Self::new_spann(config),
117        }
118    }
119
120    /// Create Vamana backend explicitly
121    pub fn new_vamana(config: BackendConfig) -> Result<Self> {
122        let vamana_config = VamanaConfig {
123            dimension: config.dimension,
124            max_degree: config.vamana_max_degree,
125            search_list_size: config.vamana_search_list_size,
126            distance_metric: config.distance_metric,
127            ..Default::default()
128        };
129        Ok(Self::Vamana(VamanaIndex::new(vamana_config)?))
130    }
131
132    /// Create SPANN backend explicitly
133    pub fn new_spann(config: BackendConfig) -> Result<Self> {
134        let spann_config = SpannConfig {
135            dimension: config.dimension,
136            use_pq: config.use_pq,
137            num_probes: config.spann_probes,
138            distance_metric: config.distance_metric,
139            ..Default::default()
140        };
141        Ok(Self::Spann(SpannIndex::new(spann_config)))
142    }
143
144    /// Get backend type
145    pub fn backend_type(&self) -> BackendType {
146        match self {
147            Self::Vamana(_) => BackendType::Vamana,
148            Self::Spann(_) => BackendType::Spann,
149        }
150    }
151
152    /// Add a vector to the index, returns vector ID
153    pub fn add_vector(&mut self, vector: Vec<f32>) -> Result<u32> {
154        match self {
155            Self::Vamana(idx) => idx.add_vector(vector),
156            Self::Spann(idx) => {
157                let id = idx.len() as u32;
158                idx.insert(id, &vector)?;
159                Ok(id)
160            }
161        }
162    }
163
164    /// Search for k nearest neighbors
165    pub fn search(&self, query: &[f32], k: usize) -> Result<Vec<(u32, f32)>> {
166        match self {
167            Self::Vamana(idx) => idx.search(query, k),
168            Self::Spann(idx) => idx.search(query, k),
169        }
170    }
171
172    /// Number of vectors in the index
173    pub fn len(&self) -> usize {
174        match self {
175            Self::Vamana(idx) => idx.len(),
176            Self::Spann(idx) => idx.len(),
177        }
178    }
179
180    /// Check if index is empty
181    pub fn is_empty(&self) -> bool {
182        self.len() == 0
183    }
184
185    /// Save index to file
186    pub fn save_to_file(&self, path: &Path) -> Result<()> {
187        match self {
188            Self::Vamana(idx) => idx.save_to_file(path),
189            Self::Spann(idx) => idx.save_to_file(path),
190        }
191    }
192
193    /// Load index from file
194    pub fn load_from_file(path: &Path, backend_type: BackendType) -> Result<Self> {
195        match backend_type {
196            BackendType::Vamana => Ok(Self::Vamana(VamanaIndex::load_from_file(path)?)),
197            BackendType::Spann => Ok(Self::Spann(SpannIndex::load_from_file(path)?)),
198        }
199    }
200
201    /// Build index from vectors (for SPANN, Vamana builds incrementally)
202    pub fn build(&mut self, vectors: Vec<Vec<f32>>) -> Result<()> {
203        match self {
204            Self::Vamana(idx) => idx.build(vectors),
205            Self::Spann(idx) => idx.build(vectors),
206        }
207    }
208
209    /// Check if index needs rebuild (Vamana only)
210    pub fn needs_rebuild(&self) -> bool {
211        match self {
212            Self::Vamana(idx) => idx.needs_rebuild(),
213            Self::Spann(_) => false, // SPANN doesn't need rebuild
214        }
215    }
216
217    /// Auto-rebuild if needed (Vamana only)
218    pub fn auto_rebuild_if_needed(&mut self) -> Result<bool> {
219        match self {
220            Self::Vamana(idx) => idx.auto_rebuild_if_needed(),
221            Self::Spann(_) => Ok(false),
222        }
223    }
224
225    /// Get incremental insert count (Vamana only)
226    pub fn incremental_insert_count(&self) -> usize {
227        match self {
228            Self::Vamana(idx) => idx.incremental_insert_count(),
229            Self::Spann(_) => 0,
230        }
231    }
232
233    /// Get deleted count (Vamana only)
234    pub fn deleted_count(&self) -> usize {
235        match self {
236            Self::Vamana(idx) => idx.deleted_count(),
237            Self::Spann(_) => 0,
238        }
239    }
240
241    /// Get deletion ratio (Vamana only)
242    pub fn deletion_ratio(&self) -> f32 {
243        match self {
244            Self::Vamana(idx) => idx.deletion_ratio(),
245            Self::Spann(_) => 0.0,
246        }
247    }
248
249    /// Check if needs compaction (Vamana only)
250    pub fn needs_compaction(&self) -> bool {
251        match self {
252            Self::Vamana(idx) => idx.needs_compaction(),
253            Self::Spann(_) => false,
254        }
255    }
256
257    /// Verify index file integrity
258    pub fn verify_index_file(path: &Path, backend_type: BackendType) -> Result<bool> {
259        match backend_type {
260            BackendType::Vamana => VamanaIndex::verify_index_file(path),
261            BackendType::Spann => SpannIndex::verify_index_file(path),
262        }
263    }
264}