vecstore/
query_optimizer.rs

1//! Query cost estimation and optimization
2//!
3//! Analyzes queries to estimate execution cost and suggest optimizations.
4//! Helps users understand and improve query performance.
5//!
6//! # Features
7//!
8//! - **Cost Estimation**: Predict query execution time
9//! - **Optimization Hints**: Suggest improvements
10//! - **Query Analysis**: Identify bottlenecks
11//! - **Index Selection**: Recommend best indexes
12//!
13//! # Example
14//!
15//! ```rust
16//! use vecstore::query_optimizer::QueryOptimizer;
17//!
18//! let optimizer = QueryOptimizer::new(&store);
19//!
20//! // Analyze query
21//! let analysis = optimizer.analyze_query(&query)?;
22//! println!("Estimated cost: {}", analysis.estimated_cost);
23//!
24//! // Get optimization hints
25//! for hint in analysis.hints {
26//!     println!("Hint: {}", hint.suggestion);
27//! }
28//! ```
29
30use anyhow::Result;
31use serde::{Deserialize, Serialize};
32use std::time::Duration;
33
34use crate::store::{Query, VecStore};
35
36/// Query optimization hint
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct OptimizationHint {
39    /// Hint category
40    pub category: HintCategory,
41    /// Suggestion text
42    pub suggestion: String,
43    /// Expected impact
44    pub impact: Impact,
45    /// Estimated improvement
46    pub estimated_improvement: f32, // percentage
47}
48
49/// Hint category
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum HintCategory {
52    /// Index-related optimization
53    Index,
54    /// Query parameter optimization
55    QueryParam,
56    /// Filter optimization
57    Filter,
58    /// Vector dimension optimization
59    Dimension,
60    /// Batching opportunity
61    Batching,
62}
63
64/// Impact level
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66pub enum Impact {
67    High,   // >50% improvement
68    Medium, // 20-50% improvement
69    Low,    // <20% improvement
70}
71
72/// Query cost breakdown
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct CostBreakdown {
75    /// Vector similarity computation cost
76    pub similarity_cost: f32,
77    /// Filter evaluation cost
78    pub filter_cost: f32,
79    /// Index lookup cost
80    pub index_cost: f32,
81    /// Result sorting cost
82    pub sorting_cost: f32,
83    /// Total estimated cost (milliseconds)
84    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/// Query execution plan
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ExecutionPlan {
96    /// Steps in execution order
97    pub steps: Vec<PlanStep>,
98    /// Estimated rows at each step
99    pub estimated_rows: Vec<usize>,
100    /// Whether indexes will be used
101    pub uses_index: bool,
102}
103
104/// Execution plan step
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct PlanStep {
107    /// Step name
108    pub name: String,
109    /// Description
110    pub description: String,
111    /// Estimated cost (ms)
112    pub cost: f32,
113}
114
115/// Query analysis result
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct QueryAnalysis {
118    /// Estimated total cost (milliseconds)
119    pub estimated_cost: f32,
120    /// Cost breakdown
121    pub cost_breakdown: CostBreakdown,
122    /// Optimization hints
123    pub hints: Vec<OptimizationHint>,
124    /// Execution plan
125    pub execution_plan: ExecutionPlan,
126    /// Query complexity level
127    pub complexity: QueryComplexity,
128}
129
130/// Query complexity
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
132pub enum QueryComplexity {
133    Simple,   // Fast, < 10ms
134    Moderate, // Medium, 10-100ms
135    Complex,  // Slow, > 100ms
136}
137
138/// Query optimizer
139pub struct QueryOptimizer<'a> {
140    store: &'a VecStore,
141}
142
143impl<'a> QueryOptimizer<'a> {
144    /// Create new optimizer
145    pub fn new(store: &'a VecStore) -> Self {
146        Self { store }
147    }
148
149    /// Analyze query and provide optimization suggestions
150    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            // Estimate dimension from store
154            128 // Default estimate
155        } else {
156            128
157        };
158
159        // Estimate costs
160        let cost_breakdown = self.estimate_costs(query, store_size, vector_dim);
161        let total_cost = cost_breakdown.total();
162
163        // Generate execution plan
164        let execution_plan = self.generate_execution_plan(query, store_size);
165
166        // Generate optimization hints
167        let hints = self.generate_hints(query, store_size, vector_dim, &cost_breakdown);
168
169        // Determine complexity
170        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    /// Estimate query costs
188    fn estimate_costs(&self, query: &Query, store_size: usize, vector_dim: usize) -> CostBreakdown {
189        // Base cost per vector comparison (microseconds)
190        let base_comparison_cost = 0.001 * vector_dim as f32;
191
192        // Similarity computation cost
193        let vectors_to_compare = if query.filter.is_some() {
194            // With filter, assume 50% selectivity
195            store_size / 2
196        } else {
197            store_size
198        };
199
200        let similarity_cost = vectors_to_compare as f32 * base_comparison_cost;
201
202        // Filter evaluation cost
203        let filter_cost = if query.filter.is_some() {
204            store_size as f32 * 0.0005 // 0.5 microseconds per filter check
205        } else {
206            0.0
207        };
208
209        // Index lookup cost (if available)
210        let index_cost = if query.filter.is_some() {
211            // Assume index lookup is O(log N)
212            (store_size as f32).log2() * 0.001
213        } else {
214            0.0
215        };
216
217        // Result sorting cost
218        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    /// Generate optimization hints
235    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        // Check for large K values
245        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        // Check if filter could benefit from index
258        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        // Check for high-dimensional vectors
268        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        // Check if similarity cost dominates
281        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        // Check for potential batching
291        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        // Check query vector quality
301        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    /// Generate execution plan
319    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        // Step 1: Filter application
325        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; // Assume 50% selectivity
332            estimated_rows.push(filtered_rows);
333            uses_index = true;
334        }
335
336        // Step 2: Vector similarity computation
337        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        // Step 3: Top-K selection
345        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    /// Compare two queries
361    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    /// Get optimization summary for the entire store
394    pub fn store_optimization_summary(&self) -> StoreOptimizationSummary {
395        let store_size = self.store.len();
396        let mut recommendations = Vec::new();
397
398        // Check store size
399        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    /// Estimate average query time
422    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/// Query comparison result
429#[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/// Store optimization summary
440#[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        // Add test vectors
459        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        // Should suggest index for filtered queries
502        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        // Should suggest reducing K
516        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        // Comparison should work (either query could be marginally faster)
553        assert!(comparison.faster_query == 1 || comparison.faster_query == 2);
554        assert!(comparison.cost_difference >= 0.0);
555
556        Ok(())
557    }
558}