1#![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, VectorDB as CoreVectorDB, VectorEntry,
14};
15use std::sync::Arc;
16use std::sync::RwLock;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use ruvector_collections::CollectionManager as CoreCollectionManager;
21use ruvector_filter::FilterExpression;
22use ruvector_metrics::{gather_metrics, HealthChecker, HealthStatus};
23use std::path::PathBuf;
24
25#[napi(string_enum)]
27#[derive(Debug)]
28pub enum JsDistanceMetric {
29 Euclidean,
31 Cosine,
33 DotProduct,
35 Manhattan,
37}
38
39impl From<JsDistanceMetric> for DistanceMetric {
40 fn from(metric: JsDistanceMetric) -> Self {
41 match metric {
42 JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
43 JsDistanceMetric::Cosine => DistanceMetric::Cosine,
44 JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
45 JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
46 }
47 }
48}
49
50#[napi(object)]
52#[derive(Debug, Clone)]
53pub struct JsQuantizationConfig {
54 pub r#type: String,
56 pub subspaces: Option<u32>,
58 pub k: Option<u32>,
60}
61
62impl From<JsQuantizationConfig> for QuantizationConfig {
63 fn from(config: JsQuantizationConfig) -> Self {
64 match config.r#type.as_str() {
65 "none" => QuantizationConfig::None,
66 "scalar" => QuantizationConfig::Scalar,
67 "product" => QuantizationConfig::Product {
68 subspaces: config.subspaces.unwrap_or(16) as usize,
69 k: config.k.unwrap_or(256) as usize,
70 },
71 "binary" => QuantizationConfig::Binary,
72 _ => QuantizationConfig::Scalar,
73 }
74 }
75}
76
77#[napi(object)]
79#[derive(Debug, Clone)]
80pub struct JsHnswConfig {
81 pub m: Option<u32>,
83 pub ef_construction: Option<u32>,
85 pub ef_search: Option<u32>,
87 pub max_elements: Option<u32>,
89}
90
91impl From<JsHnswConfig> for HnswConfig {
92 fn from(config: JsHnswConfig) -> Self {
93 HnswConfig {
94 m: config.m.unwrap_or(32) as usize,
95 ef_construction: config.ef_construction.unwrap_or(200) as usize,
96 ef_search: config.ef_search.unwrap_or(100) as usize,
97 max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
98 }
99 }
100}
101
102#[napi(object)]
104#[derive(Debug)]
105pub struct JsDbOptions {
106 pub dimensions: u32,
108 pub distance_metric: Option<JsDistanceMetric>,
110 pub storage_path: Option<String>,
112 pub hnsw_config: Option<JsHnswConfig>,
114 pub quantization: Option<JsQuantizationConfig>,
116}
117
118impl From<JsDbOptions> for DbOptions {
119 fn from(options: JsDbOptions) -> Self {
120 DbOptions {
121 dimensions: options.dimensions as usize,
122 distance_metric: options
123 .distance_metric
124 .map(Into::into)
125 .unwrap_or(DistanceMetric::Cosine),
126 storage_path: options
127 .storage_path
128 .unwrap_or_else(|| "./ruvector.db".to_string()),
129 hnsw_config: options.hnsw_config.map(Into::into),
130 quantization: options.quantization.map(Into::into),
131 }
132 }
133}
134
135#[napi(object)]
137pub struct JsVectorEntry {
138 pub id: Option<String>,
140 pub vector: Float32Array,
142}
143
144impl JsVectorEntry {
145 fn to_core(&self) -> Result<VectorEntry> {
146 Ok(VectorEntry {
147 id: self.id.clone(),
148 vector: self.vector.to_vec(),
149 metadata: None,
150 })
151 }
152}
153
154#[napi(object)]
156pub struct JsSearchQuery {
157 pub vector: Float32Array,
159 pub k: u32,
161 pub ef_search: Option<u32>,
163}
164
165impl JsSearchQuery {
166 fn to_core(&self) -> Result<SearchQuery> {
167 Ok(SearchQuery {
168 vector: self.vector.to_vec(),
169 k: self.k as usize,
170 filter: None,
171 ef_search: self.ef_search.map(|v| v as usize),
172 })
173 }
174}
175
176#[napi(object)]
178#[derive(Debug, Clone)]
179pub struct JsSearchResult {
180 pub id: String,
182 pub score: f64,
184}
185
186impl From<SearchResult> for JsSearchResult {
187 fn from(result: SearchResult) -> Self {
188 JsSearchResult {
189 id: result.id,
190 score: f64::from(result.score),
191 }
192 }
193}
194
195#[napi]
197pub struct VectorDB {
198 inner: Arc<RwLock<CoreVectorDB>>,
199}
200
201#[napi]
202impl VectorDB {
203 #[napi(constructor)]
219 pub fn new(options: JsDbOptions) -> Result<Self> {
220 let core_options: DbOptions = options.into();
221 let db = CoreVectorDB::new(core_options)
222 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
223
224 Ok(Self {
225 inner: Arc::new(RwLock::new(db)),
226 })
227 }
228
229 #[napi(factory)]
236 pub fn with_dimensions(dimensions: u32) -> Result<Self> {
237 let db = CoreVectorDB::with_dimensions(dimensions as usize)
238 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
239
240 Ok(Self {
241 inner: Arc::new(RwLock::new(db)),
242 })
243 }
244
245 #[napi]
257 pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
258 let core_entry = entry.to_core()?;
259 let db = self.inner.clone();
260
261 tokio::task::spawn_blocking(move || {
262 let db = db.read().expect("RwLock poisoned");
263 db.insert(core_entry)
264 })
265 .await
266 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
267 .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
268 }
269
270 #[napi]
282 pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
283 let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
284 let core_entries = core_entries?;
285 let db = self.inner.clone();
286
287 tokio::task::spawn_blocking(move || {
288 let db = db.read().expect("RwLock poisoned");
289 db.insert_batch(core_entries)
290 })
291 .await
292 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
293 .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
294 }
295
296 #[napi]
309 pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
310 let core_query = query.to_core()?;
311 let db = self.inner.clone();
312
313 tokio::task::spawn_blocking(move || {
314 let db = db.read().expect("RwLock poisoned");
315 db.search(core_query)
316 })
317 .await
318 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
319 .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
320 .map(|results| results.into_iter().map(Into::into).collect())
321 }
322
323 #[napi]
332 pub async fn delete(&self, id: String) -> Result<bool> {
333 let db = self.inner.clone();
334
335 tokio::task::spawn_blocking(move || {
336 let db = db.read().expect("RwLock poisoned");
337 db.delete(&id)
338 })
339 .await
340 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
341 .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
342 }
343
344 #[napi]
356 pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
357 let db = self.inner.clone();
358
359 let result = tokio::task::spawn_blocking(move || {
360 let db = db.read().expect("RwLock poisoned");
361 db.get(&id)
362 })
363 .await
364 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
365 .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
366
367 Ok(result.map(|entry| JsVectorEntry {
368 id: entry.id,
369 vector: Float32Array::new(entry.vector),
370 }))
371 }
372
373 #[napi]
381 pub async fn len(&self) -> Result<u32> {
382 let db = self.inner.clone();
383
384 tokio::task::spawn_blocking(move || {
385 let db = db.read().expect("RwLock poisoned");
386 db.len()
387 })
388 .await
389 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
390 .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
391 .map(|len| len as u32)
392 }
393
394 #[napi]
403 pub async fn is_empty(&self) -> Result<bool> {
404 let db = self.inner.clone();
405
406 tokio::task::spawn_blocking(move || {
407 let db = db.read().expect("RwLock poisoned");
408 db.is_empty()
409 })
410 .await
411 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
412 .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
413 }
414}
415
416#[napi]
418pub fn version() -> String {
419 env!("CARGO_PKG_VERSION").to_string()
420}
421
422#[napi]
424pub fn hello() -> String {
425 "Hello from Ruvector Node.js bindings!".to_string()
426}
427
428#[napi(object)]
430#[derive(Debug, Clone)]
431pub struct JsFilter {
432 pub field: String,
434 pub operator: String,
436 pub value: String,
438}
439
440impl JsFilter {
441 fn to_filter_expression(&self) -> Result<FilterExpression> {
442 let value: serde_json::Value = serde_json::from_str(&self.value)
443 .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
444
445 Ok(match self.operator.as_str() {
446 "eq" => FilterExpression::eq(&self.field, value),
447 "ne" => FilterExpression::ne(&self.field, value),
448 "gt" => FilterExpression::gt(&self.field, value),
449 "gte" => FilterExpression::gte(&self.field, value),
450 "lt" => FilterExpression::lt(&self.field, value),
451 "lte" => FilterExpression::lte(&self.field, value),
452 "match" => FilterExpression::Match {
453 field: self.field.clone(),
454 text: value.as_str().unwrap_or("").to_string(),
455 },
456 _ => FilterExpression::eq(&self.field, value),
457 })
458 }
459}
460
461#[napi(object)]
463#[derive(Debug, Clone)]
464pub struct JsCollectionConfig {
465 pub dimensions: u32,
467 pub distance_metric: Option<JsDistanceMetric>,
469 pub hnsw_config: Option<JsHnswConfig>,
471 pub quantization: Option<JsQuantizationConfig>,
473}
474
475impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
476 fn from(config: JsCollectionConfig) -> Self {
477 ruvector_collections::CollectionConfig {
478 dimensions: config.dimensions as usize,
479 distance_metric: config
480 .distance_metric
481 .map(Into::into)
482 .unwrap_or(DistanceMetric::Cosine),
483 hnsw_config: config.hnsw_config.map(Into::into),
484 quantization: config.quantization.map(Into::into),
485 on_disk_payload: true,
486 }
487 }
488}
489
490#[napi(object)]
492#[derive(Debug, Clone)]
493pub struct JsCollectionStats {
494 pub vectors_count: u32,
496 pub disk_size_bytes: i64,
498 pub ram_size_bytes: i64,
500}
501
502impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
503 fn from(stats: ruvector_collections::CollectionStats) -> Self {
504 JsCollectionStats {
505 vectors_count: stats.vectors_count as u32,
506 disk_size_bytes: stats.disk_size_bytes as i64,
507 ram_size_bytes: stats.ram_size_bytes as i64,
508 }
509 }
510}
511
512#[napi(object)]
514#[derive(Debug, Clone)]
515pub struct JsAlias {
516 pub alias: String,
518 pub collection: String,
520}
521
522impl From<(String, String)> for JsAlias {
523 fn from(tuple: (String, String)) -> Self {
524 JsAlias {
525 alias: tuple.0,
526 collection: tuple.1,
527 }
528 }
529}
530
531#[napi]
533pub struct CollectionManager {
534 inner: Arc<RwLock<CoreCollectionManager>>,
535}
536
537#[napi]
538impl CollectionManager {
539 #[napi(constructor)]
546 pub fn new(base_path: Option<String>) -> Result<Self> {
547 let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
548 let manager = CoreCollectionManager::new(path).map_err(|e| {
549 Error::from_reason(format!("Failed to create collection manager: {}", e))
550 })?;
551
552 Ok(Self {
553 inner: Arc::new(RwLock::new(manager)),
554 })
555 }
556
557 #[napi]
567 pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
568 let core_config: ruvector_collections::CollectionConfig = config.into();
569 let manager = self.inner.clone();
570
571 tokio::task::spawn_blocking(move || {
572 let manager = manager.write().expect("RwLock poisoned");
573 manager.create_collection(&name, core_config)
574 })
575 .await
576 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
577 .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
578 }
579
580 #[napi]
588 pub async fn list_collections(&self) -> Result<Vec<String>> {
589 let manager = self.inner.clone();
590
591 tokio::task::spawn_blocking(move || {
592 let manager = manager.read().expect("RwLock poisoned");
593 manager.list_collections()
594 })
595 .await
596 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
597 }
598
599 #[napi]
606 pub async fn delete_collection(&self, name: String) -> Result<()> {
607 let manager = self.inner.clone();
608
609 tokio::task::spawn_blocking(move || {
610 let manager = manager.write().expect("RwLock poisoned");
611 manager.delete_collection(&name)
612 })
613 .await
614 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
615 .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
616 }
617
618 #[napi]
626 pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
627 let manager = self.inner.clone();
628
629 tokio::task::spawn_blocking(move || {
630 let manager = manager.read().expect("RwLock poisoned");
631 manager.collection_stats(&name)
632 })
633 .await
634 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
635 .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
636 .map(Into::into)
637 }
638
639 #[napi]
646 pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
647 let manager = self.inner.clone();
648
649 tokio::task::spawn_blocking(move || {
650 let manager = manager.write().expect("RwLock poisoned");
651 manager.create_alias(&alias, &collection)
652 })
653 .await
654 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
655 .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
656 }
657
658 #[napi]
665 pub async fn delete_alias(&self, alias: String) -> Result<()> {
666 let manager = self.inner.clone();
667
668 tokio::task::spawn_blocking(move || {
669 let manager = manager.write().expect("RwLock poisoned");
670 manager.delete_alias(&alias)
671 })
672 .await
673 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
674 .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
675 }
676
677 #[napi]
687 pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
688 let manager = self.inner.clone();
689
690 let aliases = tokio::task::spawn_blocking(move || {
691 let manager = manager.read().expect("RwLock poisoned");
692 manager.list_aliases()
693 })
694 .await
695 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
696
697 Ok(aliases.into_iter().map(Into::into).collect())
698 }
699}
700
701#[napi(object)]
703#[derive(Debug, Clone)]
704pub struct JsHealthResponse {
705 pub status: String,
707 pub version: String,
709 pub uptime_seconds: i64,
711}
712
713#[napi]
721pub fn get_metrics() -> String {
722 gather_metrics()
723}
724
725#[napi]
734pub fn get_health() -> JsHealthResponse {
735 let checker = HealthChecker::new();
736 let health = checker.health();
737
738 JsHealthResponse {
739 status: match health.status {
740 HealthStatus::Healthy => "healthy".to_string(),
741 HealthStatus::Degraded => "degraded".to_string(),
742 HealthStatus::Unhealthy => "unhealthy".to_string(),
743 },
744 version: health.version,
745 uptime_seconds: health.uptime_seconds as i64,
746 }
747}