Skip to main content

oxirs_vec/
nsg.rs

1//! NSG (Navigable Small World Graph) index
2//!
3//! NSG is a graph-based approximate nearest neighbor search algorithm that builds
4//! a monotonic navigable graph structure. It provides:
5//!
6//! - **Monotonic Search Path**: Guarantees that search always moves closer to query
7//! - **Memory Efficiency**: Controlled out-degree for compact graph structure
8//! - **High Accuracy**: Better recall than NSW with similar or better performance
9//! - **Fast Search**: O(log n) expected search complexity
10//!
11//! # Algorithm Overview
12//!
13//! NSG construction has two stages:
14//! 1. Build initial kNN graph using any ANN algorithm
15//! 2. Refine graph to ensure navigability and monotonicity
16//!
17//! The key innovation is the monotonic search property: each hop in the search
18//! path gets closer to the query point, preventing cycles and dead-ends.
19//!
20//! # References
21//!
22//! - Fu, Cong, et al. "Fast approximate nearest neighbor search with the navigable
23//!   small world graph." arXiv preprint arXiv:1707.00143 (2017).
24//!
25//! # Example
26//!
27//! ```rust
28//! use oxirs_vec::{Vector, VectorIndex};
29//! use oxirs_vec::nsg::{NsgConfig, NsgIndex};
30//!
31//! let config = NsgConfig {
32//!     out_degree: 32,
33//!     candidate_pool_size: 100,
34//!     search_length: 50,
35//!     ..Default::default()
36//! };
37//!
38//! let mut index = NsgIndex::new(config).expect("should succeed");
39//!
40//! // Add vectors
41//! for i in 0..1000 {
42//!     let vector = Vector::new(vec![i as f32, (i * 2) as f32, (i * 3) as f32]);
43//!     index.insert(format!("vec_{}", i), vector).expect("should succeed");
44//! }
45//!
46//! // Build the NSG structure
47//! index.build().expect("should succeed");
48//!
49//! // Search
50//! let query = Vector::new(vec![100.0, 200.0, 300.0]);
51//! let results = index.search_knn(&query, 10).expect("should succeed");
52//! ```
53
54use crate::{Vector, VectorIndex};
55use anyhow::Result;
56use oxirs_core::simd::SimdOps;
57use parking_lot::RwLock as ParkingLotRwLock;
58use scirs2_core::random::Random;
59use std::cmp::Ordering;
60use std::collections::{BinaryHeap, HashMap, HashSet};
61use std::sync::{Arc, RwLock};
62
63/// Configuration for NSG index
64#[derive(Debug, Clone)]
65pub struct NsgConfig {
66    /// Maximum out-degree (number of outgoing edges per node)
67    pub out_degree: usize,
68    /// Candidate pool size during graph construction
69    pub candidate_pool_size: usize,
70    /// Search length during graph refinement
71    pub search_length: usize,
72    /// Distance metric to use
73    pub distance_metric: DistanceMetric,
74    /// Random seed for reproducibility
75    pub random_seed: Option<u64>,
76    /// Enable parallel construction
77    pub parallel_construction: bool,
78    /// Number of threads for parallel construction
79    pub num_threads: usize,
80    /// Initial kNN graph degree
81    pub initial_knn_degree: usize,
82    /// Pruning threshold for edge quality
83    pub pruning_threshold: f32,
84}
85
86impl Default for NsgConfig {
87    fn default() -> Self {
88        Self {
89            out_degree: 32,
90            candidate_pool_size: 100,
91            search_length: 50,
92            distance_metric: DistanceMetric::Euclidean,
93            random_seed: None,
94            parallel_construction: true,
95            num_threads: std::thread::available_parallelism()
96                .map(|n| n.get())
97                .unwrap_or(1),
98            initial_knn_degree: 64,
99            pruning_threshold: 1.0,
100        }
101    }
102}
103
104/// Distance metrics supported by NSG
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum DistanceMetric {
107    Euclidean,
108    Manhattan,
109    Cosine,
110    Angular,
111    InnerProduct,
112}
113
114impl DistanceMetric {
115    /// Calculate distance between two vectors
116    pub fn distance(&self, a: &[f32], b: &[f32]) -> f32 {
117        match self {
118            DistanceMetric::Euclidean => f32::euclidean_distance(a, b),
119            DistanceMetric::Manhattan => f32::manhattan_distance(a, b),
120            DistanceMetric::Cosine => f32::cosine_distance(a, b),
121            DistanceMetric::Angular => {
122                let cos_sim = 1.0 - f32::cosine_distance(a, b);
123                cos_sim.clamp(-1.0, 1.0).acos() / std::f32::consts::PI
124            }
125            DistanceMetric::InnerProduct => {
126                // Negative inner product (to use as distance)
127                -a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>()
128            }
129        }
130    }
131}
132
133/// Search candidate with distance
134#[derive(Debug, Clone)]
135struct Candidate {
136    id: usize,
137    distance: f32,
138}
139
140impl PartialEq for Candidate {
141    fn eq(&self, other: &Self) -> bool {
142        self.distance == other.distance && self.id == other.id
143    }
144}
145
146impl Eq for Candidate {}
147
148impl PartialOrd for Candidate {
149    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
150        Some(self.cmp(other))
151    }
152}
153
154impl Ord for Candidate {
155    fn cmp(&self, other: &Self) -> Ordering {
156        // Reverse ordering for max-heap (we want min distance at top)
157        other
158            .distance
159            .partial_cmp(&self.distance)
160            .unwrap_or(Ordering::Equal)
161            .then_with(|| self.id.cmp(&other.id))
162    }
163}
164
165/// NSG index structure
166pub struct NsgIndex {
167    /// Configuration
168    config: NsgConfig,
169    /// Stored vectors with URIs
170    data: Vec<(String, Vector)>,
171    /// Forward adjacency list (outgoing edges)
172    graph: Vec<Vec<usize>>,
173    /// Entry point for search
174    entry_point: Option<usize>,
175    /// Whether the index is built
176    is_built: bool,
177    /// URI to index mapping
178    uri_to_idx: HashMap<String, usize>,
179    /// Statistics
180    stats: Arc<RwLock<NsgStats>>,
181}
182
183/// NSG index statistics
184#[derive(Debug, Clone, Default)]
185pub struct NsgStats {
186    /// Number of vectors indexed
187    pub num_vectors: usize,
188    /// Number of edges in the graph
189    pub num_edges: usize,
190    /// Average out-degree
191    pub avg_out_degree: f64,
192    /// Maximum out-degree
193    pub max_out_degree: usize,
194    /// Number of searches performed
195    pub num_searches: usize,
196    /// Average search path length
197    pub avg_search_path_length: f64,
198    /// Total distance computations
199    pub total_distance_computations: usize,
200}
201
202impl NsgIndex {
203    /// Create a new NSG index with given configuration
204    pub fn new(config: NsgConfig) -> Result<Self> {
205        Ok(Self {
206            config,
207            data: Vec::new(),
208            graph: Vec::new(),
209            entry_point: None,
210            is_built: false,
211            uri_to_idx: HashMap::new(),
212            stats: Arc::new(RwLock::new(NsgStats::default())),
213        })
214    }
215
216    /// Add a vector to the index (must call build() after adding all vectors)
217    pub fn add(&mut self, uri: String, vector: Vector) -> Result<()> {
218        if self.is_built {
219            return Err(anyhow::anyhow!(
220                "Cannot add vectors after index is built. Call rebuild() or create a new index."
221            ));
222        }
223
224        let idx = self.data.len();
225        self.uri_to_idx.insert(uri.clone(), idx);
226        self.data.push((uri, vector));
227
228        Ok(())
229    }
230
231    /// Build the NSG structure
232    ///
233    /// This is a two-stage process:
234    /// 1. Build initial kNN graph
235    /// 2. Refine to create navigable monotonic graph
236    pub fn build(&mut self) -> Result<()> {
237        if self.data.is_empty() {
238            return Err(anyhow::anyhow!("Cannot build index with no vectors"));
239        }
240
241        tracing::info!("Building NSG index with {} vectors", self.data.len());
242
243        // Stage 1: Build initial kNN graph
244        tracing::debug!("Stage 1: Building initial kNN graph");
245        self.build_knn_graph()?;
246
247        // Stage 2: Refine to NSG
248        tracing::debug!("Stage 2: Refining to navigable monotonic graph");
249        self.refine_to_nsg()?;
250
251        // Select entry point
252        self.select_entry_point()?;
253
254        self.is_built = true;
255
256        // Update statistics
257        self.update_stats();
258
259        tracing::info!(
260            "NSG index built successfully. {} vectors, {} edges, avg out-degree: {:.2}",
261            self.data.len(),
262            self.count_edges(),
263            self.avg_out_degree()
264        );
265
266        Ok(())
267    }
268
269    /// Build initial kNN graph using brute-force search
270    fn build_knn_graph(&mut self) -> Result<()> {
271        let n = self.data.len();
272        self.graph = vec![Vec::new(); n];
273
274        if self.config.parallel_construction && n > 1000 {
275            self.build_knn_graph_parallel()?;
276        } else {
277            self.build_knn_graph_sequential()?;
278        }
279
280        Ok(())
281    }
282
283    /// Sequential kNN graph construction
284    fn build_knn_graph_sequential(&mut self) -> Result<()> {
285        let n = self.data.len();
286        let k = self.config.initial_knn_degree.min(n - 1);
287
288        for i in 0..n {
289            let mut neighbors = Vec::new();
290
291            // Find k nearest neighbors
292            for j in 0..n {
293                if i == j {
294                    continue;
295                }
296
297                let dist = self.calculate_distance(i, j);
298                neighbors.push(Candidate {
299                    id: j,
300                    distance: dist,
301                });
302            }
303
304            // Sort and keep top-k
305            neighbors.sort_by(|a, b| {
306                a.distance
307                    .partial_cmp(&b.distance)
308                    .unwrap_or(Ordering::Equal)
309            });
310            neighbors.truncate(k);
311
312            // Add bidirectional edges
313            self.graph[i] = neighbors.iter().map(|c| c.id).collect();
314        }
315
316        Ok(())
317    }
318
319    /// Parallel kNN graph construction
320    fn build_knn_graph_parallel(&mut self) -> Result<()> {
321        let n = self.data.len();
322        let k = self.config.initial_knn_degree.min(n - 1);
323
324        // Create thread-safe graph structure
325        let graph = Arc::new(ParkingLotRwLock::new(vec![Vec::new(); n]));
326        let data = Arc::new(self.data.clone());
327        let config = self.config.clone();
328
329        // Process in parallel chunks
330        let chunk_size = (n + self.config.num_threads - 1) / self.config.num_threads;
331        let mut handles = Vec::new();
332
333        for chunk_start in (0..n).step_by(chunk_size) {
334            let chunk_end = (chunk_start + chunk_size).min(n);
335            let graph_clone = Arc::clone(&graph);
336            let data_clone = Arc::clone(&data);
337            let config_clone = config.clone();
338
339            let handle = std::thread::spawn(move || {
340                for i in chunk_start..chunk_end {
341                    let mut neighbors = Vec::new();
342
343                    for j in 0..n {
344                        if i == j {
345                            continue;
346                        }
347
348                        let vec_i = &data_clone[i].1.as_f32();
349                        let vec_j = &data_clone[j].1.as_f32();
350                        let dist = config_clone.distance_metric.distance(vec_i, vec_j);
351
352                        neighbors.push(Candidate {
353                            id: j,
354                            distance: dist,
355                        });
356                    }
357
358                    neighbors.sort_by(|a, b| {
359                        a.distance
360                            .partial_cmp(&b.distance)
361                            .unwrap_or(Ordering::Equal)
362                    });
363                    neighbors.truncate(k);
364
365                    let mut graph_lock = graph_clone.write();
366                    graph_lock[i] = neighbors.iter().map(|c| c.id).collect();
367                }
368            });
369
370            handles.push(handle);
371        }
372
373        // Wait for all threads
374        for handle in handles {
375            handle
376                .join()
377                .map_err(|_| anyhow::anyhow!("Thread panicked"))?;
378        }
379
380        // Copy results back
381        self.graph = Arc::try_unwrap(graph)
382            .map_err(|_| anyhow::anyhow!("Failed to unwrap graph"))?
383            .into_inner();
384
385        Ok(())
386    }
387
388    /// Refine kNN graph to NSG with monotonic navigability
389    fn refine_to_nsg(&mut self) -> Result<()> {
390        let n = self.data.len();
391        let mut new_graph = vec![Vec::new(); n];
392
393        // Select a temporary entry point for refinement
394        let temp_entry = self.select_temp_entry_point();
395
396        #[allow(clippy::needless_range_loop)]
397        for i in 0..n {
398            // Find candidate neighbors through navigation
399            let candidates = self.search_for_neighbors(i, temp_entry)?;
400
401            // Prune to maintain out-degree constraint
402            let neighbors = self.prune_neighbors(i, candidates)?;
403
404            new_graph[i] = neighbors;
405        }
406
407        // Ensure connectivity by adding reverse edges where needed
408        self.ensure_connectivity(&mut new_graph)?;
409
410        self.graph = new_graph;
411
412        Ok(())
413    }
414
415    /// Search for candidate neighbors during graph refinement
416    fn search_for_neighbors(&self, query_id: usize, entry_id: usize) -> Result<Vec<Candidate>> {
417        let mut visited = HashSet::new();
418        let mut candidates = BinaryHeap::new();
419        let mut result = Vec::new();
420
421        // Start from entry point
422        let entry_dist = self.calculate_distance(query_id, entry_id);
423        candidates.push(Candidate {
424            id: entry_id,
425            distance: entry_dist,
426        });
427        visited.insert(entry_id);
428
429        while let Some(current) = candidates.pop() {
430            if result.len() >= self.config.candidate_pool_size {
431                break;
432            }
433
434            result.push(current.clone());
435
436            // Explore neighbors
437            for &neighbor_id in &self.graph[current.id] {
438                if visited.contains(&neighbor_id) {
439                    continue;
440                }
441
442                visited.insert(neighbor_id);
443
444                let dist = self.calculate_distance(query_id, neighbor_id);
445                candidates.push(Candidate {
446                    id: neighbor_id,
447                    distance: dist,
448                });
449
450                if visited.len() >= self.config.search_length {
451                    break;
452                }
453            }
454        }
455
456        // Sort by distance
457        result.sort_by(|a, b| {
458            a.distance
459                .partial_cmp(&b.distance)
460                .unwrap_or(Ordering::Equal)
461        });
462
463        Ok(result)
464    }
465
466    /// Prune neighbors to maintain graph quality and out-degree constraint
467    fn prune_neighbors(
468        &self,
469        _query_id: usize,
470        mut candidates: Vec<Candidate>,
471    ) -> Result<Vec<usize>> {
472        if candidates.is_empty() {
473            return Ok(Vec::new());
474        }
475
476        let mut result = Vec::new();
477        let mut pruned = HashSet::new();
478
479        while !candidates.is_empty() && result.len() < self.config.out_degree {
480            // Find best candidate (minimum distance)
481            let best_idx = candidates
482                .iter()
483                .position_min_by(|a, b| {
484                    a.distance
485                        .partial_cmp(&b.distance)
486                        .unwrap_or(Ordering::Equal)
487                })
488                .expect("candidates should not be empty during pruning");
489
490            let best = candidates.swap_remove(best_idx);
491
492            if pruned.contains(&best.id) {
493                continue;
494            }
495
496            result.push(best.id);
497            pruned.insert(best.id);
498
499            // Prune candidates that are too close to the selected neighbor
500            candidates.retain(|c| {
501                let dist_to_best = self.calculate_distance(c.id, best.id);
502                dist_to_best > best.distance * self.config.pruning_threshold
503            });
504        }
505
506        Ok(result)
507    }
508
509    /// Ensure graph connectivity by adding reverse edges
510    fn ensure_connectivity(&self, graph: &mut [Vec<usize>]) -> Result<()> {
511        let n = graph.len();
512
513        // Build reverse index
514        let mut in_edges: Vec<HashSet<usize>> = vec![HashSet::new(); n];
515        for (i, neighbors) in graph.iter().enumerate() {
516            for &j in neighbors {
517                in_edges[j].insert(i);
518            }
519        }
520
521        // For each node, ensure it has at least one incoming edge
522        for (i, edges) in in_edges.iter().enumerate() {
523            if edges.is_empty() && i != 0 {
524                // Find closest node that has outgoing edges
525                let mut min_dist = f32::INFINITY;
526                let mut closest = 0;
527
528                for (j, neighbors) in graph.iter().enumerate() {
529                    if i == j || neighbors.len() >= self.config.out_degree {
530                        continue;
531                    }
532
533                    let dist = self.calculate_distance(i, j);
534                    if dist < min_dist {
535                        min_dist = dist;
536                        closest = j;
537                    }
538                }
539
540                // Add edge from closest to i
541                if !graph[closest].contains(&i) {
542                    graph[closest].push(i);
543                }
544            }
545        }
546
547        Ok(())
548    }
549
550    /// Select entry point for search (node with highest out-degree)
551    fn select_entry_point(&mut self) -> Result<()> {
552        if self.data.is_empty() {
553            return Ok(());
554        }
555
556        let mut max_degree = 0;
557        let mut entry = 0;
558
559        for i in 0..self.graph.len() {
560            if self.graph[i].len() > max_degree {
561                max_degree = self.graph[i].len();
562                entry = i;
563            }
564        }
565
566        self.entry_point = Some(entry);
567
568        Ok(())
569    }
570
571    /// Select temporary entry point for graph refinement
572    fn select_temp_entry_point(&self) -> usize {
573        if let Some(seed) = self.config.random_seed {
574            let mut rng = Random::seed(seed);
575            rng.random_range(0..self.data.len())
576        } else {
577            // Use centroid as entry point
578            self.find_centroid()
579        }
580    }
581
582    /// Find centroid of all vectors
583    fn find_centroid(&self) -> usize {
584        if self.data.is_empty() {
585            return 0;
586        }
587
588        let dim = self.data[0].1.dimensions;
589        let mut centroid = vec![0.0f32; dim];
590
591        // Calculate mean
592        for (_, vec) in &self.data {
593            let vals = vec.as_f32();
594            for i in 0..dim {
595                centroid[i] += vals[i];
596            }
597        }
598
599        let n = self.data.len() as f32;
600        for val in &mut centroid {
601            *val /= n;
602        }
603
604        // Find closest vector to centroid
605        let mut min_dist = f32::INFINITY;
606        let mut closest = 0;
607
608        for i in 0..self.data.len() {
609            let dist = self
610                .config
611                .distance_metric
612                .distance(&centroid, &self.data[i].1.as_f32());
613            if dist < min_dist {
614                min_dist = dist;
615                closest = i;
616            }
617        }
618
619        closest
620    }
621
622    /// Calculate distance between two vectors by index
623    fn calculate_distance(&self, i: usize, j: usize) -> f32 {
624        let vec_i = self.data[i].1.as_f32();
625        let vec_j = self.data[j].1.as_f32();
626        self.config.distance_metric.distance(&vec_i, &vec_j)
627    }
628
629    /// Perform greedy search on the graph
630    fn greedy_search(&self, query: &[f32], k: usize, ef: usize) -> Result<Vec<Candidate>> {
631        if !self.is_built {
632            return Err(anyhow::anyhow!("Index not built. Call build() first."));
633        }
634
635        let entry = self
636            .entry_point
637            .ok_or_else(|| anyhow::anyhow!("No entry point set"))?;
638
639        let mut visited = HashSet::new();
640        let mut candidates = BinaryHeap::new();
641        let mut result_set = BinaryHeap::new();
642
643        // Initialize with entry point
644        let entry_dist = self
645            .config
646            .distance_metric
647            .distance(query, &self.data[entry].1.as_f32());
648        candidates.push(Candidate {
649            id: entry,
650            distance: entry_dist,
651        });
652        result_set.push(Candidate {
653            id: entry,
654            distance: entry_dist,
655        });
656        visited.insert(entry);
657
658        while let Some(current) = candidates.pop() {
659            // Check if we've explored enough
660            if result_set.len() >= ef
661                && current.distance
662                    > result_set
663                        .peek()
664                        .expect("result_set should not be empty during search")
665                        .distance
666            {
667                break;
668            }
669
670            // Explore neighbors
671            for &neighbor_id in &self.graph[current.id] {
672                if visited.contains(&neighbor_id) {
673                    continue;
674                }
675
676                visited.insert(neighbor_id);
677
678                let dist = self
679                    .config
680                    .distance_metric
681                    .distance(query, &self.data[neighbor_id].1.as_f32());
682                let candidate = Candidate {
683                    id: neighbor_id,
684                    distance: dist,
685                };
686
687                if result_set.len() < ef
688                    || dist
689                        < result_set
690                            .peek()
691                            .expect("result_set should not be empty during search")
692                            .distance
693                {
694                    candidates.push(candidate.clone());
695                    result_set.push(candidate);
696
697                    if result_set.len() > ef {
698                        result_set.pop();
699                    }
700                }
701            }
702        }
703
704        // Convert to sorted vector
705        let mut results: Vec<_> = result_set.into_sorted_vec();
706        results.truncate(k);
707
708        Ok(results)
709    }
710
711    /// Update index statistics
712    fn update_stats(&self) {
713        let mut stats = self
714            .stats
715            .write()
716            .expect("stats lock should not be poisoned");
717        stats.num_vectors = self.data.len();
718        stats.num_edges = self.count_edges();
719        stats.avg_out_degree = self.avg_out_degree();
720        stats.max_out_degree = self.max_out_degree();
721    }
722
723    /// Count total edges in graph
724    fn count_edges(&self) -> usize {
725        self.graph.iter().map(|neighbors| neighbors.len()).sum()
726    }
727
728    /// Calculate average out-degree
729    fn avg_out_degree(&self) -> f64 {
730        if self.graph.is_empty() {
731            return 0.0;
732        }
733        self.count_edges() as f64 / self.graph.len() as f64
734    }
735
736    /// Get maximum out-degree
737    fn max_out_degree(&self) -> usize {
738        self.graph
739            .iter()
740            .map(|neighbors| neighbors.len())
741            .max()
742            .unwrap_or(0)
743    }
744
745    /// Get index statistics
746    pub fn stats(&self) -> NsgStats {
747        self.stats
748            .read()
749            .expect("stats lock should not be poisoned")
750            .clone()
751    }
752
753    /// Get number of vectors in the index
754    pub fn len(&self) -> usize {
755        self.data.len()
756    }
757
758    /// Check if index is empty
759    pub fn is_empty(&self) -> bool {
760        self.data.is_empty()
761    }
762
763    /// Check if index is built
764    pub fn is_built(&self) -> bool {
765        self.is_built
766    }
767}
768
769impl VectorIndex for NsgIndex {
770    fn insert(&mut self, uri: String, vector: Vector) -> Result<()> {
771        self.add(uri, vector)
772    }
773
774    fn search_knn(&self, query: &Vector, k: usize) -> Result<Vec<(String, f32)>> {
775        let query_vals = query.as_f32();
776        let ef = k.max(self.config.search_length);
777        let candidates = self.greedy_search(&query_vals, k, ef)?;
778
779        // Convert to (URI, similarity) format
780        // Note: NSG uses distance, so we convert to similarity (1 / (1 + distance))
781        // Candidates are sorted by distance (ascending), so we reverse to get descending similarity
782        let mut results: Vec<_> = candidates
783            .into_iter()
784            .map(|c| {
785                let uri = self.data[c.id].0.clone();
786                let similarity = 1.0 / (1.0 + c.distance);
787                (uri, similarity)
788            })
789            .collect();
790
791        // Reverse to get descending order of similarity
792        results.reverse();
793
794        Ok(results)
795    }
796
797    fn search_threshold(&self, query: &Vector, threshold: f32) -> Result<Vec<(String, f32)>> {
798        // Search with a large k and filter by threshold
799        let k = self.data.len().min(1000);
800        let all_results = self.search_knn(query, k)?;
801
802        let filtered: Vec<_> = all_results
803            .into_iter()
804            .filter(|(_, similarity)| *similarity >= threshold)
805            .collect();
806
807        Ok(filtered)
808    }
809
810    fn get_vector(&self, uri: &str) -> Option<&Vector> {
811        self.uri_to_idx
812            .get(uri)
813            .and_then(|&idx| self.data.get(idx))
814            .map(|(_, vec)| vec)
815    }
816
817    fn remove_vector(&mut self, id: String) -> Result<()> {
818        if self.is_built {
819            return Err(anyhow::anyhow!(
820                "Cannot remove vectors from built index. Rebuild index instead."
821            ));
822        }
823
824        if let Some(&idx) = self.uri_to_idx.get(&id) {
825            self.data.remove(idx);
826            self.uri_to_idx.remove(&id);
827
828            // Rebuild index mapping
829            self.uri_to_idx.clear();
830            for (i, (uri, _)) in self.data.iter().enumerate() {
831                self.uri_to_idx.insert(uri.clone(), i);
832            }
833
834            Ok(())
835        } else {
836            Err(anyhow::anyhow!("Vector with id '{}' not found", id))
837        }
838    }
839}
840
841// Helper trait for finding min by
842trait IteratorExt: Iterator {
843    fn position_min_by<F>(self, compare: F) -> Option<usize>
844    where
845        F: FnMut(&Self::Item, &Self::Item) -> Ordering;
846}
847
848impl<I: Iterator> IteratorExt for I {
849    fn position_min_by<F>(mut self, mut compare: F) -> Option<usize>
850    where
851        F: FnMut(&Self::Item, &Self::Item) -> Ordering,
852    {
853        let first = self.next()?;
854        let mut min_item = first;
855        let mut min_pos = 0;
856
857        for (pos, item) in self.enumerate() {
858            if compare(&item, &min_item) == Ordering::Less {
859                min_item = item;
860                min_pos = pos + 1;
861            }
862        }
863
864        Some(min_pos)
865    }
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871
872    #[test]
873    fn test_nsg_creation() -> Result<()> {
874        let config = NsgConfig::default();
875        let index = NsgIndex::new(config)?;
876        assert_eq!(index.len(), 0);
877        assert!(!index.is_built());
878        Ok(())
879    }
880
881    #[test]
882    fn test_nsg_add_vectors() -> Result<()> {
883        let config = NsgConfig::default();
884        let mut index = NsgIndex::new(config)?;
885
886        for i in 0..10 {
887            let vec = Vector::new(vec![i as f32, (i * 2) as f32, (i * 3) as f32]);
888            index.add(format!("vec_{}", i), vec)?;
889        }
890
891        assert_eq!(index.len(), 10);
892        Ok(())
893    }
894
895    #[test]
896    fn test_nsg_build_and_search() -> Result<()> {
897        let config = NsgConfig {
898            out_degree: 32,
899            candidate_pool_size: 100,
900            search_length: 50,
901            initial_knn_degree: 64,
902            ..Default::default()
903        };
904        let mut index = NsgIndex::new(config)?;
905
906        // Add vectors in a more structured way to ensure connectivity
907        for i in 0..100 {
908            let vec = Vector::new(vec![i as f32, (i * 2) as f32, (i * 3) as f32]);
909            index.add(format!("vec_{}", i), vec)?;
910        }
911
912        // Build index
913        index.build()?;
914        assert!(index.is_built());
915
916        // Search with a query close to vec_10 (easier to verify)
917        let query = Vector::new(vec![10.1, 20.1, 30.1]);
918        let results = index.search_knn(&query, 10)?;
919
920        assert!(!results.is_empty());
921        assert_eq!(results.len(), 10);
922
923        // Results should be sorted by similarity (descending)
924        for i in 1..results.len() {
925            assert!(
926                results[i - 1].1 >= results[i].1,
927                "Results not sorted: {}@{} < {}@{}",
928                results[i - 1].1,
929                i - 1,
930                results[i].1,
931                i
932            );
933        }
934
935        // The closest vectors should be vec_10, vec_11, vec_9, etc.
936        // At least one of these should be in top 10
937        let nearby_found = results.iter().take(10).any(|(uri, _)| {
938            uri.contains("10")
939                || uri.contains("11")
940                || uri.contains("9")
941                || uri.contains("12")
942                || uri.contains("8")
943        });
944        assert!(
945            nearby_found,
946            "Expected nearby vectors (8-12) in top 10 results"
947        );
948        Ok(())
949    }
950
951    #[test]
952    fn test_nsg_distance_metrics() -> Result<()> {
953        for metric in [
954            DistanceMetric::Euclidean,
955            DistanceMetric::Manhattan,
956            DistanceMetric::Cosine,
957            DistanceMetric::Angular,
958        ] {
959            let config = NsgConfig {
960                distance_metric: metric,
961                out_degree: 8,
962                ..Default::default()
963            };
964            let mut index = NsgIndex::new(config)?;
965
966            for i in 0..20 {
967                let vec = Vector::new(vec![i as f32, (i * 2) as f32]);
968                index.add(format!("vec_{}", i), vec)?;
969            }
970
971            index.build()?;
972
973            let query = Vector::new(vec![10.0, 20.0]);
974            let results = index.search_knn(&query, 3)?;
975
976            assert!(!results.is_empty());
977        }
978        Ok(())
979    }
980
981    #[test]
982    fn test_nsg_stats() -> Result<()> {
983        let config = NsgConfig::default();
984        let mut index = NsgIndex::new(config)?;
985
986        for i in 0..50 {
987            let vec = Vector::new(vec![i as f32, (i * 2) as f32]);
988            index.add(format!("vec_{}", i), vec)?;
989        }
990
991        index.build()?;
992
993        let stats = index.stats();
994        assert_eq!(stats.num_vectors, 50);
995        assert!(stats.num_edges > 0);
996        assert!(stats.avg_out_degree > 0.0);
997        Ok(())
998    }
999
1000    #[test]
1001    fn test_nsg_threshold_search() -> Result<()> {
1002        let config = NsgConfig::default();
1003        let mut index = NsgIndex::new(config)?;
1004
1005        for i in 0..30 {
1006            let vec = Vector::new(vec![i as f32, (i * 2) as f32]);
1007            index.add(format!("vec_{}", i), vec)?;
1008        }
1009
1010        index.build()?;
1011
1012        let query = Vector::new(vec![15.0, 30.0]);
1013        let results = index.search_threshold(&query, 0.5)?;
1014
1015        assert!(!results.is_empty());
1016        // All results should have similarity >= 0.5
1017        for (_, similarity) in results {
1018            assert!(similarity >= 0.5);
1019        }
1020        Ok(())
1021    }
1022}