#![allow(clippy::cast_precision_loss)]
use super::native::rabitq_precision::RaBitQPrecisionHnsw;
use super::native::sq8_precision::Sq8PrecisionHnsw;
use super::native::{CachedSimdDistance, NativeHnsw, NativeNeighbour, ResumableSearch};
use crate::distance::DistanceMetric;
use std::path::Path;
#[cfg(feature = "persistence")]
#[path = "native_inner_unlinked.rs"]
mod unlinked;
pub struct SearchResume(ResumableSearch);
#[allow(clippy::large_enum_variant)]
enum HnswBackend {
Standard(NativeHnsw<CachedSimdDistance>),
RaBitQ(Box<RaBitQPrecisionHnsw<CachedSimdDistance>>),
Sq8(Box<Sq8PrecisionHnsw<CachedSimdDistance>>),
}
pub struct NativeHnswInner {
backend: HnswBackend,
#[allow(dead_code)] metric: DistanceMetric,
}
pub(crate) struct InnerBuild<'a> {
pub metric: DistanceMetric,
pub max_connections: usize,
pub max_elements: usize,
pub ef_construction: usize,
pub dimension: usize,
pub storage_mode: crate::StorageMode,
pub alpha: f32,
pub arena_dir: Option<&'a std::path::Path>,
}
impl NativeHnswInner {
pub fn new_with_options(
metric: DistanceMetric,
max_connections: usize,
max_elements: usize,
ef_construction: usize,
dimension: usize,
storage_mode: crate::StorageMode,
alpha: f32,
) -> crate::error::Result<Self> {
Self::build(&InnerBuild {
metric,
max_connections,
max_elements,
ef_construction,
dimension,
storage_mode,
alpha,
arena_dir: None,
})
}
pub(crate) fn build(opts: &InnerBuild<'_>) -> crate::error::Result<Self> {
let distance = CachedSimdDistance::new_prenormalized(opts.metric, opts.dimension);
let backend = match opts.storage_mode {
crate::StorageMode::RaBitQ => {
HnswBackend::RaBitQ(Box::new(RaBitQPrecisionHnsw::with_optional_arena_dir(
distance,
opts.dimension,
opts.max_connections,
opts.ef_construction,
opts.max_elements,
opts.alpha,
opts.arena_dir,
)?))
}
crate::StorageMode::SQ8 => {
HnswBackend::Sq8(Box::new(Sq8PrecisionHnsw::with_optional_arena_dir(
distance,
opts.dimension,
opts.max_connections,
opts.ef_construction,
opts.max_elements,
opts.alpha,
opts.arena_dir,
)?))
}
_ => Self::new_standard_backend(
distance,
opts.max_connections,
opts.ef_construction,
opts.max_elements,
opts.dimension,
opts.alpha,
opts.arena_dir,
)?,
};
Ok(Self {
backend,
metric: opts.metric,
})
}
#[allow(clippy::too_many_arguments)]
fn new_standard_backend(
distance: CachedSimdDistance,
max_connections: usize,
ef_construction: usize,
max_elements: usize,
dimension: usize,
alpha: f32,
arena_dir: Option<&std::path::Path>,
) -> crate::error::Result<HnswBackend> {
#[cfg(feature = "persistence")]
if let Some(dir) = arena_dir {
let inner = NativeHnsw::standard_in_dir(
distance,
max_connections,
ef_construction,
max_elements,
dimension,
alpha,
dir,
)?;
return Ok(HnswBackend::Standard(inner));
}
#[cfg(not(feature = "persistence"))]
let _ = arena_dir;
let inner = if dimension > 0 {
NativeHnsw::new_with_dimension_and_alpha(
distance,
max_connections,
ef_construction,
max_elements,
dimension,
alpha,
)?
} else {
NativeHnsw::with_alpha(
distance,
max_connections,
ef_construction,
max_elements,
alpha,
)
};
Ok(HnswBackend::Standard(inner))
}
#[must_use]
pub fn storage_mode(&self) -> crate::StorageMode {
match &self.backend {
HnswBackend::Standard(_) => crate::StorageMode::Full,
HnswBackend::RaBitQ(_) => crate::StorageMode::RaBitQ,
HnswBackend::Sq8(_) => crate::StorageMode::SQ8,
}
}
#[must_use]
pub(crate) fn alpha(&self) -> f32 {
match &self.backend {
HnswBackend::Standard(g) => g.get_alpha(),
HnswBackend::RaBitQ(p) => p.inner.get_alpha(),
HnswBackend::Sq8(p) => p.inner.get_alpha(),
}
}
#[must_use]
pub(crate) fn max_connections(&self) -> usize {
match &self.backend {
HnswBackend::Standard(g) => g.get_max_connections(),
HnswBackend::RaBitQ(p) => p.inner.get_max_connections(),
HnswBackend::Sq8(p) => p.inner.get_max_connections(),
}
}
#[must_use]
pub(crate) fn ef_construction(&self) -> usize {
match &self.backend {
HnswBackend::Standard(g) => g.get_ef_construction(),
HnswBackend::RaBitQ(p) => p.inner.get_ef_construction(),
HnswBackend::Sq8(p) => p.inner.get_ef_construction(),
}
}
#[cfg(feature = "persistence")]
pub fn install_trained_rabitq(
&self,
rabitq: std::sync::Arc<crate::quantization::RaBitQIndex>,
) -> crate::error::Result<bool> {
match &self.backend {
HnswBackend::RaBitQ(precision) => {
precision.install_trained_rabitq(rabitq)?;
Ok(true)
}
_ => Ok(false),
}
}
#[cfg(feature = "persistence")]
pub fn install_trained_sq8(
&self,
quantizer: std::sync::Arc<crate::index::hnsw::native::ScalarQuantizer>,
) -> crate::error::Result<bool> {
match &self.backend {
HnswBackend::Sq8(precision) => precision.install_trained_sq8(quantizer),
_ => Ok(false),
}
}
#[must_use]
pub fn is_quantized_backend(&self) -> bool {
matches!(self.backend, HnswBackend::RaBitQ(_) | HnswBackend::Sq8(_))
}
pub fn promote_to_rabitq(self, dimension: usize) -> Self {
match self.backend {
HnswBackend::Standard(inner) => Self {
backend: HnswBackend::RaBitQ(Box::new(RaBitQPrecisionHnsw::from_inner(
inner, dimension,
))),
metric: self.metric,
},
backend => Self {
backend,
metric: self.metric,
},
}
}
pub fn promote_to_sq8(self, dimension: usize) -> Self {
match self.backend {
HnswBackend::Standard(inner) => Self {
backend: HnswBackend::Sq8(Box::new(Sq8PrecisionHnsw::from_inner(inner, dimension))),
metric: self.metric,
},
backend => Self {
backend,
metric: self.metric,
},
}
}
pub fn is_rabitq_quantizer_trained(&self) -> bool {
matches!(&self.backend, HnswBackend::RaBitQ(precision) if precision.is_quantizer_trained())
}
pub fn is_sq8_quantizer_trained(&self) -> bool {
matches!(&self.backend, HnswBackend::Sq8(precision) if precision.is_quantizer_trained())
}
#[cfg(feature = "persistence")]
#[must_use]
pub fn rabitq_quantizer(&self) -> Option<std::sync::Arc<crate::quantization::RaBitQIndex>> {
match &self.backend {
HnswBackend::RaBitQ(precision) => precision.trained_quantizer(),
_ => None,
}
}
#[cfg(feature = "persistence")]
#[must_use]
pub fn sq8_quantizer(
&self,
) -> Option<std::sync::Arc<crate::index::hnsw::native::ScalarQuantizer>> {
match &self.backend {
HnswBackend::Sq8(precision) => precision.trained_quantizer(),
_ => None,
}
}
}
impl NativeHnswInner {
#[inline]
#[must_use]
pub fn search(&self, query: &[f32], k: usize, ef_search: usize) -> Vec<(usize, f32)> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.search(query, k, ef_search),
HnswBackend::RaBitQ(rabitq) => rabitq.search(query, k, ef_search),
HnswBackend::Sq8(sq8) => sq8.search(query, k, ef_search),
}
}
#[must_use]
pub fn search_auto(&self, query: &[f32], k: usize, ef_search: usize) -> Vec<(usize, f32)> {
if let Some(results) = self.try_gpu_route(query, k, ef_search) {
return results;
}
self.search(query, k, ef_search)
}
#[allow(unused_variables)] #[allow(clippy::unused_self)] fn try_gpu_route(
&self,
query: &[f32],
k: usize,
ef_search: usize,
) -> Option<Vec<(usize, f32)>> {
#[cfg(feature = "gpu")]
if let HnswBackend::Standard(hnsw) = &self.backend {
if crate::gpu::should_traverse_gpu(hnsw.len(), query.len()) {
if let Some(results) = self.search_gpu(query, k, ef_search) {
return Some(results);
}
}
}
None
}
#[must_use]
pub fn search_resumable(
&self,
query: &[f32],
k: usize,
ef_search: usize,
) -> (Vec<(usize, f32)>, Option<SearchResume>) {
if let Some(results) = self.try_gpu_route(query, k, ef_search) {
return (results, None);
}
match &self.backend {
HnswBackend::Standard(hnsw) => {
let (results, resume) = hnsw.search_resumable(query, k, ef_search);
(results, resume.map(SearchResume))
}
HnswBackend::RaBitQ(rabitq) => (rabitq.search(query, k, ef_search), None),
HnswBackend::Sq8(sq8) => (sq8.search(query, k, ef_search), None),
}
}
#[must_use]
pub fn resume_search(
&self,
resume: SearchResume,
query: &[f32],
k: usize,
ef_search: usize,
) -> Vec<(usize, f32)> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.resume_search(resume.0, query, k, ef_search),
HnswBackend::RaBitQ(rabitq) => rabitq.search(query, k, ef_search),
HnswBackend::Sq8(sq8) => sq8.search(query, k, ef_search),
}
}
#[cfg(feature = "gpu")]
fn search_gpu(&self, query: &[f32], k: usize, ef_search: usize) -> Option<Vec<(usize, f32)>> {
let hnsw = match &self.backend {
HnswBackend::Standard(hnsw) => hnsw,
HnswBackend::RaBitQ(_) | HnswBackend::Sq8(_) => return None,
};
hnsw.search_gpu(query, k, ef_search, self.metric)
}
#[allow(dead_code)] #[inline]
#[must_use]
pub fn search_neighbours(
&self,
query: &[f32],
k: usize,
ef_search: usize,
) -> Vec<NativeNeighbour> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.search_neighbours(query, k, ef_search),
HnswBackend::RaBitQ(_) | HnswBackend::Sq8(_) => self
.search(query, k, ef_search)
.into_iter()
.map(|(id, dist)| NativeNeighbour {
d_id: id,
distance: dist,
})
.collect(),
}
}
}
impl NativeHnswInner {
pub fn insert(&self, vector: &[f32]) -> crate::error::Result<usize> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.insert(vector),
HnswBackend::RaBitQ(rabitq) => rabitq.insert(vector),
HnswBackend::Sq8(sq8) => sq8.insert(vector),
}
}
pub fn parallel_insert(&self, vectors: &[&[f32]]) -> crate::error::Result<Vec<usize>> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.place_batch(vectors),
HnswBackend::RaBitQ(_) | HnswBackend::Sq8(_) => {
let mut ids = Vec::with_capacity(vectors.len());
for vector in vectors {
ids.push(self.insert(vector)?);
}
Ok(ids)
}
}
}
pub fn set_searching_mode(&mut self, mode: bool) {
match &mut self.backend {
HnswBackend::Standard(hnsw) => hnsw.set_searching_mode(mode),
HnswBackend::RaBitQ(rabitq) => rabitq.inner.set_searching_mode(mode),
HnswBackend::Sq8(sq8) => sq8.inner.set_searching_mode(mode),
}
}
pub fn reorder_for_locality(&self) -> crate::error::Result<Option<Vec<usize>>> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.reorder_for_locality(),
HnswBackend::RaBitQ(rabitq) => rabitq.reorder_for_locality(),
HnswBackend::Sq8(sq8) => sq8.reorder_for_locality(),
}
}
}
impl NativeHnswInner {
pub(crate) fn file_dump_counted(&self, path: &Path, basename: &str) -> std::io::Result<usize> {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.file_dump_counted(path, basename),
HnswBackend::RaBitQ(rabitq) => rabitq.inner.file_dump_counted(path, basename),
HnswBackend::Sq8(sq8) => sq8.inner.file_dump_counted(path, basename),
}
}
pub fn file_load_with_storage_mode(
path: &Path,
basename: &str,
metric: DistanceMetric,
dimension: usize,
storage_mode: crate::StorageMode,
) -> std::io::Result<Self> {
let distance = CachedSimdDistance::new_prenormalized(metric, dimension);
let arena_dir = match storage_mode {
crate::StorageMode::RaBitQ | crate::StorageMode::SQ8 => Some(path),
_ => None,
};
let inner = NativeHnsw::file_load_with_arena(path, basename, distance, arena_dir)?;
let backend = match storage_mode {
crate::StorageMode::RaBitQ => {
HnswBackend::RaBitQ(Box::new(RaBitQPrecisionHnsw::from_inner(inner, dimension)))
}
crate::StorageMode::SQ8 => {
HnswBackend::Sq8(Box::new(Sq8PrecisionHnsw::from_inner(inner, dimension)))
}
_ => HnswBackend::Standard(inner),
};
Ok(Self { backend, metric })
}
}
impl NativeHnswInner {
#[inline]
#[must_use]
pub fn transform_score(&self, raw_distance: f32) -> f32 {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.transform_score(raw_distance),
HnswBackend::RaBitQ(_) | HnswBackend::Sq8(_) => raw_distance,
}
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.len(),
HnswBackend::RaBitQ(rabitq) => rabitq.len(),
HnswBackend::Sq8(sq8) => sq8.len(),
}
}
#[allow(dead_code)] #[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.is_empty(),
HnswBackend::RaBitQ(rabitq) => rabitq.is_empty(),
HnswBackend::Sq8(sq8) => sq8.is_empty(),
}
}
#[allow(dead_code)] #[inline]
#[must_use]
pub fn metric(&self) -> DistanceMetric {
self.metric
}
#[inline]
#[must_use]
pub fn compute_distance(&self, a: &[f32], b: &[f32]) -> f32 {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.compute_distance(a, b),
HnswBackend::RaBitQ(rabitq) => rabitq.inner.compute_distance(a, b),
HnswBackend::Sq8(sq8) => sq8.inner.compute_distance(a, b),
}
}
#[inline]
pub fn with_contiguous_vectors<R: Default>(
&self,
f: impl FnOnce(&crate::perf_optimizations::ContiguousVectors) -> R,
) -> R {
match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.with_vectors_read(f),
HnswBackend::RaBitQ(rabitq) => rabitq.inner.with_vectors_read(f),
HnswBackend::Sq8(sq8) => sq8.inner.with_vectors_read(f),
}
}
#[cfg(test)]
#[inline]
pub fn with_contiguous_vectors_read<R: Default>(
&self,
f: impl FnOnce(&crate::perf_optimizations::ContiguousVectors) -> R,
) -> R {
self.with_contiguous_vectors(f)
}
}
#[must_use = "a placed slot left unmapped stays a tombstone"]
pub(crate) struct Placed<'guard> {
slot: usize,
_guard: std::marker::PhantomData<&'guard NativeHnswInner>,
}
impl<'guard> Placed<'guard> {
fn new(slot: usize) -> Self {
Self {
slot,
_guard: std::marker::PhantomData,
}
}
#[must_use]
pub(crate) fn slot(&self) -> usize {
self.slot
}
#[must_use]
pub(crate) fn into_slot(self) -> usize {
self.slot
}
pub(crate) fn installed(
guard: &'guard parking_lot::RwLockWriteGuard<'_, std::mem::ManuallyDrop<NativeHnswInner>>,
slot: usize,
) -> Self {
debug_assert!(
slot < guard.len(),
"slot {slot} is not in the installed graph ({} vectors)",
guard.len()
);
Self::new(slot)
}
#[cfg(test)]
pub(crate) fn for_test(slot: usize) -> Placed<'static> {
Placed::new(slot)
}
}
impl NativeHnswInner {
pub(crate) fn place(&self, vector: &[f32]) -> crate::error::Result<Placed<'_>> {
self.insert(vector).map(Placed::new)
}
pub(crate) fn place_parallel(
&self,
vectors: &[&[f32]],
) -> crate::error::Result<Vec<Placed<'_>>> {
Ok(self
.parallel_insert(vectors)?
.into_iter()
.map(Placed::new)
.collect())
}
pub(crate) fn place_unlinked(
&self,
vectors: &[&[f32]],
) -> crate::error::Result<Vec<Placed<'_>>> {
let first = match &self.backend {
HnswBackend::Standard(hnsw) => hnsw.push_unlinked(vectors),
HnswBackend::RaBitQ(rabitq) => rabitq.inner.push_unlinked(vectors),
HnswBackend::Sq8(sq8) => sq8.inner.push_unlinked(vectors),
}?;
Ok((first..first + vectors.len()).map(Placed::new).collect())
}
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<NativeHnswInner>();
};