1use 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#[derive(Debug, Clone)]
65pub struct NsgConfig {
66 pub out_degree: usize,
68 pub candidate_pool_size: usize,
70 pub search_length: usize,
72 pub distance_metric: DistanceMetric,
74 pub random_seed: Option<u64>,
76 pub parallel_construction: bool,
78 pub num_threads: usize,
80 pub initial_knn_degree: usize,
82 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub enum DistanceMetric {
107 Euclidean,
108 Manhattan,
109 Cosine,
110 Angular,
111 InnerProduct,
112}
113
114impl DistanceMetric {
115 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 -a.iter().zip(b.iter()).map(|(x, y)| x * y).sum::<f32>()
128 }
129 }
130 }
131}
132
133#[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 other
158 .distance
159 .partial_cmp(&self.distance)
160 .unwrap_or(Ordering::Equal)
161 .then_with(|| self.id.cmp(&other.id))
162 }
163}
164
165pub struct NsgIndex {
167 config: NsgConfig,
169 data: Vec<(String, Vector)>,
171 graph: Vec<Vec<usize>>,
173 entry_point: Option<usize>,
175 is_built: bool,
177 uri_to_idx: HashMap<String, usize>,
179 stats: Arc<RwLock<NsgStats>>,
181}
182
183#[derive(Debug, Clone, Default)]
185pub struct NsgStats {
186 pub num_vectors: usize,
188 pub num_edges: usize,
190 pub avg_out_degree: f64,
192 pub max_out_degree: usize,
194 pub num_searches: usize,
196 pub avg_search_path_length: f64,
198 pub total_distance_computations: usize,
200}
201
202impl NsgIndex {
203 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 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 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 tracing::debug!("Stage 1: Building initial kNN graph");
245 self.build_knn_graph()?;
246
247 tracing::debug!("Stage 2: Refining to navigable monotonic graph");
249 self.refine_to_nsg()?;
250
251 self.select_entry_point()?;
253
254 self.is_built = true;
255
256 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 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 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 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 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 self.graph[i] = neighbors.iter().map(|c| c.id).collect();
314 }
315
316 Ok(())
317 }
318
319 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 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 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 for handle in handles {
375 handle
376 .join()
377 .map_err(|_| anyhow::anyhow!("Thread panicked"))?;
378 }
379
380 self.graph = Arc::try_unwrap(graph)
382 .map_err(|_| anyhow::anyhow!("Failed to unwrap graph"))?
383 .into_inner();
384
385 Ok(())
386 }
387
388 fn refine_to_nsg(&mut self) -> Result<()> {
390 let n = self.data.len();
391 let mut new_graph = vec![Vec::new(); n];
392
393 let temp_entry = self.select_temp_entry_point();
395
396 #[allow(clippy::needless_range_loop)]
397 for i in 0..n {
398 let candidates = self.search_for_neighbors(i, temp_entry)?;
400
401 let neighbors = self.prune_neighbors(i, candidates)?;
403
404 new_graph[i] = neighbors;
405 }
406
407 self.ensure_connectivity(&mut new_graph)?;
409
410 self.graph = new_graph;
411
412 Ok(())
413 }
414
415 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 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 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 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 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 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 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 fn ensure_connectivity(&self, graph: &mut [Vec<usize>]) -> Result<()> {
511 let n = graph.len();
512
513 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 (i, edges) in in_edges.iter().enumerate() {
523 if edges.is_empty() && i != 0 {
524 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 if !graph[closest].contains(&i) {
542 graph[closest].push(i);
543 }
544 }
545 }
546
547 Ok(())
548 }
549
550 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 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 self.find_centroid()
579 }
580 }
581
582 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 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 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(¢roid, &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 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 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 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 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 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 let mut results: Vec<_> = result_set.into_sorted_vec();
706 results.truncate(k);
707
708 Ok(results)
709 }
710
711 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 fn count_edges(&self) -> usize {
725 self.graph.iter().map(|neighbors| neighbors.len()).sum()
726 }
727
728 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 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 pub fn stats(&self) -> NsgStats {
747 self.stats
748 .read()
749 .expect("stats lock should not be poisoned")
750 .clone()
751 }
752
753 pub fn len(&self) -> usize {
755 self.data.len()
756 }
757
758 pub fn is_empty(&self) -> bool {
760 self.data.is_empty()
761 }
762
763 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 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 results.reverse();
793
794 Ok(results)
795 }
796
797 fn search_threshold(&self, query: &Vector, threshold: f32) -> Result<Vec<(String, f32)>> {
798 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 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
841trait 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 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 index.build()?;
914 assert!(index.is_built());
915
916 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 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 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 for (_, similarity) in results {
1018 assert!(similarity >= 0.5);
1019 }
1020 Ok(())
1021 }
1022}