//! Node.js bindings for Ruvector via NAPI-RS //! //! High-performance Rust vector database with zero-copy buffer sharing, //! async/await support, and complete TypeScript type definitions. #![deny(clippy::all)] #![warn(clippy::pedantic)] use napi::bindgen_prelude::*; use napi_derive::napi; use ruvector_core::{ types::{DbOptions, HnswConfig, QuantizationConfig}, DistanceMetric, SearchQuery, SearchResult, VectorDB as CoreVectorDB, VectorEntry, }; use std::sync::Arc; use std::sync::RwLock; use std::time::{SystemTime, UNIX_EPOCH}; // Import new crates use ruvector_collections::CollectionManager as CoreCollectionManager; use ruvector_filter::FilterExpression; use ruvector_metrics::{gather_metrics, HealthChecker, HealthStatus}; use std::path::PathBuf; /// Distance metric for similarity calculation #[napi(string_enum)] #[derive(Debug)] pub enum JsDistanceMetric { /// Euclidean (L2) distance Euclidean, /// Cosine similarity (converted to distance) Cosine, /// Dot product (converted to distance for maximization) DotProduct, /// Manhattan (L1) distance Manhattan, } impl From for DistanceMetric { fn from(metric: JsDistanceMetric) -> Self { match metric { JsDistanceMetric::Euclidean => DistanceMetric::Euclidean, JsDistanceMetric::Cosine => DistanceMetric::Cosine, JsDistanceMetric::DotProduct => DistanceMetric::DotProduct, JsDistanceMetric::Manhattan => DistanceMetric::Manhattan, } } } /// Quantization configuration #[napi(object)] #[derive(Debug, Clone)] pub struct JsQuantizationConfig { /// Quantization type: "none", "scalar", "product", "binary" pub r#type: String, /// Number of subspaces (for product quantization) pub subspaces: Option, /// Codebook size (for product quantization) pub k: Option, } impl From for QuantizationConfig { fn from(config: JsQuantizationConfig) -> Self { match config.r#type.as_str() { "none" => QuantizationConfig::None, "scalar" => QuantizationConfig::Scalar, "product" => QuantizationConfig::Product { subspaces: config.subspaces.unwrap_or(16) as usize, k: config.k.unwrap_or(256) as usize, }, "binary" => QuantizationConfig::Binary, _ => QuantizationConfig::Scalar, } } } /// HNSW index configuration #[napi(object)] #[derive(Debug, Clone)] pub struct JsHnswConfig { /// Number of connections per layer (M) pub m: Option, /// Size of dynamic candidate list during construction pub ef_construction: Option, /// Size of dynamic candidate list during search pub ef_search: Option, /// Maximum number of elements pub max_elements: Option, } impl From for HnswConfig { fn from(config: JsHnswConfig) -> Self { HnswConfig { m: config.m.unwrap_or(32) as usize, ef_construction: config.ef_construction.unwrap_or(200) as usize, ef_search: config.ef_search.unwrap_or(100) as usize, max_elements: config.max_elements.unwrap_or(10_000_000) as usize, } } } /// Database configuration options #[napi(object)] #[derive(Debug)] pub struct JsDbOptions { /// Vector dimensions pub dimensions: u32, /// Distance metric pub distance_metric: Option, /// Storage path pub storage_path: Option, /// HNSW configuration pub hnsw_config: Option, /// Quantization configuration pub quantization: Option, } impl From for DbOptions { fn from(options: JsDbOptions) -> Self { DbOptions { dimensions: options.dimensions as usize, distance_metric: options .distance_metric .map(Into::into) .unwrap_or(DistanceMetric::Cosine), storage_path: options .storage_path .unwrap_or_else(|| "./ruvector.db".to_string()), hnsw_config: options.hnsw_config.map(Into::into), quantization: options.quantization.map(Into::into), } } } /// Vector entry #[napi(object)] pub struct JsVectorEntry { /// Optional ID (auto-generated if not provided) pub id: Option, /// Vector data as Float32Array or array of numbers pub vector: Float32Array, /// Optional metadata as JSON string (use JSON.stringify on objects) pub metadata: Option, } impl JsVectorEntry { fn to_core(&self) -> Result { // Parse JSON string to HashMap let metadata = self.metadata.as_ref().and_then(|s| { serde_json::from_str::>(s).ok() }); Ok(VectorEntry { id: self.id.clone(), vector: self.vector.to_vec(), metadata, }) } } /// Search query parameters #[napi(object)] pub struct JsSearchQuery { /// Query vector as Float32Array or array of numbers pub vector: Float32Array, /// Number of results to return (top-k) pub k: u32, /// Optional ef_search parameter for HNSW pub ef_search: Option, /// Optional metadata filter as JSON string (use JSON.stringify on objects) pub filter: Option, } impl JsSearchQuery { fn to_core(&self) -> Result { // Parse JSON string to HashMap let filter = self.filter.as_ref().and_then(|s| { serde_json::from_str::>(s).ok() }); Ok(SearchQuery { vector: self.vector.to_vec(), k: self.k as usize, filter, ef_search: self.ef_search.map(|v| v as usize), }) } } /// Search result with similarity score #[napi(object)] #[derive(Clone)] pub struct JsSearchResult { /// Vector ID pub id: String, /// Distance/similarity score (lower is better for distance metrics) pub score: f64, /// Vector data (if requested) pub vector: Option, /// Metadata as JSON string (use JSON.parse to convert to object) pub metadata: Option, } impl From for JsSearchResult { fn from(result: SearchResult) -> Self { // Convert Vec to Float32Array let vector = result.vector.map(|v| Float32Array::new(v)); // Convert HashMap to JSON string let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok()); JsSearchResult { id: result.id, score: f64::from(result.score), vector, metadata, } } } /// High-performance vector database with HNSW indexing #[napi] pub struct VectorDB { inner: Arc>, } #[napi] impl VectorDB { /// Create a new vector database with the given options /// /// # Example /// ```javascript /// const db = new VectorDB({ /// dimensions: 384, /// distanceMetric: 'Cosine', /// storagePath: './vectors.db', /// hnswConfig: { /// m: 32, /// efConstruction: 200, /// efSearch: 100 /// } /// }); /// ``` #[napi(constructor)] pub fn new(options: JsDbOptions) -> Result { let core_options: DbOptions = options.into(); let db = CoreVectorDB::new(core_options) .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?; Ok(Self { inner: Arc::new(RwLock::new(db)), }) } /// Create a vector database with default options /// /// # Example /// ```javascript /// const db = VectorDB.withDimensions(384); /// ``` #[napi(factory)] pub fn with_dimensions(dimensions: u32) -> Result { let db = CoreVectorDB::with_dimensions(dimensions as usize) .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?; Ok(Self { inner: Arc::new(RwLock::new(db)), }) } /// Insert a vector entry into the database /// /// Returns the ID of the inserted vector (auto-generated if not provided) /// /// # Example /// ```javascript /// const id = await db.insert({ /// vector: new Float32Array([1.0, 2.0, 3.0]), /// metadata: { text: 'example' } /// }); /// ``` #[napi] pub async fn insert(&self, entry: JsVectorEntry) -> Result { let core_entry = entry.to_core()?; let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.insert(core_entry) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Insert failed: {}", e))) } /// Insert multiple vectors in a batch /// /// Returns an array of IDs for the inserted vectors /// /// # Example /// ```javascript /// const ids = await db.insertBatch([ /// { vector: new Float32Array([1, 2, 3]) }, /// { vector: new Float32Array([4, 5, 6]) } /// ]); /// ``` #[napi] pub async fn insert_batch(&self, entries: Vec) -> Result> { let core_entries: Result> = entries.iter().map(|e| e.to_core()).collect(); let core_entries = core_entries?; let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.insert_batch(core_entries) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e))) } /// Search for similar vectors /// /// Returns an array of search results sorted by similarity /// /// # Example /// ```javascript /// const results = await db.search({ /// vector: new Float32Array([1, 2, 3]), /// k: 10, /// filter: { category: 'example' } /// }); /// ``` #[napi] pub async fn search(&self, query: JsSearchQuery) -> Result> { let core_query = query.to_core()?; let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.search(core_query) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Search failed: {}", e))) .map(|results| results.into_iter().map(Into::into).collect()) } /// Delete a vector by ID /// /// Returns true if the vector was deleted, false if not found /// /// # Example /// ```javascript /// const deleted = await db.delete('vector-id'); /// ``` #[napi] pub async fn delete(&self, id: String) -> Result { let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.delete(&id) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Delete failed: {}", e))) } /// Get a vector by ID /// /// Returns the vector entry if found, null otherwise /// /// # Example /// ```javascript /// const entry = await db.get('vector-id'); /// if (entry) { /// console.log('Found:', entry.metadata); /// } /// ``` #[napi] pub async fn get(&self, id: String) -> Result> { let db = self.inner.clone(); let result = tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.get(&id) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?; Ok(result.map(|entry| { // Convert HashMap to JSON string let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok()); JsVectorEntry { id: entry.id, vector: Float32Array::new(entry.vector), metadata, } })) } /// Get the number of vectors in the database /// /// # Example /// ```javascript /// const count = await db.len(); /// console.log(`Database contains ${count} vectors`); /// ``` #[napi] pub async fn len(&self) -> Result { let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.len() }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Len failed: {}", e))) .map(|len| len as u32) } /// Check if the database is empty /// /// # Example /// ```javascript /// if (await db.isEmpty()) { /// console.log('Database is empty'); /// } /// ``` #[napi] pub async fn is_empty(&self) -> Result { let db = self.inner.clone(); tokio::task::spawn_blocking(move || { let db = db.read().expect("RwLock poisoned"); db.is_empty() }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e))) } } /// Get the version of the Ruvector library #[napi] pub fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } /// Test function to verify the bindings are working #[napi] pub fn hello() -> String { "Hello from Ruvector Node.js bindings!".to_string() } /// Filter for metadata-based search #[napi(object)] #[derive(Debug, Clone)] pub struct JsFilter { /// Field name to filter on pub field: String, /// Operator: "eq", "ne", "gt", "gte", "lt", "lte", "in", "match" pub operator: String, /// Value to compare against (JSON string) pub value: String, } impl JsFilter { fn to_filter_expression(&self) -> Result { let value: serde_json::Value = serde_json::from_str(&self.value) .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?; Ok(match self.operator.as_str() { "eq" => FilterExpression::eq(&self.field, value), "ne" => FilterExpression::ne(&self.field, value), "gt" => FilterExpression::gt(&self.field, value), "gte" => FilterExpression::gte(&self.field, value), "lt" => FilterExpression::lt(&self.field, value), "lte" => FilterExpression::lte(&self.field, value), "match" => FilterExpression::Match { field: self.field.clone(), text: value.as_str().unwrap_or("").to_string(), }, _ => FilterExpression::eq(&self.field, value), }) } } /// Collection configuration #[napi(object)] #[derive(Debug, Clone)] pub struct JsCollectionConfig { /// Vector dimensions pub dimensions: u32, /// Distance metric pub distance_metric: Option, /// HNSW configuration pub hnsw_config: Option, /// Quantization configuration pub quantization: Option, } impl From for ruvector_collections::CollectionConfig { fn from(config: JsCollectionConfig) -> Self { ruvector_collections::CollectionConfig { dimensions: config.dimensions as usize, distance_metric: config .distance_metric .map(Into::into) .unwrap_or(DistanceMetric::Cosine), hnsw_config: config.hnsw_config.map(Into::into), quantization: config.quantization.map(Into::into), on_disk_payload: true, } } } /// Collection statistics #[napi(object)] #[derive(Debug, Clone)] pub struct JsCollectionStats { /// Number of vectors in the collection pub vectors_count: u32, /// Disk space used in bytes pub disk_size_bytes: i64, /// RAM space used in bytes pub ram_size_bytes: i64, } impl From for JsCollectionStats { fn from(stats: ruvector_collections::CollectionStats) -> Self { JsCollectionStats { vectors_count: stats.vectors_count as u32, disk_size_bytes: stats.disk_size_bytes as i64, ram_size_bytes: stats.ram_size_bytes as i64, } } } /// Collection alias #[napi(object)] #[derive(Debug, Clone)] pub struct JsAlias { /// Alias name pub alias: String, /// Collection name pub collection: String, } impl From<(String, String)> for JsAlias { fn from(tuple: (String, String)) -> Self { JsAlias { alias: tuple.0, collection: tuple.1, } } } /// Collection manager for multi-collection support #[napi] pub struct CollectionManager { inner: Arc>, } #[napi] impl CollectionManager { /// Create a new collection manager /// /// # Example /// ```javascript /// const manager = new CollectionManager('./collections'); /// ``` #[napi(constructor)] pub fn new(base_path: Option) -> Result { let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string())); let manager = CoreCollectionManager::new(path).map_err(|e| { Error::from_reason(format!("Failed to create collection manager: {}", e)) })?; Ok(Self { inner: Arc::new(RwLock::new(manager)), }) } /// Create a new collection /// /// # Example /// ```javascript /// await manager.createCollection('my_vectors', { /// dimensions: 384, /// distanceMetric: 'Cosine' /// }); /// ``` #[napi] pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> { let core_config: ruvector_collections::CollectionConfig = config.into(); let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.write().expect("RwLock poisoned"); manager.create_collection(&name, core_config) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e))) } /// List all collections /// /// # Example /// ```javascript /// const collections = await manager.listCollections(); /// console.log('Collections:', collections); /// ``` #[napi] pub async fn list_collections(&self) -> Result> { let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.read().expect("RwLock poisoned"); manager.list_collections() }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e))) } /// Delete a collection /// /// # Example /// ```javascript /// await manager.deleteCollection('my_vectors'); /// ``` #[napi] pub async fn delete_collection(&self, name: String) -> Result<()> { let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.write().expect("RwLock poisoned"); manager.delete_collection(&name) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e))) } /// Get collection statistics /// /// # Example /// ```javascript /// const stats = await manager.getStats('my_vectors'); /// console.log(`Vectors: ${stats.vectorsCount}`); /// ``` #[napi] pub async fn get_stats(&self, name: String) -> Result { let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.read().expect("RwLock poisoned"); manager.collection_stats(&name) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e))) .map(Into::into) } /// Create an alias for a collection /// /// # Example /// ```javascript /// await manager.createAlias('latest', 'my_vectors_v2'); /// ``` #[napi] pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> { let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.write().expect("RwLock poisoned"); manager.create_alias(&alias, &collection) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e))) } /// Delete an alias /// /// # Example /// ```javascript /// await manager.deleteAlias('latest'); /// ``` #[napi] pub async fn delete_alias(&self, alias: String) -> Result<()> { let manager = self.inner.clone(); tokio::task::spawn_blocking(move || { let manager = manager.write().expect("RwLock poisoned"); manager.delete_alias(&alias) }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))? .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e))) } /// List all aliases /// /// # Example /// ```javascript /// const aliases = await manager.listAliases(); /// for (const alias of aliases) { /// console.log(`${alias.alias} -> ${alias.collection}`); /// } /// ``` #[napi] pub async fn list_aliases(&self) -> Result> { let manager = self.inner.clone(); let aliases = tokio::task::spawn_blocking(move || { let manager = manager.read().expect("RwLock poisoned"); manager.list_aliases() }) .await .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?; Ok(aliases.into_iter().map(Into::into).collect()) } } /// Health response #[napi(object)] #[derive(Debug, Clone)] pub struct JsHealthResponse { /// Status: "healthy", "degraded", or "unhealthy" pub status: String, /// Version string pub version: String, /// Uptime in seconds pub uptime_seconds: i64, } /// Get Prometheus metrics /// /// # Example /// ```javascript /// const metrics = getMetrics(); /// console.log(metrics); /// ``` #[napi] pub fn get_metrics() -> String { gather_metrics() } /// Get health status /// /// # Example /// ```javascript /// const health = getHealth(); /// console.log(`Status: ${health.status}`); /// console.log(`Uptime: ${health.uptimeSeconds}s`); /// ``` #[napi] pub fn get_health() -> JsHealthResponse { let checker = HealthChecker::new(); let health = checker.health(); JsHealthResponse { status: match health.status { HealthStatus::Healthy => "healthy".to_string(), HealthStatus::Degraded => "degraded".to_string(), HealthStatus::Unhealthy => "unhealthy".to_string(), }, version: health.version, uptime_seconds: health.uptime_seconds as i64, } }