use ann_search_rs::utils::KnnValidation;
use ann_search_rs::utils::dist::Dist;
use ann_search_rs::*;
use faer::{MatRef, RowRef};
use std::time::Instant;
use crate::core::math::sparse::coo_to_csr;
use crate::prelude::*;
#[derive(Default)]
pub enum KnnSearch {
#[default]
Hnsw,
Annoy,
NNDescent,
Exhaustive,
}
pub fn parse_knn_method(s: &str) -> Option<KnnSearch> {
match s.to_lowercase().as_str() {
"annoy" => Some(KnnSearch::Annoy),
"hnsw" => Some(KnnSearch::Hnsw),
"nndescent" => Some(KnnSearch::NNDescent),
"exhaustive" => Some(KnnSearch::Exhaustive),
_ => 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,
}
impl KnnParams {
pub fn new() -> Self {
Self {
knn_method: "hnsw".to_string(),
ann_dist: "cosine".to_string(),
k: 15,
n_tree: 50,
search_budget: None,
diversify_prob: 0.5,
delta: 0.001,
ef_budget: None,
m: 16,
ef_construction: 200,
ef_search: 100,
}
}
}
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 {
unsafe {
let a_ptr = a.as_ptr();
let b_ptr = b.as_ptr();
match metric {
Dist::Euclidean => {
let mut sum = 0.0f32;
for i in 0..ncols {
let diff = *a_ptr.add(i) - *b_ptr.add(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 {
let av = *a_ptr.add(i);
let bv = *b_ptr.add(i);
dot += av * bv;
norm_a += av * av;
norm_b += bv * bv;
}
1.0 - (dot / (norm_a.sqrt() * norm_b.sqrt()))
}
}
}
} else {
match metric {
Dist::Euclidean => {
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()))
}
}
}
}
pub fn knn_to_sparse_dist(
knn_indices: &[Vec<usize>],
knn_dists: &[Vec<f32>],
n_obs: usize,
) -> CompressedSparseData<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, &cols, &vals, (n_obs, n_obs))
}
fn build_and_query_knn<I>(
no_neighbours: usize,
verbose: bool,
build_index: impl FnOnce() -> I,
query_index: impl FnOnce(&I) -> (Vec<Vec<usize>>, Option<Vec<Vec<f32>>>),
index_name: &str,
) -> (Vec<Vec<usize>>, I) {
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()
);
}
(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,
verbose: bool,
) -> Vec<Vec<usize>> {
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| 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 verbose {
let recall = index.validate_index(no_neighbours, seed, None);
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
res
}
pub fn generate_knn_annoy(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
n_trees: usize,
search_budget: Option<usize>,
seed: usize,
verbose: bool,
) -> Vec<Vec<usize>> {
let (res, index) = build_and_query_knn(
no_neighbours,
verbose,
|| build_annoy_index(mat, dist_metric.to_string(), n_trees, seed),
|idx| query_annoy_self(idx, no_neighbours + 1, search_budget, false, verbose),
"Annoy",
);
if verbose {
let recall = index.validate_index(no_neighbours, seed, None);
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
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,
verbose: bool,
) -> Vec<Vec<usize>> {
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 verbose {
let recall = index.validate_index(no_neighbours, seed, None);
println!(
"Recall of approximate nearest neighbours search in random subset: {:.2}",
recall
);
}
res
}
pub fn generate_knn_exhaustive(
mat: MatRef<f32>,
dist_metric: &str,
no_neighbours: usize,
verbose: bool,
) -> Vec<Vec<usize>> {
let (res, _) = build_and_query_knn(
no_neighbours,
verbose,
|| build_exhaustive_index(mat, dist_metric),
|idx| query_exhaustive_self(idx, no_neighbours + 1, false, verbose),
"exhaustive linear search",
);
res
}
pub fn generate_knn_with_dist(
embd: MatRef<f32>,
knn_params: &KnnParams,
return_dist: bool,
seed: usize,
verbose: bool,
) -> (Vec<Vec<usize>>, Option<Vec<Vec<f32>>>) {
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.clone(), knn_params.n_tree, seed)
});
let (indices, distances) = timed("Queried Annoy index", verbose, || {
query_annoy_index(
embd,
&index,
k_plus_one,
knn_params.search_budget,
return_dist,
verbose,
)
});
if 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,
)
});
let (indices, distances) = timed("Queried HNSW index", verbose, || {
query_hnsw_index(
embd,
&index,
k_plus_one,
knn_params.ef_search,
return_dist,
verbose,
)
});
if 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,
)
});
let (indices, distances) = timed("Queried NNDescent index", verbose, || {
query_nndescent_index(
embd,
&index,
k_plus_one,
knn_params.ef_budget,
true,
verbose,
)
});
if 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.knn_method)
});
timed("Queried Exhaustive index", verbose, || {
query_exhaustive_index(embd, &index, k_plus_one, true, verbose)
})
}
};
remove_self(indices, distances)
}