use std::sync::Arc;
use parking_lot::{Mutex, RwLock};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use nitrite::collection::NitriteId;
use nitrite::common::Value;
use nitrite::errors::{ErrorKind, NitriteError, NitriteResult};
use nitrite::index::IndexDescriptor;
use nitrite::nitrite_config::NitriteConfig;
use nitrite::store::NitriteMap;
use crate::diskann::{DiskAnnConfig, DiskAnnIndex};
use crate::distance::Metric;
use crate::filter::value_to_vector;
use crate::hnsw::{DirtyChanges, Hnsw};
use crate::node::{from_bytes, to_bytes, HnswHeader};
use crate::precision::Precision;
const META_KEY: &str = "__hnsw_meta__";
const HNSW_FORMAT_VERSION: u32 = 2;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct StoredHeader {
format_version: u32,
precision: Precision,
graph: HnswHeader,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IndexBackend {
Hnsw,
DiskAnn,
}
#[derive(Debug, Clone, Copy)]
pub struct VectorIndexConfig {
pub dim: usize,
pub metric: Metric,
pub backend: IndexBackend,
pub precision: Precision,
pub m: usize,
pub ef_construction: usize,
pub ef_search: usize,
pub diskann: DiskAnnConfig,
}
impl VectorIndexConfig {
pub fn new(dim: usize, metric: Metric) -> Self {
VectorIndexConfig {
dim,
metric,
backend: IndexBackend::Hnsw,
precision: Precision::F32,
m: 16,
ef_construction: 200,
ef_search: 64,
diskann: DiskAnnConfig::default(),
}
}
pub fn backend(mut self, backend: IndexBackend) -> Self {
self.backend = backend;
self
}
pub fn precision(mut self, precision: Precision) -> Self {
self.precision = precision;
self
}
pub fn with_m(mut self, m: usize) -> Self {
self.m = m;
self
}
pub fn with_ef_construction(mut self, ef: usize) -> Self {
self.ef_construction = ef;
self
}
pub fn with_ef_search(mut self, ef: usize) -> Self {
self.ef_search = ef;
self
}
pub fn cache_bytes(mut self, bytes: usize) -> Self {
self.diskann.cache_bytes = bytes;
self
}
pub fn degree(mut self, degree: usize) -> Self {
self.diskann.degree = degree;
self
}
pub fn build_beam(mut self, beam: usize) -> Self {
self.diskann.build_beam = beam;
self
}
pub fn search_beam(mut self, beam: usize) -> Self {
self.diskann.search_beam = beam;
self
}
pub fn alpha(mut self, alpha: f32) -> Self {
self.diskann.alpha = alpha;
self
}
pub fn pq_subvectors(mut self, m: usize) -> Self {
self.diskann.pq_subvectors = m;
self
}
pub fn pq_train_threshold(mut self, n: usize) -> Self {
self.diskann.pq_train_threshold = n;
self
}
pub fn consolidate_threshold(mut self, n: usize) -> Self {
self.diskann.consolidate_threshold = n;
self
}
}
impl Default for VectorIndexConfig {
fn default() -> Self {
VectorIndexConfig::new(0, Metric::Cosine)
}
}
pub fn derive_vector_map_name(descriptor: &IndexDescriptor) -> String {
let collection = descriptor.collection_name();
let fields = descriptor.index_fields().field_names().join("_");
format!("{}_{}_vector_idx", collection, fields)
}
#[derive(Clone)]
pub enum VectorIndex {
Hnsw(HnswBackend),
DiskAnn(DiskAnnIndex),
}
impl VectorIndex {
pub fn open(
descriptor: &IndexDescriptor,
config: &NitriteConfig,
params: &VectorIndexConfig,
) -> NitriteResult<Self> {
let base = derive_vector_map_name(descriptor);
let (index, needs_rebuild) = match params.backend {
IndexBackend::Hnsw => {
let (backend, rebuild) = HnswBackend::open(&base, config, params)?;
(VectorIndex::Hnsw(backend), rebuild)
}
IndexBackend::DiskAnn => {
let (backend, rebuild) = DiskAnnIndex::open(
config,
&base,
params.dim,
params.metric,
params.precision,
¶ms.diskann,
)?;
(VectorIndex::DiskAnn(backend), rebuild)
}
};
if needs_rebuild {
let n = rebuild_from_collection(&index, descriptor, config)?;
log::info!("vector index '{base}' was stale or damaged; rebuilt from collection ({n} vectors)");
index.flush()?;
}
Ok(index)
}
pub fn metric(&self) -> Metric {
match self {
VectorIndex::Hnsw(b) => b.metric(),
VectorIndex::DiskAnn(b) => b.metric(),
}
}
pub fn len(&self) -> usize {
match self {
VectorIndex::Hnsw(b) => b.len(),
VectorIndex::DiskAnn(b) => b.len(),
}
}
pub fn is_empty(&self) -> bool {
match self {
VectorIndex::Hnsw(b) => b.is_empty(),
VectorIndex::DiskAnn(b) => b.is_empty(),
}
}
pub fn insert(&self, id: u64, vector: Vec<f32>) -> NitriteResult<()> {
match self {
VectorIndex::Hnsw(b) => b.insert(id, vector),
VectorIndex::DiskAnn(b) => b.insert(id, vector),
}
}
pub fn remove(&self, id: u64) -> NitriteResult<()> {
match self {
VectorIndex::Hnsw(b) => b.remove(id),
VectorIndex::DiskAnn(b) => b.remove(id),
}
}
pub fn search(
&self,
query: &[f32],
k: usize,
ef: Option<usize>,
) -> NitriteResult<Vec<(NitriteId, f32)>> {
match self {
VectorIndex::Hnsw(b) => b.search(query, k, ef),
VectorIndex::DiskAnn(b) => b.search(query, k, ef),
}
}
pub fn flush(&self) -> NitriteResult<()> {
match self {
VectorIndex::Hnsw(_) => Ok(()),
VectorIndex::DiskAnn(b) => b.flush(),
}
}
pub fn destroy(&self, base: &str, config: &NitriteConfig) -> NitriteResult<()> {
match self {
VectorIndex::Hnsw(_) => config.nitrite_store()?.remove_map(base),
VectorIndex::DiskAnn(b) => b.destroy(),
}
}
}
fn rebuild_from_collection(
index: &VectorIndex,
descriptor: &IndexDescriptor,
config: &NitriteConfig,
) -> NitriteResult<usize> {
let field = descriptor
.index_fields()
.field_names()
.first()
.cloned()
.ok_or_else(|| {
NitriteError::new("Vector index has no field", ErrorKind::IndexingError)
})?;
let store = config.nitrite_store()?;
let collection = store.open_map(&descriptor.collection_name())?;
let mut count = 0usize;
for entry in collection.entries()? {
let (key, value) = entry?;
let Value::Document(mut doc) = value else { continue };
let id = match key {
Value::NitriteId(nid) => nid.id_value(),
_ => match doc.id() {
Ok(nid) => nid.id_value(),
Err(_) => continue,
},
};
let Ok(field_value) = doc.get(&field) else { continue };
let Some(vector) = value_to_vector(&field_value) else { continue };
index.insert(id, vector)?;
count += 1;
}
Ok(count)
}
#[derive(Clone)]
pub struct HnswBackend {
inner: Arc<HnswInner>,
}
struct HnswInner {
hnsw: RwLock<Hnsw>,
persist_gate: Mutex<()>,
map: NitriteMap,
precision: Precision,
}
impl HnswBackend {
pub fn open(
base: &str,
config: &NitriteConfig,
params: &VectorIndexConfig,
) -> NitriteResult<(Self, bool)> {
let store = config.nitrite_store()?;
let map = store.open_map(base)?;
let stored = match map.get(&Value::String(META_KEY.to_string()))? {
Some(Value::Bytes(bytes)) => match decode::<StoredHeader>(&bytes) {
Ok(h) if h.format_version == HNSW_FORMAT_VERSION => Some(Ok(h)),
_ => Some(Err(())), },
_ if map.is_empty()? => None, _ => Some(Err(())), };
let (graph, precision, needs_rebuild) = match stored {
Some(Ok(header)) => {
let precision = header.precision;
let (vectors, adjacency) = load_parts(&map, &header)?;
(Hnsw::from_parts(header.graph, vectors, adjacency), precision, false)
}
Some(Err(())) => {
log::warn!("HNSW index '{base}' has an unreadable header; wiping for rebuild");
map.clear()?;
let graph = new_graph(params)?;
(graph, params.precision, true)
}
None => (new_graph(params)?, params.precision, false),
};
let backend = HnswBackend {
inner: Arc::new(HnswInner {
hnsw: RwLock::new(graph),
persist_gate: Mutex::new(()),
map,
precision,
}),
};
let header = backend.inner.hnsw.read().header();
backend.persist(DirtyChanges::default(), header)?;
Ok((backend, needs_rebuild))
}
pub fn dim(&self) -> usize {
self.inner.hnsw.read().header().dim
}
pub fn metric(&self) -> Metric {
self.inner.hnsw.read().metric()
}
pub fn len(&self) -> usize {
self.inner.hnsw.read().len()
}
pub fn is_empty(&self) -> bool {
self.inner.hnsw.read().is_empty()
}
pub fn insert(&self, id: u64, vector: Vec<f32>) -> NitriteResult<()> {
let (changes, header) = {
let mut guard = self.inner.hnsw.write();
guard.insert(id, vector).map_err(|e| {
NitriteError::new(&format!("Vector insert failed: {e}"), ErrorKind::IndexingError)
})?;
(guard.take_dirty(), guard.header())
};
self.persist(changes, header)
}
pub fn remove(&self, id: u64) -> NitriteResult<()> {
let (changes, header) = {
let mut guard = self.inner.hnsw.write();
guard.remove(id);
(guard.take_dirty(), guard.header())
};
self.persist(changes, header)
}
pub fn search(
&self,
query: &[f32],
k: usize,
ef: Option<usize>,
) -> NitriteResult<Vec<(NitriteId, f32)>> {
let guard = self.inner.hnsw.read();
guard
.search(query, k, ef)
.into_iter()
.map(|(id, dist)| NitriteId::create_id(id).map(|nid| (nid, dist)))
.collect()
}
fn persist(&self, changes: DirtyChanges, header: HnswHeader) -> NitriteResult<()> {
let _gate = self.inner.persist_gate.lock();
for id in &changes.deleted {
self.inner.map.remove(&Value::String(format!("v{id}")))?;
self.inner.map.remove(&Value::String(format!("a{id}")))?;
}
let mut batch: Vec<(Value, Value)> =
Vec::with_capacity(changes.vectors.len() + changes.adjacency.len() + 1);
for (id, vector) in &changes.vectors {
batch.push((
Value::String(format!("v{id}")),
Value::Bytes(self.inner.precision.encode(vector)),
));
}
for (id, neighbors) in &changes.adjacency {
batch.push((
Value::String(format!("a{id}")),
Value::Bytes(encode(neighbors)?),
));
}
let stored = StoredHeader {
format_version: HNSW_FORMAT_VERSION,
precision: self.inner.precision,
graph: header,
};
batch.push((Value::String(META_KEY.to_string()), Value::Bytes(encode(&stored)?)));
self.inner.map.put_all(batch)
}
}
fn new_graph(params: &VectorIndexConfig) -> NitriteResult<Hnsw> {
if params.dim == 0 {
return Err(NitriteError::new(
"Vector index dimension must be greater than zero",
ErrorKind::IndexingError,
));
}
Ok(Hnsw::new(
params.dim,
params.metric,
params.m,
params.ef_construction,
params.ef_search,
))
}
#[allow(clippy::type_complexity)]
fn load_parts(
map: &NitriteMap,
header: &StoredHeader,
) -> NitriteResult<(Vec<(u64, Vec<f32>)>, FxHashMap<u64, Vec<Vec<u64>>>)> {
let mut vectors = Vec::new();
let mut adjacency = FxHashMap::default();
let expected_len = header.precision.encoded_len(header.graph.dim);
for entry in map.entries()? {
let (key, value) = entry?;
let Value::String(key) = key else { continue };
let Value::Bytes(bytes) = value else { continue };
if let Some(id_str) = key.strip_prefix('v') {
if let Ok(id) = id_str.parse::<u64>() {
if bytes.len() == expected_len {
vectors.push((id, header.precision.decode(&bytes, header.graph.dim)));
}
}
} else if let Some(id_str) = key.strip_prefix('a') {
if let Ok(id) = id_str.parse::<u64>() {
if let Ok(neighbors) = decode::<Vec<Vec<u64>>>(&bytes) {
adjacency.insert(id, neighbors);
}
}
}
}
Ok((vectors, adjacency))
}
fn encode<T: serde::Serialize>(value: &T) -> NitriteResult<Vec<u8>> {
to_bytes(value)
.map_err(|e| NitriteError::new(&format!("encode failed: {e}"), ErrorKind::IndexingError))
}
fn decode<T: for<'de> serde::Deserialize<'de>>(bytes: &[u8]) -> NitriteResult<T> {
from_bytes(bytes)
.map_err(|e| NitriteError::new(&format!("decode failed: {e}"), ErrorKind::IndexingError))
}