Security review of this PR found three HIGH issues, each demonstrated by running code rather than inferred. All three are fixed here, with regression tests that were verified to fail against the unfixed code before the fix was applied. HIGH-1 -- concurrent open regressed in ruvector-core. Moving DB_POOL to Weak without giving VectorStorage a Drop meant Weak::upgrade went None the instant the strong count hit zero, while redb's Database::drop (a write txn plus fsync) and the flock release were still pending. A concurrent VectorStorage::new then called Database::create against a still-locked file: 194/200 failures with "Database already open", against 0/200 on main. The Weak change is kept -- it fixes a real data-correctness bug -- and the lifetime is made explicit instead: VectorStorage now has a Drop that clears the slot and drops the Database while still holding the path guard, so the lock is released before a racing open can observe an empty slot. The pool is now per-path, so one path's fsync no longer blocks opens of unrelated paths, and dead entries are reaped rather than accumulating forever. HIGH-2 -- create_shard adopted a pre-existing file. open() verified the scope-to-filename binding but the create path did not, and VectorDB::new on an existing path silently inherits that file's stored config and vectors. A shard planted at a victim's computed filename while the index was open was adopted on the next insert, and because create_shard then rewrote the manifest to the victim scope, the rows were laundered: after a restart the binding check passed and the victim scope permanently contained them. Creation now refuses an existing file, and refuses again after opening if the database carries a manifest, is non-empty, or opened with unrequested dimensions or metric -- closing the check-to-open window. Neither refusal unlinks, so a planted file survives to be quarantined rather than deleted. HIGH-3 -- erase_scope resurrected data. Two index handles over one root shared the process-global pool while keeping independent in-memory indexes, so an erase reported success while the other handle kept the rows alive, and the erasing handle's next insert got the unlinked database back. open() now takes an exclusive advisory lock on the root, so a second handle fails loudly instead of silently sharing state. This also closes the divergence where one handle's search could not see another's insert while scope_stats reported the higher count. Also fixed: erase_scope now unlinks before mutating the catalog, so a failed unlink can no longer report an in-memory erase over a surviving file; a single unreadable shard-shaped file is quarantined and surfaced through quarantined_shards() instead of denying open() to every tenant; max_results is capped so usize::MAX is refused by validate() rather than reaching Vec::with_capacity and panicking out of a #![forbid(unsafe_code)] crate; and errors report the shard filename rather than an absolute path. The per-shard search counter is now type-enforced. Shard moved into a private module where ann_search is the only route to the ANN index and always increments, so an uncounted traversal no longer compiles. The counter previously guarded one of five db touch sites, which made the isolation claim close to tautological. The integration suite is parameterized over both FlatIndex and HNSW. It previously set hnsw_config: None throughout, so the claim that a tenant search never calls another tenant's ANN index was established with no ANN index in the process. The claim holds under HNSW. ADR renumbered 332 -> 334: 332 was already taken by the RF-sensing ADR merged earlier the same day, on a branch that already contained that merge. The ADR now also records that the manifest binds scope to filename and not to content, that there is no MAC over stored vectors, and that the global scope ceiling and eager open are explicit non-goals. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_016QSCkKnxDjqU49NVVpWMK5 |
||
|---|---|---|
| .. | ||
| benches | ||
| docs | ||
| examples | ||
| fuzz | ||
| src | ||
| tests | ||
| Cargo.toml | ||
| README.md | ||
Ruvector Core
The pure-Rust vector database engine behind RuVector -- HNSW indexing, quantization, and SIMD acceleration in a single crate.
ruvector-core is the foundational library that powers the entire RuVector ecosystem. It gives you a production-grade vector database you can embed directly into any Rust application: insert vectors, search them in under a millisecond, filter by metadata, and compress storage up to 32x -- all without external services. If you need vector search as a library instead of a server, this is the crate.
| ruvector-core | Typical Vector Database | |
|---|---|---|
| Deployment | Embed as a Rust dependency -- no server, no network calls | Run a separate service, manage connections |
| Query latency | <0.5 ms p50 at 1M vectors with HNSW | ~1-5 ms depending on network and index |
| Memory compression | Scalar (4x), Product (8-32x), Binary (32x) quantization built in | Often requires paid tiers or external tools |
| SIMD acceleration | SimSIMD hardware-optimized distance calculations, automatic | Manual tuning or not available |
| Search modes | Dense vectors, sparse BM25, hybrid, MMR diversity, filtered -- all in one API | Typically dense-only; hybrid and filtering are add-ons |
| Storage | Zero-copy mmap with redb -- instant loading, no deserialization |
Load time scales with dataset size |
| Concurrency | Lock-free indexing with parallel batch processing via Rayon | Varies; many require single-writer locks |
| Dependencies | Minimal -- pure Rust, compiles anywhere rustc runs |
Often depends on C/C++ libraries (BLAS, LAPACK) |
| Cost | Free forever -- open source (MIT) | Per-vector or per-query pricing on managed tiers |
Installation
Add ruvector-core to your Cargo.toml:
[dependencies]
ruvector-core = "0.1.0"
Feature Flags
[dependencies]
ruvector-core = { version = "0.1.0", features = ["simd", "uuid-support"] }
Available features:
simd(default): Enable SIMD-optimized distance calculationsuuid-support(default): Enable UUID generation for vector IDs
Key Features
| Feature | What It Does | Why It Matters |
|---|---|---|
| HNSW Indexing | Hierarchical Navigable Small World graphs for O(log n) approximate nearest neighbor search | Sub-millisecond queries at million-vector scale |
| Multiple Distance Metrics | Euclidean, Cosine, Dot Product, Manhattan | Match the metric to your embedding model without conversion |
| Scalar Quantization | Compress vectors to 8-bit integers (4x reduction) | Cut memory by 75% with 98% recall preserved |
| Product Quantization | Split vectors into subspaces with codebooks (8-32x reduction) | Store millions of vectors on a single machine |
| Binary Quantization | 1-bit representation (32x reduction) | Ultra-fast screening pass for massive datasets |
| SIMD Distance | Hardware-accelerated distance via SimSIMD | Up to 80K QPS on 8 cores without code changes |
| Zero-Copy I/O | Memory-mapped storage loads instantly | No deserialization step -- open a file and search immediately |
| Hybrid Search | Combine dense vector similarity with sparse BM25 text scoring | One query handles both semantic and keyword matching |
| Metadata Filtering | Apply key-value filters during search | No post-filtering needed -- results are already filtered |
| MMR Diversification | Maximal Marginal Relevance re-ranking | Avoid redundant results when top-K are too similar |
| Conformal Prediction | Uncertainty quantification on search results | Know when to trust (or distrust) a match |
| Lock-Free Indexing | Concurrent reads and writes without blocking | High-throughput ingestion while serving queries |
| Batch Processing | Parallel insert and search via Rayon | Saturate all cores for bulk operations |
Quick Start
Basic Usage
use ruvector_core::{VectorDB, DbOptions, VectorEntry, SearchQuery, DistanceMetric};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new vector database
let mut options = DbOptions::default();
options.dimensions = 384; // Vector dimensions
options.storage_path = "./my_vectors.db".to_string();
options.distance_metric = DistanceMetric::Cosine;
let db = VectorDB::new(options)?;
// Insert vectors
db.insert(VectorEntry {
id: Some("doc1".to_string()),
vector: vec![0.1, 0.2, 0.3, /* ... 384 dimensions */],
metadata: None,
})?;
db.insert(VectorEntry {
id: Some("doc2".to_string()),
vector: vec![0.4, 0.5, 0.6, /* ... 384 dimensions */],
metadata: None,
})?;
// Search for similar vectors
let results = db.search(SearchQuery {
vector: vec![0.1, 0.2, 0.3, /* ... 384 dimensions */],
k: 10, // Return top 10 results
filter: None,
ef_search: None,
})?;
for result in results {
println!("ID: {}, Score: {}", result.id, result.score);
}
Ok(())
}
Batch Operations
use ruvector_core::{VectorDB, VectorEntry};
// Insert multiple vectors efficiently
let entries = vec![
VectorEntry {
id: Some("doc1".to_string()),
vector: vec![0.1, 0.2, 0.3],
metadata: None,
},
VectorEntry {
id: Some("doc2".to_string()),
vector: vec![0.4, 0.5, 0.6],
metadata: None,
},
];
let ids = db.insert_batch(entries)?;
println!("Inserted {} vectors", ids.len());
With Metadata Filtering
use std::collections::HashMap;
use serde_json::json;
// Insert with metadata
db.insert(VectorEntry {
id: Some("product1".to_string()),
vector: vec![0.1, 0.2, 0.3],
metadata: Some(HashMap::from([
("category".to_string(), json!("electronics")),
("price".to_string(), json!(299.99)),
])),
})?;
// Search with metadata filter
let results = db.search(SearchQuery {
vector: vec![0.1, 0.2, 0.3],
k: 10,
filter: Some(HashMap::from([
("category".to_string(), json!("electronics")),
])),
ef_search: None,
})?;
HNSW Configuration
use ruvector_core::{DbOptions, HnswConfig, DistanceMetric};
let mut options = DbOptions::default();
options.dimensions = 384;
options.distance_metric = DistanceMetric::Cosine;
// Configure HNSW index parameters
options.hnsw_config = Some(HnswConfig {
m: 32, // Connections per layer (16-64 typical)
ef_construction: 200, // Build-time accuracy (100-500 typical)
ef_search: 100, // Search-time accuracy (50-200 typical)
max_elements: 10_000_000, // Maximum vectors
});
let db = VectorDB::new(options)?;
Quantization
use ruvector_core::{DbOptions, QuantizationConfig};
let mut options = DbOptions::default();
options.dimensions = 384;
// Enable scalar quantization (4x compression)
options.quantization = Some(QuantizationConfig::Scalar);
// Or product quantization (8-32x compression)
options.quantization = Some(QuantizationConfig::Product {
subspaces: 8, // Number of subspaces
k: 256, // Codebook size
});
let db = VectorDB::new(options)?;
API Overview
Core Types
// Main database interface
pub struct VectorDB { /* ... */ }
// Vector entry with optional ID and metadata
pub struct VectorEntry {
pub id: Option<VectorId>,
pub vector: Vec<f32>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
// Search query parameters
pub struct SearchQuery {
pub vector: Vec<f32>,
pub k: usize,
pub filter: Option<HashMap<String, serde_json::Value>>,
pub ef_search: Option<usize>,
}
// Search result with score
pub struct SearchResult {
pub id: VectorId,
pub score: f32,
pub vector: Option<Vec<f32>>,
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
Main Operations
impl VectorDB {
// Create new database with options
pub fn new(options: DbOptions) -> Result<Self>;
// Create with just dimensions (uses defaults)
pub fn with_dimensions(dimensions: usize) -> Result<Self>;
// Insert single vector
pub fn insert(&self, entry: VectorEntry) -> Result<VectorId>;
// Insert multiple vectors
pub fn insert_batch(&self, entries: Vec<VectorEntry>) -> Result<Vec<VectorId>>;
// Search for similar vectors
pub fn search(&self, query: SearchQuery) -> Result<Vec<SearchResult>>;
// Delete vector by ID
pub fn delete(&self, id: &str) -> Result<bool>;
// Get vector by ID
pub fn get(&self, id: &str) -> Result<Option<VectorEntry>>;
// Get total count
pub fn len(&self) -> Result<usize>;
// Check if empty
pub fn is_empty(&self) -> Result<bool>;
}
Distance Metrics
pub enum DistanceMetric {
Euclidean, // L2 distance - default for embeddings
Cosine, // Cosine similarity (1 - similarity)
DotProduct, // Negative dot product (for maximization)
Manhattan, // L1 distance
}
Advanced Features
// Hybrid search (dense + sparse)
use ruvector_core::{HybridSearch, HybridConfig};
let hybrid = HybridSearch::new(HybridConfig {
alpha: 0.7, // Balance between dense (0.7) and sparse (0.3)
..Default::default()
});
// Filtered search with expressions
use ruvector_core::{FilteredSearch, FilterExpression};
let filtered = FilteredSearch::new(db);
let expr = FilterExpression::And(vec![
FilterExpression::Equals("category".to_string(), json!("books")),
FilterExpression::GreaterThan("price".to_string(), json!(10.0)),
]);
// MMR diversification
use ruvector_core::{MMRSearch, MMRConfig};
let mmr = MMRSearch::new(MMRConfig {
lambda: 0.5, // Balance relevance (0.5) and diversity (0.5)
..Default::default()
});
Performance
Latency (Single Query)
Operation Flat Index HNSW Index
---------------------------------------------
Search (1K vecs) ~0.1ms ~0.2ms
Search (100K vecs) ~10ms ~0.5ms
Search (1M vecs) ~100ms <1ms
Insert ~0.1ms ~1ms
Batch (1000) ~50ms ~500ms
Memory Usage (1M Vectors, 384 Dimensions)
Configuration Memory Recall
---------------------------------------------
Full Precision (f32) ~1.5GB 100%
Scalar Quantization ~400MB 98%
Product Quantization ~200MB 95%
Binary Quantization ~50MB 85%
Throughput (Queries Per Second)
Configuration QPS Latency (p50)
-----------------------------------------------------
Single Thread ~2,000 ~0.5ms
Multi-Thread (8 cores) ~50,000 <0.5ms
With SIMD ~80,000 <0.3ms
With Quantization ~100,000 <0.2ms
Configuration Guide
For Maximum Accuracy
let options = DbOptions {
dimensions: 384,
distance_metric: DistanceMetric::Cosine,
hnsw_config: Some(HnswConfig {
m: 64,
ef_construction: 500,
ef_search: 200,
max_elements: 10_000_000,
}),
quantization: None, // Full precision
..Default::default()
};
For Maximum Speed
let options = DbOptions {
dimensions: 384,
distance_metric: DistanceMetric::DotProduct,
hnsw_config: Some(HnswConfig {
m: 16,
ef_construction: 100,
ef_search: 50,
max_elements: 10_000_000,
}),
quantization: Some(QuantizationConfig::Binary),
..Default::default()
};
For Balanced Performance
let options = DbOptions::default(); // Recommended defaults
Building and Testing
Build
# Build with default features
cargo build --release
# Build without SIMD
cargo build --release --no-default-features --features uuid-support
# Build for specific target with optimizations
RUSTFLAGS="-C target-cpu=native" cargo build --release
Testing
# Run all tests
cargo test
# Run with specific features
cargo test --features simd
# Run with logging
RUST_LOG=debug cargo test
Benchmarks
# Run all benchmarks
cargo bench
# Run specific benchmark
cargo bench --bench hnsw_search
# Run with features
cargo bench --features simd
Available benchmarks:
distance_metrics- SIMD-optimized distance calculationshnsw_search- HNSW index search performancequantization_bench- Quantization techniquesbatch_operations- Batch insert/search operationscomprehensive_bench- Full system benchmarks
Related Crates
ruvector-core is the foundation for platform-specific bindings:
- ruvector-node - Node.js bindings via NAPI-RS
- ruvector-wasm - WebAssembly bindings for browsers
- ruvector-gnn - Graph Neural Network layer for learned search
- ruvector-cli - Command-line interface
- ruvector-bench - Performance benchmarks
Documentation
- Main README - Complete project overview
- Getting Started Guide - Quick start tutorial
- Rust API Reference - Detailed API documentation
- Advanced Features Guide - Quantization, indexing, tuning
- Performance Tuning - Optimization strategies
- API Documentation - Full API reference on docs.rs
Acknowledgments
Built with state-of-the-art algorithms and libraries:
- hnsw_rs - HNSW implementation
- simsimd - SIMD distance calculations
- redb - Embedded database
- rayon - Data parallelism
- memmap2 - Memory-mapped files
License
MIT License - see LICENSE for details.