1use anyhow::Result;
31use serde::{Deserialize, Serialize};
32use std::time::Duration;
33
34use crate::store::{Query, VecStore};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct OptimizationHint {
39 pub category: HintCategory,
41 pub suggestion: String,
43 pub impact: Impact,
45 pub estimated_improvement: f32, }
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum HintCategory {
52 Index,
54 QueryParam,
56 Filter,
58 Dimension,
60 Batching,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum Impact {
67 High, Medium, Low, }
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CostBreakdown {
75 pub similarity_cost: f32,
77 pub filter_cost: f32,
79 pub index_cost: f32,
81 pub sorting_cost: f32,
83 pub total_cost: f32,
85}
86
87impl CostBreakdown {
88 fn total(&self) -> f32 {
89 self.similarity_cost + self.filter_cost + self.index_cost + self.sorting_cost
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ExecutionPlan {
96 pub steps: Vec<PlanStep>,
98 pub estimated_rows: Vec<usize>,
100 pub uses_index: bool,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct PlanStep {
107 pub name: String,
109 pub description: String,
111 pub cost: f32,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct QueryAnalysis {
118 pub estimated_cost: f32,
120 pub cost_breakdown: CostBreakdown,
122 pub hints: Vec<OptimizationHint>,
124 pub execution_plan: ExecutionPlan,
126 pub complexity: QueryComplexity,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum QueryComplexity {
133 Simple, Moderate, Complex, }
137
138pub struct QueryOptimizer<'a> {
140 store: &'a VecStore,
141}
142
143impl<'a> QueryOptimizer<'a> {
144 pub fn new(store: &'a VecStore) -> Self {
146 Self { store }
147 }
148
149 pub fn analyze_query(&self, query: &Query) -> Result<QueryAnalysis> {
151 let store_size = self.store.len();
152 let vector_dim = if store_size > 0 {
153 128 } else {
156 128
157 };
158
159 let cost_breakdown = self.estimate_costs(query, store_size, vector_dim);
161 let total_cost = cost_breakdown.total();
162
163 let execution_plan = self.generate_execution_plan(query, store_size);
165
166 let hints = self.generate_hints(query, store_size, vector_dim, &cost_breakdown);
168
169 let complexity = if total_cost < 10.0 {
171 QueryComplexity::Simple
172 } else if total_cost < 100.0 {
173 QueryComplexity::Moderate
174 } else {
175 QueryComplexity::Complex
176 };
177
178 Ok(QueryAnalysis {
179 estimated_cost: total_cost,
180 cost_breakdown,
181 hints,
182 execution_plan,
183 complexity,
184 })
185 }
186
187 fn estimate_costs(&self, query: &Query, store_size: usize, vector_dim: usize) -> CostBreakdown {
189 let base_comparison_cost = 0.001 * vector_dim as f32;
191
192 let vectors_to_compare = if query.filter.is_some() {
194 store_size / 2
196 } else {
197 store_size
198 };
199
200 let similarity_cost = vectors_to_compare as f32 * base_comparison_cost;
201
202 let filter_cost = if query.filter.is_some() {
204 store_size as f32 * 0.0005 } else {
206 0.0
207 };
208
209 let index_cost = if query.filter.is_some() {
211 (store_size as f32).log2() * 0.001
213 } else {
214 0.0
215 };
216
217 let k = query.k;
219 let sorting_cost = if vectors_to_compare > k {
220 (vectors_to_compare as f32 * k as f32).log2() * 0.002
221 } else {
222 0.0
223 };
224
225 CostBreakdown {
226 similarity_cost,
227 filter_cost,
228 index_cost,
229 sorting_cost,
230 total_cost: similarity_cost + filter_cost + index_cost + sorting_cost,
231 }
232 }
233
234 fn generate_hints(
236 &self,
237 query: &Query,
238 store_size: usize,
239 vector_dim: usize,
240 costs: &CostBreakdown,
241 ) -> Vec<OptimizationHint> {
242 let mut hints = Vec::new();
243
244 if query.k > 100 {
246 hints.push(OptimizationHint {
247 category: HintCategory::QueryParam,
248 suggestion: format!(
249 "Consider reducing k from {} to 100 or less. Large K values increase memory and sorting overhead.",
250 query.k
251 ),
252 impact: Impact::Medium,
253 estimated_improvement: 20.0,
254 });
255 }
256
257 if query.filter.is_some() && store_size > 1000 {
259 hints.push(OptimizationHint {
260 category: HintCategory::Index,
261 suggestion: "Add metadata index for filtered fields to speed up filtering. Use MetadataIndexManager to create indexes.".to_string(),
262 impact: Impact::High,
263 estimated_improvement: 70.0,
264 });
265 }
266
267 if vector_dim > 512 {
269 hints.push(OptimizationHint {
270 category: HintCategory::Dimension,
271 suggestion: format!(
272 "Consider dimensionality reduction from {} to 128-256 dimensions using PCA. This can speed up similarity computation by 2-4x.",
273 vector_dim
274 ),
275 impact: Impact::High,
276 estimated_improvement: 60.0,
277 });
278 }
279
280 if costs.similarity_cost > costs.total_cost * 0.8 && store_size > 10000 {
282 hints.push(OptimizationHint {
283 category: HintCategory::Index,
284 suggestion: "Similarity computation dominates cost. Consider using IVF-PQ or LSH indexing for approximate search on large datasets.".to_string(),
285 impact: Impact::High,
286 estimated_improvement: 90.0,
287 });
288 }
289
290 if store_size > 5000 {
292 hints.push(OptimizationHint {
293 category: HintCategory::Batching,
294 suggestion: "For multiple queries, use batch operations to amortize index lookup costs across queries.".to_string(),
295 impact: Impact::Medium,
296 estimated_improvement: 30.0,
297 });
298 }
299
300 let vector = &query.vector;
302 let magnitude: f32 = vector.iter().map(|x| x * x).sum::<f32>().sqrt();
303 if (magnitude - 1.0).abs() > 0.1 {
304 hints.push(OptimizationHint {
305 category: HintCategory::QueryParam,
306 suggestion: format!(
307 "Query vector is not normalized (magnitude: {:.3}). Normalize vectors for cosine similarity to improve accuracy.",
308 magnitude
309 ),
310 impact: Impact::Low,
311 estimated_improvement: 5.0,
312 });
313 }
314
315 hints
316 }
317
318 fn generate_execution_plan(&self, query: &Query, store_size: usize) -> ExecutionPlan {
320 let mut steps = Vec::new();
321 let mut estimated_rows = vec![store_size];
322 let mut uses_index = false;
323
324 if query.filter.is_some() {
326 steps.push(PlanStep {
327 name: "Filter".to_string(),
328 description: "Apply metadata filter to reduce candidate set".to_string(),
329 cost: 0.5,
330 });
331 let filtered_rows = store_size / 2; estimated_rows.push(filtered_rows);
333 uses_index = true;
334 }
335
336 let candidates = *estimated_rows.last().unwrap();
338 steps.push(PlanStep {
339 name: "Similarity".to_string(),
340 description: format!("Compute similarity for {} vectors", candidates),
341 cost: candidates as f32 * 0.001,
342 });
343
344 let k = query.k;
346 steps.push(PlanStep {
347 name: "Top-K".to_string(),
348 description: format!("Select top {} results", k),
349 cost: 0.1,
350 });
351 estimated_rows.push(k);
352
353 ExecutionPlan {
354 steps,
355 estimated_rows,
356 uses_index,
357 }
358 }
359
360 pub fn compare_queries(&self, query1: &Query, query2: &Query) -> Result<QueryComparison> {
362 let analysis1 = self.analyze_query(query1)?;
363 let analysis2 = self.analyze_query(query2)?;
364
365 let faster_query = if analysis1.estimated_cost < analysis2.estimated_cost {
366 1
367 } else {
368 2
369 };
370
371 let cost_difference = (analysis1.estimated_cost - analysis2.estimated_cost).abs();
372 let relative_difference =
373 cost_difference / analysis1.estimated_cost.min(analysis2.estimated_cost);
374
375 Ok(QueryComparison {
376 query1_cost: analysis1.estimated_cost,
377 query2_cost: analysis2.estimated_cost,
378 faster_query,
379 cost_difference,
380 relative_difference,
381 recommendation: if relative_difference > 0.3 {
382 format!(
383 "Query {} is significantly faster ({:.1}% improvement)",
384 faster_query,
385 relative_difference * 100.0
386 )
387 } else {
388 "Both queries have similar performance".to_string()
389 },
390 })
391 }
392
393 pub fn store_optimization_summary(&self) -> StoreOptimizationSummary {
395 let store_size = self.store.len();
396 let mut recommendations = Vec::new();
397
398 if store_size > 100000 {
400 recommendations.push(
401 "Consider partitioning large dataset by metadata for faster queries".to_string(),
402 );
403 }
404
405 if store_size > 50000 {
406 recommendations
407 .push("Use approximate indexes (IVF-PQ, LSH) for better scaling".to_string());
408 }
409
410 if store_size > 10000 {
411 recommendations.push("Add metadata indexes for frequently filtered fields".to_string());
412 }
413
414 StoreOptimizationSummary {
415 store_size,
416 estimated_query_time: self.estimate_avg_query_time(store_size),
417 recommendations,
418 }
419 }
420
421 fn estimate_avg_query_time(&self, store_size: usize) -> Duration {
423 let ms = (store_size as f32 * 0.001).max(0.1);
424 Duration::from_millis(ms as u64)
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct QueryComparison {
431 pub query1_cost: f32,
432 pub query2_cost: f32,
433 pub faster_query: u8,
434 pub cost_difference: f32,
435 pub relative_difference: f32,
436 pub recommendation: String,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct StoreOptimizationSummary {
442 pub store_size: usize,
443 pub estimated_query_time: Duration,
444 pub recommendations: Vec<String>,
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::Metadata;
451 use std::collections::HashMap;
452 use tempfile::TempDir;
453
454 fn create_test_store() -> Result<VecStore> {
455 let temp_dir = TempDir::new()?;
456 let mut store = VecStore::open(temp_dir.path().join("test.db"))?;
457
458 for i in 0..100 {
460 let mut metadata = Metadata {
461 fields: HashMap::new(),
462 };
463 metadata
464 .fields
465 .insert("category".to_string(), serde_json::json!("test"));
466
467 store.upsert(format!("doc{}", i), vec![i as f32 * 0.01; 128], metadata)?;
468 }
469
470 Ok(store)
471 }
472
473 #[test]
474 fn test_basic_analysis() -> Result<()> {
475 let store = create_test_store()?;
476 let optimizer = QueryOptimizer::new(&store);
477
478 let query = Query::new(vec![0.5; 128]).with_limit(10);
479 let analysis = optimizer.analyze_query(&query)?;
480
481 assert!(analysis.estimated_cost > 0.0);
482 assert!(matches!(
483 analysis.complexity,
484 QueryComplexity::Simple | QueryComplexity::Moderate
485 ));
486
487 Ok(())
488 }
489
490 #[test]
491 fn test_filter_hint() -> Result<()> {
492 let store = create_test_store()?;
493 let optimizer = QueryOptimizer::new(&store);
494
495 let query = Query::new(vec![0.5; 128])
496 .with_limit(10)
497 .with_filter("category = 'test'");
498
499 let analysis = optimizer.analyze_query(&query)?;
500
501 assert!(!analysis.hints.is_empty());
503
504 Ok(())
505 }
506
507 #[test]
508 fn test_large_k_hint() -> Result<()> {
509 let store = create_test_store()?;
510 let optimizer = QueryOptimizer::new(&store);
511
512 let query = Query::new(vec![0.5; 128]).with_limit(200);
513 let analysis = optimizer.analyze_query(&query)?;
514
515 let has_k_hint = analysis
517 .hints
518 .iter()
519 .any(|h| matches!(h.category, HintCategory::QueryParam));
520 assert!(has_k_hint);
521
522 Ok(())
523 }
524
525 #[test]
526 fn test_execution_plan() -> Result<()> {
527 let store = create_test_store()?;
528 let optimizer = QueryOptimizer::new(&store);
529
530 let query = Query::new(vec![0.5; 128])
531 .with_limit(10)
532 .with_filter("category = 'test'");
533
534 let analysis = optimizer.analyze_query(&query)?;
535
536 assert!(!analysis.execution_plan.steps.is_empty());
537 assert!(analysis.execution_plan.uses_index);
538
539 Ok(())
540 }
541
542 #[test]
543 fn test_query_comparison() -> Result<()> {
544 let store = create_test_store()?;
545 let optimizer = QueryOptimizer::new(&store);
546
547 let query1 = Query::new(vec![0.5; 128]).with_limit(10);
548 let query2 = Query::new(vec![0.5; 128]).with_limit(100);
549
550 let comparison = optimizer.compare_queries(&query1, &query2)?;
551
552 assert!(comparison.faster_query == 1 || comparison.faster_query == 2);
554 assert!(comparison.cost_difference >= 0.0);
555
556 Ok(())
557 }
558}