1use crate::config::{DistanceMetric, KnnMethod};
8use crate::error::PaCMAPError;
9use rayon::prelude::*;
10
11pub struct NeighborList {
13 pub indices: Vec<u32>,
15 pub distances: Vec<f32>,
17}
18
19pub fn compute_knn(
25 data: &[f32],
26 n: usize,
27 d: usize,
28 k: usize,
29 method: &KnnMethod,
30 metric: DistanceMetric,
31) -> Result<Vec<NeighborList>, PaCMAPError> {
32 match method {
33 #[cfg(feature = "hnsw")]
34 KnnMethod::Hnsw(params) => hnsw_knn(data, n, d, k, params, metric),
35 KnnMethod::Exact => exact_knn(data, n, d, k, metric),
36 #[cfg(feature = "kdtree")]
37 KnnMethod::KdTree => kdtree_knn(data, n, d, k, metric),
38 KnnMethod::Annoy => Err(PaCMAPError::MethodNotImplemented {
39 method: "Annoy".to_string(),
40 }),
41 #[allow(unreachable_patterns)]
44 _ => Err(PaCMAPError::MethodNotImplemented {
45 method: "unknown (feature disabled)".to_string(),
46 }),
47 }
48}
49
50#[inline(always)]
53fn l2_sq(a: &[f32], b: &[f32]) -> f32 {
54 a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
55}
56
57#[inline(always)]
58fn dist(a: &[f32], b: &[f32], metric: DistanceMetric) -> f32 {
59 match metric {
60 DistanceMetric::Euclidean => l2_sq(a, b).sqrt(),
61 DistanceMetric::EuclideanSq => l2_sq(a, b),
62 DistanceMetric::Cosine => {
63 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
64 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
65 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
66 1.0 - dot / (na * nb + f32::EPSILON)
67 }
68 DistanceMetric::Manhattan => a.iter().zip(b).map(|(x, y)| (x - y).abs()).sum(),
69 }
70}
71
72pub fn exact_knn(
75 data: &[f32],
76 n: usize,
77 d: usize,
78 k: usize,
79 metric: DistanceMetric,
80) -> Result<Vec<NeighborList>, PaCMAPError> {
81 let k_capped = k.min(n - 1);
82 let result: Vec<NeighborList> = (0..n)
83 .into_par_iter()
84 .map(|i| {
85 let row_i = &data[i * d..(i + 1) * d];
86 let mut heap: Vec<(f32, u32)> = Vec::with_capacity(k_capped + 1);
89
90 for j in 0..n {
91 if j == i {
92 continue;
93 }
94 let row_j = &data[j * d..(j + 1) * d];
95 let d_ij = dist(row_i, row_j, metric);
96
97 if heap.len() < k_capped {
98 heap.push((d_ij, j as u32));
99 heap.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
101 } else if d_ij < heap[0].0 {
102 heap[0] = (d_ij, j as u32);
103 heap.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
104 }
105 }
106
107 heap.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
109 NeighborList {
110 indices: heap.iter().map(|(_, idx)| *idx).collect(),
111 distances: heap.iter().map(|(d, _)| *d).collect(),
112 }
113 })
114 .collect();
115
116 Ok(result)
117}
118
119#[cfg(feature = "hnsw")]
122fn hnsw_knn(
123 data: &[f32],
124 n: usize,
125 d: usize,
126 k: usize,
127 params: &crate::config::HnswParams,
128 metric: DistanceMetric,
129) -> Result<Vec<NeighborList>, PaCMAPError> {
130 use usearch::{Index, IndexOptions, MetricKind, ScalarKind};
131
132 if metric == DistanceMetric::Manhattan {
134 return exact_knn(data, n, d, k, metric);
135 }
136
137 let metric_kind = match metric {
138 DistanceMetric::Euclidean | DistanceMetric::EuclideanSq => MetricKind::L2sq,
139 DistanceMetric::Cosine => MetricKind::Cos,
140 DistanceMetric::Manhattan => unreachable!(),
141 };
142
143 let scalar_kind = match params.quantization {
144 crate::config::Quantization::F32 => ScalarKind::F32,
145 crate::config::Quantization::F16 => ScalarKind::F16,
146 crate::config::Quantization::I8 => ScalarKind::I8,
147 };
148
149 let options = IndexOptions {
150 dimensions: d,
151 metric: metric_kind,
152 quantization: scalar_kind,
153 connectivity: params.m,
154 expansion_add: params.ef_construction,
155 expansion_search: params.ef_search,
156 ..Default::default()
157 };
158
159 let index = Index::new(&options)
160 .map_err(|e| PaCMAPError::KnnIndex(e.to_string()))?;
161 index.reserve(n)
162 .map_err(|e| PaCMAPError::KnnIndex(e.to_string()))?;
163
164 (0..n).into_par_iter().try_for_each(|i| {
166 let row = &data[i * d..(i + 1) * d];
167 index.add(i as u64, row)
168 .map_err(|e| PaCMAPError::KnnIndex(e.to_string()))
169 })?;
170
171 let k_fetch = k + 1; let result: Vec<NeighborList> = (0..n)
174 .into_par_iter()
175 .map(|i| {
176 let row = &data[i * d..(i + 1) * d];
177 let matches = index.search(row, k_fetch).unwrap_or_else(|_| {
178 usearch::ffi::Matches {
179 keys: vec![],
180 distances: vec![],
181 }
182 });
183
184 let mut indices = Vec::with_capacity(k);
185 let mut distances = Vec::with_capacity(k);
186 for (&key, &dist) in matches.keys.iter().zip(matches.distances.iter()) {
187 if key == i as u64 {
188 continue;
189 }
190 if indices.len() >= k {
191 break;
192 }
193 indices.push(key as u32);
194 distances.push(dist);
195 }
196 NeighborList { indices, distances }
197 })
198 .collect();
199
200 drop(index);
202
203 Ok(result)
204}
205
206#[cfg(feature = "kdtree")]
209fn kdtree_knn(
210 data: &[f32],
211 n: usize,
212 d: usize,
213 k: usize,
214 _metric: DistanceMetric,
215) -> Result<Vec<NeighborList>, PaCMAPError> {
216 exact_knn(data, n, d, k, _metric)
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn make_grid(n: usize) -> Vec<f32> {
228 (0..n).flat_map(|i| [i as f32, 0.0]).collect()
229 }
230
231 #[test]
232 fn exact_knn_nearest_neighbour() {
233 let data = make_grid(10);
234 let knn = exact_knn(&data, 10, 2, 2, DistanceMetric::Euclidean).unwrap();
235 let nbrs = &knn[5].indices;
237 assert!(nbrs.contains(&4u32) || nbrs.contains(&6u32));
238 }
239}