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 pub metadata: Option<String>,
144}
145
146impl JsVectorEntry {
147 fn to_core(&self) -> Result<VectorEntry> {
148 let metadata = self.metadata.as_ref().and_then(|s| {
150 serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
151 });
152
153 Ok(VectorEntry {
154 id: self.id.clone(),
155 vector: self.vector.to_vec(),
156 metadata,
157 })
158 }
159}
160
161#[napi(object)]
163pub struct JsSearchQuery {
164 pub vector: Float32Array,
166 pub k: u32,
168 pub ef_search: Option<u32>,
170 pub filter: Option<String>,
172}
173
174impl JsSearchQuery {
175 fn to_core(&self) -> Result<SearchQuery> {
176 let filter = self.filter.as_ref().and_then(|s| {
178 serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
179 });
180
181 Ok(SearchQuery {
182 vector: self.vector.to_vec(),
183 k: self.k as usize,
184 filter,
185 ef_search: self.ef_search.map(|v| v as usize),
186 })
187 }
188}
189
190#[napi(object)]
192#[derive(Clone)]
193pub struct JsSearchResult {
194 pub id: String,
196 pub score: f64,
198 pub vector: Option<Float32Array>,
200 pub metadata: Option<String>,
202}
203
204impl From<SearchResult> for JsSearchResult {
205 fn from(result: SearchResult) -> Self {
206 let vector = result.vector.map(|v| Float32Array::new(v));
208
209 let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok());
211
212 JsSearchResult {
213 id: result.id,
214 score: f64::from(result.score),
215 vector,
216 metadata,
217 }
218 }
219}
220
221#[napi]
223pub struct VectorDB {
224 inner: Arc<RwLock<CoreVectorDB>>,
225}
226
227#[napi]
228impl VectorDB {
229 #[napi(constructor)]
245 pub fn new(options: JsDbOptions) -> Result<Self> {
246 let core_options: DbOptions = options.into();
247 let db = CoreVectorDB::new(core_options)
248 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
249
250 Ok(Self {
251 inner: Arc::new(RwLock::new(db)),
252 })
253 }
254
255 #[napi(factory)]
262 pub fn with_dimensions(dimensions: u32) -> Result<Self> {
263 let db = CoreVectorDB::with_dimensions(dimensions as usize)
264 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
265
266 Ok(Self {
267 inner: Arc::new(RwLock::new(db)),
268 })
269 }
270
271 #[napi]
283 pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
284 let core_entry = entry.to_core()?;
285 let db = self.inner.clone();
286
287 tokio::task::spawn_blocking(move || {
288 let db = db.read().expect("RwLock poisoned");
289 db.insert(core_entry)
290 })
291 .await
292 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
293 .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
294 }
295
296 #[napi]
308 pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
309 let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
310 let core_entries = core_entries?;
311 let db = self.inner.clone();
312
313 tokio::task::spawn_blocking(move || {
314 let db = db.read().expect("RwLock poisoned");
315 db.insert_batch(core_entries)
316 })
317 .await
318 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
319 .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
320 }
321
322 #[napi]
335 pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
336 let core_query = query.to_core()?;
337 let db = self.inner.clone();
338
339 tokio::task::spawn_blocking(move || {
340 let db = db.read().expect("RwLock poisoned");
341 db.search(core_query)
342 })
343 .await
344 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
345 .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
346 .map(|results| results.into_iter().map(Into::into).collect())
347 }
348
349 #[napi]
358 pub async fn delete(&self, id: String) -> Result<bool> {
359 let db = self.inner.clone();
360
361 tokio::task::spawn_blocking(move || {
362 let db = db.read().expect("RwLock poisoned");
363 db.delete(&id)
364 })
365 .await
366 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
367 .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
368 }
369
370 #[napi]
382 pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
383 let db = self.inner.clone();
384
385 let result = tokio::task::spawn_blocking(move || {
386 let db = db.read().expect("RwLock poisoned");
387 db.get(&id)
388 })
389 .await
390 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
391 .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
392
393 Ok(result.map(|entry| {
394 let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok());
396
397 JsVectorEntry {
398 id: entry.id,
399 vector: Float32Array::new(entry.vector),
400 metadata,
401 }
402 }))
403 }
404
405 #[napi]
413 pub async fn len(&self) -> Result<u32> {
414 let db = self.inner.clone();
415
416 tokio::task::spawn_blocking(move || {
417 let db = db.read().expect("RwLock poisoned");
418 db.len()
419 })
420 .await
421 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
422 .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
423 .map(|len| len as u32)
424 }
425
426 #[napi]
435 pub async fn is_empty(&self) -> Result<bool> {
436 let db = self.inner.clone();
437
438 tokio::task::spawn_blocking(move || {
439 let db = db.read().expect("RwLock poisoned");
440 db.is_empty()
441 })
442 .await
443 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
444 .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
445 }
446}
447
448#[napi]
450pub fn version() -> String {
451 env!("CARGO_PKG_VERSION").to_string()
452}
453
454#[napi]
456pub fn hello() -> String {
457 "Hello from Ruvector Node.js bindings!".to_string()
458}
459
460#[napi(object)]
462#[derive(Debug, Clone)]
463pub struct JsFilter {
464 pub field: String,
466 pub operator: String,
468 pub value: String,
470}
471
472impl JsFilter {
473 fn to_filter_expression(&self) -> Result<FilterExpression> {
474 let value: serde_json::Value = serde_json::from_str(&self.value)
475 .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
476
477 Ok(match self.operator.as_str() {
478 "eq" => FilterExpression::eq(&self.field, value),
479 "ne" => FilterExpression::ne(&self.field, value),
480 "gt" => FilterExpression::gt(&self.field, value),
481 "gte" => FilterExpression::gte(&self.field, value),
482 "lt" => FilterExpression::lt(&self.field, value),
483 "lte" => FilterExpression::lte(&self.field, value),
484 "match" => FilterExpression::Match {
485 field: self.field.clone(),
486 text: value.as_str().unwrap_or("").to_string(),
487 },
488 _ => FilterExpression::eq(&self.field, value),
489 })
490 }
491}
492
493#[napi(object)]
495#[derive(Debug, Clone)]
496pub struct JsCollectionConfig {
497 pub dimensions: u32,
499 pub distance_metric: Option<JsDistanceMetric>,
501 pub hnsw_config: Option<JsHnswConfig>,
503 pub quantization: Option<JsQuantizationConfig>,
505}
506
507impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
508 fn from(config: JsCollectionConfig) -> Self {
509 ruvector_collections::CollectionConfig {
510 dimensions: config.dimensions as usize,
511 distance_metric: config
512 .distance_metric
513 .map(Into::into)
514 .unwrap_or(DistanceMetric::Cosine),
515 hnsw_config: config.hnsw_config.map(Into::into),
516 quantization: config.quantization.map(Into::into),
517 on_disk_payload: true,
518 }
519 }
520}
521
522#[napi(object)]
524#[derive(Debug, Clone)]
525pub struct JsCollectionStats {
526 pub vectors_count: u32,
528 pub disk_size_bytes: i64,
530 pub ram_size_bytes: i64,
532}
533
534impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
535 fn from(stats: ruvector_collections::CollectionStats) -> Self {
536 JsCollectionStats {
537 vectors_count: stats.vectors_count as u32,
538 disk_size_bytes: stats.disk_size_bytes as i64,
539 ram_size_bytes: stats.ram_size_bytes as i64,
540 }
541 }
542}
543
544#[napi(object)]
546#[derive(Debug, Clone)]
547pub struct JsAlias {
548 pub alias: String,
550 pub collection: String,
552}
553
554impl From<(String, String)> for JsAlias {
555 fn from(tuple: (String, String)) -> Self {
556 JsAlias {
557 alias: tuple.0,
558 collection: tuple.1,
559 }
560 }
561}
562
563#[napi]
565pub struct CollectionManager {
566 inner: Arc<RwLock<CoreCollectionManager>>,
567}
568
569#[napi]
570impl CollectionManager {
571 #[napi(constructor)]
578 pub fn new(base_path: Option<String>) -> Result<Self> {
579 let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
580 let manager = CoreCollectionManager::new(path).map_err(|e| {
581 Error::from_reason(format!("Failed to create collection manager: {}", e))
582 })?;
583
584 Ok(Self {
585 inner: Arc::new(RwLock::new(manager)),
586 })
587 }
588
589 #[napi]
599 pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
600 let core_config: ruvector_collections::CollectionConfig = config.into();
601 let manager = self.inner.clone();
602
603 tokio::task::spawn_blocking(move || {
604 let manager = manager.write().expect("RwLock poisoned");
605 manager.create_collection(&name, core_config)
606 })
607 .await
608 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
609 .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
610 }
611
612 #[napi]
620 pub async fn list_collections(&self) -> Result<Vec<String>> {
621 let manager = self.inner.clone();
622
623 tokio::task::spawn_blocking(move || {
624 let manager = manager.read().expect("RwLock poisoned");
625 manager.list_collections()
626 })
627 .await
628 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
629 }
630
631 #[napi]
638 pub async fn delete_collection(&self, name: String) -> Result<()> {
639 let manager = self.inner.clone();
640
641 tokio::task::spawn_blocking(move || {
642 let manager = manager.write().expect("RwLock poisoned");
643 manager.delete_collection(&name)
644 })
645 .await
646 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
647 .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
648 }
649
650 #[napi]
658 pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
659 let manager = self.inner.clone();
660
661 tokio::task::spawn_blocking(move || {
662 let manager = manager.read().expect("RwLock poisoned");
663 manager.collection_stats(&name)
664 })
665 .await
666 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
667 .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
668 .map(Into::into)
669 }
670
671 #[napi]
678 pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
679 let manager = self.inner.clone();
680
681 tokio::task::spawn_blocking(move || {
682 let manager = manager.write().expect("RwLock poisoned");
683 manager.create_alias(&alias, &collection)
684 })
685 .await
686 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
687 .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
688 }
689
690 #[napi]
697 pub async fn delete_alias(&self, alias: String) -> Result<()> {
698 let manager = self.inner.clone();
699
700 tokio::task::spawn_blocking(move || {
701 let manager = manager.write().expect("RwLock poisoned");
702 manager.delete_alias(&alias)
703 })
704 .await
705 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
706 .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
707 }
708
709 #[napi]
719 pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
720 let manager = self.inner.clone();
721
722 let aliases = tokio::task::spawn_blocking(move || {
723 let manager = manager.read().expect("RwLock poisoned");
724 manager.list_aliases()
725 })
726 .await
727 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
728
729 Ok(aliases.into_iter().map(Into::into).collect())
730 }
731}
732
733#[napi(object)]
735#[derive(Debug, Clone)]
736pub struct JsHealthResponse {
737 pub status: String,
739 pub version: String,
741 pub uptime_seconds: i64,
743}
744
745#[napi]
753pub fn get_metrics() -> String {
754 gather_metrics()
755}
756
757#[napi]
766pub fn get_health() -> JsHealthResponse {
767 let checker = HealthChecker::new();
768 let health = checker.health();
769
770 JsHealthResponse {
771 status: match health.status {
772 HealthStatus::Healthy => "healthy".to_string(),
773 HealthStatus::Degraded => "degraded".to_string(),
774 HealthStatus::Unhealthy => "unhealthy".to_string(),
775 },
776 version: health.version,
777 uptime_seconds: health.uptime_seconds as i64,
778 }
779}