1use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25pub enum SearchStrategy {
26 BruteForce,
28 Hnsw,
30 IvfPq,
32 Distributed,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct OptimizerConfig {
39 pub brute_force_threshold: usize,
41 pub hnsw_threshold: usize,
43 pub distributed_threshold: usize,
45 pub min_recall: f32,
47 pub enable_caching: bool,
49}
50
51impl Default for OptimizerConfig {
52 fn default() -> Self {
53 Self {
54 brute_force_threshold: 10_000,
55 hnsw_threshold: 1_000_000,
56 distributed_threshold: 10_000_000,
57 min_recall: 0.90,
58 enable_caching: true,
59 }
60 }
61}
62
63impl OptimizerConfig {
64 pub fn high_accuracy() -> Self {
66 Self {
67 brute_force_threshold: 50_000, hnsw_threshold: 5_000_000,
69 distributed_threshold: 10_000_000,
70 min_recall: 0.98,
71 enable_caching: true,
72 }
73 }
74
75 pub fn high_speed() -> Self {
77 Self {
78 brute_force_threshold: 5_000, hnsw_threshold: 500_000,
80 distributed_threshold: 5_000_000,
81 min_recall: 0.80,
82 enable_caching: true,
83 }
84 }
85
86 pub fn memory_efficient() -> Self {
88 Self {
89 brute_force_threshold: 10_000,
90 hnsw_threshold: 100_000, distributed_threshold: 10_000_000,
92 min_recall: 0.90,
93 enable_caching: false, }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct QueryOptimizer {
101 config: OptimizerConfig,
102}
103
104impl QueryOptimizer {
105 pub fn new(config: OptimizerConfig) -> Self {
107 Self { config }
108 }
109
110 pub fn recommend_strategy(&self, num_vectors: usize, required_recall: f32) -> SearchStrategy {
116 if required_recall >= 0.99 && num_vectors < self.config.brute_force_threshold * 2 {
118 return SearchStrategy::BruteForce;
119 }
120
121 if num_vectors < self.config.brute_force_threshold {
123 SearchStrategy::BruteForce
124 } else if num_vectors < self.config.hnsw_threshold {
125 SearchStrategy::Hnsw
126 } else if num_vectors < self.config.distributed_threshold {
127 SearchStrategy::IvfPq
128 } else {
129 SearchStrategy::Distributed
130 }
131 }
132
133 pub fn recommend_prefiltering(&self, num_vectors: usize, filter_selectivity: f32) -> bool {
142 if num_vectors < 1000 {
147 return false;
148 }
149
150 filter_selectivity < 0.10
152 }
153
154 pub fn recommend_batch_size(&self, num_queries: usize, num_vectors: usize) -> usize {
163 if num_queries < 10 {
165 return num_queries;
166 }
167
168 let base_batch_size = if num_vectors < 100_000 {
171 1000
172 } else if num_vectors < 1_000_000 {
173 500
174 } else {
175 100
176 };
177
178 base_batch_size.min(num_queries)
179 }
180
181 #[allow(dead_code)]
185 pub fn estimate_cost(&self, strategy: SearchStrategy, num_vectors: usize, k: usize) -> f64 {
186 match strategy {
187 SearchStrategy::BruteForce => {
188 num_vectors as f64 * 768.0 * k as f64
190 }
191 SearchStrategy::Hnsw => {
192 let ef_search = 50.0;
194 (num_vectors as f64).log2() * ef_search * k as f64
195 }
196 SearchStrategy::IvfPq => {
197 let nprobe = 16.0;
199 let avg_cluster_size = num_vectors as f64 / 256.0;
200 nprobe * avg_cluster_size * k as f64
201 }
202 SearchStrategy::Distributed => {
203 let nprobe = 16.0;
205 let avg_cluster_size = num_vectors as f64 / 256.0;
206 nprobe * avg_cluster_size * k as f64 * 1.5 }
208 }
209 }
210
211 pub fn config(&self) -> &OptimizerConfig {
213 &self.config
214 }
215}
216
217#[derive(Debug, Clone)]
219pub struct QueryPlan {
220 pub strategy: SearchStrategy,
222 pub use_prefiltering: bool,
224 pub batch_size: usize,
226 pub estimated_cost: f64,
228}
229
230impl QueryPlan {
231 pub fn new(
233 strategy: SearchStrategy,
234 use_prefiltering: bool,
235 batch_size: usize,
236 estimated_cost: f64,
237 ) -> Self {
238 Self {
239 strategy,
240 use_prefiltering,
241 batch_size,
242 estimated_cost,
243 }
244 }
245
246 pub fn optimize(
248 optimizer: &QueryOptimizer,
249 num_vectors: usize,
250 num_queries: usize,
251 k: usize,
252 filter_selectivity: Option<f32>,
253 required_recall: f32,
254 ) -> Self {
255 let strategy = optimizer.recommend_strategy(num_vectors, required_recall);
256 let use_prefiltering = if let Some(selectivity) = filter_selectivity {
257 optimizer.recommend_prefiltering(num_vectors, selectivity)
258 } else {
259 false
260 };
261 let batch_size = optimizer.recommend_batch_size(num_queries, num_vectors);
262 let estimated_cost = optimizer.estimate_cost(strategy, num_vectors, k);
263
264 Self::new(strategy, use_prefiltering, batch_size, estimated_cost)
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn test_recommend_strategy_small_dataset() {
274 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
275 let strategy = optimizer.recommend_strategy(5_000, 0.95);
276 assert_eq!(strategy, SearchStrategy::BruteForce);
277 }
278
279 #[test]
280 fn test_recommend_strategy_medium_dataset() {
281 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
282 let strategy = optimizer.recommend_strategy(100_000, 0.95);
283 assert_eq!(strategy, SearchStrategy::Hnsw);
284 }
285
286 #[test]
287 fn test_recommend_strategy_large_dataset() {
288 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
289 let strategy = optimizer.recommend_strategy(2_000_000, 0.95);
290 assert_eq!(strategy, SearchStrategy::IvfPq);
291 }
292
293 #[test]
294 fn test_recommend_strategy_very_large_dataset() {
295 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
296 let strategy = optimizer.recommend_strategy(20_000_000, 0.95);
297 assert_eq!(strategy, SearchStrategy::Distributed);
298 }
299
300 #[test]
301 fn test_recommend_strategy_high_recall() {
302 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
303 let strategy = optimizer.recommend_strategy(15_000, 0.99);
305 assert_eq!(strategy, SearchStrategy::BruteForce);
306 }
307
308 #[test]
309 fn test_recommend_prefiltering_selective() {
310 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
311 let use_prefilter = optimizer.recommend_prefiltering(100_000, 0.05);
313 assert!(use_prefilter);
314 }
315
316 #[test]
317 fn test_recommend_prefiltering_not_selective() {
318 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
319 let use_prefilter = optimizer.recommend_prefiltering(100_000, 0.50);
321 assert!(!use_prefilter);
322 }
323
324 #[test]
325 fn test_recommend_batch_size() {
326 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
327
328 let batch_size = optimizer.recommend_batch_size(5, 100_000);
330 assert_eq!(batch_size, 5);
331
332 let batch_size = optimizer.recommend_batch_size(2000, 50_000);
334 assert_eq!(batch_size, 1000);
335
336 let batch_size = optimizer.recommend_batch_size(2000, 2_000_000);
338 assert_eq!(batch_size, 100);
339 }
340
341 #[test]
342 fn test_estimate_cost() {
343 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
344
345 let brute_cost = optimizer.estimate_cost(SearchStrategy::BruteForce, 100_000, 10);
347 let hnsw_cost = optimizer.estimate_cost(SearchStrategy::Hnsw, 100_000, 10);
348
349 assert!(brute_cost > hnsw_cost);
350 }
351
352 #[test]
353 fn test_high_accuracy_config() {
354 let config = OptimizerConfig::high_accuracy();
355 assert_eq!(config.brute_force_threshold, 50_000);
356 assert_eq!(config.min_recall, 0.98);
357 }
358
359 #[test]
360 fn test_high_speed_config() {
361 let config = OptimizerConfig::high_speed();
362 assert_eq!(config.brute_force_threshold, 5_000);
363 assert_eq!(config.min_recall, 0.80);
364 }
365
366 #[test]
367 fn test_memory_efficient_config() {
368 let config = OptimizerConfig::memory_efficient();
369 assert_eq!(config.hnsw_threshold, 100_000);
370 assert!(!config.enable_caching);
371 }
372
373 #[test]
374 fn test_query_plan_optimize() {
375 let optimizer = QueryOptimizer::new(OptimizerConfig::default());
376
377 let plan = QueryPlan::optimize(
379 &optimizer,
380 100_000, 50, 10, Some(0.05), 0.95, );
386
387 assert_eq!(plan.strategy, SearchStrategy::Hnsw);
388 assert!(plan.use_prefiltering); assert!(plan.batch_size <= 50);
390 assert!(plan.estimated_cost > 0.0);
391 }
392}