use ann_search_rs::cpu::{annoy::AnnoyIndex, hnsw::HnswIndex, nndescent::NNDescent};
use ann_search_rs::utils::dist::{Dist, parse_ann_dist};
use faer::MatRef;
use rayon::prelude::*;
use crate::core::math::vector_helpers::median;
use crate::prelude::*;
pub struct MiloRParams {
pub prop: f64,
pub k_refine: usize,
pub refinement_strategy: String,
pub index_type: String,
pub knn_params: KnnParams,
}
pub enum KnnIndex {
Annoy(AnnoyIndex<f32>),
Hnsw(HnswIndex<f32>),
NNDescent(NNDescent<f32>),
}
impl KnnIndex {
pub fn new(
embd: MatRef<f32>,
index_type: KnnIndexType,
knn_params: &KnnParams,
seed: usize,
verbose: bool,
) -> Result<Self, BixverseErrors> {
match index_type {
KnnIndexType::AnnoyIndex => {
let dist = ann_search_rs::utils::dist::parse_ann_dist(&knn_params.ann_dist)
.unwrap_or_else(|| {
println!("[WARNING] Weird string used for distance metric. Using default squared Euclidean distance");
Dist::default()
});
Ok(KnnIndex::Annoy(AnnoyIndex::new(
embd,
knn_params.n_tree,
dist,
seed,
)?))
}
KnnIndexType::HnswIndex => {
let dist = ann_search_rs::utils::dist::parse_ann_dist(&knn_params.ann_dist)
.unwrap_or_else(|| {
println!("[WARNING] Weird string used for distance metric. Using default squared Euclidean distance");
Dist::default()
});
Ok(KnnIndex::Hnsw(HnswIndex::build(
embd,
knn_params.m,
knn_params.ef_construction,
&dist,
seed,
verbose,
)))
}
KnnIndexType::NNDescentIndex => {
let dist = ann_search_rs::utils::dist::parse_ann_dist(&knn_params.ann_dist)
.unwrap_or_else(|| {
println!("[WARNING] Weird string used for distance metric. Using default squared Euclidean distance");
Dist::default()
});
Ok(KnnIndex::NNDescent(NNDescent::new(
embd,
dist,
None,
None,
None,
None,
knn_params.delta,
knn_params.diversify_prob,
seed,
verbose,
)?))
}
}
}
pub fn query_single(
&self,
query_point: &[f32],
knn_params: &KnnParams,
k: usize,
) -> Result<(Vec<usize>, Vec<f32>), BixverseErrors> {
match self {
KnnIndex::Annoy(index) => Ok(index.query(query_point, k, knn_params.search_budget)?),
KnnIndex::Hnsw(index) => Ok(index.query(query_point, k, knn_params.ef_search)?),
KnnIndex::NNDescent(index) => Ok(index.query(query_point, k, None)?),
}
}
}
#[allow(clippy::enum_variant_names)]
pub enum KnnIndexType {
AnnoyIndex,
HnswIndex,
NNDescentIndex,
}
#[derive(Debug, Clone, Copy)]
pub enum RefinementStrategy {
Approximate,
BruteForce,
IndexBased,
}
pub fn parse_refinement_strategy(s: &str) -> Option<RefinementStrategy> {
match s.to_lowercase().as_str() {
"approximate" => Some(RefinementStrategy::Approximate),
"bruteforce" => Some(RefinementStrategy::BruteForce),
"index" => Some(RefinementStrategy::IndexBased),
_ => None,
}
}
pub fn parse_index_type(s: &str) -> Option<KnnIndexType> {
match s.to_lowercase().as_str() {
"annoy" => Some(KnnIndexType::AnnoyIndex),
"hnsw" => Some(KnnIndexType::HnswIndex),
"nndescent" => Some(KnnIndexType::NNDescentIndex),
_ => None,
}
}
fn compute_median_position(embd: MatRef<f32>, neighbours: &[usize]) -> Vec<f32> {
let n_feature = embd.ncols();
let mut median_point = vec![0.0f32; n_feature];
for feat_idx in 0..n_feature {
let values = neighbours
.iter()
.map(|&nb_idx| embd[(nb_idx, feat_idx)])
.collect::<Vec<f32>>();
median_point[feat_idx] = median(&values).unwrap_or(0_f32);
}
median_point
}
fn find_nearest_in_subset(
embd: MatRef<f32>,
median_point: &[f32],
candidates: &[usize],
metric: &Dist,
) -> usize {
let median_row = MatRef::from_row_major_slice(median_point, 1, embd.ncols()).row(0);
candidates
.par_iter()
.map(|&idx| {
let dist = compute_distance_knn(median_row, embd.row(idx), metric);
(idx, dist)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap()
.0
}
fn find_nearest_bruteforce(embd: MatRef<f32>, median_point: &[f32], metric: &Dist) -> usize {
let median_row = MatRef::from_row_major_slice(median_point, 1, embd.ncols()).row(0);
(0..embd.nrows())
.into_par_iter()
.map(|idx| {
let dist = compute_distance_knn(median_row, embd.row(idx), metric);
(idx, dist)
})
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.unwrap()
.0
}
fn find_nearest_with_index(
index: &KnnIndex,
knn_params: &KnnParams,
median_point: &[f32],
) -> Result<usize, BixverseErrors> {
let (indices, _) = index.query_single(median_point, knn_params, 1)?;
Ok(indices[0])
}
#[allow(clippy::too_many_arguments)]
pub fn refine_sampling_with_strategy(
embd: MatRef<f32>,
knn_indices: &[Vec<usize>],
sampled_indices: &[usize],
k_refine: usize,
knn_params: &KnnParams,
strategy: &RefinementStrategy,
knn_index: Option<&KnnIndex>,
verbose: usize,
) -> Result<Vec<usize>, BixverseErrors> {
let verbosity = parse_verbosity_level(verbose);
if verbosity.normal_verbosity() {
println!("Running refined sampling");
}
let mut refined = Vec::with_capacity(sampled_indices.len());
let dist_metric = parse_ann_dist(&knn_params.ann_dist).unwrap_or_default();
for &sample_idx in sampled_indices {
let mut neighbours = Vec::with_capacity(k_refine);
for j in 0..k_refine.min(knn_indices[0].len()) {
let neighbour_idx = knn_indices[sample_idx][j];
neighbours.push(neighbour_idx);
}
let median_point = compute_median_position(embd, &neighbours);
let best_idx = match strategy {
RefinementStrategy::Approximate => {
find_nearest_in_subset(embd, &median_point, &neighbours, &dist_metric)
}
RefinementStrategy::BruteForce => {
find_nearest_bruteforce(embd, &median_point, &dist_metric)
}
RefinementStrategy::IndexBased => {
if let Some(index) = knn_index {
find_nearest_with_index(index, knn_params, &median_point)?
} else {
find_nearest_bruteforce(embd, &median_point, &dist_metric)
}
}
};
refined.push(best_idx);
}
Ok(refined)
}
pub fn compute_kth_distances_from_matrix(
embd: MatRef<f32>,
knn_indices: &[Vec<usize>],
index_cells: &[usize],
kth_col: usize,
) -> Vec<f64> {
index_cells
.par_iter()
.map(|&cell_idx| {
let kth_neighbour = knn_indices[cell_idx][kth_col];
compute_distance_knn(
embd.row(cell_idx),
embd.row(kth_neighbour),
&Dist::SquaredEuclidean,
) as f64
})
.collect()
}
pub fn build_nhood_matrix(
knn_indices: &[Vec<usize>],
index_cells: &[usize],
) -> (Vec<usize>, Vec<usize>, Vec<f64>) {
let k = knn_indices[0].len();
let n_nhoods = index_cells.len();
let mut row_indices = Vec::with_capacity(n_nhoods * (k + 1));
let mut col_indices = Vec::with_capacity(n_nhoods * (k + 1));
let mut values = Vec::with_capacity(n_nhoods * (k + 1));
for (nh_idx, &cell_idx) in index_cells.iter().enumerate() {
row_indices.push(cell_idx);
col_indices.push(nh_idx);
values.push(1.0);
for j in 0..k {
let neighbor_idx = knn_indices[cell_idx][j];
row_indices.push(neighbor_idx);
col_indices.push(nh_idx);
values.push(1.0);
}
}
(row_indices, col_indices, values)
}