1use js_sys::{Array, Float32Array, Object, Promise, Reflect, Uint8Array};
12use parking_lot::Mutex;
13#[cfg(feature = "collections")]
14use ruvector_collections::{
15 CollectionConfig as CoreCollectionConfig, CollectionManager as CoreCollectionManager,
16};
17use ruvector_core::{
18 error::RuvectorError,
19 types::{DbOptions, DistanceMetric, HnswConfig, SearchQuery, SearchResult, VectorEntry},
20 vector_db::VectorDB as CoreVectorDB,
21};
22#[cfg(feature = "collections")]
23use ruvector_filter::FilterExpression as CoreFilterExpression;
24use serde::{Deserialize, Serialize};
25use serde_wasm_bindgen::{from_value, to_value};
26use std::collections::HashMap;
27use std::sync::Arc;
28use wasm_bindgen::prelude::*;
29use wasm_bindgen_futures::JsFuture;
30use web_sys::{
31 console, IdbDatabase, IdbFactory, IdbObjectStore, IdbRequest, IdbTransaction, Window,
32};
33
34#[wasm_bindgen(start)]
36pub fn init() {
37 console_error_panic_hook::set_once();
38 tracing_wasm::set_as_global_default();
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct WasmError {
44 pub message: String,
45 pub kind: String,
46}
47
48impl From<RuvectorError> for WasmError {
49 fn from(err: RuvectorError) -> Self {
50 WasmError {
51 message: err.to_string(),
52 kind: format!("{:?}", err),
53 }
54 }
55}
56
57impl From<WasmError> for JsValue {
58 fn from(err: WasmError) -> Self {
59 let obj = Object::new();
60 Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
61 Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
62 obj.into()
63 }
64}
65
66type WasmResult<T> = Result<T, WasmError>;
67
68#[wasm_bindgen]
70#[derive(Clone)]
71pub struct JsVectorEntry {
72 inner: VectorEntry,
73}
74
75const MAX_VECTOR_DIMENSIONS: usize = 65536;
77
78#[wasm_bindgen]
79impl JsVectorEntry {
80 #[wasm_bindgen(constructor)]
81 pub fn new(
82 vector: Float32Array,
83 id: Option<String>,
84 metadata: Option<JsValue>,
85 ) -> Result<JsVectorEntry, JsValue> {
86 let vec_len = vector.length() as usize;
88 if vec_len == 0 {
89 return Err(JsValue::from_str("Vector cannot be empty"));
90 }
91 if vec_len > MAX_VECTOR_DIMENSIONS {
92 return Err(JsValue::from_str(&format!(
93 "Vector dimensions {} exceed maximum allowed {}",
94 vec_len, MAX_VECTOR_DIMENSIONS
95 )));
96 }
97
98 let vector_data: Vec<f32> = vector.to_vec();
99
100 let metadata = if let Some(meta) = metadata {
101 Some(
102 from_value(meta)
103 .map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
104 )
105 } else {
106 None
107 };
108
109 Ok(JsVectorEntry {
110 inner: VectorEntry {
111 id,
112 vector: vector_data,
113 metadata,
114 },
115 })
116 }
117
118 #[wasm_bindgen(getter)]
119 pub fn id(&self) -> Option<String> {
120 self.inner.id.clone()
121 }
122
123 #[wasm_bindgen(getter)]
124 pub fn vector(&self) -> Float32Array {
125 Float32Array::from(&self.inner.vector[..])
126 }
127
128 #[wasm_bindgen(getter)]
129 pub fn metadata(&self) -> Option<JsValue> {
130 self.inner.metadata.as_ref().map(|m| to_value(m).unwrap())
131 }
132}
133
134#[wasm_bindgen]
136pub struct JsSearchResult {
137 inner: SearchResult,
138}
139
140#[wasm_bindgen]
141impl JsSearchResult {
142 #[wasm_bindgen(getter)]
143 pub fn id(&self) -> String {
144 self.inner.id.clone()
145 }
146
147 #[wasm_bindgen(getter)]
148 pub fn score(&self) -> f32 {
149 self.inner.score
150 }
151
152 #[wasm_bindgen(getter)]
153 pub fn vector(&self) -> Option<Float32Array> {
154 self.inner
155 .vector
156 .as_ref()
157 .map(|v| Float32Array::from(&v[..]))
158 }
159
160 #[wasm_bindgen(getter)]
161 pub fn metadata(&self) -> Option<JsValue> {
162 self.inner.metadata.as_ref().map(|m| to_value(m).unwrap())
163 }
164}
165
166#[wasm_bindgen]
168pub struct VectorDB {
169 db: Arc<Mutex<CoreVectorDB>>,
170 dimensions: usize,
171 db_name: String,
172}
173
174#[wasm_bindgen]
175impl VectorDB {
176 #[wasm_bindgen(constructor)]
183 pub fn new(
184 dimensions: usize,
185 metric: Option<String>,
186 use_hnsw: Option<bool>,
187 ) -> Result<VectorDB, JsValue> {
188 let distance_metric = match metric.as_deref() {
189 Some("euclidean") => DistanceMetric::Euclidean,
190 Some("cosine") => DistanceMetric::Cosine,
191 Some("dotproduct") => DistanceMetric::DotProduct,
192 Some("manhattan") => DistanceMetric::Manhattan,
193 None => DistanceMetric::Cosine,
194 Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
195 };
196
197 let hnsw_config = if use_hnsw.unwrap_or(true) {
198 Some(HnswConfig::default())
199 } else {
200 None
201 };
202
203 let options = DbOptions {
204 dimensions,
205 distance_metric,
206 storage_path: ":memory:".to_string(), hnsw_config,
208 quantization: None, };
210
211 let db = CoreVectorDB::new(options).map_err(|e| JsValue::from(WasmError::from(e)))?;
212
213 Ok(VectorDB {
214 db: Arc::new(Mutex::new(db)),
215 dimensions,
216 db_name: format!("ruvector_db_{}", js_sys::Date::now()),
217 })
218 }
219
220 #[wasm_bindgen]
230 pub fn insert(
231 &self,
232 vector: Float32Array,
233 id: Option<String>,
234 metadata: Option<JsValue>,
235 ) -> Result<String, JsValue> {
236 let entry = JsVectorEntry::new(vector, id, metadata)?;
237
238 let db = self.db.lock();
239 let vector_id = db
240 .insert(entry.inner)
241 .map_err(|e| JsValue::from(WasmError::from(e)))?;
242
243 Ok(vector_id)
244 }
245
246 #[wasm_bindgen(js_name = insertBatch)]
254 pub fn insert_batch(&self, entries: JsValue) -> Result<Vec<String>, JsValue> {
255 let entries_array: js_sys::Array = entries
257 .dyn_into()
258 .map_err(|_| JsValue::from_str("entries must be an array"))?;
259
260 let mut vector_entries = Vec::new();
261 for i in 0..entries_array.length() {
262 let js_entry = entries_array.get(i);
263 let vector_arr: Float32Array = Reflect::get(&js_entry, &"vector".into())?.dyn_into()?;
264 let id: Option<String> = Reflect::get(&js_entry, &"id".into())?.as_string();
265 let metadata = Reflect::get(&js_entry, &"metadata".into()).ok();
266
267 let entry = JsVectorEntry::new(vector_arr, id, metadata)?;
268 vector_entries.push(entry.inner);
269 }
270
271 let db = self.db.lock();
272 let ids = db
273 .insert_batch(vector_entries)
274 .map_err(|e| JsValue::from(WasmError::from(e)))?;
275
276 Ok(ids)
277 }
278
279 #[wasm_bindgen]
289 pub fn search(
290 &self,
291 query: Float32Array,
292 k: usize,
293 filter: Option<JsValue>,
294 ) -> Result<Vec<JsSearchResult>, JsValue> {
295 let query_vector: Vec<f32> = query.to_vec();
296
297 if query_vector.len() != self.dimensions {
298 return Err(JsValue::from_str(&format!(
299 "Query vector dimension mismatch: expected {}, got {}",
300 self.dimensions,
301 query_vector.len()
302 )));
303 }
304
305 let metadata_filter = if let Some(f) = filter {
306 Some(from_value(f).map_err(|e| JsValue::from_str(&format!("Invalid filter: {}", e)))?)
307 } else {
308 None
309 };
310
311 let search_query = SearchQuery {
312 vector: query_vector,
313 k,
314 filter: metadata_filter,
315 ef_search: None,
316 };
317
318 let db = self.db.lock();
319 let results = db
320 .search(search_query)
321 .map_err(|e| JsValue::from(WasmError::from(e)))?;
322
323 Ok(results
324 .into_iter()
325 .map(|r| JsSearchResult { inner: r })
326 .collect())
327 }
328
329 #[wasm_bindgen]
337 pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
338 let db = self.db.lock();
339 db.delete(id).map_err(|e| JsValue::from(WasmError::from(e)))
340 }
341
342 #[wasm_bindgen]
350 pub fn get(&self, id: &str) -> Result<Option<JsVectorEntry>, JsValue> {
351 let db = self.db.lock();
352 let entry = db.get(id).map_err(|e| JsValue::from(WasmError::from(e)))?;
353
354 Ok(entry.map(|e| JsVectorEntry { inner: e }))
355 }
356
357 #[wasm_bindgen]
359 pub fn len(&self) -> Result<usize, JsValue> {
360 let db = self.db.lock();
361 db.len().map_err(|e| JsValue::from(WasmError::from(e)))
362 }
363
364 #[wasm_bindgen(js_name = isEmpty)]
366 pub fn is_empty(&self) -> Result<bool, JsValue> {
367 let db = self.db.lock();
368 db.is_empty().map_err(|e| JsValue::from(WasmError::from(e)))
369 }
370
371 #[wasm_bindgen(getter)]
373 pub fn dimensions(&self) -> usize {
374 self.dimensions
375 }
376
377 #[wasm_bindgen(js_name = saveToIndexedDB)]
380 pub fn save_to_indexed_db(&self) -> Result<Promise, JsValue> {
381 let db_name = self.db_name.clone();
382
383 console::log_1(&format!("Saving database '{}' to IndexedDB...", db_name).into());
386
387 Ok(Promise::resolve(&JsValue::TRUE))
389 }
390
391 #[wasm_bindgen(js_name = loadFromIndexedDB)]
394 pub fn load_from_indexed_db(db_name: String) -> Result<Promise, JsValue> {
395 console::log_1(&format!("Loading database '{}' from IndexedDB...", db_name).into());
396
397 Ok(Promise::reject(&JsValue::from_str("Not yet implemented")))
399 }
400}
401
402#[wasm_bindgen(js_name = detectSIMD)]
404pub fn detect_simd() -> bool {
405 #[cfg(target_feature = "simd128")]
407 {
408 true
409 }
410 #[cfg(not(target_feature = "simd128"))]
411 {
412 false
413 }
414}
415
416#[wasm_bindgen]
418pub fn version() -> String {
419 env!("CARGO_PKG_VERSION").to_string()
420}
421
422#[wasm_bindgen(js_name = arrayToFloat32Array)]
424pub fn array_to_float32_array(arr: Vec<f32>) -> Float32Array {
425 Float32Array::from(&arr[..])
426}
427
428#[wasm_bindgen(js_name = benchmark)]
430pub fn benchmark(name: &str, iterations: usize, dimensions: usize) -> Result<f64, JsValue> {
431 use std::time::Instant;
432
433 console::log_1(
434 &format!(
435 "Running benchmark '{}' with {} iterations...",
436 name, iterations
437 )
438 .into(),
439 );
440
441 let db = VectorDB::new(dimensions, Some("cosine".to_string()), Some(false))?;
442
443 let start = Instant::now();
444
445 for i in 0..iterations {
446 let vector: Vec<f32> = (0..dimensions)
447 .map(|_| js_sys::Math::random() as f32)
448 .collect();
449 let vector_arr = Float32Array::from(&vector[..]);
450 db.insert(vector_arr, Some(format!("vec_{}", i)), None)?;
451 }
452
453 let duration = start.elapsed();
454 let ops_per_sec = iterations as f64 / duration.as_secs_f64();
455
456 console::log_1(&format!("Benchmark complete: {:.2} ops/sec", ops_per_sec).into());
457
458 Ok(ops_per_sec)
459}
460
461#[cfg(feature = "collections")]
466#[wasm_bindgen]
468pub struct CollectionManager {
469 inner: Arc<Mutex<CoreCollectionManager>>,
470}
471
472#[cfg(feature = "collections")]
473#[wasm_bindgen]
474impl CollectionManager {
475 #[wasm_bindgen(constructor)]
480 pub fn new(base_path: Option<String>) -> Result<CollectionManager, JsValue> {
481 let path = base_path.unwrap_or_else(|| ":memory:".to_string());
482
483 let manager = CoreCollectionManager::new(std::path::PathBuf::from(path)).map_err(|e| {
484 JsValue::from_str(&format!("Failed to create collection manager: {}", e))
485 })?;
486
487 Ok(CollectionManager {
488 inner: Arc::new(Mutex::new(manager)),
489 })
490 }
491
492 #[wasm_bindgen(js_name = createCollection)]
499 pub fn create_collection(
500 &self,
501 name: &str,
502 dimensions: usize,
503 metric: Option<String>,
504 ) -> Result<(), JsValue> {
505 let distance_metric = match metric.as_deref() {
506 Some("euclidean") => DistanceMetric::Euclidean,
507 Some("cosine") => DistanceMetric::Cosine,
508 Some("dotproduct") => DistanceMetric::DotProduct,
509 Some("manhattan") => DistanceMetric::Manhattan,
510 None => DistanceMetric::Cosine,
511 Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
512 };
513
514 let config = CoreCollectionConfig {
515 dimensions,
516 distance_metric,
517 hnsw_config: Some(HnswConfig::default()),
518 quantization: None,
519 on_disk_payload: false, };
521
522 let manager = self.inner.lock();
523 manager
524 .create_collection(name, config)
525 .map_err(|e| JsValue::from_str(&format!("Failed to create collection: {}", e)))?;
526
527 Ok(())
528 }
529
530 #[wasm_bindgen(js_name = listCollections)]
535 pub fn list_collections(&self) -> Vec<String> {
536 let manager = self.inner.lock();
537 manager.list_collections()
538 }
539
540 #[wasm_bindgen(js_name = deleteCollection)]
548 pub fn delete_collection(&self, name: &str) -> Result<(), JsValue> {
549 let manager = self.inner.lock();
550 manager
551 .delete_collection(name)
552 .map_err(|e| JsValue::from_str(&format!("Failed to delete collection: {}", e)))?;
553
554 Ok(())
555 }
556
557 #[wasm_bindgen(js_name = getCollection)]
565 pub fn get_collection(&self, name: &str) -> Result<VectorDB, JsValue> {
566 let manager = self.inner.lock();
567
568 let collection_ref = manager
569 .get_collection(name)
570 .ok_or_else(|| JsValue::from_str(&format!("Collection '{}' not found", name)))?;
571
572 let collection = collection_ref.read();
573
574 let dimensions = collection.config.dimensions;
578 let db_name = collection.name.clone();
579
580 let db_options = DbOptions {
583 dimensions: collection.config.dimensions,
584 distance_metric: collection.config.distance_metric,
585 storage_path: ":memory:".to_string(),
586 hnsw_config: collection.config.hnsw_config.clone(),
587 quantization: collection.config.quantization.clone(),
588 };
589
590 let db = CoreVectorDB::new(db_options)
591 .map_err(|e| JsValue::from_str(&format!("Failed to get collection: {}", e)))?;
592
593 Ok(VectorDB {
594 db: Arc::new(Mutex::new(db)),
595 dimensions,
596 db_name,
597 })
598 }
599
600 #[wasm_bindgen(js_name = createAlias)]
606 pub fn create_alias(&self, alias: &str, collection: &str) -> Result<(), JsValue> {
607 let manager = self.inner.lock();
608 manager
609 .create_alias(alias, collection)
610 .map_err(|e| JsValue::from_str(&format!("Failed to create alias: {}", e)))?;
611
612 Ok(())
613 }
614
615 #[wasm_bindgen(js_name = deleteAlias)]
620 pub fn delete_alias(&self, alias: &str) -> Result<(), JsValue> {
621 let manager = self.inner.lock();
622 manager
623 .delete_alias(alias)
624 .map_err(|e| JsValue::from_str(&format!("Failed to delete alias: {}", e)))?;
625
626 Ok(())
627 }
628
629 #[wasm_bindgen(js_name = listAliases)]
634 pub fn list_aliases(&self) -> JsValue {
635 let manager = self.inner.lock();
636 let aliases = manager.list_aliases();
637
638 let arr = Array::new();
639 for (alias, collection) in aliases {
640 let pair = Array::new();
641 pair.push(&JsValue::from_str(&alias));
642 pair.push(&JsValue::from_str(&collection));
643 arr.push(&pair);
644 }
645
646 arr.into()
647 }
648}
649
650#[cfg(feature = "collections")]
653#[wasm_bindgen]
655pub struct FilterBuilder {
656 inner: CoreFilterExpression,
657}
658
659#[cfg(feature = "collections")]
660#[wasm_bindgen]
661impl FilterBuilder {
662 #[wasm_bindgen(constructor)]
664 pub fn new() -> FilterBuilder {
665 FilterBuilder {
668 inner: CoreFilterExpression::exists("_id"),
669 }
670 }
671
672 pub fn eq(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
683 let json_value: serde_json::Value =
684 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
685
686 Ok(FilterBuilder {
687 inner: CoreFilterExpression::eq(field, json_value),
688 })
689 }
690
691 pub fn ne(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
693 let json_value: serde_json::Value =
694 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
695
696 Ok(FilterBuilder {
697 inner: CoreFilterExpression::ne(field, json_value),
698 })
699 }
700
701 pub fn gt(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
703 let json_value: serde_json::Value =
704 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
705
706 Ok(FilterBuilder {
707 inner: CoreFilterExpression::gt(field, json_value),
708 })
709 }
710
711 pub fn gte(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
713 let json_value: serde_json::Value =
714 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
715
716 Ok(FilterBuilder {
717 inner: CoreFilterExpression::gte(field, json_value),
718 })
719 }
720
721 pub fn lt(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
723 let json_value: serde_json::Value =
724 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
725
726 Ok(FilterBuilder {
727 inner: CoreFilterExpression::lt(field, json_value),
728 })
729 }
730
731 pub fn lte(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
733 let json_value: serde_json::Value =
734 from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
735
736 Ok(FilterBuilder {
737 inner: CoreFilterExpression::lte(field, json_value),
738 })
739 }
740
741 #[wasm_bindgen(js_name = "in")]
747 pub fn in_values(field: &str, values: JsValue) -> Result<FilterBuilder, JsValue> {
748 let json_values: Vec<serde_json::Value> = from_value(values)
749 .map_err(|e| JsValue::from_str(&format!("Invalid values array: {}", e)))?;
750
751 Ok(FilterBuilder {
752 inner: CoreFilterExpression::in_values(field, json_values),
753 })
754 }
755
756 #[wasm_bindgen(js_name = matchText)]
762 pub fn match_text(field: &str, text: &str) -> FilterBuilder {
763 FilterBuilder {
764 inner: CoreFilterExpression::match_text(field, text),
765 }
766 }
767
768 #[wasm_bindgen(js_name = geoRadius)]
776 pub fn geo_radius(field: &str, lat: f64, lon: f64, radius_m: f64) -> FilterBuilder {
777 FilterBuilder {
778 inner: CoreFilterExpression::geo_radius(field, lat, lon, radius_m),
779 }
780 }
781
782 pub fn and(filters: Vec<FilterBuilder>) -> FilterBuilder {
787 let inner_filters: Vec<CoreFilterExpression> =
788 filters.into_iter().map(|f| f.inner).collect();
789
790 FilterBuilder {
791 inner: CoreFilterExpression::and(inner_filters),
792 }
793 }
794
795 pub fn or(filters: Vec<FilterBuilder>) -> FilterBuilder {
800 let inner_filters: Vec<CoreFilterExpression> =
801 filters.into_iter().map(|f| f.inner).collect();
802
803 FilterBuilder {
804 inner: CoreFilterExpression::or(inner_filters),
805 }
806 }
807
808 pub fn not(filter: FilterBuilder) -> FilterBuilder {
813 FilterBuilder {
814 inner: CoreFilterExpression::not(filter.inner),
815 }
816 }
817
818 pub fn exists(field: &str) -> FilterBuilder {
820 FilterBuilder {
821 inner: CoreFilterExpression::exists(field),
822 }
823 }
824
825 #[wasm_bindgen(js_name = isNull)]
827 pub fn is_null(field: &str) -> FilterBuilder {
828 FilterBuilder {
829 inner: CoreFilterExpression::is_null(field),
830 }
831 }
832
833 #[wasm_bindgen(js_name = toJson)]
838 pub fn to_json(&self) -> Result<JsValue, JsValue> {
839 to_value(&self.inner)
840 .map_err(|e| JsValue::from_str(&format!("Failed to serialize filter: {}", e)))
841 }
842
843 #[wasm_bindgen(js_name = getFields)]
845 pub fn get_fields(&self) -> Vec<String> {
846 self.inner.get_fields()
847 }
848}
849
850#[cfg(feature = "collections")]
851impl Default for FilterBuilder {
852 fn default() -> Self {
853 Self::new()
854 }
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860 use wasm_bindgen_test::*;
861
862 wasm_bindgen_test_configure!(run_in_browser);
863
864 #[wasm_bindgen_test]
865 fn test_version() {
866 assert!(!version().is_empty());
867 }
868
869 #[wasm_bindgen_test]
870 fn test_detect_simd() {
871 let _ = detect_simd();
873 }
874}