use crate::distance;
use crate::hnsw::graph::{HNSWIndex, Layer};
use crate::hnsw::search::greedy_search_layer;
use crate::RetrieveError;
use smallvec::SmallVec;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[inline(always)]
fn dist_fn_for(index: &HNSWIndex) -> fn(&[f32], &[f32]) -> f32 {
use crate::distance::DistanceMetric;
match index.params.metric {
DistanceMetric::L2 => distance::l2_distance,
DistanceMetric::Cosine => distance::cosine_distance_normalized,
DistanceMetric::Angular => distance::angular_distance,
DistanceMetric::InnerProduct => distance::inner_product_distance,
}
}
fn select_neighbors_rnd(
_query_vector: &[f32],
candidates: &mut [(u32, f32)],
m: usize,
vectors: &[f32],
dimension: usize,
dist_fn: fn(&[f32], &[f32]) -> f32,
) -> Vec<u32> {
if candidates.is_empty() {
return Vec::new();
}
candidates.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
let mut selected = Vec::with_capacity(m.min(candidates.len()));
let mut rejected = Vec::new();
selected.push(candidates[0].0);
for &(candidate_id, query_to_candidate_dist) in candidates.iter().skip(1) {
if selected.len() >= m {
break;
}
let candidate_vec = get_vector(vectors, dimension, candidate_id as usize);
let mut can_add = true;
for &selected_id in &selected {
let selected_vec = get_vector(vectors, dimension, selected_id as usize);
let inter_distance = dist_fn(selected_vec, candidate_vec);
if query_to_candidate_dist >= inter_distance {
can_add = false;
break;
}
}
if can_add {
selected.push(candidate_id);
} else {
rejected.push(candidate_id);
}
}
let mut ri = 0;
while selected.len() < m && ri < rejected.len() {
selected.push(rejected[ri]);
ri += 1;
}
selected
}
fn select_neighbors_mond(
query_vector: &[f32],
candidates: &mut [(u32, f32)],
m: usize,
vectors: &[f32],
dimension: usize,
min_angle_degrees: f32,
) -> Vec<u32> {
if candidates.is_empty() {
return Vec::new();
}
let min_angle_rad = min_angle_degrees.to_radians();
let min_cos = min_angle_rad.cos();
candidates.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
let mut selected = Vec::with_capacity(m.min(candidates.len()));
let mut rejected = Vec::new();
selected.push(candidates[0].0);
use crate::simd;
let dot_qq = simd::dot(query_vector, query_vector);
for &(candidate_id, _) in candidates.iter().skip(1) {
if selected.len() >= m {
break;
}
let candidate_vec = get_vector(vectors, dimension, candidate_id as usize);
let mut can_add = true;
let dot_cq = simd::dot(candidate_vec, query_vector);
let dot_cc_self = simd::dot(candidate_vec, candidate_vec);
for &selected_id in &selected {
let selected_vec = get_vector(vectors, dimension, selected_id as usize);
let dot_cc = simd::dot(candidate_vec, selected_vec);
let dot_sq = simd::dot(selected_vec, query_vector);
let dot_qc_qs = dot_cc - dot_cq - dot_sq + dot_qq;
let norm_c_sq = dot_cc_self + dot_qq - 2.0 * dot_cq;
let norm_s_sq = simd::dot(selected_vec, selected_vec) + dot_qq - 2.0 * dot_sq;
if norm_c_sq > 0.0 && norm_s_sq > 0.0 {
let norm_c = norm_c_sq.sqrt();
let norm_s = norm_s_sq.sqrt();
let cos_angle = dot_qc_qs / (norm_c * norm_s);
if cos_angle >= min_cos {
can_add = false;
break;
}
}
}
if can_add {
selected.push(candidate_id);
} else {
rejected.push(candidate_id);
}
}
let mut ri = 0;
while selected.len() < m && ri < rejected.len() {
selected.push(rejected[ri]);
ri += 1;
}
selected
}
fn select_neighbors_rrnd(
_query_vector: &[f32],
candidates: &mut [(u32, f32)],
m: usize,
vectors: &[f32],
dimension: usize,
alpha: f32,
dist_fn: fn(&[f32], &[f32]) -> f32,
) -> Vec<u32> {
if candidates.is_empty() {
return Vec::new();
}
candidates.sort_unstable_by(|a, b| a.1.total_cmp(&b.1));
let mut selected = Vec::with_capacity(m.min(candidates.len()));
let mut rejected = Vec::new();
selected.push(candidates[0].0);
for &(candidate_id, query_to_candidate_dist) in candidates.iter().skip(1) {
if selected.len() >= m {
break;
}
let candidate_vec = get_vector(vectors, dimension, candidate_id as usize);
let mut can_add = true;
for &selected_id in &selected {
let selected_vec = get_vector(vectors, dimension, selected_id as usize);
let inter_distance = dist_fn(selected_vec, candidate_vec);
if query_to_candidate_dist >= alpha * inter_distance {
can_add = false;
break;
}
}
if can_add {
selected.push(candidate_id);
} else {
rejected.push(candidate_id);
}
}
let mut ri = 0;
while selected.len() < m && ri < rejected.len() {
selected.push(rejected[ri]);
ri += 1;
}
selected
}
pub fn select_neighbors(
query_vector: &[f32],
candidates: &mut [(u32, f32)],
m: usize,
vectors: &[f32],
dimension: usize,
strategy: &crate::hnsw::graph::NeighborhoodDiversification,
dist_fn: fn(&[f32], &[f32]) -> f32,
) -> Vec<u32> {
match strategy {
crate::hnsw::graph::NeighborhoodDiversification::RelativeNeighborhood => {
select_neighbors_rnd(query_vector, candidates, m, vectors, dimension, dist_fn)
}
crate::hnsw::graph::NeighborhoodDiversification::MaximumOriented { min_angle_degrees } => {
select_neighbors_mond(
query_vector,
candidates,
m,
vectors,
dimension,
*min_angle_degrees,
)
}
crate::hnsw::graph::NeighborhoodDiversification::RelaxedRelative { alpha } => {
select_neighbors_rrnd(
query_vector,
candidates,
m,
vectors,
dimension,
*alpha,
dist_fn,
)
}
}
}
#[inline]
pub fn get_vector(vectors: &[f32], dimension: usize, idx: usize) -> &[f32] {
let start = idx * dimension;
let end = start + dimension;
&vectors[start..end]
}
pub fn construct_graph(index: &mut HNSWIndex) -> Result<(), RetrieveError> {
if index.num_vectors == 0 {
return Err(RetrieveError::EmptyIndex);
}
let max_layer = index.layer_assignments.iter().max().copied().unwrap_or(0) as usize;
index.layers = (0..=max_layer)
.map(|_| Layer::new_uncompressed(vec![SmallVec::new(); index.num_vectors]))
.collect();
let dist_fn = dist_fn_for(index);
let mut global_entry_point = 0u32;
let mut global_entry_layer = index.layer_assignments[0];
for current_id in 0..index.num_vectors {
let current_layer = index.layer_assignments[current_id] as usize;
let current_vector = get_vector(&index.vectors, index.dimension, current_id);
if current_id == 0 {
global_entry_point = 0;
global_entry_layer = index.layer_assignments[0];
continue;
}
let mut layer_entry_point = global_entry_point;
let entry_layer = (global_entry_layer as usize).min(max_layer);
if entry_layer > current_layer {
for layer_idx in ((current_layer + 1)..=entry_layer).rev() {
let layer = &index.layers[layer_idx];
let results = greedy_search_layer(
current_vector,
layer_entry_point,
layer,
&index.vectors,
index.dimension,
1,
dist_fn,
);
if let Some((best_id, _)) = results.first() {
layer_entry_point = *best_id;
}
}
}
for layer_idx in (0..=current_layer.min(entry_layer)).rev() {
let candidates = greedy_search_layer(
current_vector,
layer_entry_point,
&index.layers[layer_idx],
&index.vectors,
index.dimension,
index.params.ef_construction,
dist_fn,
);
if let Some((best_id, _)) = candidates.first() {
layer_entry_point = *best_id;
}
let m_actual = if layer_idx == 0 {
index.params.m_max
} else {
index.params.m
};
let mut candidates = candidates;
let mut selected = select_neighbors(
current_vector,
&mut candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
selected.retain(|&id| {
let id_usize = id as usize;
id_usize < current_id && (index.layer_assignments[id_usize] as usize) >= layer_idx
});
let neighbor_data: Vec<(u32, f32)> = selected
.iter()
.map(|&id| {
let vec = get_vector(&index.vectors, index.dimension, id as usize);
let dist = dist_fn(current_vector, vec);
(id, dist)
})
.collect();
let current_existing_neighbors: Vec<u32> = if layer_idx < index.layers.len() {
index.layers[layer_idx]
.get_neighbors(current_id as u32)
.iter()
.copied()
.collect()
} else {
Vec::new()
};
let mut all_current_distances: Vec<(u32, f32)> =
Vec::with_capacity(current_existing_neighbors.len() + selected.len());
for &id in ¤t_existing_neighbors {
let vec = get_vector(&index.vectors, index.dimension, id as usize);
let dist = dist_fn(current_vector, vec);
all_current_distances.push((id, dist));
}
for &(nid, dist) in &neighbor_data {
all_current_distances.push((nid, dist));
}
let mut all_reverse_distances: Vec<Vec<(u32, f32)>> =
Vec::with_capacity(neighbor_data.len());
for &(neighbor_id, dist_to_current) in &neighbor_data {
let neighbor_vec =
get_vector(&index.vectors, index.dimension, neighbor_id as usize);
let existing: Vec<u32> = if layer_idx < index.layers.len() {
index.layers[layer_idx]
.get_neighbors(neighbor_id)
.iter()
.copied()
.collect()
} else {
Vec::new()
};
let mut distances = Vec::with_capacity(1 + existing.len());
distances.push((current_id as u32, dist_to_current));
for &existing_id in &existing {
let existing_vec =
get_vector(&index.vectors, index.dimension, existing_id as usize);
let dist = dist_fn(neighbor_vec, existing_vec);
distances.push((existing_id, dist));
}
all_reverse_distances.push(distances);
}
let layer = &mut index.layers[layer_idx];
let neighbors_vec = layer.get_neighbors_mut();
for &neighbor_id in &selected {
let neighbors = &mut neighbors_vec[current_id];
if !neighbors.contains(&neighbor_id) {
neighbors.push(neighbor_id);
}
let reverse_neighbors = &mut neighbors_vec[neighbor_id as usize];
if !reverse_neighbors.contains(&(current_id as u32)) {
reverse_neighbors.push(current_id as u32);
}
}
{
let neighbors = &mut neighbors_vec[current_id];
if neighbors.len() > m_actual {
let mut neighbor_candidates: Vec<(u32, f32)> = neighbors
.iter()
.map(|&id| {
let dist = all_current_distances
.iter()
.find(|(k, _)| *k == id)
.map(|(_, v)| *v)
.unwrap_or_else(|| {
let vec =
get_vector(&index.vectors, index.dimension, id as usize);
dist_fn(current_vector, vec)
});
(id, dist)
})
.collect();
let pruned = select_neighbors(
current_vector,
&mut neighbor_candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
*neighbors = pruned.into_iter().collect();
}
}
for (idx, &neighbor_id) in selected.iter().enumerate() {
let reverse_neighbors = &mut neighbors_vec[neighbor_id as usize];
if reverse_neighbors.len() > m_actual {
let distances = &all_reverse_distances[idx];
let neighbor_vec =
get_vector(&index.vectors, index.dimension, neighbor_id as usize);
let mut reverse_candidates: Vec<(u32, f32)> = reverse_neighbors
.iter()
.map(|&id| {
let dist = distances
.iter()
.find(|(k, _)| *k == id)
.map(|(_, v)| *v)
.unwrap_or_else(|| {
let vec =
get_vector(&index.vectors, index.dimension, id as usize);
dist_fn(neighbor_vec, vec)
});
(id, dist)
})
.collect();
let pruned = select_neighbors(
neighbor_vec,
&mut reverse_candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
*reverse_neighbors = pruned.into_iter().collect();
}
}
}
if current_layer > (global_entry_layer as usize) {
global_entry_point = current_id as u32;
global_entry_layer = index.layer_assignments[current_id];
}
}
Ok(())
}
#[cfg(feature = "parallel")]
struct SearchResult {
current_id: usize,
per_layer: Vec<(usize, Vec<u32>)>,
}
#[cfg(feature = "parallel")]
pub fn construct_graph_parallel(
index: &mut HNSWIndex,
batch_size: usize,
) -> Result<(), RetrieveError> {
if index.num_vectors == 0 {
return Err(RetrieveError::EmptyIndex);
}
let max_layer = index.layer_assignments.iter().max().copied().unwrap_or(0) as usize;
index.layers = (0..=max_layer)
.map(|_| Layer::new_uncompressed(vec![SmallVec::new(); index.num_vectors]))
.collect();
let dist_fn = dist_fn_for(index);
let sequential_count = (index.params.ef_construction * 2).min(index.num_vectors);
let mut global_entry_point = 0u32;
let mut global_entry_layer = index.layer_assignments[0];
for current_id in 0..sequential_count {
insert_single(
index,
current_id,
&mut global_entry_point,
&mut global_entry_layer,
dist_fn,
);
}
let batch_sz = batch_size.max(1);
let remaining = sequential_count..index.num_vectors;
for batch_start in remaining.step_by(batch_sz) {
let batch_end = (batch_start + batch_sz).min(index.num_vectors);
let batch_ids: Vec<usize> = (batch_start..batch_end).collect();
let ep = global_entry_point;
let ep_layer = global_entry_layer as usize;
let search_results: Vec<SearchResult> = batch_ids
.par_iter()
.map(|¤t_id| search_for_neighbors(index, current_id, ep, ep_layer, dist_fn))
.collect();
for result in search_results {
commit_edges(index, &result, dist_fn);
let current_layer = index.layer_assignments[result.current_id] as usize;
if current_layer > (global_entry_layer as usize) {
global_entry_point = result.current_id as u32;
global_entry_layer = index.layer_assignments[result.current_id];
}
}
prune_overweight_nodes(index, dist_fn);
}
Ok(())
}
#[cfg(feature = "parallel")]
fn insert_single(
index: &mut HNSWIndex,
current_id: usize,
global_entry_point: &mut u32,
global_entry_layer: &mut u8,
dist_fn: fn(&[f32], &[f32]) -> f32,
) {
let current_layer = index.layer_assignments[current_id] as usize;
let current_vector = get_vector(&index.vectors, index.dimension, current_id);
let max_layer = index.layers.len().saturating_sub(1);
if current_id == 0 {
*global_entry_point = 0;
*global_entry_layer = index.layer_assignments[0];
return;
}
let mut layer_entry_point = *global_entry_point;
let entry_layer = (*global_entry_layer as usize).min(max_layer);
if entry_layer > current_layer {
for layer_idx in ((current_layer + 1)..=entry_layer).rev() {
if layer_idx >= index.layers.len() {
continue;
}
let results = greedy_search_layer(
current_vector,
layer_entry_point,
&index.layers[layer_idx],
&index.vectors,
index.dimension,
1,
dist_fn,
);
if let Some((best_id, _)) = results.first() {
layer_entry_point = *best_id;
}
}
}
for layer_idx in (0..=current_layer.min(entry_layer)).rev() {
let candidates = greedy_search_layer(
current_vector,
layer_entry_point,
&index.layers[layer_idx],
&index.vectors,
index.dimension,
index.params.ef_construction,
dist_fn,
);
if let Some((best_id, _)) = candidates.first() {
layer_entry_point = *best_id;
}
let m_actual = if layer_idx == 0 {
index.params.m_max
} else {
index.params.m
};
let mut candidates = candidates;
let mut selected = select_neighbors(
current_vector,
&mut candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
selected.retain(|&id| {
let id_usize = id as usize;
id_usize < current_id && (index.layer_assignments[id_usize] as usize) >= layer_idx
});
let layer = &mut index.layers[layer_idx];
let neighbors_vec = layer.get_neighbors_mut();
for &neighbor_id in &selected {
let neighbors = &mut neighbors_vec[current_id];
if !neighbors.contains(&neighbor_id) {
neighbors.push(neighbor_id);
}
let reverse = &mut neighbors_vec[neighbor_id as usize];
if !reverse.contains(&(current_id as u32)) {
reverse.push(current_id as u32);
}
}
}
if current_layer > (*global_entry_layer as usize) {
*global_entry_point = current_id as u32;
*global_entry_layer = index.layer_assignments[current_id];
}
}
#[cfg(feature = "parallel")]
fn search_for_neighbors(
index: &HNSWIndex,
current_id: usize,
entry_point: u32,
entry_layer: usize,
dist_fn: fn(&[f32], &[f32]) -> f32,
) -> SearchResult {
let current_layer = index.layer_assignments[current_id] as usize;
let current_vector = get_vector(&index.vectors, index.dimension, current_id);
let max_layer = index.layers.len().saturating_sub(1);
let mut layer_entry_point = entry_point;
if entry_layer > current_layer {
for layer_idx in ((current_layer + 1)..=entry_layer.min(max_layer)).rev() {
let results = greedy_search_layer(
current_vector,
layer_entry_point,
&index.layers[layer_idx],
&index.vectors,
index.dimension,
1,
dist_fn,
);
if let Some((best_id, _)) = results.first() {
layer_entry_point = *best_id;
}
}
}
let mut per_layer = Vec::new();
for layer_idx in (0..=current_layer.min(entry_layer.min(max_layer))).rev() {
let candidates = greedy_search_layer(
current_vector,
layer_entry_point,
&index.layers[layer_idx],
&index.vectors,
index.dimension,
index.params.ef_construction,
dist_fn,
);
if let Some((best_id, _)) = candidates.first() {
layer_entry_point = *best_id;
}
let m_actual = if layer_idx == 0 {
index.params.m_max
} else {
index.params.m
};
let mut candidates = candidates;
let mut selected = select_neighbors(
current_vector,
&mut candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
selected.retain(|&id| (index.layer_assignments[id as usize] as usize) >= layer_idx);
per_layer.push((layer_idx, selected));
}
SearchResult {
current_id,
per_layer,
}
}
#[cfg(feature = "parallel")]
fn commit_edges(index: &mut HNSWIndex, result: &SearchResult, dist_fn: fn(&[f32], &[f32]) -> f32) {
let current_id = result.current_id;
let current_vector = get_vector(&index.vectors, index.dimension, current_id);
for &(layer_idx, ref selected) in &result.per_layer {
let m_actual = if layer_idx == 0 {
index.params.m_max
} else {
index.params.m
};
let layer = &mut index.layers[layer_idx];
let neighbors_vec = layer.get_neighbors_mut();
for &neighbor_id in selected {
let fwd = &mut neighbors_vec[current_id];
if !fwd.contains(&neighbor_id) {
fwd.push(neighbor_id);
}
let rev = &mut neighbors_vec[neighbor_id as usize];
if !rev.contains(&(current_id as u32)) {
rev.push(current_id as u32);
}
}
let neighbors = &mut neighbors_vec[current_id];
if neighbors.len() > m_actual {
let mut neighbor_candidates: Vec<(u32, f32)> = neighbors
.iter()
.map(|&id| {
let vec = get_vector(&index.vectors, index.dimension, id as usize);
(id, dist_fn(current_vector, vec))
})
.collect();
let pruned = select_neighbors(
current_vector,
&mut neighbor_candidates,
m_actual,
&index.vectors,
index.dimension,
&index.params.neighborhood_diversification,
dist_fn,
);
*neighbors = pruned.into_iter().collect();
}
}
}
#[cfg(feature = "parallel")]
fn prune_overweight_nodes(index: &mut HNSWIndex, dist_fn: fn(&[f32], &[f32]) -> f32) {
let num_vectors = index.num_vectors;
let dimension = index.dimension;
let diversification = index.params.neighborhood_diversification.clone();
let m = index.params.m;
let m_max = index.params.m_max;
for layer_idx in 0..index.layers.len() {
let m_actual = if layer_idx == 0 { m_max } else { m };
let layer = &mut index.layers[layer_idx];
let neighbors_vec = layer.get_neighbors_mut();
let overweight: Vec<usize> = (0..num_vectors)
.filter(|&id| neighbors_vec[id].len() > m_actual)
.collect();
if overweight.is_empty() {
continue;
}
let pruned_lists: Vec<(usize, SmallVec<[u32; 16]>)> = overweight
.par_iter()
.map(|&node_id| {
let node_vec = get_vector(&index.vectors, dimension, node_id);
let mut candidates: Vec<(u32, f32)> = neighbors_vec[node_id]
.iter()
.map(|&id| {
let vec = get_vector(&index.vectors, dimension, id as usize);
(id, dist_fn(node_vec, vec))
})
.collect();
let pruned = select_neighbors(
node_vec,
&mut candidates,
m_actual,
&index.vectors,
dimension,
&diversification,
dist_fn,
);
(node_id, pruned.into_iter().collect::<SmallVec<_>>())
})
.collect();
for (node_id, pruned) in pruned_lists {
neighbors_vec[node_id] = pruned;
}
}
}