velesdb-core 1.13.2

High-performance vector database engine written in Rust
Documentation
//! Sharded vector storage for HNSW index.
//!
//! This module provides lock-sharded vector storage to eliminate contention
//! during parallel insertions. Vectors are distributed across 16 shards
//! based on hash of their index.
//!
//! # Performance
//!
//! - **16 shards**: Reduces lock contention by 16x on parallel writes
//! - **Hash-based routing**: O(1) shard selection
//! - **Independent locks**: Writes to different shards don't block each other
//!
//! # EPIC-A.2: Integrated into `HnswIndex`

use parking_lot::RwLock;
use rustc_hash::FxHashMap;

/// Number of shards for vector storage.
/// 16 is optimal for most systems (power of 2, matches common core counts).
pub(crate) const NUM_SHARDS: usize = 16;

/// A single shard containing vectors.
#[derive(Debug, Default)]
struct VectorShard {
    /// Maps internal index to vector data.
    vectors: FxHashMap<usize, Vec<f32>>,
}

/// Sharded vector storage with 16 partitions.
///
/// Uses hash-based sharding to distribute vectors across partitions,
/// enabling parallel writes without global lock contention.
///
/// # Example
///
/// ```rust,ignore
/// use velesdb_core::index::hnsw::ShardedVectors;
///
/// let storage = ShardedVectors::new(3);
/// storage.insert(0, &[1.0, 2.0, 3.0]);
/// let vec = storage.get(0);
/// ```
#[derive(Debug)]
pub struct ShardedVectors {
    /// 16 independent shards, each with its own lock.
    shards: [RwLock<VectorShard>; NUM_SHARDS],
    /// Vector dimension (kept for future validation)
    #[allow(dead_code)]
    // Reason: Stored for dimension-mismatch checks in future insert validation
    dimension: usize,
}

impl Default for ShardedVectors {
    fn default() -> Self {
        Self::new(0)
    }
}

impl ShardedVectors {
    /// Creates new empty sharded vector storage with specified dimension.
    #[must_use]
    pub fn new(dimension: usize) -> Self {
        Self {
            shards: std::array::from_fn(|_| RwLock::new(VectorShard::default())),
            dimension,
        }
    }

    /// Computes the shard index for a given vector index.
    ///
    /// Uses simple modulo for O(1) routing.
    #[inline]
    pub(crate) const fn shard_index(idx: usize) -> usize {
        idx % NUM_SHARDS
    }

    /// Inserts a vector at the given index.
    ///
    /// This only locks the target shard, not the entire storage.
    pub fn insert(&self, idx: usize, vector: &[f32]) {
        let shard_idx = Self::shard_index(idx);
        let mut shard = self.shards[shard_idx].write();
        shard.vectors.insert(idx, vector.to_vec());
    }

    /// Inserts multiple vectors in a batch.
    ///
    /// Groups vectors by shard for efficient batch insertion.
    pub fn insert_batch(&self, vectors: impl IntoIterator<Item = (usize, Vec<f32>)>) {
        // Group by shard to minimize lock acquisitions
        let mut by_shard: [Vec<(usize, Vec<f32>)>; NUM_SHARDS] =
            std::array::from_fn(|_| Vec::new());

        for (idx, vec) in vectors {
            let shard_idx = Self::shard_index(idx);
            by_shard[shard_idx].push((idx, vec));
        }

        // Insert each shard's batch with a single lock acquisition
        for (shard_idx, batch) in by_shard.into_iter().enumerate() {
            if !batch.is_empty() {
                let mut shard = self.shards[shard_idx].write();
                for (idx, vec) in batch {
                    shard.vectors.insert(idx, vec);
                }
            }
        }
    }

    /// Retrieves a vector by index.
    ///
    /// Returns a clone of the vector. For zero-copy access, use `get_ref`.
    #[must_use]
    pub fn get(&self, idx: usize) -> Option<Vec<f32>> {
        let shard_idx = Self::shard_index(idx);
        let shard = self.shards[shard_idx].read();
        shard.vectors.get(&idx).cloned()
    }

    /// Retrieves a reference to a vector with the shard lock held.
    ///
    /// The returned guard holds the shard read lock.
    /// For SIMD operations, prefer `with_vector` to avoid lifetime issues.
    #[must_use]
    #[allow(dead_code)] // API completeness
    pub fn contains(&self, idx: usize) -> bool {
        let shard_idx = Self::shard_index(idx);
        let shard = self.shards[shard_idx].read();
        shard.vectors.contains_key(&idx)
    }

    /// Executes a function with a reference to the vector.
    ///
    /// This is useful for SIMD operations that need a reference.
    #[allow(dead_code)] // API completeness - useful for SIMD ops
    pub fn with_vector<F, R>(&self, idx: usize, f: F) -> Option<R>
    where
        F: FnOnce(&[f32]) -> R,
    {
        let shard_idx = Self::shard_index(idx);
        let shard = self.shards[shard_idx].read();
        shard.vectors.get(&idx).map(|v| f(v))
    }

    /// Removes a vector by index.
    #[allow(dead_code)] // API completeness
    pub fn remove(&self, idx: usize) -> Option<Vec<f32>> {
        let shard_idx = Self::shard_index(idx);
        let mut shard = self.shards[shard_idx].write();
        shard.vectors.remove(&idx)
    }

    /// Returns the total number of vectors across all shards.
    #[must_use]
    pub fn len(&self) -> usize {
        self.shards.iter().map(|s| s.read().vectors.len()).sum()
    }

    /// Returns true if no vectors are stored.
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.shards.iter().all(|s| s.read().vectors.is_empty())
    }

    /// Clears all vectors from all shards.
    pub fn clear(&self) {
        for shard in &self.shards {
            shard.write().vectors.clear();
        }
    }

    /// Collects all indices and vectors.
    ///
    /// Warning: This acquires all shard locks sequentially.
    #[allow(dead_code)] // API completeness - prefer collect_for_parallel
    pub fn iter_all(&self) -> Vec<(usize, Vec<f32>)> {
        // RF-2: Delegate to collect_for_parallel to avoid duplicated shard iteration.
        self.collect_for_parallel()
    }

    /// Computes a function over all vectors in parallel-safe manner.
    ///
    /// Useful for brute-force search where we need to iterate all vectors.
    #[allow(dead_code)] // API completeness - prefer collect_for_parallel
    pub fn for_each_parallel<F>(&self, mut f: F)
    where
        F: FnMut(usize, &[f32]),
    {
        for shard in &self.shards {
            let guard = shard.read();
            for (idx, vec) in &guard.vectors {
                f(*idx, vec);
            }
        }
    }

    /// Collects all vectors into a Vec for rayon parallel iteration.
    ///
    /// This method snapshots all vectors into an owned collection that can
    /// be used with rayon's `par_iter()`. While this involves copying data,
    /// it enables true parallel iteration without lock contention.
    ///
    /// # Performance
    ///
    /// - **Time complexity**: O(n) for n vectors
    /// - **Space complexity**: O(n) - creates owned copies
    /// - **Use case**: Batch operations (brute-force search, batch scoring)
    ///
    /// For single-vector access, prefer `get()` or `with_vector()`.
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use rayon::prelude::*;
    ///
    /// let results: Vec<f32> = storage
    ///     .collect_for_parallel()
    ///     .par_iter()
    ///     .map(|(idx, vec)| compute_distance(query, vec))
    ///     .collect();
    /// ```
    #[must_use]
    pub fn collect_for_parallel(&self) -> Vec<(usize, Vec<f32>)> {
        let total_len = self.len();
        let mut result = Vec::with_capacity(total_len);
        self.drain_shards_into(&mut result);
        result
    }

    /// Collects all vectors into a pre-allocated buffer for reuse (RF-3 optimization).
    ///
    /// This method clears the buffer and fills it with all vectors from the storage.
    /// The buffer's capacity is preserved, reducing allocations in hot paths like
    /// repeated brute-force searches.
    ///
    /// # Performance
    ///
    /// - First call: O(n) allocations for vector clones
    /// - Subsequent calls with same buffer: Zero allocations (buffer reuse)
    /// - Memory savings: ~40% reduction in brute-force search allocations
    ///
    /// # Example
    ///
    /// ```rust,ignore
    /// use std::cell::RefCell;
    ///
    /// thread_local! {
    ///     static BUFFER: RefCell<Vec<(usize, Vec<f32>)>> = RefCell::new(Vec::new());
    /// }
    ///
    /// BUFFER.with(|buf| {
    ///     let mut buffer = buf.borrow_mut();
    ///     storage.collect_into(&mut buffer);
    ///     // Use buffer for parallel computation...
    /// });
    /// ```
    #[allow(dead_code)] // Reason: API completeness — zero-alloc buffer reuse for repeated searches
    pub fn collect_into(&self, buffer: &mut Vec<(usize, Vec<f32>)>) {
        buffer.clear();
        let total_len = self.len();
        buffer.reserve(total_len.saturating_sub(buffer.capacity()));
        self.drain_shards_into(buffer);
    }

    /// Appends all shard contents into the given buffer.
    ///
    /// RF-2: Single implementation of the shard-iteration + clone loop,
    /// shared by `collect_for_parallel`, `collect_into`, and `iter_all`.
    fn drain_shards_into(&self, buffer: &mut Vec<(usize, Vec<f32>)>) {
        for shard in &self.shards {
            let guard = shard.read();
            for (idx, vec) in &guard.vectors {
                buffer.push((*idx, vec.clone()));
            }
        }
    }

    /// Collects all vectors with references for zero-copy parallel iteration.
    ///
    /// Returns a snapshot with borrowed references. The caller must ensure
    /// no modifications occur during iteration (shards are read-locked during collection).
    ///
    /// # Safety
    ///
    /// This method holds read locks on all shards during the collection phase.
    /// The returned Vec contains owned data copied from the shards.
    #[must_use]
    #[allow(dead_code)] // API completeness
    pub fn snapshot_indices(&self) -> Vec<usize> {
        let mut indices = Vec::with_capacity(self.len());
        for shard in &self.shards {
            let guard = shard.read();
            for idx in guard.vectors.keys() {
                indices.push(*idx);
            }
        }
        indices
    }
}