1use crate::query_planning::*;
37use crate::{hnsw::HnswIndex, ivf::IvfIndex, lsh::LshIndex, nsg::NsgIndex};
38use crate::{Vector, VectorIndex};
39use anyhow::Result;
40use std::collections::HashMap;
41use std::sync::{Arc, RwLock};
42use tracing::{debug, info};
43
44#[derive(Debug, Clone)]
46pub struct IndexSelectorConfig {
47 pub enable_hnsw: bool,
49 pub enable_nsg: bool,
51 pub enable_ivf: bool,
53 pub enable_lsh: bool,
55 pub min_recall: f32,
57 pub max_latency_ms: f64,
59 pub enable_learning: bool,
61 pub eager_build: bool,
63}
64
65impl Default for IndexSelectorConfig {
66 fn default() -> Self {
67 Self {
68 enable_hnsw: true,
69 enable_nsg: true,
70 enable_ivf: true,
71 enable_lsh: false, min_recall: 0.90,
73 max_latency_ms: 100.0,
74 enable_learning: true,
75 eager_build: true,
76 }
77 }
78}
79
80pub struct DynamicIndexSelector {
82 config: IndexSelectorConfig,
83 hnsw_index: Option<HnswIndex>,
84 nsg_index: Option<NsgIndex>,
85 ivf_index: Option<IvfIndex>,
86 lsh_index: Option<LshIndex>,
87 query_planner: Arc<RwLock<QueryPlanner>>,
88 data: Vec<(String, Vector)>,
89 is_built: bool,
90 performance_stats: Arc<RwLock<PerformanceStats>>,
91}
92
93#[derive(Debug, Clone, Default)]
95struct PerformanceStats {
96 strategy_latencies: HashMap<QueryStrategy, Vec<f64>>,
97 strategy_recalls: HashMap<QueryStrategy, Vec<f32>>,
98 total_queries: usize,
99}
100
101impl PerformanceStats {
102 fn record(&mut self, strategy: QueryStrategy, latency_ms: f64, recall: f32) {
103 self.strategy_latencies
104 .entry(strategy)
105 .or_default()
106 .push(latency_ms);
107
108 self.strategy_recalls
109 .entry(strategy)
110 .or_default()
111 .push(recall);
112
113 self.total_queries += 1;
114 }
115
116 fn avg_latency(&self, strategy: QueryStrategy) -> Option<f64> {
117 self.strategy_latencies
118 .get(&strategy)
119 .and_then(|latencies| {
120 if latencies.is_empty() {
121 None
122 } else {
123 Some(latencies.iter().sum::<f64>() / latencies.len() as f64)
124 }
125 })
126 }
127
128 fn avg_recall(&self, strategy: QueryStrategy) -> Option<f32> {
129 self.strategy_recalls.get(&strategy).and_then(|recalls| {
130 if recalls.is_empty() {
131 None
132 } else {
133 Some(recalls.iter().sum::<f32>() / recalls.len() as f32)
134 }
135 })
136 }
137}
138
139impl DynamicIndexSelector {
140 pub fn new(config: IndexSelectorConfig) -> Result<Self> {
142 let mut available_indices = Vec::new();
144 if config.enable_hnsw {
145 available_indices.push(QueryStrategy::HnswApproximate);
146 }
147 if config.enable_nsg {
148 available_indices.push(QueryStrategy::NsgApproximate);
149 }
150 if config.enable_ivf {
151 available_indices.push(QueryStrategy::IvfCoarse);
152 }
153 if config.enable_lsh {
154 available_indices.push(QueryStrategy::LocalitySensitiveHashing);
155 }
156
157 if available_indices.is_empty() {
158 return Err(anyhow::anyhow!("At least one index type must be enabled"));
159 }
160
161 let index_stats = IndexStatistics {
163 vector_count: 0,
164 dimensions: 0,
165 available_indices,
166 avg_latencies: HashMap::new(),
167 avg_recalls: HashMap::new(),
168 };
169
170 let cost_model = CostModel::default();
171 let query_planner = Arc::new(RwLock::new(QueryPlanner::new(cost_model, index_stats)));
172
173 Ok(Self {
174 config,
175 hnsw_index: None,
176 nsg_index: None,
177 ivf_index: None,
178 lsh_index: None,
179 query_planner,
180 data: Vec::new(),
181 is_built: false,
182 performance_stats: Arc::new(RwLock::new(PerformanceStats::default())),
183 })
184 }
185
186 pub fn add(&mut self, uri: String, vector: Vector) -> Result<()> {
188 if self.is_built && self.config.eager_build {
189 return Err(anyhow::anyhow!(
190 "Cannot add vectors after indices are built in eager mode"
191 ));
192 }
193
194 self.data.push((uri, vector));
195 Ok(())
196 }
197
198 pub fn build(&mut self) -> Result<()> {
200 if self.data.is_empty() {
201 return Err(anyhow::anyhow!("No vectors to index"));
202 }
203
204 let dimensions = self.data[0].1.dimensions;
205 let vector_count = self.data.len();
206
207 info!(
208 "Building dynamic index selector with {} vectors, {} dimensions",
209 vector_count, dimensions
210 );
211
212 if self.config.enable_hnsw {
214 debug!("Building HNSW index");
215 let mut hnsw = HnswIndex::new(Default::default())?;
216 for (uri, vec) in &self.data {
217 hnsw.insert(uri.clone(), vec.clone())?;
218 }
219 self.hnsw_index = Some(hnsw);
220 }
221
222 if self.config.enable_nsg {
224 debug!("Building NSG index");
225 let mut nsg = NsgIndex::new(Default::default())?;
226 for (uri, vec) in &self.data {
227 nsg.insert(uri.clone(), vec.clone())?;
228 }
229 nsg.build()?;
230 self.nsg_index = Some(nsg);
231 }
232
233 if self.config.enable_ivf {
235 debug!("Building IVF index");
236 let mut ivf = IvfIndex::new(Default::default())?;
237 for (uri, vec) in &self.data {
238 ivf.insert(uri.clone(), vec.clone())?;
239 }
240 self.ivf_index = Some(ivf);
242 }
243
244 if self.config.enable_lsh {
246 debug!("Building LSH index");
247 let lsh = LshIndex::new(Default::default());
248 let mut lsh_mut = lsh;
249 for (uri, vec) in &self.data {
250 lsh_mut.insert(uri.clone(), vec.clone())?;
251 }
252 self.lsh_index = Some(lsh_mut);
253 }
254
255 let mut planner = self
257 .query_planner
258 .write()
259 .expect("query_planner write lock should not be poisoned");
260 planner.update_index_metadata(vector_count, dimensions);
261
262 self.is_built = true;
263
264 info!("Dynamic index selector built successfully");
265
266 Ok(())
267 }
268
269 pub fn search_knn(&self, query: &Vector, k: usize) -> Result<Vec<(String, f32)>> {
271 if !self.is_built {
272 return Err(anyhow::anyhow!("Indices not built. Call build() first."));
273 }
274
275 let query_chars = QueryCharacteristics {
277 k,
278 dimensions: query.dimensions,
279 min_recall: self.config.min_recall,
280 max_latency_ms: self.config.max_latency_ms,
281 query_type: VectorQueryType::Single,
282 };
283
284 let planner = self
286 .query_planner
287 .read()
288 .expect("query_planner read lock should not be poisoned");
289 let plan = planner.plan(&query_chars)?;
290 drop(planner); debug!(
293 "Selected strategy: {:?} (estimated cost: {:.2} µs, recall: {:.2})",
294 plan.strategy, plan.estimated_cost_us, plan.estimated_recall
295 );
296
297 let start = std::time::Instant::now();
299 let results = self.execute_strategy(plan.strategy, query, k)?;
300 let elapsed = start.elapsed().as_secs_f64() * 1000.0; if self.config.enable_learning {
304 let mut stats = self
305 .performance_stats
306 .write()
307 .expect("performance_stats write lock should not be poisoned");
308 stats.record(plan.strategy, elapsed, plan.estimated_recall);
309 drop(stats);
310
311 let mut planner = self
313 .query_planner
314 .write()
315 .expect("query_planner write lock should not be poisoned");
316 if let Some(avg_latency) = self
317 .performance_stats
318 .read()
319 .expect("performance_stats read lock should not be poisoned")
320 .avg_latency(plan.strategy)
321 {
322 planner.update_statistics(plan.strategy, avg_latency, plan.estimated_recall);
323 }
324 }
325
326 Ok(results)
327 }
328
329 fn execute_strategy(
331 &self,
332 strategy: QueryStrategy,
333 query: &Vector,
334 k: usize,
335 ) -> Result<Vec<(String, f32)>> {
336 match strategy {
337 QueryStrategy::HnswApproximate => {
338 if let Some(ref index) = self.hnsw_index {
339 <HnswIndex as crate::VectorIndex>::search_knn(index, query, k)
346 } else {
347 Err(anyhow::anyhow!("HNSW index not available"))
348 }
349 }
350 QueryStrategy::NsgApproximate => {
351 if let Some(ref index) = self.nsg_index {
352 index.search_knn(query, k)
353 } else {
354 Err(anyhow::anyhow!("NSG index not available"))
355 }
356 }
357 QueryStrategy::IvfCoarse => {
358 if let Some(ref index) = self.ivf_index {
359 index.search_knn(query, k)
360 } else {
361 Err(anyhow::anyhow!("IVF index not available"))
362 }
363 }
364 QueryStrategy::LocalitySensitiveHashing => {
365 if let Some(ref index) = self.lsh_index {
366 index.search_knn(query, k)
367 } else {
368 Err(anyhow::anyhow!("LSH index not available"))
369 }
370 }
371 _ => Err(anyhow::anyhow!(
372 "Strategy {:?} not supported by dynamic selector",
373 strategy
374 )),
375 }
376 }
377
378 pub fn get_stats(&self) -> HashMap<String, String> {
380 let mut stats = HashMap::new();
381 let perf_stats = self
382 .performance_stats
383 .read()
384 .expect("performance_stats read lock should not be poisoned");
385
386 stats.insert(
387 "total_queries".to_string(),
388 perf_stats.total_queries.to_string(),
389 );
390 stats.insert("vector_count".to_string(), self.data.len().to_string());
391 stats.insert("is_built".to_string(), self.is_built.to_string());
392
393 for strategy in &[
395 QueryStrategy::HnswApproximate,
396 QueryStrategy::NsgApproximate,
397 QueryStrategy::IvfCoarse,
398 QueryStrategy::LocalitySensitiveHashing,
399 ] {
400 if let Some(avg_lat) = perf_stats.avg_latency(*strategy) {
401 stats.insert(
402 format!("{:?}_avg_latency_ms", strategy),
403 format!("{:.2}", avg_lat),
404 );
405 }
406 if let Some(avg_rec) = perf_stats.avg_recall(*strategy) {
407 stats.insert(
408 format!("{:?}_avg_recall", strategy),
409 format!("{:.2}", avg_rec),
410 );
411 }
412 }
413
414 stats
415 }
416
417 pub fn is_built(&self) -> bool {
419 self.is_built
420 }
421
422 pub fn len(&self) -> usize {
424 self.data.len()
425 }
426
427 pub fn is_empty(&self) -> bool {
429 self.data.is_empty()
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn test_dynamic_selector_creation() {
439 let config = IndexSelectorConfig::default();
440 let selector = DynamicIndexSelector::new(config);
441 assert!(selector.is_ok());
442 }
443
444 #[test]
445 fn test_add_vectors() -> Result<()> {
446 let config = IndexSelectorConfig::default();
447 let mut selector = DynamicIndexSelector::new(config)?;
448
449 for i in 0..10 {
450 let vec = Vector::new(vec![i as f32, (i * 2) as f32]);
451 selector.add(format!("vec_{}", i), vec)?;
452 }
453
454 assert_eq!(selector.len(), 10);
455 Ok(())
456 }
457
458 #[test]
459 fn test_build_and_search() -> Result<()> {
460 let config = IndexSelectorConfig {
461 enable_hnsw: true,
462 enable_nsg: true,
463 enable_ivf: false, enable_lsh: false,
465 ..Default::default()
466 };
467 let mut selector = DynamicIndexSelector::new(config)?;
468
469 for i in 0..50 {
471 let vec = Vector::new(vec![i as f32, (i * 2) as f32, (i * 3) as f32]);
472 selector.add(format!("vec_{}", i), vec)?;
473 }
474
475 selector.build()?;
477 assert!(selector.is_built());
478
479 let query = Vector::new(vec![25.0, 50.0, 75.0]);
481 let results = selector.search_knn(&query, 5)?;
482
483 assert_eq!(results.len(), 5);
484 for i in 1..results.len() {
486 assert!(results[i - 1].1 >= results[i].1);
487 }
488 Ok(())
489 }
490
491 #[test]
492 fn test_performance_learning() -> Result<()> {
493 let config = IndexSelectorConfig {
494 enable_hnsw: true,
495 enable_nsg: true,
496 enable_ivf: false, enable_lsh: false,
498 enable_learning: true,
499 ..Default::default()
500 };
501 let mut selector = DynamicIndexSelector::new(config)?;
502
503 for i in 0..30 {
505 let vec = Vector::new(vec![i as f32, (i * 2) as f32]);
506 selector.add(format!("vec_{}", i), vec)?;
507 }
508
509 selector.build()?;
510
511 for _ in 0..5 {
513 let query = Vector::new(vec![15.0, 30.0]);
514 let _ = selector.search_knn(&query, 5);
515 }
516
517 let stats = selector.get_stats();
519 assert!(stats.contains_key("total_queries"));
520 let total_queries: usize = stats
521 .get("total_queries")
522 .expect("total_queries key missing")
523 .parse()?;
524 assert!(total_queries >= 5);
525 Ok(())
526 }
527}