Skip to main content

flat_knn/
lib.rs

1#![feature(binary_heap_into_iter_sorted)]
2use half::f16;
3use ordered_float::OrderedFloat;
4use rayon::prelude::*;
5use simsimd::{f16 as simd_f16, SpatialSimilarity};
6use std::{cmp::Reverse, collections::BinaryHeap};
7
8/// A trait defining a specific distance metric for comparing vectors.
9///
10/// It encapsulates the internal logic for computing the distance between a query and a data chunk.
11/// It defines the `HeapItem` used for heap ordering, ensuring that `knn` correctly maintains
12/// the most appropriate neighbors irrespective of whether the metric requires a min-heap or max-heap structure.
13pub trait DistanceMetric<T>: Send + Sync {
14    type HeapItem: Ord + Send + Copy;
15    fn build_item(query: &[T], chunk: &[T], i: usize) -> Self::HeapItem;
16    fn extract_item(item: Self::HeapItem) -> (f32, usize);
17}
18
19/// Represents the L2 (Euclidean) squared distance metric.
20///
21/// For L2 squared, smaller distances mean higher similarity.
22pub struct L2;
23impl<T: VectorType> DistanceMetric<T> for L2 {
24    type HeapItem = (OrderedFloat<f32>, usize);
25
26    #[inline(always)]
27    fn build_item(query: &[T], chunk: &[T], i: usize) -> Self::HeapItem {
28        (OrderedFloat(T::l2_squared(query, chunk)), i)
29    }
30
31    #[inline(always)]
32    fn extract_item(item: Self::HeapItem) -> (f32, usize) {
33        (item.0 .0, item.1)
34    }
35}
36
37/// Represents the Dot Product similarity metric.
38///
39/// For Dot product, larger values mean higher similarity. A `Reverse` wrapper is used
40/// for the `HeapItem` to naturally maintain the correct maximum items within a minimum-backed `BinaryHeap`.
41pub struct Dot;
42impl<T: VectorType> DistanceMetric<T> for Dot {
43    type HeapItem = Reverse<(OrderedFloat<f32>, usize)>;
44
45    #[inline(always)]
46    fn build_item(query: &[T], chunk: &[T], i: usize) -> Self::HeapItem {
47        Reverse((OrderedFloat(T::dot_product(query, chunk)), i))
48    }
49
50    #[inline(always)]
51    fn extract_item(item: Self::HeapItem) -> (f32, usize) {
52        (item.0 .0 .0, item.0 .1)
53    }
54}
55
56/// A trait representing a dataset that can be accessed into vector chunks.
57///
58/// This abstracts over the actual memory layout of the dataset, allowing the `knn` function
59/// to seamlessly map over a sliced array `(&[T], dim)`, vectors of vectors `Vec<Vec<T>>`,
60/// or arrays of references `Vec<&[T]>`.
61pub trait Indexable<T>: Send + Sync {
62    fn get(&self, i: usize) -> &[T];
63    fn len(&self) -> usize;
64    fn is_empty(&self) -> bool {
65        self.len() == 0
66    }
67}
68
69impl<T, C> Indexable<T> for (C, usize)
70where
71    T: Send + Sync,
72    C: AsRef<[T]> + Send + Sync,
73{
74    #[inline(always)]
75    fn get(&self, i: usize) -> &[T] {
76        &self.0.as_ref()[i * self.1..(i + 1) * self.1]
77    }
78
79    #[inline(always)]
80    fn len(&self) -> usize {
81        self.0.as_ref().len() / self.1
82    }
83}
84
85impl<T, U> Indexable<T> for &[U]
86where
87    T: Send + Sync,
88    U: AsRef<[T]> + Send + Sync,
89{
90    #[inline(always)]
91    fn get(&self, i: usize) -> &[T] {
92        self[i].as_ref()
93    }
94
95    #[inline(always)]
96    fn len(&self) -> usize {
97        <[U]>::len(self)
98    }
99}
100
101// Blanket implementation for Vec representations, like Vec<Vec<T>> or Vec<&[T]>
102impl<T, U> Indexable<T> for Vec<U>
103where
104    T: Send + Sync,
105    U: AsRef<[T]> + Send + Sync,
106{
107    #[inline(always)]
108    fn get(&self, i: usize) -> &[T] {
109        self[i].as_ref()
110    }
111
112    #[inline(always)]
113    fn len(&self) -> usize {
114        self.len()
115    }
116}
117
118// Often passed as reference to Vec
119impl<T, U> Indexable<T> for &Vec<U>
120where
121    T: Send + Sync,
122    U: AsRef<[T]> + Send + Sync,
123{
124    #[inline(always)]
125    fn get(&self, i: usize) -> &[T] {
126        self[i].as_ref()
127    }
128
129    #[inline(always)]
130    fn len(&self) -> usize {
131        Vec::len(self)
132    }
133}
134
135/// Performs a K-Nearest Neighbors (KNN) search in parallel.
136///
137/// This function finds the `k` closest vectors in the `data` to the given `query` vector,
138/// using the specified distance metric `M`. It utilizes data parallelization to efficiently
139/// compare entries and keeps track of the nearest neighbors.
140///
141/// # Arguments
142///
143/// * `data` - The dataset to search against. Must implement `Indexable<T>`. Common usage
144///   includes passing a tuple of `(&[T], dim)` for flattened arrays, or passing nested vectors.
145/// * `query` - The target query vector.
146/// * `k` - The maximum number of nearest neighbors to retrieve.
147///
148/// # Returns
149///
150/// A `Vec<(f32, usize)>` sorted from the closest to the furthest distance.
151/// Each element contains the calculated distance and the index of the neighbor.
152///
153/// # Example
154///
155/// ```
156/// use flat_knn::{knn, L2};
157///
158/// let data = vec![
159///     1.0, 2.0, 3.0, 4.0, // index 0 (dist = 1)
160///     8.0, 7.0, 6.0, 5.0, // index 1 (dist = 84)
161///     1.0, 2.0, 3.0, 9.0, // index 2 (dist = 16)
162/// ];
163/// let dim = 4;
164/// let query = [1.0, 2.0, 3.0, 5.0];
165///
166/// // Find 2 nearest neighbors using L2 distance
167/// let neighbors = knn::<_, L2>((&data, dim), &query, 2);
168///
169/// assert_eq!(neighbors.len(), 2);
170/// assert_eq!(neighbors[0], (1.0, 0));
171/// assert_eq!(neighbors[1], (16.0, 2));
172/// ```
173pub fn knn<T: VectorType, M: DistanceMetric<T>>(
174    data: impl Indexable<T>,
175    query: &[T],
176    k: usize,
177) -> Vec<(f32, usize)> {
178    let heap = (0..data.len())
179        .into_par_iter()
180        .map(|i| M::build_item(query, data.get(i), i))
181        .fold(
182            || BinaryHeap::with_capacity(k + 1),
183            |mut local_heap, item| {
184                if local_heap.len() < k {
185                    local_heap.push(item);
186                } else if let Some(mut top) = local_heap.peek_mut() {
187                    if item < *top {
188                        *top = item;
189                    }
190                }
191                local_heap
192            },
193        )
194        .reduce(
195            || BinaryHeap::with_capacity(k + 1),
196            |mut heap1, heap2| {
197                for item in heap2.into_iter() {
198                    if heap1.len() < k {
199                        heap1.push(item);
200                    } else if let Some(mut top) = heap1.peek_mut() {
201                        if item < *top {
202                            *top = item;
203                        }
204                    }
205                }
206                heap1
207            },
208        );
209
210    let res: Vec<_> = heap.into_iter_sorted().map(M::extract_item).collect();
211    res.into_iter().rev().collect()
212}
213
214pub trait VectorType: Send + Sync + Sized {
215    fn l2_squared(query: &[Self], chunk: &[Self]) -> f32;
216    fn dot_product(query: &[Self], chunk: &[Self]) -> f32;
217}
218
219impl VectorType for f32 {
220    #[inline(always)]
221    fn l2_squared(query: &[f32], chunk: &[f32]) -> f32 {
222        f32::l2sq(query, chunk).unwrap() as f32
223    }
224
225    #[inline(always)]
226    fn dot_product(query: &[f32], chunk: &[f32]) -> f32 {
227        f32::dot(query, chunk).unwrap() as f32
228    }
229}
230
231#[inline(always)]
232fn as_simsimd_slice(slice: &[f16]) -> &[simd_f16] {
233    // SAFETY: Both types are exactly 16 bits (u16) under the hood
234    // and represent the identical IEEE 754 half-precision format.
235    unsafe { std::slice::from_raw_parts(slice.as_ptr() as *const simd_f16, slice.len()) }
236}
237
238impl VectorType for f16 {
239    #[inline(always)]
240    fn l2_squared(query: &[f16], chunk: &[f16]) -> f32 {
241        simd_f16::l2sq(as_simsimd_slice(query), as_simsimd_slice(chunk)).unwrap() as f32
242    }
243
244    #[inline(always)]
245    fn dot_product(query: &[f16], chunk: &[f16]) -> f32 {
246        simd_f16::dot(as_simsimd_slice(query), as_simsimd_slice(chunk)).unwrap() as f32
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use linfa_nn::{distance::L2Dist, LinearSearch, NearestNeighbour};
253    use ndarray::Array2;
254    use rand::{distr::Uniform, Rng};
255
256    use super::*;
257
258    const DIM: usize = 768;
259
260    fn generate_random_data(num_vectors: usize) -> Vec<f32> {
261        let mut rng = rand::rng();
262        let uniform = Uniform::new(0.0, 10.0).unwrap();
263        let mut data = Vec::with_capacity(num_vectors * DIM);
264
265        for _ in 0..(num_vectors * DIM) {
266            data.push(rng.sample(uniform));
267        }
268        data
269    }
270
271    #[test]
272    fn test_knn_search_l2() {
273        let data = vec![
274            1.0, 2.0, 3.0, 4.0, // 0: dist = 1
275            8.0, 7.0, 6.0, 5.0, // 1: dist = 84
276            1.0, 2.0, 3.0, 9.0, // 2: dist = 16
277            4.0, 3.0, 2.0, 1.0, // 3: dist = 27
278        ];
279        let dim: usize = 4;
280        let query = [1.0, 2.0, 3.0, 5.0];
281        let k = 4;
282        let neighbors = knn::<_, L2>((&data, dim), &query, k);
283
284        assert_eq!(neighbors.len(), 4);
285        assert_eq!(neighbors[0], (1.0, 0));
286        assert_eq!(neighbors[1], (16.0, 2));
287    }
288
289    #[test]
290    fn test_knn_search_dot() {
291        let data = vec![1.0, -0.1, 0.3, 1.0, -1.0, 0.0, 0.0, -1.0];
292        let dim = 2;
293        let query = [0.9, 0.1];
294        let k = 4;
295        let neighbors = knn::<_, Dot>((&data, dim), &query, k);
296
297        assert_eq!(neighbors.len(), 4);
298        assert_eq!(neighbors[0], (0.89, 0));
299        assert_eq!(neighbors[1], (0.37, 1));
300        assert_eq!(neighbors[2], (-0.1, 3));
301        assert_eq!(neighbors[3], (-0.9, 2));
302    }
303
304    #[test]
305    fn test_compare_with_linfa_l2() {
306        let num_vectors = 10_000;
307        let data = generate_random_data(num_vectors);
308        let dataset = Array2::from_shape_vec((num_vectors, DIM), data.clone())
309            .expect("Failed to reshape data.");
310        let query = generate_random_data(1);
311
312        let index = LinearSearch::new()
313            .from_batch(&dataset, L2Dist {})
314            .expect("Failed to build LinearSearch index");
315
316        let gt = index
317            .k_nearest((&query).into(), 30)
318            .unwrap()
319            .iter()
320            .map(|(_, i)| *i)
321            .collect::<Vec<_>>();
322        let pred = knn::<_, L2>((&data, DIM), &query, 30)
323            .iter()
324            .map(|(_, i)| *i)
325            .collect::<Vec<_>>();
326        assert_eq!(gt, pred);
327    }
328
329    #[test]
330    fn test_knn_f16_l2() {
331        let data = vec![
332            f16::from_f32(1.0),
333            f16::from_f32(2.0),
334            f16::from_f32(3.0),
335            f16::from_f32(4.0),
336            f16::from_f32(8.0),
337            f16::from_f32(7.0),
338            f16::from_f32(6.0),
339            f16::from_f32(5.0),
340            f16::from_f32(1.0),
341            f16::from_f32(2.0),
342            f16::from_f32(3.0),
343            f16::from_f32(9.0),
344        ];
345        let dim = 4;
346        let query = [
347            f16::from_f32(1.0),
348            f16::from_f32(2.0),
349            f16::from_f32(3.0),
350            f16::from_f32(5.0),
351        ];
352        let k = 2;
353        let neighbors = knn::<_, L2>((&data, dim), &query, k);
354
355        assert_eq!(neighbors.len(), 2);
356        assert_eq!(neighbors[0].1, 0); // closest is the first one
357        assert_eq!(neighbors[1].1, 2); // second is the third one
358    }
359
360    #[test]
361    fn test_knn_f16_dot() {
362        let data = vec![
363            f16::from_f32(1.0),
364            f16::from_f32(-0.1),
365            f16::from_f32(0.3),
366            f16::from_f32(1.0),
367            f16::from_f32(-1.0),
368            f16::from_f32(0.0),
369            f16::from_f32(0.0),
370            f16::from_f32(-1.0),
371        ];
372        let dim = 2;
373        let query = [f16::from_f32(0.9), f16::from_f32(0.1)];
374        let k = 4;
375        let neighbors = knn::<_, Dot>((&data, dim), &query, k);
376
377        assert_eq!(neighbors.len(), 4);
378        assert_eq!(neighbors[0].1, 0); // 0.89
379        assert_eq!(neighbors[1].1, 1); // 0.37
380        assert_eq!(neighbors[2].1, 3); // -0.1
381        assert_eq!(neighbors[3].1, 2); // -0.9
382    }
383}