use ann_search_rs::utils::KnnValidation;
use ann_search_rs::utils::dist::{Dist, SimdDistance, parse_ann_dist};
use ann_search_rs::*;
use faer::{MatRef, RowRef};
use rayon::prelude::*;
use rustc_hash::FxHashSet;
use std::time::Instant;
use crate::core::math::sparse::coo_to_csr;
use crate::prelude::*;
pub type ScKnnOptionVersion = Result<(Vec<Vec<usize>>, Option<Vec<Vec<f32>>>), BixverseErrors>;
pub type ScKnnResults = Result<(Vec<Vec<usize>>, Vec<Vec<f32>>), BixverseErrors>;
#[derive(Default)]
pub enum KnnSearch {
#[default]
KmKnn,
Hnsw,
Annoy,
NNDescent,
Ivf,
Exhaustive,
}
pub fn parse_knn_method(s: &str) -> Option<KnnSearch> {
match s.to_lowercase().as_str() {
"kmknn" => Some(KnnSearch::KmKnn),
"annoy" => Some(KnnSearch::Annoy),
"hnsw" => Some(KnnSearch::Hnsw),
"nndescent" => Some(KnnSearch::NNDescent),
"exhaustive" => Some(KnnSearch::Exhaustive),
"ivf" => Some(KnnSearch::Ivf),
_ => None,
}
}
#[derive(Clone, Debug)]
pub struct KnnParams {
pub knn_method: String,
pub ann_dist: String,
pub k: usize,
pub n_tree: usize,
pub search_budget: Option<usize>,
pub diversify_prob: f32,
pub delta: f32,
pub ef_budget: Option<usize>,
pub m: usize,
pub ef_construction: usize,
pub ef_search: usize,
pub n_list: Option<usize>,
pub n_probe: Option<usize>,
}
impl KnnParams {
pub fn new() -> Self {
Self {
knn_method: "kmknn".to_string(),
ann_dist: "euclidean".to_string(),
k: 15,
n_tree: 50,
search_budget: None,
diversify_prob: 0.0,
delta: 0.001,
ef_budget: None,
m: 16,
ef_construction: 200,
ef_search: 100,
n_list: None,
n_probe: None,
}
}
}
impl Default for KnnParams {
fn default() -> Self {
Self::new()
}
}
pub fn build_nn_map(knn_graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
(0..knn_graph.len())
.map(|i| {
let mut neighbors = knn_graph[i].clone();
neighbors.push(i);
neighbors
})
.collect()
}
#[inline(always)]
pub fn compute_distance_knn(a: RowRef<f32>, b: RowRef<f32>, metric: &Dist) -> f32 {
let ncols = a.ncols();
if a.col_stride() == 1 && b.col_stride() == 1 {
let a_slice = unsafe { std::slice::from_raw_parts(a.as_ptr(), ncols) };
let b_slice = unsafe { std::slice::from_raw_parts(b.as_ptr(), ncols) };
match metric {
Dist::SquaredEuclidean => f32::euclidean_simd(a_slice, b_slice).sqrt(),
Dist::Cosine => {
let dot = f32::dot_simd(a_slice, b_slice);
let norm_a = f32::calculate_l2_norm(a_slice);
let norm_b = f32::calculate_l2_norm(b_slice);
1.0 - (dot / (norm_a * norm_b))
}
Dist::Manhattan => unreachable!(),
}
} else {
match metric {
Dist::SquaredEuclidean => {
let mut sum = 0.0f32;
for i in 0..ncols {
let diff = a[i] - b[i];
sum += diff * diff;
}
sum.sqrt()
}
Dist::Cosine => {
let mut dot = 0.0f32;
let mut norm_a = 0.0f32;
let mut norm_b = 0.0f32;
for i in 0..ncols {
dot += a[i] * b[i];
norm_a += a[i] * a[i];
norm_b += b[i] * b[i];
}
1.0 - (dot / (norm_a.sqrt() * norm_b.sqrt()))
}
Dist::Manhattan => unreachable!(),
}
}
}
pub fn knn_to_sparse_dist(
knn_indices: &[Vec<usize>],
knn_dists: &[Vec<f32>],
n_obs: usize,
) -> CompressedSparseData2<f32> {
let mut rows = Vec::new();
let mut cols = Vec::new();
let mut vals = Vec::new();
for i in 0..knn_indices.len() {
for j in 0..knn_indices[i].len() {
let neighbor = knn_indices[i][j];
let dist = if neighbor == i { 0.0 } else { knn_dists[i][j] };
if dist != 0.0 {
rows.push(i);
cols.push(neighbor);
vals.push(dist);
}
}
}
coo_to_csr(
&rows.index_cast(),
&cols.index_cast(),
&vals,
(n_obs, n_obs),
)
}
fn build_and_query_knn<I, E>(
no_neighbours: usize,
verbose: bool,
build_index: impl FnOnce() -> Result<I, E>,
query_index: impl FnOnce(&I) -> Result<(Vec<Vec<usize>>, Option<Vec<Vec<f32>>>), E>,
index_name: &str,
) -> Result<(Vec<Vec<usize>>, I), E> {
let start = Instant::now();
let index = build_index()?;
if verbose {
println!("Generated {} index: {:.2?}", index_name, start.elapsed());
}
let start = Instant::now();
let (indices, _) = query_index(&index)?;
let res: Vec<Vec<usize>> = indices
.into_iter()
.enumerate()
.map(|(i, mut neighbors)| {
neighbors.retain(|&x| x != i);
neighbors.truncate(no_neighbours);
neighbors
})
.collect();
if verbose {
println!(
"Identified approximate nearest neighbours via {}: {:.2?}",
index_name,
start.elapsed()
);
}
Ok((res, index))
}
#[allow(clippy::too_many_arguments)]
pub fn generate_knn_hnsw(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
m: usize,
ef_const: usize,
ef_search: usize,
seed: usize,
validate_index: bool,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
if ef_search / no_neighbours <= 2 {
println!(
"[!WARNING!] Your 'ef_search' is set to {} for k {}. 'ef_search' should be 2 to 4x 'k'!",
ef_search, no_neighbours
)
}
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| {
Ok(build_hnsw_index(
mat,
m,
ef_const,
dist_metric,
seed,
verbose,
))
},
|idx| query_hnsw_self(idx, no_neighbours + 1, ef_search, false, true),
"HNSW",
)?;
if validate_index && verbose {
let recall = index.validate_index(no_neighbours, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
Ok(res)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_knn_annoy(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
n_trees: usize,
search_budget: Option<usize>,
seed: usize,
validate_index: bool,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| build_annoy_index(mat, dist_metric, n_trees, seed),
|idx| query_annoy_self(idx, no_neighbours + 1, search_budget, false, verbose),
"Annoy",
)?;
if validate_index && verbose {
let recall = index.validate_index(no_neighbours, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
Ok(res)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_knn_ivf(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
n_list: Option<usize>,
n_probe: Option<usize>,
seed: usize,
validate_index: bool,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| build_ivf_index(mat, n_list, None, dist_metric, seed, verbose),
|idx| query_ivf_self(idx, no_neighbours + 1, n_probe, false, verbose),
"IVF",
)?;
if validate_index && verbose {
let recall = index.validate_index(no_neighbours, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
Ok(res)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_knn_nndescent(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
diversify_prob: f32,
ef_budget: Option<usize>,
delta: f32,
seed: usize,
validate_index: bool,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
if ef_budget.is_none() && no_neighbours > 150 {
println!(
"[WARNING!] Your 'ef_budget' is set to auto ((k * 2).clamp(50, 200)) for k {}. 'ef_search' should be 2 to 4x 'k'",
no_neighbours
)
}
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| {
build_nndescent_index(
mat,
dist_metric,
delta,
diversify_prob,
None,
None,
None,
None,
seed,
verbose,
)
},
|idx| query_nndescent_self(idx, no_neighbours + 1, ef_budget, false, verbose),
"NNDescent",
)?;
if validate_index && verbose {
let recall = index.validate_index(no_neighbours, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
Ok(res)
}
pub fn generate_knn_exhaustive(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
let (res, _) = build_and_query_knn(
no_neighbours,
verbose,
|| Ok(build_exhaustive_index(mat, dist_metric)),
|idx| query_exhaustive_self(idx, no_neighbours + 1, false, verbose),
"exhaustive linear search",
)?;
Ok(res)
}
#[allow(clippy::too_many_arguments)]
pub fn generate_knn_kmknn(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
n_list: Option<usize>,
seed: usize,
verbose: bool,
) -> Result<Vec<Vec<usize>>, BixverseErrors> {
let (res, _) = build_and_query_knn(
no_neighbours,
verbose,
|| build_kmknn_index(mat, dist_metric, n_list, None, seed, verbose),
|idx| query_kmknn_self(idx, no_neighbours + 1, false, verbose),
"KmKnn",
)?;
Ok(res)
}
pub fn generate_knn_with_dist(
embd: MatRef<f32>,
knn_params: &KnnParams,
return_dist: bool,
validate_index: bool,
seed: usize,
verbose: bool,
) -> ScKnnOptionVersion {
fn remove_self(
mut indices: Vec<Vec<usize>>,
distances: Option<Vec<Vec<f32>>>,
) -> (Vec<Vec<usize>>, Option<Vec<Vec<f32>>>) {
for idx_vec in indices.iter_mut() {
idx_vec.remove(0);
}
let distances = distances.map(|mut dists| {
for dist_vec in dists.iter_mut() {
dist_vec.remove(0);
}
dists
});
(indices, distances)
}
fn timed<T>(name: &str, verbose: bool, f: impl FnOnce() -> T) -> T {
let start = Instant::now();
let result = f();
if verbose {
println!("{}: {:.2?}", name, start.elapsed());
}
result
}
let knn_method = parse_knn_method(&knn_params.knn_method).unwrap_or_default();
let k_plus_one = knn_params.k + 1;
let (indices, distances) = match knn_method {
KnnSearch::Annoy => {
let index = timed("Generated Annoy index", verbose, || {
build_annoy_index(embd, &knn_params.ann_dist, knn_params.n_tree, seed)
})?;
let (indices, distances) = timed("Queried Annoy index", verbose, || {
query_annoy_self(
&index,
k_plus_one,
knn_params.search_budget,
return_dist,
verbose,
)
})?;
if validate_index && verbose {
let recall = index.validate_index(k_plus_one, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
(indices, distances)
}
KnnSearch::Hnsw => {
let index = timed("Generated HNSW index", verbose, || {
build_hnsw_index(
embd,
knn_params.m,
knn_params.ef_construction,
&knn_params.ann_dist,
seed,
verbose,
)
});
if knn_params.ef_search / k_plus_one <= 2 {
println!(
"[WARNING!] Your 'ef_search' is set to {} for k {}. 'ef_search' should be 2 to 4x 'k'!",
knn_params.ef_search, k_plus_one
)
}
let (indices, distances) = timed("Queried HNSW index", verbose, || {
query_hnsw_self(
&index,
k_plus_one,
knn_params.ef_search,
return_dist,
verbose,
)
})?;
if validate_index && verbose {
let recall = index.validate_index(k_plus_one, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
(indices, distances)
}
KnnSearch::NNDescent => {
let index = timed("Generated NNDescent index", verbose, || {
build_nndescent_index(
embd,
&knn_params.ann_dist,
knn_params.delta,
knn_params.diversify_prob,
None,
None,
None,
None,
seed,
verbose,
)
})?;
if knn_params.ef_budget.is_none() && k_plus_one > 150 {
println!(
"[WARNING!] Your 'ef_budget' is set to auto ((k * 2).clamp(50, 200)) for k {}. 'ef_search' should be 2 to 4x 'k'",
k_plus_one
)
}
let (indices, distances) = timed("Queried NNDescent index", verbose, || {
query_nndescent_self(&index, k_plus_one, knn_params.ef_budget, true, verbose)
})?;
if validate_index && verbose {
let recall = index.validate_index(k_plus_one, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
(indices, distances)
}
KnnSearch::Exhaustive => {
let index = timed("Generated Exhaustive index", verbose, || {
build_exhaustive_index(embd, &knn_params.ann_dist)
});
timed("Queried Exhaustive index", verbose, || {
query_exhaustive_self(&index, k_plus_one, true, verbose)
})?
}
KnnSearch::KmKnn => {
let index = timed("Generated KmKnn index", verbose, || {
build_kmknn_index(
embd,
&knn_params.ann_dist,
knn_params.n_list,
None,
seed,
verbose,
)
})?;
timed("Queried KmKnn index", verbose, || {
query_kmknn_self(&index, k_plus_one, true, verbose)
})?
}
KnnSearch::Ivf => {
let index = timed("Generated IVF index", verbose, || {
build_ivf_index(
embd,
knn_params.n_list,
None,
&knn_params.ann_dist,
seed,
verbose,
)
})?;
let (indices, distances) = timed("Queried IVF index", verbose, || {
query_ivf_self(&index, k_plus_one, knn_params.n_probe, true, verbose)
})?;
if validate_index && verbose {
let recall = index.validate_index(k_plus_one, seed, None)?;
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
(indices, distances)
}
};
Ok(remove_self(indices, distances))
}
pub fn compare_knn_graphs(a: MatRef<i32>, b: MatRef<i32>) -> Vec<i32> {
assert_eq!(a.nrows(), b.nrows());
(0..a.nrows())
.into_par_iter()
.map(|row| {
let set: FxHashSet<i32> = (0..a.ncols()).map(|j| a[(row, j)]).collect();
(0..b.ncols())
.filter(|&j| set.contains(&b[(row, j)]))
.count() as i32
})
.collect()
}
pub fn knn_distance_weights(
distances: &[Vec<f32>],
neighborhood_factor: f32,
squared: bool,
) -> Vec<Vec<f32>> {
distances
.par_iter()
.map(|row| {
if row.is_empty() {
return Vec::new();
}
let radius =
((row.len() as f32 / neighborhood_factor).ceil() as usize).clamp(1, row.len());
let sigma_sq = if squared {
row[radius - 1]
} else {
row[radius - 1] * row[radius - 1]
};
let sigma_sq = if sigma_sq == 0.0 { 1.0 } else { sigma_sq };
let mut weights: Vec<f32> = row
.iter()
.map(|&d| {
let d_sq = if squared { d } else { d * d };
(-d_sq / sigma_sq).exp()
})
.collect();
let total: f32 = weights.iter().sum();
if total != 0.0 {
for w in weights.iter_mut() {
*w /= total;
}
}
weights
})
.collect()
}
pub fn distances_are_squared(ann_dist: &str) -> bool {
matches!(parse_ann_dist(ann_dist), Some(Dist::SquaredEuclidean))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_knn_weights_match_the_reference_kernel() {
let distances = vec![
vec![0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
vec![0.0; 6],
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
];
let expected = [
0.612_048_74,
0.289_111_36,
0.082_831_79,
0.014_394_007,
0.001_517_117_2,
0.000_096_986_055,
];
let weights = knn_distance_weights(&distances, 3.0, false);
for row in [0, 2] {
for (got, want) in weights[row].iter().zip(expected.iter()) {
assert_relative_eq!(got, want, epsilon = 1e-6);
}
}
for &w in &weights[1] {
assert_relative_eq!(w, 1.0 / 6.0, epsilon = 1e-6);
}
for row in &weights {
assert_relative_eq!(row.iter().sum::<f32>(), 1.0, epsilon = 1e-6);
}
}
#[test]
fn test_squared_and_plain_distances_agree() {
let plain = vec![
vec![0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
];
let squared: Vec<Vec<f32>> = plain
.iter()
.map(|row| row.iter().map(|d| d * d).collect())
.collect();
let from_plain = knn_distance_weights(&plain, 3.0, false);
let from_squared = knn_distance_weights(&squared, 3.0, true);
for (a, b) in from_plain.iter().zip(from_squared.iter()) {
for (x, y) in a.iter().zip(b.iter()) {
assert_relative_eq!(x, y, epsilon = 1e-6);
}
}
let mis_read = knn_distance_weights(&squared, 3.0, false);
assert!((mis_read[0][5] - from_plain[0][5]).abs() > 1e-6);
}
#[test]
fn test_neighborhood_factor_widens_the_kernel() {
let distances = vec![vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]];
let tight = knn_distance_weights(&distances, 6.0, false);
let wide = knn_distance_weights(&distances, 1.0, false);
assert!(tight[0][0] > wide[0][0]);
assert!(tight[0][5] < wide[0][5]);
}
#[test]
fn test_squared_metric_detection() {
assert!(distances_are_squared("euclidean"));
assert!(distances_are_squared("l2"));
assert!(!distances_are_squared("cosine"));
assert!(!distances_are_squared("manhattan"));
assert!(!distances_are_squared("not a metric"));
}
}