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,
14 VectorDB as CoreVectorDB, VectorEntry,
15};
16use std::sync::Arc;
17use std::sync::RwLock;
18
19#[napi(string_enum)]
21#[derive(Debug)]
22pub enum JsDistanceMetric {
23 Euclidean,
25 Cosine,
27 DotProduct,
29 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#[napi(object)]
46#[derive(Debug)]
47pub struct JsQuantizationConfig {
48 pub r#type: String,
50 pub subspaces: Option<u32>,
52 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#[napi(object)]
73#[derive(Debug)]
74pub struct JsHnswConfig {
75 pub m: Option<u32>,
77 pub ef_construction: Option<u32>,
79 pub ef_search: Option<u32>,
81 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#[napi(object)]
98#[derive(Debug)]
99pub struct JsDbOptions {
100 pub dimensions: u32,
102 pub distance_metric: Option<JsDistanceMetric>,
104 pub storage_path: Option<String>,
106 pub hnsw_config: Option<JsHnswConfig>,
108 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#[napi(object)]
131pub struct JsVectorEntry {
132 pub id: Option<String>,
134 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#[napi(object)]
150pub struct JsSearchQuery {
151 pub vector: Float32Array,
153 pub k: u32,
155 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#[napi(object)]
172#[derive(Debug, Clone)]
173pub struct JsSearchResult {
174 pub id: String,
176 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#[napi]
191pub struct VectorDB {
192 inner: Arc<RwLock<CoreVectorDB>>,
193}
194
195#[napi]
196impl VectorDB {
197 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[napi]
417pub fn version() -> String {
418 env!("CARGO_PKG_VERSION").to_string()
419}
420
421#[napi]
423pub fn hello() -> String {
424 "Hello from Ruvector Node.js bindings!".to_string()
425}