diff --git a/crates/ruvector-cli/src/main.rs b/crates/ruvector-cli/src/main.rs index c1546e7d1..a2169d626 100644 --- a/crates/ruvector-cli/src/main.rs +++ b/crates/ruvector-cli/src/main.rs @@ -41,14 +41,14 @@ enum Commands { path: String, /// Vector dimensions - #[arg(short, long)] + #[arg(short = 'D', long)] dimensions: usize, }, /// Insert vectors from a file Insert { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, /// Input file path @@ -67,7 +67,7 @@ enum Commands { /// Search for similar vectors Search { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, /// Query vector (comma-separated floats or JSON array) @@ -86,14 +86,14 @@ enum Commands { /// Show database information Info { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, }, /// Run a quick performance benchmark Benchmark { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, /// Number of queries to run @@ -104,7 +104,7 @@ enum Commands { /// Export database to file Export { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, /// Output file path @@ -119,7 +119,7 @@ enum Commands { /// Import from other vector databases Import { /// Database file path - #[arg(short, long, default_value = "./ruvector.db")] + #[arg(short = 'b', long, default_value = "./ruvector.db")] db: String, /// Source database type (faiss, pinecone, weaviate) diff --git a/crates/ruvector-cli/tests/cli_tests.rs b/crates/ruvector-cli/tests/cli_tests.rs index 366ba88a5..45050ca53 100644 --- a/crates/ruvector-cli/tests/cli_tests.rs +++ b/crates/ruvector-cli/tests/cli_tests.rs @@ -192,9 +192,11 @@ fn test_benchmark_command() { #[test] fn test_error_handling() { - // Test with non-existent database + // Test with invalid database path - /dev/null is a device file, not a directory, + // so we cannot create a database file inside it. This guarantees failure + // regardless of user permissions. let mut cmd = Command::cargo_bin("ruvector").unwrap(); - cmd.arg("info").arg("--db").arg("/nonexistent/path/db.db"); + cmd.arg("info").arg("--db").arg("/dev/null/db.db"); cmd.assert() .failure() diff --git a/crates/ruvector-core/src/quantization.rs b/crates/ruvector-core/src/quantization.rs index ee2b9f2bc..03ed45a9e 100644 --- a/crates/ruvector-core/src/quantization.rs +++ b/crates/ruvector-core/src/quantization.rs @@ -30,11 +30,17 @@ impl QuantizedVector for ScalarQuantized { fn quantize(vector: &[f32]) -> Self { let min = vector.iter().copied().fold(f32::INFINITY, f32::min); let max = vector.iter().copied().fold(f32::NEG_INFINITY, f32::max); - let scale = (max - min) / 255.0; + + // Handle edge case where all values are the same (scale = 0) + let scale = if (max - min).abs() < f32::EPSILON { + 1.0 // Arbitrary non-zero scale when all values are identical + } else { + (max - min) / 255.0 + }; let data = vector .iter() - .map(|&v| ((v - min) / scale).round() as u8) + .map(|&v| ((v - min) / scale).round().clamp(0.0, 255.0) as u8) .collect(); Self { data, min, scale } @@ -42,11 +48,12 @@ impl QuantizedVector for ScalarQuantized { fn distance(&self, other: &Self) -> f32 { // Fast int8 distance calculation + // Use i32 to avoid overflow: max diff is 255, and 255*255=65025 fits in i32 self.data .iter() .zip(&other.data) .map(|(&a, &b)| { - let diff = a as i16 - b as i16; + let diff = a as i32 - b as i32; (diff * diff) as f32 }) .sum::() diff --git a/crates/ruvector-core/src/storage.rs b/crates/ruvector-core/src/storage.rs index e97785fbd..6e3b5ad19 100644 --- a/crates/ruvector-core/src/storage.rs +++ b/crates/ruvector-core/src/storage.rs @@ -6,7 +6,7 @@ #[cfg(feature = "storage")] use crate::error::{Result, RuvectorError}; #[cfg(feature = "storage")] -use crate::types::{VectorEntry, VectorId}; +use crate::types::{DbOptions, VectorEntry, VectorId}; #[cfg(feature = "storage")] use bincode::config; #[cfg(feature = "storage")] @@ -28,6 +28,10 @@ use std::sync::Arc; const VECTORS_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("vectors"); const METADATA_TABLE: TableDefinition<&str, &str> = TableDefinition::new("metadata"); +const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config"); + +/// Key used to store database configuration in CONFIG_TABLE +const DB_CONFIG_KEY: &str = "__ruvector_db_config__"; // Global database connection pool to allow multiple VectorDB instances // to share the same underlying database file @@ -112,6 +116,7 @@ impl VectorStorage { { let _ = write_txn.open_table(VECTORS_TABLE)?; let _ = write_txn.open_table(METADATA_TABLE)?; + let _ = write_txn.open_table(CONFIG_TABLE)?; } write_txn.commit()?; @@ -277,6 +282,46 @@ impl VectorStorage { Ok(ids) } + + /// Save database configuration to persistent storage + pub fn save_config(&self, options: &DbOptions) -> Result<()> { + let config_json = serde_json::to_string(options) + .map_err(|e| RuvectorError::SerializationError(e.to_string()))?; + + let write_txn = self.db.begin_write()?; + { + let mut table = write_txn.open_table(CONFIG_TABLE)?; + table.insert(DB_CONFIG_KEY, config_json.as_str())?; + } + write_txn.commit()?; + + Ok(()) + } + + /// Load database configuration from persistent storage + pub fn load_config(&self) -> Result> { + let read_txn = self.db.begin_read()?; + + // Try to open config table - may not exist in older databases + let table = match read_txn.open_table(CONFIG_TABLE) { + Ok(t) => t, + Err(_) => return Ok(None), + }; + + let Some(config_data) = table.get(DB_CONFIG_KEY)? else { + return Ok(None); + }; + + let config: DbOptions = serde_json::from_str(config_data.value()) + .map_err(|e| RuvectorError::SerializationError(e.to_string()))?; + + Ok(Some(config)) + } + + /// Get the stored dimensions + pub fn dimensions(&self) -> usize { + self.dimensions + } } // Add uuid dependency diff --git a/crates/ruvector-core/src/vector_db.rs b/crates/ruvector-core/src/vector_db.rs index 716baf0c8..8484db468 100644 --- a/crates/ruvector-core/src/vector_db.rs +++ b/crates/ruvector-core/src/vector_db.rs @@ -30,12 +30,50 @@ impl VectorDB { /// /// If a storage path is provided and contains persisted vectors, /// the HNSW index will be automatically rebuilt from storage. - pub fn new(options: DbOptions) -> Result { + /// If opening an existing database, the stored configuration (dimensions, + /// distance metric, etc.) will be used instead of the provided options. + pub fn new(mut options: DbOptions) -> Result { #[cfg(feature = "storage")] - let storage = Arc::new(VectorStorage::new( - &options.storage_path, - options.dimensions, - )?); + let storage = { + // First, try to load existing configuration from the database + // We create a temporary storage to check for config + let temp_storage = VectorStorage::new( + &options.storage_path, + options.dimensions, + )?; + + let stored_config = temp_storage.load_config()?; + + if let Some(config) = stored_config { + // Existing database - use stored configuration + tracing::info!( + "Loading existing database with {} dimensions", + config.dimensions + ); + options = DbOptions { + // Keep the provided storage path (may have changed) + storage_path: options.storage_path.clone(), + // Use stored configuration for everything else + dimensions: config.dimensions, + distance_metric: config.distance_metric, + hnsw_config: config.hnsw_config, + quantization: config.quantization, + }; + // Recreate storage with correct dimensions + Arc::new(VectorStorage::new( + &options.storage_path, + options.dimensions, + )?) + } else { + // New database - save the configuration + tracing::info!( + "Creating new database with {} dimensions", + options.dimensions + ); + temp_storage.save_config(&options)?; + Arc::new(temp_storage) + } + }; #[cfg(not(feature = "storage"))] let storage = Arc::new(VectorStorage::new(options.dimensions)?); diff --git a/crates/ruvector-core/tests/property_tests.rs b/crates/ruvector-core/tests/property_tests.rs index 242720088..ee9d0924b 100644 --- a/crates/ruvector-core/tests/property_tests.rs +++ b/crates/ruvector-core/tests/property_tests.rs @@ -12,12 +12,11 @@ use ruvector_core::types::DistanceMetric; // Distance Metric Properties // ============================================================================ -// Strategy to generate valid vectors +// Strategy to generate valid vectors with bounded values to prevent overflow +// Using range that won't overflow when squared: sqrt(f32::MAX) ≈ 1.84e19 +// We use a more conservative range for numerical stability in distance calculations fn vector_strategy(dim: usize) -> impl Strategy> { - prop::collection::vec( - any::().prop_filter("Must be finite", |x| x.is_finite()), - dim, - ) + prop::collection::vec(-1000.0f32..1000.0f32, dim) } // Strategy for normalized vectors (for cosine similarity) diff --git a/examples/ruvLLM/src/memory.rs b/examples/ruvLLM/src/memory.rs index a6826708d..f344b5712 100644 --- a/examples/ruvLLM/src/memory.rs +++ b/examples/ruvLLM/src/memory.rs @@ -176,7 +176,8 @@ struct MemoryStats { impl MemoryService { /// Create a new memory service pub async fn new(config: &MemoryConfig) -> Result { - let ml = 1.0 / (config.hnsw_m as f32).ln(); + // Note: ml (level multiplier) is computed per-insert in hnsw_insert() + // to avoid storing it and to handle edge cases properly Ok(Self { vectors: RwLock::new(Vec::new()), @@ -408,7 +409,10 @@ impl MemoryService { fn hnsw_insert(&self, node_idx: usize, vector: &[f32]) { let m = self.config.hnsw_m; let m_max = m * 2; - let ml = 1.0 / (m as f32).ln(); + // Guard against m=1 which would cause ln(1)=0 and division by zero + // Use m=2 as minimum for level calculation + let m_for_level = m.max(2) as f32; + let ml = 1.0 / m_for_level.ln(); // Determine level for this node let level = self.random_level(ml); @@ -549,7 +553,14 @@ impl MemoryService { fn random_level(&self, ml: f32) -> usize { let mut rng = rand::thread_rng(); let r: f32 = rng.gen(); - (-r.ln() * ml).floor() as usize + // Guard against r=0 which would cause ln(0) = -inf + // Also clamp result to prevent overflow when casting to usize + if r <= f32::EPSILON { + return 0; + } + let level = (-r.ln() * ml).floor(); + // Clamp to reasonable max level to prevent overflow + level.min(32.0) as usize } /// Insert an edge diff --git a/examples/ruvLLM/src/router.rs b/examples/ruvLLM/src/router.rs index e16add3e7..b9ec0ef96 100644 --- a/examples/ruvLLM/src/router.rs +++ b/examples/ruvLLM/src/router.rs @@ -666,7 +666,8 @@ fn softmax_array(x: &Array1) -> Array1 { let max = x.fold(f32::NEG_INFINITY, |a, &b| a.max(b)); let exp = x.mapv(|v| (v - max).exp()); let sum = exp.sum(); - exp / sum + // Guard against division by zero (all -inf inputs) + if sum > 0.0 { exp / sum } else { Array1::from_elem(len, 1.0 / len as f32) } } }