use super::native_inner::NativeHnswInner;
use super::params::{HnswParams, SearchQuality};
use super::sharded_mappings::ShardedMappings;
use super::upsert;
use crate::distance::DistanceMetric;
use crate::index::VectorIndex;
use crate::scored_result::ScoredResult;
use crate::validation::validate_dimension_match;
use parking_lot::{Mutex, RwLock};
pub struct NativeHnswIndex {
pub(crate) dimension: usize,
pub(crate) metric: DistanceMetric,
pub(crate) inner: RwLock<NativeHnswInner>,
pub(crate) mappings: ShardedMappings,
pub(crate) enable_vector_storage: bool,
#[allow(dead_code)] pub(crate) params: HnswParams,
pub(crate) saving: Mutex<()>,
}
impl NativeHnswIndex {
pub fn new(dimension: usize, metric: DistanceMetric) -> crate::error::Result<Self> {
Self::with_params(dimension, metric, HnswParams::auto(dimension))
}
pub fn with_params(
dimension: usize,
metric: DistanceMetric,
params: HnswParams,
) -> crate::error::Result<Self> {
let inner = NativeHnswInner::new_with_options(
metric,
params.max_connections,
params.max_elements,
params.ef_construction,
dimension,
params.storage_mode,
params.alpha,
)?;
Ok(Self {
dimension,
metric,
inner: RwLock::new(inner),
mappings: ShardedMappings::new(),
enable_vector_storage: true,
params,
saving: Mutex::new(()),
})
}
pub fn new_turbo(dimension: usize, metric: DistanceMetric) -> crate::error::Result<Self> {
Self::with_params(dimension, metric, HnswParams::turbo())
}
pub fn new_fast_insert(dimension: usize, metric: DistanceMetric) -> crate::error::Result<Self> {
let mut index = Self::new(dimension, metric)?;
index.enable_vector_storage = false;
Ok(index)
}
#[inline]
#[must_use]
pub fn dimension(&self) -> usize {
self.dimension
}
#[inline]
#[must_use]
pub fn metric(&self) -> DistanceMetric {
self.metric
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.mappings.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.mappings.is_empty()
}
#[inline]
#[must_use]
pub fn has_vector_storage(&self) -> bool {
self.enable_vector_storage
}
#[must_use]
pub fn search(&self, query: &[f32], k: usize) -> Vec<ScoredResult> {
self.search_with_quality(query, k, SearchQuality::Balanced)
}
#[must_use]
pub fn search_with_quality(
&self,
query: &[f32],
k: usize,
quality: SearchQuality,
) -> Vec<ScoredResult> {
let ef_search = quality.ef_search_for_scale(k, self.len());
let inner = self.inner.read();
let neighbors = inner.search_auto(query, k, ef_search);
let results: Vec<ScoredResult> = neighbors
.into_iter()
.filter_map(|(node_id, raw_dist)| {
self.mappings.get_id(node_id).map(|id| {
let score = inner.transform_score(raw_dist);
ScoredResult::new(id, score)
})
})
.collect();
drop(inner);
results
}
pub fn insert(&self, id: u64, vector: &[f32]) -> crate::error::Result<()> {
validate_dimension_match(self.dimension, vector.len())?;
let inner = self.inner.read();
let placed = inner.place(vector)?;
self.mappings.assign(id, placed);
drop(inner);
Ok(())
}
pub fn insert_batch(&self, items: &[(u64, Vec<f32>)]) -> crate::error::Result<()> {
for (_, vector) in items {
validate_dimension_match(self.dimension, vector.len())?;
}
let vectors: Vec<&[f32]> = items.iter().map(|(_, vector)| vector.as_slice()).collect();
let inner = self.inner.read();
let placements = inner.place_parallel(&vectors)?;
for ((id, _), placed) in items.iter().zip(placements) {
self.mappings.assign(*id, placed);
}
drop(inner);
Ok(())
}
pub fn remove(&self, id: u64) -> bool {
upsert::soft_delete(&self.mappings, id, &self.inner.read())
}
#[allow(clippy::unused_self)]
pub fn set_searching_mode(&self) {}
#[allow(clippy::needless_pass_by_value)]
pub fn insert_batch_parallel<I>(&self, items: I) -> usize
where
I: IntoIterator<Item = (u64, Vec<f32>)>,
{
let items: Vec<_> = items.into_iter().collect();
let count = items.len();
if let Err(e) = self.insert_batch(items.as_slice()) {
tracing::error!("insert_batch_parallel failed: {e}");
return 0;
}
count
}
#[must_use]
pub fn search_batch_parallel(
&self,
queries: &[&[f32]],
k: usize,
quality: SearchQuality,
) -> Vec<Vec<ScoredResult>> {
use rayon::prelude::*;
queries
.par_iter()
.map(|q| self.search_with_quality(q, k, quality))
.collect()
}
#[expect(clippy::significant_drop_tightening)]
#[must_use]
pub fn brute_force_search_parallel(&self, query: &[f32], k: usize) -> Vec<ScoredResult> {
use rayon::prelude::*;
if !self.enable_vector_storage {
return Vec::new();
}
let inner = self.inner.read();
let (flat, dimension) = inner.with_contiguous_vectors(|vectors| {
(vectors.as_flat_slice().to_vec(), vectors.dimension())
});
if flat.is_empty() || dimension == 0 {
return Vec::new();
}
let mut results: Vec<ScoredResult> = flat
.par_chunks(dimension)
.enumerate()
.filter_map(|(idx, vec)| {
let id = self.mappings.get_id(idx)?;
let raw_distance = inner.compute_distance(query, vec);
let score = inner.transform_score(raw_distance);
Some(ScoredResult::new(id, score))
})
.collect();
self.metric.sort_scored_results(&mut results);
results.truncate(k);
results
}
}
impl VectorIndex for NativeHnswIndex {
fn insert(&self, id: u64, vector: &[f32]) {
if let Err(e) = NativeHnswIndex::insert(self, id, vector) {
tracing::error!("NativeHnswIndex::insert failed for id={id}: {e}");
}
}
fn remove(&self, id: u64) -> bool {
NativeHnswIndex::remove(self, id)
}
fn search(&self, query: &[f32], k: usize) -> Vec<ScoredResult> {
NativeHnswIndex::search(self, query, k)
}
fn len(&self) -> usize {
NativeHnswIndex::len(self)
}
fn dimension(&self) -> usize {
self.dimension
}
fn metric(&self) -> DistanceMetric {
self.metric
}
}