weavatrix-search-vector 0.3.1

Persistent, mutable, bounded vector candidate search for Rust and Weavatrix
Documentation
use super::MutableVectorIndex;
use crate::error::SearchError;
use crate::hnsw::VectorIndex;
use std::sync::Arc;

impl MutableVectorIndex {
    /// Rebuilds only the mutable delta into its own HNSW graph.
    ///
    /// This bounds exact staged scanning without rebuilding the immutable
    /// base. Concurrent mutations are retried and never lost.
    ///
    /// # Errors
    ///
    /// Returns a build error or [`SearchError::MutationConflict`] after three
    /// conflicting attempts.
    pub fn seal_delta(&self) -> Result<(), SearchError> {
        for _ in 0..3 {
            let (generation, sealed, pending, deleted) = {
                let state = self
                    .state
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                (
                    state.generation,
                    state.sealed.clone(),
                    state.pending.clone(),
                    state.deleted.clone(),
                )
            };
            if pending.is_empty() {
                return Ok(());
            }
            let mut owned = Vec::new();
            owned
                .try_reserve_exact(
                    sealed
                        .as_ref()
                        .map_or(0, |index| index.len())
                        .saturating_add(pending.len()),
                )
                .map_err(|_| SearchError::AllocationFailed)?;
            if let Some(index) = sealed {
                for key in index.keys() {
                    if deleted.contains(&key) || pending.contains_key(&key) {
                        continue;
                    }
                    owned.push((
                        key,
                        index
                            .vector(key)
                            .ok_or(SearchError::MissingKey(key))?
                            .to_vec(),
                    ));
                }
            }
            owned.extend(pending.iter().map(|(key, vector)| (*key, vector.clone())));
            owned.sort_unstable_by_key(|record| record.0);
            let borrowed = owned
                .iter()
                .map(|(key, vector)| (*key, vector.as_slice()))
                .collect::<Vec<_>>();
            let rebuilt = Arc::new(VectorIndex::build(self.config.clone(), &borrowed)?);
            let mut state = self
                .state
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            if state.generation != generation {
                continue;
            }
            state.sealed = Some(rebuilt);
            state.pending.clear();
            state.generation = state.generation.wrapping_add(1);
            return Ok(());
        }
        Err(SearchError::MutationConflict)
    }

    /// Deterministically rebuilds the immutable base. Concurrent mutations are
    /// detected and retried without losing updates.
    ///
    /// # Errors
    ///
    /// Returns a build error or [`SearchError::MutationConflict`] after three
    /// conflicting rebuilds.
    pub fn compact(&self) -> Result<(), SearchError> {
        for _ in 0..3 {
            let (generation, base, sealed, pending, deleted) = {
                let state = self
                    .state
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                (
                    state.generation,
                    Arc::clone(&state.base),
                    state.sealed.clone(),
                    state.pending.clone(),
                    state.deleted.clone(),
                )
            };
            let mut owned = Vec::new();
            owned
                .try_reserve_exact(
                    base.len()
                        .saturating_sub(deleted.len())
                        .saturating_add(sealed.as_ref().map_or(0, |index| index.len()))
                        .saturating_add(pending.len()),
                )
                .map_err(|_| SearchError::AllocationFailed)?;
            for key in base.keys() {
                if deleted.contains(&key)
                    || pending.contains_key(&key)
                    || sealed
                        .as_ref()
                        .is_some_and(|index| index.vector(key).is_some())
                {
                    continue;
                }
                let vector = base.vector(key).ok_or(SearchError::MissingKey(key))?;
                owned.push((key, vector.to_vec()));
            }
            if let Some(index) = sealed {
                for key in index.keys() {
                    if deleted.contains(&key) || pending.contains_key(&key) {
                        continue;
                    }
                    let vector = index.vector(key).ok_or(SearchError::MissingKey(key))?;
                    owned.push((key, vector.to_vec()));
                }
            }
            owned.extend(pending.iter().map(|(key, vector)| (*key, vector.clone())));
            owned.sort_unstable_by_key(|record| record.0);
            let borrowed = owned
                .iter()
                .map(|(key, vector)| (*key, vector.as_slice()))
                .collect::<Vec<_>>();
            let rebuilt = Arc::new(VectorIndex::build(self.config.clone(), &borrowed)?);
            let mut state = self
                .state
                .write()
                .unwrap_or_else(std::sync::PoisonError::into_inner);
            if state.generation != generation {
                continue;
            }
            state.base = rebuilt;
            state.sealed = None;
            state.pending.clear();
            state.deleted.clear();
            return Ok(());
        }
        Err(SearchError::MutationConflict)
    }
}