1#![allow(clippy::all)]
7#![allow(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 "turbo4" => QuantizationConfig::Turbo4 {
75 rotation_seed: ruvector_core::types::default_turbo4_rotation_seed(),
76 rescore_multiplier: ruvector_core::types::default_turbo4_rescore_multiplier(),
77 policy: ruvector_core::types::SearchPolicy::default(),
78 search_quantization: ruvector_core::types::SearchQuantization::default(),
79 },
80 _ => QuantizationConfig::Scalar,
81 }
82 }
83}
84
85#[napi(object)]
87#[derive(Debug, Clone)]
88pub struct JsHnswConfig {
89 pub m: Option<u32>,
91 pub ef_construction: Option<u32>,
93 pub ef_search: Option<u32>,
95 pub max_elements: Option<u32>,
97}
98
99impl From<JsHnswConfig> for HnswConfig {
100 fn from(config: JsHnswConfig) -> Self {
101 HnswConfig {
102 m: config.m.unwrap_or(32) as usize,
103 ef_construction: config.ef_construction.unwrap_or(200) as usize,
104 ef_search: config.ef_search.unwrap_or(100) as usize,
105 max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
106 }
107 }
108}
109
110#[napi(object)]
112#[derive(Debug)]
113pub struct JsDbOptions {
114 pub dimensions: u32,
116 pub distance_metric: Option<JsDistanceMetric>,
118 pub storage_path: Option<String>,
120 pub hnsw_config: Option<JsHnswConfig>,
122 pub quantization: Option<JsQuantizationConfig>,
124}
125
126impl From<JsDbOptions> for DbOptions {
127 fn from(options: JsDbOptions) -> Self {
128 DbOptions {
129 dimensions: options.dimensions as usize,
130 distance_metric: options
131 .distance_metric
132 .map(Into::into)
133 .unwrap_or(DistanceMetric::Cosine),
134 storage_path: options
135 .storage_path
136 .unwrap_or_else(|| "./ruvector.db".to_string()),
137 hnsw_config: options.hnsw_config.map(Into::into),
138 quantization: options.quantization.map(Into::into),
139 }
140 }
141}
142
143#[napi(object)]
145pub struct JsVectorEntry {
146 pub id: Option<String>,
148 pub vector: Float32Array,
150 pub metadata: Option<String>,
152}
153
154impl JsVectorEntry {
155 fn to_core(&self) -> Result<VectorEntry> {
156 let metadata = self.metadata.as_ref().and_then(|s| {
158 serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
159 });
160
161 Ok(VectorEntry {
162 id: self.id.clone(),
163 vector: self.vector.to_vec(),
164 metadata,
165 })
166 }
167}
168
169#[napi(object)]
171pub struct JsSearchQuery {
172 pub vector: Float32Array,
174 pub k: u32,
176 pub ef_search: Option<u32>,
178 pub filter: Option<String>,
180}
181
182impl JsSearchQuery {
183 fn to_core(&self) -> Result<SearchQuery> {
184 let filter = self.filter.as_ref().and_then(|s| {
186 serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
187 });
188
189 Ok(SearchQuery {
190 vector: self.vector.to_vec(),
191 k: self.k as usize,
192 filter,
193 ef_search: self.ef_search.map(|v| v as usize),
194 })
195 }
196}
197
198#[napi(object)]
200#[derive(Clone)]
201pub struct JsSearchResult {
202 pub id: String,
204 pub score: f64,
206 pub vector: Option<Float32Array>,
208 pub metadata: Option<String>,
210}
211
212impl From<SearchResult> for JsSearchResult {
213 fn from(result: SearchResult) -> Self {
214 let vector = result.vector.map(|v| Float32Array::new(v));
216
217 let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok());
219
220 JsSearchResult {
221 id: result.id,
222 score: f64::from(result.score),
223 vector,
224 metadata,
225 }
226 }
227}
228
229#[napi]
231pub struct VectorDB {
232 inner: Arc<RwLock<CoreVectorDB>>,
233}
234
235#[napi]
236impl VectorDB {
237 #[napi(constructor)]
253 pub fn new(options: JsDbOptions) -> Result<Self> {
254 let core_options: DbOptions = options.into();
255 let db = CoreVectorDB::new(core_options)
256 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
257
258 Ok(Self {
259 inner: Arc::new(RwLock::new(db)),
260 })
261 }
262
263 #[napi(factory)]
270 pub fn with_dimensions(dimensions: u32) -> Result<Self> {
271 let db = CoreVectorDB::with_dimensions(dimensions as usize)
272 .map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
273
274 Ok(Self {
275 inner: Arc::new(RwLock::new(db)),
276 })
277 }
278
279 #[napi]
291 pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
292 let core_entry = entry.to_core()?;
293 let db = self.inner.clone();
294
295 tokio::task::spawn_blocking(move || {
296 let db = db.read().expect("RwLock poisoned");
297 db.insert(core_entry)
298 })
299 .await
300 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
301 .map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
302 }
303
304 #[napi]
316 pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
317 let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
318 let core_entries = core_entries?;
319 let db = self.inner.clone();
320
321 tokio::task::spawn_blocking(move || {
322 let db = db.read().expect("RwLock poisoned");
323 db.insert_batch(core_entries)
324 })
325 .await
326 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
327 .map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
328 }
329
330 #[napi]
343 pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
344 let core_query = query.to_core()?;
345 let db = self.inner.clone();
346
347 tokio::task::spawn_blocking(move || {
348 let db = db.read().expect("RwLock poisoned");
349 db.search(core_query)
350 })
351 .await
352 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
353 .map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
354 .map(|results| results.into_iter().map(Into::into).collect())
355 }
356
357 #[napi]
366 pub async fn delete(&self, id: String) -> Result<bool> {
367 let db = self.inner.clone();
368
369 tokio::task::spawn_blocking(move || {
370 let db = db.read().expect("RwLock poisoned");
371 db.delete(&id)
372 })
373 .await
374 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
375 .map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
376 }
377
378 #[napi]
390 pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
391 let db = self.inner.clone();
392
393 let result = tokio::task::spawn_blocking(move || {
394 let db = db.read().expect("RwLock poisoned");
395 db.get(&id)
396 })
397 .await
398 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
399 .map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
400
401 Ok(result.map(|entry| {
402 let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok());
404
405 JsVectorEntry {
406 id: entry.id,
407 vector: Float32Array::new(entry.vector),
408 metadata,
409 }
410 }))
411 }
412
413 #[napi]
421 pub async fn len(&self) -> Result<u32> {
422 let db = self.inner.clone();
423
424 tokio::task::spawn_blocking(move || {
425 let db = db.read().expect("RwLock poisoned");
426 db.len()
427 })
428 .await
429 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
430 .map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
431 .map(|len| len as u32)
432 }
433
434 #[napi]
443 pub async fn is_empty(&self) -> Result<bool> {
444 let db = self.inner.clone();
445
446 tokio::task::spawn_blocking(move || {
447 let db = db.read().expect("RwLock poisoned");
448 db.is_empty()
449 })
450 .await
451 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
452 .map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
453 }
454}
455
456#[napi]
458pub fn version() -> String {
459 env!("CARGO_PKG_VERSION").to_string()
460}
461
462#[napi]
464pub fn hello() -> String {
465 "Hello from Ruvector Node.js bindings!".to_string()
466}
467
468#[napi(object)]
470#[derive(Debug, Clone)]
471pub struct JsFilter {
472 pub field: String,
474 pub operator: String,
476 pub value: String,
478}
479
480impl JsFilter {
481 fn to_filter_expression(&self) -> Result<FilterExpression> {
482 let value: serde_json::Value = serde_json::from_str(&self.value)
483 .map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
484
485 Ok(match self.operator.as_str() {
486 "eq" => FilterExpression::eq(&self.field, value),
487 "ne" => FilterExpression::ne(&self.field, value),
488 "gt" => FilterExpression::gt(&self.field, value),
489 "gte" => FilterExpression::gte(&self.field, value),
490 "lt" => FilterExpression::lt(&self.field, value),
491 "lte" => FilterExpression::lte(&self.field, value),
492 "match" => FilterExpression::Match {
493 field: self.field.clone(),
494 text: value.as_str().unwrap_or("").to_string(),
495 },
496 _ => FilterExpression::eq(&self.field, value),
497 })
498 }
499}
500
501#[napi(object)]
503#[derive(Debug, Clone)]
504pub struct JsCollectionConfig {
505 pub dimensions: u32,
507 pub distance_metric: Option<JsDistanceMetric>,
509 pub hnsw_config: Option<JsHnswConfig>,
511 pub quantization: Option<JsQuantizationConfig>,
513}
514
515impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
516 fn from(config: JsCollectionConfig) -> Self {
517 ruvector_collections::CollectionConfig {
518 dimensions: config.dimensions as usize,
519 distance_metric: config
520 .distance_metric
521 .map(Into::into)
522 .unwrap_or(DistanceMetric::Cosine),
523 hnsw_config: config.hnsw_config.map(Into::into),
524 quantization: config.quantization.map(Into::into),
525 on_disk_payload: true,
526 }
527 }
528}
529
530#[napi(object)]
532#[derive(Debug, Clone)]
533pub struct JsCollectionStats {
534 pub vectors_count: u32,
536 pub disk_size_bytes: i64,
538 pub ram_size_bytes: i64,
540}
541
542impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
543 fn from(stats: ruvector_collections::CollectionStats) -> Self {
544 JsCollectionStats {
545 vectors_count: stats.vectors_count as u32,
546 disk_size_bytes: stats.disk_size_bytes as i64,
547 ram_size_bytes: stats.ram_size_bytes as i64,
548 }
549 }
550}
551
552#[napi(object)]
554#[derive(Debug, Clone)]
555pub struct JsAlias {
556 pub alias: String,
558 pub collection: String,
560}
561
562impl From<(String, String)> for JsAlias {
563 fn from(tuple: (String, String)) -> Self {
564 JsAlias {
565 alias: tuple.0,
566 collection: tuple.1,
567 }
568 }
569}
570
571#[napi]
573pub struct CollectionManager {
574 inner: Arc<RwLock<CoreCollectionManager>>,
575}
576
577#[napi]
578impl CollectionManager {
579 #[napi(constructor)]
586 pub fn new(base_path: Option<String>) -> Result<Self> {
587 let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
588 let manager = CoreCollectionManager::new(path).map_err(|e| {
589 Error::from_reason(format!("Failed to create collection manager: {}", e))
590 })?;
591
592 Ok(Self {
593 inner: Arc::new(RwLock::new(manager)),
594 })
595 }
596
597 #[napi]
607 pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
608 let core_config: ruvector_collections::CollectionConfig = config.into();
609 let manager = self.inner.clone();
610
611 tokio::task::spawn_blocking(move || {
612 let manager = manager.write().expect("RwLock poisoned");
613 manager.create_collection(&name, core_config)
614 })
615 .await
616 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
617 .map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
618 }
619
620 #[napi]
628 pub async fn list_collections(&self) -> Result<Vec<String>> {
629 let manager = self.inner.clone();
630
631 tokio::task::spawn_blocking(move || {
632 let manager = manager.read().expect("RwLock poisoned");
633 manager.list_collections()
634 })
635 .await
636 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
637 }
638
639 #[napi]
646 pub async fn delete_collection(&self, name: 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.delete_collection(&name)
652 })
653 .await
654 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
655 .map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
656 }
657
658 #[napi]
666 pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
667 let manager = self.inner.clone();
668
669 tokio::task::spawn_blocking(move || {
670 let manager = manager.read().expect("RwLock poisoned");
671 manager.collection_stats(&name)
672 })
673 .await
674 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
675 .map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
676 .map(Into::into)
677 }
678
679 #[napi]
686 pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
687 let manager = self.inner.clone();
688
689 tokio::task::spawn_blocking(move || {
690 let manager = manager.write().expect("RwLock poisoned");
691 manager.create_alias(&alias, &collection)
692 })
693 .await
694 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
695 .map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
696 }
697
698 #[napi]
705 pub async fn delete_alias(&self, alias: String) -> Result<()> {
706 let manager = self.inner.clone();
707
708 tokio::task::spawn_blocking(move || {
709 let manager = manager.write().expect("RwLock poisoned");
710 manager.delete_alias(&alias)
711 })
712 .await
713 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
714 .map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
715 }
716
717 #[napi]
727 pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
728 let manager = self.inner.clone();
729
730 let aliases = tokio::task::spawn_blocking(move || {
731 let manager = manager.read().expect("RwLock poisoned");
732 manager.list_aliases()
733 })
734 .await
735 .map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
736
737 Ok(aliases.into_iter().map(Into::into).collect())
738 }
739}
740
741#[napi(object)]
743#[derive(Debug, Clone)]
744pub struct JsHealthResponse {
745 pub status: String,
747 pub version: String,
749 pub uptime_seconds: i64,
751}
752
753#[napi]
761pub fn get_metrics() -> String {
762 gather_metrics()
763}
764
765#[napi]
774pub fn get_health() -> JsHealthResponse {
775 let checker = HealthChecker::new();
776 let health = checker.health();
777
778 JsHealthResponse {
779 status: match health.status {
780 HealthStatus::Healthy => "healthy".to_string(),
781 HealthStatus::Degraded => "degraded".to_string(),
782 HealthStatus::Unhealthy => "unhealthy".to_string(),
783 },
784 version: health.version,
785 uptime_seconds: health.uptime_seconds as i64,
786 }
787}