mod insert;
pub(crate) mod locking;
mod neighbors;
mod reorder;
pub(crate) mod safety_counters;
mod search;
mod search_pipeline;
mod search_pools;
mod search_state;
#[cfg(test)]
mod search_tests;
#[cfg(feature = "gpu")]
mod gpu_search;
use super::columnar_vectors::ColumnarVectors;
use super::distance::DistanceEngine;
use super::layer::Layer;
use crate::perf_optimizations::ContiguousVectors;
use locking::{record_lock_acquire, record_lock_release, LockRank};
use parking_lot::RwLock;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
pub const NO_ENTRY_POINT: usize = usize::MAX;
pub const DEFAULT_ALPHA: f32 = 1.2;
pub struct NativeHnsw<D: DistanceEngine> {
pub(in crate::index::hnsw::native) distance: D,
pub(in crate::index::hnsw::native) vectors: RwLock<Option<ContiguousVectors>>,
pub(in crate::index::hnsw::native) layers: RwLock<Vec<Layer>>,
pub(in crate::index::hnsw::native) entry_point: AtomicUsize,
pub(in crate::index::hnsw::native) max_layer: AtomicUsize,
pub(in crate::index::hnsw::native) count: AtomicUsize,
pub(in crate::index::hnsw::native) rng_state: AtomicU64,
pub(in crate::index::hnsw::native) max_connections: usize,
pub(in crate::index::hnsw::native) max_connections_0: usize,
pub(in crate::index::hnsw::native) ef_construction: usize,
pub(in crate::index::hnsw::native) level_mult: f64,
pub(in crate::index::hnsw::native) alpha: f32,
pub(crate) stagnation_limit: usize,
pub(in crate::index::hnsw::native) pre_allocated_capacity: AtomicUsize,
pub(in crate::index::hnsw::native) columnar: RwLock<Option<ColumnarVectors>>,
#[cfg(feature = "gpu")]
pub(in crate::index::hnsw::native) gpu_csr_cache: crate::gpu::gpu_csr::CsrCache,
#[cfg(feature = "gpu")]
pub(in crate::index::hnsw::native) gpu_vectors_snapshot:
parking_lot::Mutex<Option<GpuVectorsSnapshot>>,
#[cfg(feature = "gpu")]
pub(in crate::index::hnsw::native) gpu_snapshot_version: AtomicU64,
}
#[cfg(feature = "gpu")]
pub(in crate::index::hnsw::native) type GpuVectorsSnapshot = (u64, usize, std::sync::Arc<[f32]>);
impl<D: DistanceEngine> NativeHnsw<D> {
#[must_use]
pub fn new(
distance: D,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
) -> Self {
Self::build(
distance,
max_connections,
ef_construction,
max_elements,
DEFAULT_ALPHA,
None,
)
}
pub fn new_with_dimension(
distance: D,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
dimension: usize,
) -> crate::error::Result<Self> {
Self::new_with_dimension_and_alpha(
distance,
max_connections,
ef_construction,
max_elements,
dimension,
DEFAULT_ALPHA,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_with_dimension_and_alpha(
distance: D,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
dimension: usize,
alpha: f32,
) -> crate::error::Result<Self> {
let storage = ContiguousVectors::new(dimension, max_elements)?;
Ok(Self::build(
distance,
max_connections,
ef_construction,
max_elements,
alpha,
Some(storage),
))
}
#[must_use]
pub fn with_alpha(
distance: D,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
alpha: f32,
) -> Self {
Self::build(
distance,
max_connections,
ef_construction,
max_elements,
alpha,
None,
)
}
fn build(
distance: D,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
alpha: f32,
vectors: Option<ContiguousVectors>,
) -> Self {
let max_connections_0 = max_connections * 2;
let level_mult = 1.0 / (max_connections as f64).ln();
Self {
distance,
vectors: RwLock::new(vectors),
layers: RwLock::new(vec![Layer::new(max_elements)]),
entry_point: AtomicUsize::new(NO_ENTRY_POINT),
max_layer: AtomicUsize::new(0),
count: AtomicUsize::new(0),
rng_state: AtomicU64::new(0x5DEE_CE66_D1A4_B5B5),
max_connections,
max_connections_0,
ef_construction,
level_mult,
alpha,
stagnation_limit: ef_construction / 2,
pre_allocated_capacity: AtomicUsize::new(0),
columnar: RwLock::new(None),
#[cfg(feature = "gpu")]
gpu_csr_cache: crate::gpu::gpu_csr::CsrCache::new(),
#[cfg(feature = "gpu")]
gpu_vectors_snapshot: parking_lot::Mutex::new(None),
#[cfg(feature = "gpu")]
gpu_snapshot_version: AtomicU64::new(0),
}
}
#[must_use]
pub fn get_alpha(&self) -> f32 {
self.alpha
}
#[must_use]
pub fn len(&self) -> usize {
self.count.load(Ordering::Relaxed)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[cfg(feature = "gpu")]
pub(in crate::index::hnsw::native) fn invalidate_gpu_caches(&self) {
use self::locking::{record_lock_acquire, record_lock_release, LockRank};
self.gpu_snapshot_version.fetch_add(1, Ordering::Release);
self.gpu_csr_cache.invalidate();
record_lock_acquire(LockRank::GpuVectorsSnapshot);
*self.gpu_vectors_snapshot.lock() = None;
record_lock_release(LockRank::GpuVectorsSnapshot);
}
#[inline]
#[must_use]
pub fn compute_distance(&self, a: &[f32], b: &[f32]) -> f32 {
self.distance.distance(a, b)
}
#[inline]
pub(in crate::index::hnsw) fn with_vectors_read<R>(
&self,
f: impl FnOnce(&ContiguousVectors) -> R,
) -> R
where
R: Default,
{
record_lock_acquire(LockRank::Vectors);
let guard = self.vectors.read();
let result = match guard.as_ref() {
Some(v) => f(v),
None => R::default(),
};
drop(guard);
record_lock_release(LockRank::Vectors);
result
}
pub(in crate::index::hnsw) fn with_vectors_write<R>(
&self,
f: impl FnOnce(&mut ContiguousVectors) -> crate::error::Result<R>,
) -> crate::error::Result<R> {
record_lock_acquire(LockRank::Vectors);
let mut guard = self.vectors.write();
let storage = guard.as_mut().ok_or_else(|| {
crate::error::Error::Internal("ContiguousVectors not initialized".to_string())
})?;
let result = f(storage);
drop(guard);
record_lock_release(LockRank::Vectors);
result
}
#[allow(dead_code)] #[inline]
pub(in crate::index::hnsw::native) fn with_layers_read<R>(
&self,
f: impl FnOnce(&[Layer]) -> R,
) -> R {
record_lock_acquire(LockRank::Layers);
let layers = self.layers.read();
let result = f(&layers);
drop(layers);
record_lock_release(LockRank::Layers);
result
}
#[inline]
pub(in crate::index::hnsw::native) fn with_vectors_and_layers_read<R>(
&self,
f: impl FnOnce(&ContiguousVectors, &[Layer]) -> R,
) -> R
where
R: Default,
{
record_lock_acquire(LockRank::Vectors);
let vectors_guard = self.vectors.read();
let Some(vectors) = vectors_guard.as_ref() else {
drop(vectors_guard);
record_lock_release(LockRank::Vectors);
return R::default();
};
record_lock_acquire(LockRank::Layers);
let layers = self.layers.read();
let result = f(vectors, &layers);
drop(layers);
record_lock_release(LockRank::Layers);
drop(vectors_guard);
record_lock_release(LockRank::Vectors);
result
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
fn random_layer(&self) -> usize {
let old_state = self
.rng_state
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |mut state| {
if state == 0 {
state = 0x853c_49e6_748f_ea9b;
}
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
Some(state)
})
.unwrap_or_else(|s| s);
let mut state = old_state;
if state == 0 {
state = 0x853c_49e6_748f_ea9b;
}
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let uniform = (state as f64) / (u64::MAX as f64);
let uniform_safe = uniform.max(f64::MIN_POSITIVE);
let level = (-uniform_safe.ln() * self.level_mult).floor() as usize;
level.min(15)
}
}