ruvector_node/
lib.rs

1//! Node.js bindings for Ruvector via NAPI-RS
2//!
3//! High-performance Rust vector database with zero-copy buffer sharing,
4//! async/await support, and complete TypeScript type definitions.
5
6#![deny(clippy::all)]
7#![warn(clippy::pedantic)]
8
9use napi::bindgen_prelude::*;
10use napi_derive::napi;
11use ruvector_core::{
12    types::{DbOptions, HnswConfig, QuantizationConfig},
13    DistanceMetric, SearchQuery, SearchResult,
14    VectorDB as CoreVectorDB, VectorEntry,
15};
16use std::sync::Arc;
17use std::sync::RwLock;
18
19/// Distance metric for similarity calculation
20#[napi(string_enum)]
21#[derive(Debug)]
22pub enum JsDistanceMetric {
23    /// Euclidean (L2) distance
24    Euclidean,
25    /// Cosine similarity (converted to distance)
26    Cosine,
27    /// Dot product (converted to distance for maximization)
28    DotProduct,
29    /// Manhattan (L1) distance
30    Manhattan,
31}
32
33impl From<JsDistanceMetric> for DistanceMetric {
34    fn from(metric: JsDistanceMetric) -> Self {
35        match metric {
36            JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
37            JsDistanceMetric::Cosine => DistanceMetric::Cosine,
38            JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
39            JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
40        }
41    }
42}
43
44/// Quantization configuration
45#[napi(object)]
46#[derive(Debug)]
47pub struct JsQuantizationConfig {
48    /// Quantization type: "none", "scalar", "product", "binary"
49    pub r#type: String,
50    /// Number of subspaces (for product quantization)
51    pub subspaces: Option<u32>,
52    /// Codebook size (for product quantization)
53    pub k: Option<u32>,
54}
55
56impl From<JsQuantizationConfig> for QuantizationConfig {
57    fn from(config: JsQuantizationConfig) -> Self {
58        match config.r#type.as_str() {
59            "none" => QuantizationConfig::None,
60            "scalar" => QuantizationConfig::Scalar,
61            "product" => QuantizationConfig::Product {
62                subspaces: config.subspaces.unwrap_or(16) as usize,
63                k: config.k.unwrap_or(256) as usize,
64            },
65            "binary" => QuantizationConfig::Binary,
66            _ => QuantizationConfig::Scalar,
67        }
68    }
69}
70
71/// HNSW index configuration
72#[napi(object)]
73#[derive(Debug)]
74pub struct JsHnswConfig {
75    /// Number of connections per layer (M)
76    pub m: Option<u32>,
77    /// Size of dynamic candidate list during construction
78    pub ef_construction: Option<u32>,
79    /// Size of dynamic candidate list during search
80    pub ef_search: Option<u32>,
81    /// Maximum number of elements
82    pub max_elements: Option<u32>,
83}
84
85impl From<JsHnswConfig> for HnswConfig {
86    fn from(config: JsHnswConfig) -> Self {
87        HnswConfig {
88            m: config.m.unwrap_or(32) as usize,
89            ef_construction: config.ef_construction.unwrap_or(200) as usize,
90            ef_search: config.ef_search.unwrap_or(100) as usize,
91            max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
92        }
93    }
94}
95
96/// Database configuration options
97#[napi(object)]
98#[derive(Debug)]
99pub struct JsDbOptions {
100    /// Vector dimensions
101    pub dimensions: u32,
102    /// Distance metric
103    pub distance_metric: Option<JsDistanceMetric>,
104    /// Storage path
105    pub storage_path: Option<String>,
106    /// HNSW configuration
107    pub hnsw_config: Option<JsHnswConfig>,
108    /// Quantization configuration
109    pub quantization: Option<JsQuantizationConfig>,
110}
111
112impl From<JsDbOptions> for DbOptions {
113    fn from(options: JsDbOptions) -> Self {
114        DbOptions {
115            dimensions: options.dimensions as usize,
116            distance_metric: options
117                .distance_metric
118                .map(Into::into)
119                .unwrap_or(DistanceMetric::Cosine),
120            storage_path: options
121                .storage_path
122                .unwrap_or_else(|| "./ruvector.db".to_string()),
123            hnsw_config: options.hnsw_config.map(Into::into),
124            quantization: options.quantization.map(Into::into),
125        }
126    }
127}
128
129/// Vector entry
130#[napi(object)]
131pub struct JsVectorEntry {
132    /// Optional ID (auto-generated if not provided)
133    pub id: Option<String>,
134    /// Vector data as Float32Array or array of numbers
135    pub vector: Float32Array,
136}
137
138impl JsVectorEntry {
139    fn to_core(&self) -> Result<VectorEntry> {
140        Ok(VectorEntry {
141            id: self.id.clone(),
142            vector: self.vector.to_vec(),
143            metadata: None,
144        })
145    }
146}
147
148/// Search query parameters
149#[napi(object)]
150pub struct JsSearchQuery {
151    /// Query vector as Float32Array or array of numbers
152    pub vector: Float32Array,
153    /// Number of results to return (top-k)
154    pub k: u32,
155    /// Optional ef_search parameter for HNSW
156    pub ef_search: Option<u32>,
157}
158
159impl JsSearchQuery {
160    fn to_core(&self) -> Result<SearchQuery> {
161        Ok(SearchQuery {
162            vector: self.vector.to_vec(),
163            k: self.k as usize,
164            filter: None,
165            ef_search: self.ef_search.map(|v| v as usize),
166        })
167    }
168}
169
170/// Search result with similarity score
171#[napi(object)]
172#[derive(Debug, Clone)]
173pub struct JsSearchResult {
174    /// Vector ID
175    pub id: String,
176    /// Distance/similarity score (lower is better for distance metrics)
177    pub score: f64,
178}
179
180impl From<SearchResult> for JsSearchResult {
181    fn from(result: SearchResult) -> Self {
182        JsSearchResult {
183            id: result.id,
184            score: f64::from(result.score),
185        }
186    }
187}
188
189/// High-performance vector database with HNSW indexing
190#[napi]
191pub struct VectorDB {
192    inner: Arc<RwLock<CoreVectorDB>>,
193}
194
195#[napi]
196impl VectorDB {
197    /// Create a new vector database with the given options
198    ///
199    /// # Example
200    /// ```javascript
201    /// const db = new VectorDB({
202    ///   dimensions: 384,
203    ///   distanceMetric: 'Cosine',
204    ///   storagePath: './vectors.db',
205    ///   hnswConfig: {
206    ///     m: 32,
207    ///     efConstruction: 200,
208    ///     efSearch: 100
209    ///   }
210    /// });
211    /// ```
212    #[napi(constructor)]
213    pub fn new(options: JsDbOptions) -> Result<Self> {
214        let core_options: DbOptions = options.into();
215        let db = CoreVectorDB::new(core_options)
216            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
217
218        Ok(Self {
219            inner: Arc::new(RwLock::new(db)),
220        })
221    }
222
223    /// Create a vector database with default options
224    ///
225    /// # Example
226    /// ```javascript
227    /// const db = VectorDB.withDimensions(384);
228    /// ```
229    #[napi(factory)]
230    pub fn with_dimensions(dimensions: u32) -> Result<Self> {
231        let db = CoreVectorDB::with_dimensions(dimensions as usize)
232            .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
233
234        Ok(Self {
235            inner: Arc::new(RwLock::new(db)),
236        })
237    }
238
239    /// Insert a vector entry into the database
240    ///
241    /// Returns the ID of the inserted vector (auto-generated if not provided)
242    ///
243    /// # Example
244    /// ```javascript
245    /// const id = await db.insert({
246    ///   vector: new Float32Array([1.0, 2.0, 3.0]),
247    ///   metadata: { text: 'example' }
248    /// });
249    /// ```
250    #[napi]
251    pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
252        let core_entry = entry.to_core()?;
253        let db = self.inner.clone();
254
255        tokio::task::spawn_blocking(move || {
256            let db = db.read().expect("RwLock poisoned");
257            db.insert(core_entry)
258        })
259        .await
260        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
261        .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
262    }
263
264    /// Insert multiple vectors in a batch
265    ///
266    /// Returns an array of IDs for the inserted vectors
267    ///
268    /// # Example
269    /// ```javascript
270    /// const ids = await db.insertBatch([
271    ///   { vector: new Float32Array([1, 2, 3]) },
272    ///   { vector: new Float32Array([4, 5, 6]) }
273    /// ]);
274    /// ```
275    #[napi]
276    pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
277        let core_entries: Result<Vec<VectorEntry>> = entries
278            .iter()
279            .map(|e| e.to_core())
280            .collect();
281        let core_entries = core_entries?;
282        let db = self.inner.clone();
283
284        tokio::task::spawn_blocking(move || {
285            let db = db.read().expect("RwLock poisoned");
286            db.insert_batch(core_entries)
287        })
288        .await
289        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
290        .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
291    }
292
293    /// Search for similar vectors
294    ///
295    /// Returns an array of search results sorted by similarity
296    ///
297    /// # Example
298    /// ```javascript
299    /// const results = await db.search({
300    ///   vector: new Float32Array([1, 2, 3]),
301    ///   k: 10,
302    ///   filter: { category: 'example' }
303    /// });
304    /// ```
305    #[napi]
306    pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
307        let core_query = query.to_core()?;
308        let db = self.inner.clone();
309
310        tokio::task::spawn_blocking(move || {
311            let db = db.read().expect("RwLock poisoned");
312            db.search(core_query)
313        })
314        .await
315        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
316        .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
317        .map(|results| results.into_iter().map(Into::into).collect())
318    }
319
320    /// Delete a vector by ID
321    ///
322    /// Returns true if the vector was deleted, false if not found
323    ///
324    /// # Example
325    /// ```javascript
326    /// const deleted = await db.delete('vector-id');
327    /// ```
328    #[napi]
329    pub async fn delete(&self, id: String) -> Result<bool> {
330        let db = self.inner.clone();
331
332        tokio::task::spawn_blocking(move || {
333            let db = db.read().expect("RwLock poisoned");
334            db.delete(&id)
335        })
336        .await
337        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
338        .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
339    }
340
341    /// Get a vector by ID
342    ///
343    /// Returns the vector entry if found, null otherwise
344    ///
345    /// # Example
346    /// ```javascript
347    /// const entry = await db.get('vector-id');
348    /// if (entry) {
349    ///   console.log('Found:', entry.metadata);
350    /// }
351    /// ```
352    #[napi]
353    pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
354        let db = self.inner.clone();
355
356        let result = tokio::task::spawn_blocking(move || {
357            let db = db.read().expect("RwLock poisoned");
358            db.get(&id)
359        })
360        .await
361        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
362        .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
363
364        Ok(result.map(|entry| {
365            JsVectorEntry {
366                id: entry.id,
367                vector: Float32Array::new(entry.vector),
368            }
369        }))
370    }
371
372    /// Get the number of vectors in the database
373    ///
374    /// # Example
375    /// ```javascript
376    /// const count = await db.len();
377    /// console.log(`Database contains ${count} vectors`);
378    /// ```
379    #[napi]
380    pub async fn len(&self) -> Result<u32> {
381        let db = self.inner.clone();
382
383        tokio::task::spawn_blocking(move || {
384            let db = db.read().expect("RwLock poisoned");
385            db.len()
386        })
387        .await
388        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
389        .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
390        .map(|len| len as u32)
391    }
392
393    /// Check if the database is empty
394    ///
395    /// # Example
396    /// ```javascript
397    /// if (await db.isEmpty()) {
398    ///   console.log('Database is empty');
399    /// }
400    /// ```
401    #[napi]
402    pub async fn is_empty(&self) -> Result<bool> {
403        let db = self.inner.clone();
404
405        tokio::task::spawn_blocking(move || {
406            let db = db.read().expect("RwLock poisoned");
407            db.is_empty()
408        })
409        .await
410        .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
411        .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
412    }
413}
414
415/// Get the version of the Ruvector library
416#[napi]
417pub fn version() -> String {
418    env!("CARGO_PKG_VERSION").to_string()
419}
420
421/// Test function to verify the bindings are working
422#[napi]
423pub fn hello() -> String {
424    "Hello from Ruvector Node.js bindings!".to_string()
425}