Skip to main content

edgehdf5_memory/
strategy.rs

1//! Adaptive search strategy selection and timing metrics.
2//!
3//! Automatically selects the best search strategy based on collection size
4//! and available hardware (SIMD via rustyhdf5_accel, rayon parallelism, GPU).
5
6use std::time::Instant;
7
8use crate::vector_search;
9
10/// Search strategy selection based on collection size and hardware.
11///
12/// ```text
13/// < 1K:      Scalar (overhead of SIMD dispatch not worth it)
14/// 1K-100K:   Accelerate (AMX/cblas_sgemv) > BLAS (matrixmultiply) > SIMD prenorm
15/// 1K-10K:    SIMD brute force with pre-computed norms (fallback)
16/// 10K-50K:   Rayon parallel SIMD (if available) OR GPU (if available)
17/// 50K-500K:  GPU (if available) OR IVF-PQ
18/// > 100K:    IVF-PQ always (regardless of BLAS/GPU)
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SearchStrategy {
22    /// Plain scalar search for tiny collections (< 1K).
23    Scalar,
24    /// SIMD brute force with pre-computed norms (1K-10K).
25    SimdBruteForce,
26    /// BLAS batch matrix-vector multiply (1K-100K, requires `fast-math` feature).
27    Blas,
28    /// Apple Accelerate / OpenBLAS cblas_sgemv (1K-100K, requires `accelerate`/`openblas`).
29    Accelerate,
30    /// Rayon parallel SIMD search (10K-50K, requires `parallel` feature).
31    RayonParallel,
32    /// GPU-accelerated search (10K-500K, requires `gpu` feature).
33    Gpu,
34    /// IVF-PQ approximate search for large collections.
35    IvfPq,
36}
37
38impl std::fmt::Display for SearchStrategy {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            SearchStrategy::Scalar => write!(f, "scalar"),
42            SearchStrategy::SimdBruteForce => write!(f, "simd"),
43            SearchStrategy::Blas => write!(f, "blas"),
44            SearchStrategy::Accelerate => write!(f, "accelerate"),
45            SearchStrategy::RayonParallel => write!(f, "rayon"),
46            SearchStrategy::Gpu => write!(f, "gpu"),
47            SearchStrategy::IvfPq => write!(f, "ivf-pq"),
48        }
49    }
50}
51
52/// Metrics collected during a search operation.
53#[derive(Debug, Clone)]
54pub struct SearchMetrics {
55    /// Strategy used for this search ("scalar", "simd", "rayon", "gpu", "ivf-pq").
56    pub strategy: String,
57    /// Total search time in microseconds.
58    pub search_time_us: u64,
59    /// Number of candidate vectors scanned.
60    pub candidates_scanned: usize,
61    /// Re-ranking time in microseconds (for IVF-PQ).
62    pub rerank_time_us: Option<u64>,
63    /// Active SIMD/GPU backend (e.g., "neon", "avx2", "avx512", "gpu-metal").
64    pub backend: String,
65}
66
67/// Configuration flags for strategy selection.
68#[derive(Debug, Clone, Copy)]
69pub struct HardwareCapabilities {
70    /// Whether the `parallel` feature is enabled and rayon is available.
71    pub rayon_available: bool,
72    /// Whether the `gpu` feature is enabled and GPU hardware is detected.
73    pub gpu_available: bool,
74    /// Whether the `fast-math` feature is enabled (BLAS batch matmul).
75    pub blas_available: bool,
76    /// Whether the `accelerate` or `openblas` feature is enabled (cblas_sgemv).
77    pub accelerate_available: bool,
78}
79
80impl HardwareCapabilities {
81    /// Detect available hardware capabilities at runtime.
82    pub fn detect() -> Self {
83        Self {
84            rayon_available: cfg!(feature = "parallel"),
85            gpu_available: {
86                #[cfg(feature = "gpu")]
87                {
88                    rustyhdf5_gpu::GpuAccelerator::is_available()
89                }
90                #[cfg(not(feature = "gpu"))]
91                {
92                    false
93                }
94            },
95            blas_available: cfg!(feature = "fast-math"),
96            accelerate_available: cfg!(any(feature = "accelerate", feature = "openblas")),
97        }
98    }
99}
100
101/// Return the name of the active SIMD/acceleration backend.
102pub fn active_backend_name(gpu_active: bool) -> String {
103    if gpu_active {
104        return "gpu".to_string();
105    }
106    let backend = rustyhdf5_accel::detect_backend();
107    format!("{backend:?}").to_lowercase()
108}
109
110/// Auto-select the best search strategy based on collection size and hardware.
111///
112/// Updated hierarchy with Accelerate/BLAS support:
113/// ```text
114/// < 1K:       Scalar
115/// 1K-100K:    Accelerate (AMX sgemv) > BLAS (matrixmultiply) > Rayon > GPU > SIMD
116/// > 500K:     IVF-PQ always
117/// ```
118pub fn auto_select_strategy(num_vectors: usize, hw: &HardwareCapabilities) -> SearchStrategy {
119    if num_vectors > 500_000 {
120        return SearchStrategy::IvfPq;
121    }
122
123    if num_vectors > 50_000 {
124        if hw.accelerate_available {
125            return SearchStrategy::Accelerate;
126        }
127        if hw.blas_available {
128            return SearchStrategy::Blas;
129        }
130        if hw.gpu_available {
131            return SearchStrategy::Gpu;
132        }
133        return SearchStrategy::IvfPq;
134    }
135
136    if num_vectors > 10_000 {
137        if hw.accelerate_available {
138            return SearchStrategy::Accelerate;
139        }
140        if hw.blas_available {
141            return SearchStrategy::Blas;
142        }
143        if hw.rayon_available {
144            return SearchStrategy::RayonParallel;
145        }
146        if hw.gpu_available {
147            return SearchStrategy::Gpu;
148        }
149        return SearchStrategy::SimdBruteForce;
150    }
151
152    if num_vectors >= 1_000 {
153        if hw.accelerate_available {
154            return SearchStrategy::Accelerate;
155        }
156        if hw.blas_available {
157            return SearchStrategy::Blas;
158        }
159        return SearchStrategy::SimdBruteForce;
160    }
161
162    SearchStrategy::Scalar
163}
164
165/// Execute a search using the given strategy and return results with metrics.
166///
167/// This dispatches to the appropriate search implementation based on the
168/// selected strategy. For IVF-PQ, an index must be provided externally
169/// (this function uses brute-force fallback if no IVF-PQ index is available).
170#[allow(clippy::too_many_arguments)]
171pub fn search_with_metrics(
172    query: &[f32],
173    vectors: &[Vec<f32>],
174    norms: &[f32],
175    tombstones: &[u8],
176    k: usize,
177    strategy: SearchStrategy,
178    #[cfg(feature = "gpu")] gpu_backend: Option<&crate::gpu_search::GpuSearchBackend>,
179    #[cfg(not(feature = "gpu"))] _gpu_backend: Option<&()>,
180) -> (Vec<(usize, f32)>, SearchMetrics) {
181    let start = Instant::now();
182    let active_count = tombstones.iter().filter(|&&t| t == 0).count();
183
184    let gpu_active;
185    let results = match strategy {
186        SearchStrategy::Scalar => {
187            gpu_active = false;
188            scalar_search(query, vectors, tombstones, k)
189        }
190        SearchStrategy::SimdBruteForce => {
191            gpu_active = false;
192            let all = vector_search::cosine_similarity_batch_prenorm(
193                query, vectors, norms, tombstones,
194            );
195            vector_search::top_k(all, k)
196        }
197        SearchStrategy::Blas => {
198            gpu_active = false;
199            #[cfg(feature = "fast-math")]
200            {
201                crate::blas_search::blas_cosine_batch(query, vectors, norms, tombstones, k)
202            }
203            #[cfg(not(feature = "fast-math"))]
204            {
205                let all = vector_search::cosine_similarity_batch_prenorm(
206                    query, vectors, norms, tombstones,
207                );
208                vector_search::top_k(all, k)
209            }
210        }
211        SearchStrategy::Accelerate => {
212            gpu_active = false;
213            #[cfg(any(feature = "accelerate", feature = "openblas"))]
214            {
215                crate::accelerate_search::accelerate_cosine_batch_vecs(
216                    query, vectors, norms, tombstones, k,
217                )
218            }
219            #[cfg(not(any(feature = "accelerate", feature = "openblas")))]
220            {
221                let all = vector_search::cosine_similarity_batch_prenorm(
222                    query, vectors, norms, tombstones,
223                );
224                vector_search::top_k(all, k)
225            }
226        }
227        SearchStrategy::RayonParallel => {
228            gpu_active = false;
229            #[cfg(feature = "parallel")]
230            {
231                vector_search::parallel_cosine_batch_prenorm(
232                    query, vectors, norms, tombstones, k,
233                )
234            }
235            #[cfg(not(feature = "parallel"))]
236            {
237                let all = vector_search::cosine_similarity_batch_prenorm(
238                    query, vectors, norms, tombstones,
239                );
240                vector_search::top_k(all, k)
241            }
242        }
243        SearchStrategy::Gpu => {
244            #[cfg(feature = "gpu")]
245            {
246                if let Some(backend) = gpu_backend {
247                    gpu_active = backend.is_available();
248                    backend.search_cosine(query, vectors, norms, tombstones, k)
249                } else {
250                    gpu_active = false;
251                    let all = vector_search::cosine_similarity_batch_prenorm(
252                        query, vectors, norms, tombstones,
253                    );
254                    vector_search::top_k(all, k)
255                }
256            }
257            #[cfg(not(feature = "gpu"))]
258            {
259                gpu_active = false;
260                let all = vector_search::cosine_similarity_batch_prenorm(
261                    query, vectors, norms, tombstones,
262                );
263                vector_search::top_k(all, k)
264            }
265        }
266        SearchStrategy::IvfPq => {
267            gpu_active = false;
268            // IVF-PQ requires an external index; fall back to prenorm brute force
269            // when called through this generic interface.
270            let all = vector_search::cosine_similarity_batch_prenorm(
271                query, vectors, norms, tombstones,
272            );
273            vector_search::top_k(all, k)
274        }
275    };
276
277    let elapsed = start.elapsed();
278    let metrics = SearchMetrics {
279        strategy: strategy.to_string(),
280        search_time_us: elapsed.as_micros() as u64,
281        candidates_scanned: active_count,
282        rerank_time_us: None,
283        backend: active_backend_name(gpu_active),
284    };
285
286    (results, metrics)
287}
288
289/// Plain scalar cosine similarity for very small collections.
290fn scalar_search(
291    query: &[f32],
292    vectors: &[Vec<f32>],
293    tombstones: &[u8],
294    k: usize,
295) -> Vec<(usize, f32)> {
296    let query_norm = rustyhdf5_accel::vector_norm(query);
297    if query_norm == 0.0 {
298        return Vec::new();
299    }
300
301    let mut results: Vec<(usize, f32)> = Vec::with_capacity(vectors.len());
302
303    for (i, vec) in vectors.iter().enumerate() {
304        if i < tombstones.len() && tombstones[i] != 0 {
305            continue;
306        }
307        // Use rustyhdf5_accel even for scalar strategy — it's always available and
308        // the "scalar" name refers to the strategy tier, not the implementation.
309        let vec_norm = rustyhdf5_accel::vector_norm(vec);
310        let score = crate::cosine_similarity_prenorm(query, query_norm, vec, vec_norm);
311        results.push((i, score));
312    }
313
314    results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
315    results.truncate(k);
316    results
317}
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    fn make_vectors(n: usize, dim: usize, seed: u32) -> Vec<Vec<f32>> {
324        let mut s = seed;
325        let mut next = || -> f32 {
326            s = s.wrapping_mul(1103515245).wrapping_add(12345);
327            ((s >> 16) as f32) / 65536.0 - 0.5
328        };
329        (0..n).map(|_| (0..dim).map(|_| next()).collect()).collect()
330    }
331
332    // --- auto_select_strategy tests ---
333
334    #[test]
335    fn strategy_scalar_under_1k() {
336        let hw = HardwareCapabilities {
337            rayon_available: false,
338            gpu_available: false,
339            blas_available: false,
340            accelerate_available: false,
341        };
342        assert_eq!(auto_select_strategy(0, &hw), SearchStrategy::Scalar);
343        assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
344        assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
345    }
346
347    #[test]
348    fn strategy_simd_1k_to_10k() {
349        let hw = HardwareCapabilities {
350            rayon_available: false,
351            gpu_available: false,
352            blas_available: false,
353            accelerate_available: false,
354        };
355        assert_eq!(
356            auto_select_strategy(1_000, &hw),
357            SearchStrategy::SimdBruteForce
358        );
359        assert_eq!(
360            auto_select_strategy(5_000, &hw),
361            SearchStrategy::SimdBruteForce
362        );
363        assert_eq!(
364            auto_select_strategy(10_000, &hw),
365            SearchStrategy::SimdBruteForce
366        );
367    }
368
369    #[test]
370    fn strategy_rayon_10k_to_50k_when_available() {
371        let hw = HardwareCapabilities {
372            rayon_available: true,
373            gpu_available: false,
374            blas_available: false,
375            accelerate_available: false,
376        };
377        assert_eq!(
378            auto_select_strategy(10_001, &hw),
379            SearchStrategy::RayonParallel
380        );
381        assert_eq!(
382            auto_select_strategy(30_000, &hw),
383            SearchStrategy::RayonParallel
384        );
385        assert_eq!(
386            auto_select_strategy(50_000, &hw),
387            SearchStrategy::RayonParallel
388        );
389    }
390
391    #[test]
392    fn strategy_gpu_10k_to_50k_when_no_rayon() {
393        let hw = HardwareCapabilities {
394            rayon_available: false,
395            gpu_available: true,
396            blas_available: false,
397            accelerate_available: false,
398        };
399        assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Gpu);
400        assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Gpu);
401    }
402
403    #[test]
404    fn strategy_gpu_50k_to_500k() {
405        let hw = HardwareCapabilities {
406            rayon_available: true,
407            gpu_available: true,
408            blas_available: false,
409            accelerate_available: false,
410        };
411        assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::Gpu);
412        assert_eq!(auto_select_strategy(200_000, &hw), SearchStrategy::Gpu);
413        assert_eq!(auto_select_strategy(500_000, &hw), SearchStrategy::Gpu);
414    }
415
416    #[test]
417    fn strategy_ivfpq_over_500k() {
418        let hw = HardwareCapabilities {
419            rayon_available: true,
420            gpu_available: true,
421            blas_available: true,
422            accelerate_available: true,
423        };
424        assert_eq!(auto_select_strategy(500_001, &hw), SearchStrategy::IvfPq);
425        assert_eq!(auto_select_strategy(1_000_000, &hw), SearchStrategy::IvfPq);
426    }
427
428    #[test]
429    fn strategy_ivfpq_fallback_50k_no_gpu() {
430        let hw = HardwareCapabilities {
431            rayon_available: false,
432            gpu_available: false,
433            blas_available: false,
434            accelerate_available: false,
435        };
436        assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::IvfPq);
437    }
438
439    #[test]
440    fn strategy_simd_fallback_10k_no_parallel_no_gpu() {
441        let hw = HardwareCapabilities {
442            rayon_available: false,
443            gpu_available: false,
444            blas_available: false,
445            accelerate_available: false,
446        };
447        assert_eq!(
448            auto_select_strategy(15_000, &hw),
449            SearchStrategy::SimdBruteForce
450        );
451    }
452
453    // --- SearchStrategy Display ---
454
455    #[test]
456    fn strategy_display_names() {
457        assert_eq!(SearchStrategy::Scalar.to_string(), "scalar");
458        assert_eq!(SearchStrategy::SimdBruteForce.to_string(), "simd");
459        assert_eq!(SearchStrategy::Blas.to_string(), "blas");
460        assert_eq!(SearchStrategy::Accelerate.to_string(), "accelerate");
461        assert_eq!(SearchStrategy::RayonParallel.to_string(), "rayon");
462        assert_eq!(SearchStrategy::Gpu.to_string(), "gpu");
463        assert_eq!(SearchStrategy::IvfPq.to_string(), "ivf-pq");
464    }
465
466    // --- SearchMetrics ---
467
468    #[test]
469    fn search_metrics_strategy_name() {
470        let metrics = SearchMetrics {
471            strategy: "simd".to_string(),
472            search_time_us: 100,
473            candidates_scanned: 1000,
474            rerank_time_us: None,
475            backend: "neon".to_string(),
476        };
477        assert_eq!(metrics.strategy, "simd");
478        assert_eq!(metrics.candidates_scanned, 1000);
479        assert_eq!(metrics.backend, "neon");
480    }
481
482    // --- search_with_metrics ---
483
484    #[test]
485    fn search_with_metrics_scalar() {
486        let vectors = make_vectors(50, 16, 42);
487        let norms: Vec<f32> = vectors
488            .iter()
489            .map(|v| rustyhdf5_accel::vector_norm(v))
490            .collect();
491        let tombstones = vec![0u8; 50];
492        let query = vectors[0].clone();
493
494        let (results, metrics) = search_with_metrics(
495            &query,
496            &vectors,
497            &norms,
498            &tombstones,
499            5,
500            SearchStrategy::Scalar,
501            None,
502        );
503
504        assert_eq!(results.len(), 5);
505        assert_eq!(metrics.strategy, "scalar");
506        assert!(metrics.search_time_us > 0 || metrics.candidates_scanned > 0);
507        assert_eq!(metrics.candidates_scanned, 50);
508        assert!(metrics.rerank_time_us.is_none());
509        assert!(!metrics.backend.is_empty());
510        // First result should be the query itself
511        assert_eq!(results[0].0, 0);
512    }
513
514    #[test]
515    fn search_with_metrics_simd() {
516        let vectors = make_vectors(100, 32, 42);
517        let norms: Vec<f32> = vectors
518            .iter()
519            .map(|v| rustyhdf5_accel::vector_norm(v))
520            .collect();
521        let tombstones = vec![0u8; 100];
522        let query = vectors[0].clone();
523
524        let (results, metrics) = search_with_metrics(
525            &query,
526            &vectors,
527            &norms,
528            &tombstones,
529            10,
530            SearchStrategy::SimdBruteForce,
531            None,
532        );
533
534        assert_eq!(metrics.strategy, "simd");
535        assert!(!results.is_empty());
536        assert_eq!(results[0].0, 0);
537    }
538
539    #[test]
540    fn search_with_metrics_timing_nonzero() {
541        let vectors = make_vectors(1000, 64, 42);
542        let norms: Vec<f32> = vectors
543            .iter()
544            .map(|v| rustyhdf5_accel::vector_norm(v))
545            .collect();
546        let tombstones = vec![0u8; 1000];
547        let query = vectors[0].clone();
548
549        let (_, metrics) = search_with_metrics(
550            &query,
551            &vectors,
552            &norms,
553            &tombstones,
554            10,
555            SearchStrategy::SimdBruteForce,
556            None,
557        );
558
559        // With 1000 vectors, search should take > 0 microseconds
560        assert!(metrics.search_time_us < 1_000_000); // under 1 second
561        assert_eq!(metrics.candidates_scanned, 1000);
562    }
563
564    #[test]
565    fn search_with_metrics_results_match_direct() {
566        let vectors = make_vectors(200, 32, 42);
567        let norms: Vec<f32> = vectors
568            .iter()
569            .map(|v| rustyhdf5_accel::vector_norm(v))
570            .collect();
571        let tombstones = vec![0u8; 200];
572        let query = vectors[3].clone();
573
574        let (results, _) = search_with_metrics(
575            &query,
576            &vectors,
577            &norms,
578            &tombstones,
579            10,
580            SearchStrategy::SimdBruteForce,
581            None,
582        );
583
584        let direct = vector_search::cosine_similarity_batch_prenorm(
585            &query, &vectors, &norms, &tombstones,
586        );
587        let direct_top = vector_search::top_k(direct, 10);
588
589        assert_eq!(results.len(), direct_top.len());
590        for (r, d) in results.iter().zip(&direct_top) {
591            assert_eq!(r.0, d.0);
592            assert!((r.1 - d.1).abs() < 1e-6);
593        }
594    }
595
596    #[test]
597    fn search_with_metrics_respects_tombstones() {
598        let vectors = make_vectors(100, 16, 42);
599        let norms: Vec<f32> = vectors
600            .iter()
601            .map(|v| rustyhdf5_accel::vector_norm(v))
602            .collect();
603        let mut tombstones = vec![0u8; 100];
604        tombstones[0] = 1;
605        tombstones[1] = 1;
606
607        let query = vectors[2].clone();
608        let (results, metrics) = search_with_metrics(
609            &query,
610            &vectors,
611            &norms,
612            &tombstones,
613            100,
614            SearchStrategy::Scalar,
615            None,
616        );
617
618        assert!(results.iter().all(|r| r.0 != 0 && r.0 != 1));
619        assert_eq!(metrics.candidates_scanned, 98);
620    }
621
622    #[test]
623    fn hardware_capabilities_detect() {
624        let hw = HardwareCapabilities::detect();
625        // Just verify it doesn't panic and returns something
626        let _ = hw.rayon_available;
627        let _ = hw.gpu_available;
628    }
629
630    #[test]
631    fn active_backend_name_returns_valid() {
632        let name = active_backend_name(false);
633        assert!(!name.is_empty());
634        // Should be one of the known backends
635        let valid = ["neon", "avx2", "avx512", "sse4", "wasmsimd128", "scalar"];
636        assert!(
637            valid.iter().any(|v| name.contains(v)),
638            "unexpected backend: {name}"
639        );
640    }
641
642    #[test]
643    fn search_metrics_has_backend_field() {
644        let vectors = make_vectors(50, 16, 42);
645        let norms: Vec<f32> = vectors
646            .iter()
647            .map(|v| rustyhdf5_accel::vector_norm(v))
648            .collect();
649        let tombstones = vec![0u8; 50];
650        let query = vectors[0].clone();
651
652        let (_, metrics) = search_with_metrics(
653            &query,
654            &vectors,
655            &norms,
656            &tombstones,
657            5,
658            SearchStrategy::Scalar,
659            None,
660        );
661
662        assert!(!metrics.backend.is_empty());
663    }
664
665    // --- BLAS strategy selection tests ---
666
667    #[test]
668    fn strategy_blas_preferred_1k_to_100k() {
669        let hw = HardwareCapabilities {
670            rayon_available: true,
671            gpu_available: true,
672            blas_available: true,
673            accelerate_available: false,
674        };
675        // BLAS should be preferred over rayon/gpu/simd in the 1K-100K range
676        assert_eq!(auto_select_strategy(1_000, &hw), SearchStrategy::Blas);
677        assert_eq!(auto_select_strategy(5_000, &hw), SearchStrategy::Blas);
678        assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Blas);
679        assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Blas);
680        assert_eq!(auto_select_strategy(100_000, &hw), SearchStrategy::Blas);
681    }
682
683    #[test]
684    fn strategy_blas_not_for_small() {
685        let hw = HardwareCapabilities {
686            rayon_available: false,
687            gpu_available: false,
688            blas_available: true,
689            accelerate_available: false,
690        };
691        // Under 1K, still use scalar
692        assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
693        assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
694    }
695
696    #[test]
697    fn strategy_fallback_without_blas() {
698        let hw = HardwareCapabilities {
699            rayon_available: false,
700            gpu_available: false,
701            blas_available: false,
702            accelerate_available: false,
703        };
704        // Without BLAS, falls back to SIMD/IVF-PQ
705        assert_eq!(
706            auto_select_strategy(5_000, &hw),
707            SearchStrategy::SimdBruteForce
708        );
709        assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::IvfPq);
710    }
711
712    #[cfg(feature = "fast-math")]
713    #[test]
714    fn search_with_metrics_blas() {
715        let vectors = make_vectors(200, 32, 42);
716        let norms: Vec<f32> = vectors
717            .iter()
718            .map(|v| rustyhdf5_accel::vector_norm(v))
719            .collect();
720        let tombstones = vec![0u8; 200];
721        let query = vectors[0].clone();
722
723        let (results, metrics) = search_with_metrics(
724            &query,
725            &vectors,
726            &norms,
727            &tombstones,
728            10,
729            SearchStrategy::Blas,
730            None,
731        );
732
733        assert_eq!(metrics.strategy, "blas");
734        assert!(!results.is_empty());
735        assert_eq!(results[0].0, 0);
736    }
737
738    #[cfg(feature = "parallel")]
739    #[test]
740    fn search_with_metrics_rayon() {
741        let vectors = make_vectors(500, 32, 42);
742        let norms: Vec<f32> = vectors
743            .iter()
744            .map(|v| rustyhdf5_accel::vector_norm(v))
745            .collect();
746        let tombstones = vec![0u8; 500];
747        let query = vectors[0].clone();
748
749        let (results, metrics) = search_with_metrics(
750            &query,
751            &vectors,
752            &norms,
753            &tombstones,
754            10,
755            SearchStrategy::RayonParallel,
756            None,
757        );
758
759        assert_eq!(metrics.strategy, "rayon");
760        assert!(!results.is_empty());
761        assert_eq!(results[0].0, 0);
762    }
763
764    // --- Accelerate strategy selection tests ---
765
766    #[test]
767    fn strategy_accelerate_preferred_over_blas() {
768        let hw = HardwareCapabilities {
769            rayon_available: true,
770            gpu_available: true,
771            blas_available: true,
772            accelerate_available: true,
773        };
774        // Accelerate should be preferred over BLAS/rayon/gpu in the 1K-100K range
775        assert_eq!(auto_select_strategy(1_000, &hw), SearchStrategy::Accelerate);
776        assert_eq!(auto_select_strategy(5_000, &hw), SearchStrategy::Accelerate);
777        assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Accelerate);
778        assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Accelerate);
779        assert_eq!(auto_select_strategy(100_000, &hw), SearchStrategy::Accelerate);
780    }
781
782    #[test]
783    fn strategy_accelerate_not_for_small() {
784        let hw = HardwareCapabilities {
785            rayon_available: false,
786            gpu_available: false,
787            blas_available: false,
788            accelerate_available: true,
789        };
790        // Under 1K, still use scalar
791        assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
792        assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
793    }
794
795    #[test]
796    fn strategy_accelerate_not_for_huge() {
797        let hw = HardwareCapabilities {
798            rayon_available: true,
799            gpu_available: true,
800            blas_available: true,
801            accelerate_available: true,
802        };
803        // Over 500K, always IVF-PQ
804        assert_eq!(auto_select_strategy(500_001, &hw), SearchStrategy::IvfPq);
805    }
806
807    #[cfg(any(feature = "accelerate", feature = "openblas"))]
808    #[test]
809    fn search_with_metrics_accelerate() {
810        let vectors = make_vectors(200, 32, 42);
811        let norms: Vec<f32> = vectors
812            .iter()
813            .map(|v| rustyhdf5_accel::vector_norm(v))
814            .collect();
815        let tombstones = vec![0u8; 200];
816        let query = vectors[0].clone();
817
818        let (results, metrics) = search_with_metrics(
819            &query,
820            &vectors,
821            &norms,
822            &tombstones,
823            10,
824            SearchStrategy::Accelerate,
825            None,
826        );
827
828        assert_eq!(metrics.strategy, "accelerate");
829        assert!(!results.is_empty());
830        assert_eq!(results[0].0, 0);
831    }
832}