use js_sys::{Array, Float32Array, Object, Promise, Reflect, Uint8Array};
use parking_lot::Mutex;
#[cfg(feature = "collections")]
use ruvector_collections::{
CollectionConfig as CoreCollectionConfig, CollectionManager as CoreCollectionManager,
};
use ruvector_core::{
error::RuvectorError,
types::{DbOptions, DistanceMetric, HnswConfig, SearchQuery, SearchResult, VectorEntry},
vector_db::VectorDB as CoreVectorDB,
};
#[cfg(feature = "collections")]
use ruvector_filter::FilterExpression as CoreFilterExpression;
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::{from_value, to_value};
use std::collections::HashMap;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{
console, IdbDatabase, IdbFactory, IdbObjectStore, IdbRequest, IdbTransaction, Window,
};
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
tracing_wasm::set_as_global_default();
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WasmError {
pub message: String,
pub kind: String,
}
impl From<RuvectorError> for WasmError {
fn from(err: RuvectorError) -> Self {
WasmError {
message: err.to_string(),
kind: format!("{:?}", err),
}
}
}
impl From<WasmError> for JsValue {
fn from(err: WasmError) -> Self {
let obj = Object::new();
Reflect::set(&obj, &"message".into(), &err.message.into()).unwrap();
Reflect::set(&obj, &"kind".into(), &err.kind.into()).unwrap();
obj.into()
}
}
type WasmResult<T> = Result<T, WasmError>;
#[wasm_bindgen]
#[derive(Clone)]
pub struct JsVectorEntry {
inner: VectorEntry,
}
const MAX_VECTOR_DIMENSIONS: usize = 65536;
#[wasm_bindgen]
impl JsVectorEntry {
#[wasm_bindgen(constructor)]
pub fn new(
vector: Float32Array,
id: Option<String>,
metadata: Option<JsValue>,
) -> Result<JsVectorEntry, JsValue> {
let vec_len = vector.length() as usize;
if vec_len == 0 {
return Err(JsValue::from_str("Vector cannot be empty"));
}
if vec_len > MAX_VECTOR_DIMENSIONS {
return Err(JsValue::from_str(&format!(
"Vector dimensions {} exceed maximum allowed {}",
vec_len, MAX_VECTOR_DIMENSIONS
)));
}
let vector_data: Vec<f32> = vector.to_vec();
let metadata = if let Some(meta) = metadata {
Some(
from_value(meta)
.map_err(|e| JsValue::from_str(&format!("Invalid metadata: {}", e)))?,
)
} else {
None
};
Ok(JsVectorEntry {
inner: VectorEntry {
id,
vector: vector_data,
metadata,
},
})
}
#[wasm_bindgen(getter)]
pub fn id(&self) -> Option<String> {
self.inner.id.clone()
}
#[wasm_bindgen(getter)]
pub fn vector(&self) -> Float32Array {
Float32Array::from(&self.inner.vector[..])
}
#[wasm_bindgen(getter)]
pub fn metadata(&self) -> Option<JsValue> {
self.inner.metadata.as_ref().map(|m| to_value(m).unwrap())
}
}
#[wasm_bindgen]
pub struct JsSearchResult {
inner: SearchResult,
}
#[wasm_bindgen]
impl JsSearchResult {
#[wasm_bindgen(getter)]
pub fn id(&self) -> String {
self.inner.id.clone()
}
#[wasm_bindgen(getter)]
pub fn score(&self) -> f32 {
self.inner.score
}
#[wasm_bindgen(getter)]
pub fn vector(&self) -> Option<Float32Array> {
self.inner
.vector
.as_ref()
.map(|v| Float32Array::from(&v[..]))
}
#[wasm_bindgen(getter)]
pub fn metadata(&self) -> Option<JsValue> {
self.inner.metadata.as_ref().map(|m| to_value(m).unwrap())
}
}
#[wasm_bindgen]
pub struct VectorDB {
db: Arc<Mutex<CoreVectorDB>>,
dimensions: usize,
db_name: String,
}
#[wasm_bindgen]
impl VectorDB {
#[wasm_bindgen(constructor)]
pub fn new(
dimensions: usize,
metric: Option<String>,
use_hnsw: Option<bool>,
) -> Result<VectorDB, JsValue> {
let distance_metric = match metric.as_deref() {
Some("euclidean") => DistanceMetric::Euclidean,
Some("cosine") => DistanceMetric::Cosine,
Some("dotproduct") => DistanceMetric::DotProduct,
Some("manhattan") => DistanceMetric::Manhattan,
None => DistanceMetric::Cosine,
Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
};
let hnsw_config = if use_hnsw.unwrap_or(true) {
Some(HnswConfig::default())
} else {
None
};
let options = DbOptions {
dimensions,
distance_metric,
storage_path: ":memory:".to_string(), hnsw_config,
quantization: None, };
let db = CoreVectorDB::new(options).map_err(|e| JsValue::from(WasmError::from(e)))?;
Ok(VectorDB {
db: Arc::new(Mutex::new(db)),
dimensions,
db_name: format!("ruvector_db_{}", js_sys::Date::now()),
})
}
#[wasm_bindgen]
pub fn insert(
&self,
vector: Float32Array,
id: Option<String>,
metadata: Option<JsValue>,
) -> Result<String, JsValue> {
let entry = JsVectorEntry::new(vector, id, metadata)?;
let db = self.db.lock();
let vector_id = db
.insert(entry.inner)
.map_err(|e| JsValue::from(WasmError::from(e)))?;
Ok(vector_id)
}
#[wasm_bindgen(js_name = insertBatch)]
pub fn insert_batch(&self, entries: JsValue) -> Result<Vec<String>, JsValue> {
let entries_array: js_sys::Array = entries
.dyn_into()
.map_err(|_| JsValue::from_str("entries must be an array"))?;
let mut vector_entries = Vec::new();
for i in 0..entries_array.length() {
let js_entry = entries_array.get(i);
let vector_arr: Float32Array = Reflect::get(&js_entry, &"vector".into())?.dyn_into()?;
let id: Option<String> = Reflect::get(&js_entry, &"id".into())?.as_string();
let metadata = Reflect::get(&js_entry, &"metadata".into()).ok();
let entry = JsVectorEntry::new(vector_arr, id, metadata)?;
vector_entries.push(entry.inner);
}
let db = self.db.lock();
let ids = db
.insert_batch(vector_entries)
.map_err(|e| JsValue::from(WasmError::from(e)))?;
Ok(ids)
}
#[wasm_bindgen]
pub fn search(
&self,
query: Float32Array,
k: usize,
filter: Option<JsValue>,
) -> Result<Vec<JsSearchResult>, JsValue> {
let query_vector: Vec<f32> = query.to_vec();
if query_vector.len() != self.dimensions {
return Err(JsValue::from_str(&format!(
"Query vector dimension mismatch: expected {}, got {}",
self.dimensions,
query_vector.len()
)));
}
let metadata_filter = if let Some(f) = filter {
Some(from_value(f).map_err(|e| JsValue::from_str(&format!("Invalid filter: {}", e)))?)
} else {
None
};
let search_query = SearchQuery {
vector: query_vector,
k,
filter: metadata_filter,
ef_search: None,
};
let db = self.db.lock();
let results = db
.search(search_query)
.map_err(|e| JsValue::from(WasmError::from(e)))?;
Ok(results
.into_iter()
.map(|r| JsSearchResult { inner: r })
.collect())
}
#[wasm_bindgen]
pub fn delete(&self, id: &str) -> Result<bool, JsValue> {
let db = self.db.lock();
db.delete(id).map_err(|e| JsValue::from(WasmError::from(e)))
}
#[wasm_bindgen]
pub fn get(&self, id: &str) -> Result<Option<JsVectorEntry>, JsValue> {
let db = self.db.lock();
let entry = db.get(id).map_err(|e| JsValue::from(WasmError::from(e)))?;
Ok(entry.map(|e| JsVectorEntry { inner: e }))
}
#[wasm_bindgen]
pub fn len(&self) -> Result<usize, JsValue> {
let db = self.db.lock();
db.len().map_err(|e| JsValue::from(WasmError::from(e)))
}
#[wasm_bindgen(js_name = isEmpty)]
pub fn is_empty(&self) -> Result<bool, JsValue> {
let db = self.db.lock();
db.is_empty().map_err(|e| JsValue::from(WasmError::from(e)))
}
#[wasm_bindgen(getter)]
pub fn dimensions(&self) -> usize {
self.dimensions
}
#[wasm_bindgen(js_name = saveToIndexedDB)]
pub fn save_to_indexed_db(&self) -> Result<Promise, JsValue> {
let db_name = self.db_name.clone();
console::log_1(&format!("Saving database '{}' to IndexedDB...", db_name).into());
Ok(Promise::resolve(&JsValue::TRUE))
}
#[wasm_bindgen(js_name = loadFromIndexedDB)]
pub fn load_from_indexed_db(db_name: String) -> Result<Promise, JsValue> {
console::log_1(&format!("Loading database '{}' from IndexedDB...", db_name).into());
Ok(Promise::reject(&JsValue::from_str("Not yet implemented")))
}
}
#[wasm_bindgen(js_name = detectSIMD)]
pub fn detect_simd() -> bool {
#[cfg(target_feature = "simd128")]
{
true
}
#[cfg(not(target_feature = "simd128"))]
{
false
}
}
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen(js_name = arrayToFloat32Array)]
pub fn array_to_float32_array(arr: Vec<f32>) -> Float32Array {
Float32Array::from(&arr[..])
}
#[wasm_bindgen(js_name = benchmark)]
pub fn benchmark(name: &str, iterations: usize, dimensions: usize) -> Result<f64, JsValue> {
use std::time::Instant;
console::log_1(
&format!(
"Running benchmark '{}' with {} iterations...",
name, iterations
)
.into(),
);
let db = VectorDB::new(dimensions, Some("cosine".to_string()), Some(false))?;
let start = Instant::now();
for i in 0..iterations {
let vector: Vec<f32> = (0..dimensions)
.map(|_| js_sys::Math::random() as f32)
.collect();
let vector_arr = Float32Array::from(&vector[..]);
db.insert(vector_arr, Some(format!("vec_{}", i)), None)?;
}
let duration = start.elapsed();
let ops_per_sec = iterations as f64 / duration.as_secs_f64();
console::log_1(&format!("Benchmark complete: {:.2} ops/sec", ops_per_sec).into());
Ok(ops_per_sec)
}
#[cfg(feature = "collections")]
#[wasm_bindgen]
pub struct CollectionManager {
inner: Arc<Mutex<CoreCollectionManager>>,
}
#[cfg(feature = "collections")]
#[wasm_bindgen]
impl CollectionManager {
#[wasm_bindgen(constructor)]
pub fn new(base_path: Option<String>) -> Result<CollectionManager, JsValue> {
let path = base_path.unwrap_or_else(|| ":memory:".to_string());
let manager = CoreCollectionManager::new(std::path::PathBuf::from(path)).map_err(|e| {
JsValue::from_str(&format!("Failed to create collection manager: {}", e))
})?;
Ok(CollectionManager {
inner: Arc::new(Mutex::new(manager)),
})
}
#[wasm_bindgen(js_name = createCollection)]
pub fn create_collection(
&self,
name: &str,
dimensions: usize,
metric: Option<String>,
) -> Result<(), JsValue> {
let distance_metric = match metric.as_deref() {
Some("euclidean") => DistanceMetric::Euclidean,
Some("cosine") => DistanceMetric::Cosine,
Some("dotproduct") => DistanceMetric::DotProduct,
Some("manhattan") => DistanceMetric::Manhattan,
None => DistanceMetric::Cosine,
Some(other) => return Err(JsValue::from_str(&format!("Unknown metric: {}", other))),
};
let config = CoreCollectionConfig {
dimensions,
distance_metric,
hnsw_config: Some(HnswConfig::default()),
quantization: None,
on_disk_payload: false, };
let manager = self.inner.lock();
manager
.create_collection(name, config)
.map_err(|e| JsValue::from_str(&format!("Failed to create collection: {}", e)))?;
Ok(())
}
#[wasm_bindgen(js_name = listCollections)]
pub fn list_collections(&self) -> Vec<String> {
let manager = self.inner.lock();
manager.list_collections()
}
#[wasm_bindgen(js_name = deleteCollection)]
pub fn delete_collection(&self, name: &str) -> Result<(), JsValue> {
let manager = self.inner.lock();
manager
.delete_collection(name)
.map_err(|e| JsValue::from_str(&format!("Failed to delete collection: {}", e)))?;
Ok(())
}
#[wasm_bindgen(js_name = getCollection)]
pub fn get_collection(&self, name: &str) -> Result<VectorDB, JsValue> {
let manager = self.inner.lock();
let collection_ref = manager
.get_collection(name)
.ok_or_else(|| JsValue::from_str(&format!("Collection '{}' not found", name)))?;
let collection = collection_ref.read();
let dimensions = collection.config.dimensions;
let db_name = collection.name.clone();
let db_options = DbOptions {
dimensions: collection.config.dimensions,
distance_metric: collection.config.distance_metric,
storage_path: ":memory:".to_string(),
hnsw_config: collection.config.hnsw_config.clone(),
quantization: collection.config.quantization.clone(),
};
let db = CoreVectorDB::new(db_options)
.map_err(|e| JsValue::from_str(&format!("Failed to get collection: {}", e)))?;
Ok(VectorDB {
db: Arc::new(Mutex::new(db)),
dimensions,
db_name,
})
}
#[wasm_bindgen(js_name = createAlias)]
pub fn create_alias(&self, alias: &str, collection: &str) -> Result<(), JsValue> {
let manager = self.inner.lock();
manager
.create_alias(alias, collection)
.map_err(|e| JsValue::from_str(&format!("Failed to create alias: {}", e)))?;
Ok(())
}
#[wasm_bindgen(js_name = deleteAlias)]
pub fn delete_alias(&self, alias: &str) -> Result<(), JsValue> {
let manager = self.inner.lock();
manager
.delete_alias(alias)
.map_err(|e| JsValue::from_str(&format!("Failed to delete alias: {}", e)))?;
Ok(())
}
#[wasm_bindgen(js_name = listAliases)]
pub fn list_aliases(&self) -> JsValue {
let manager = self.inner.lock();
let aliases = manager.list_aliases();
let arr = Array::new();
for (alias, collection) in aliases {
let pair = Array::new();
pair.push(&JsValue::from_str(&alias));
pair.push(&JsValue::from_str(&collection));
arr.push(&pair);
}
arr.into()
}
}
#[cfg(feature = "collections")]
#[wasm_bindgen]
pub struct FilterBuilder {
inner: CoreFilterExpression,
}
#[cfg(feature = "collections")]
#[wasm_bindgen]
impl FilterBuilder {
#[wasm_bindgen(constructor)]
pub fn new() -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::exists("_id"),
}
}
pub fn eq(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::eq(field, json_value),
})
}
pub fn ne(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::ne(field, json_value),
})
}
pub fn gt(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::gt(field, json_value),
})
}
pub fn gte(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::gte(field, json_value),
})
}
pub fn lt(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::lt(field, json_value),
})
}
pub fn lte(field: &str, value: JsValue) -> Result<FilterBuilder, JsValue> {
let json_value: serde_json::Value =
from_value(value).map_err(|e| JsValue::from_str(&format!("Invalid value: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::lte(field, json_value),
})
}
#[wasm_bindgen(js_name = "in")]
pub fn in_values(field: &str, values: JsValue) -> Result<FilterBuilder, JsValue> {
let json_values: Vec<serde_json::Value> = from_value(values)
.map_err(|e| JsValue::from_str(&format!("Invalid values array: {}", e)))?;
Ok(FilterBuilder {
inner: CoreFilterExpression::in_values(field, json_values),
})
}
#[wasm_bindgen(js_name = matchText)]
pub fn match_text(field: &str, text: &str) -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::match_text(field, text),
}
}
#[wasm_bindgen(js_name = geoRadius)]
pub fn geo_radius(field: &str, lat: f64, lon: f64, radius_m: f64) -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::geo_radius(field, lat, lon, radius_m),
}
}
pub fn and(filters: Vec<FilterBuilder>) -> FilterBuilder {
let inner_filters: Vec<CoreFilterExpression> =
filters.into_iter().map(|f| f.inner).collect();
FilterBuilder {
inner: CoreFilterExpression::and(inner_filters),
}
}
pub fn or(filters: Vec<FilterBuilder>) -> FilterBuilder {
let inner_filters: Vec<CoreFilterExpression> =
filters.into_iter().map(|f| f.inner).collect();
FilterBuilder {
inner: CoreFilterExpression::or(inner_filters),
}
}
pub fn not(filter: FilterBuilder) -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::not(filter.inner),
}
}
pub fn exists(field: &str) -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::exists(field),
}
}
#[wasm_bindgen(js_name = isNull)]
pub fn is_null(field: &str) -> FilterBuilder {
FilterBuilder {
inner: CoreFilterExpression::is_null(field),
}
}
#[wasm_bindgen(js_name = toJson)]
pub fn to_json(&self) -> Result<JsValue, JsValue> {
to_value(&self.inner)
.map_err(|e| JsValue::from_str(&format!("Failed to serialize filter: {}", e)))
}
#[wasm_bindgen(js_name = getFields)]
pub fn get_fields(&self) -> Vec<String> {
self.inner.get_fields()
}
}
#[cfg(feature = "collections")]
impl Default for FilterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser);
#[wasm_bindgen_test]
fn test_version() {
assert!(!version().is_empty());
}
#[wasm_bindgen_test]
fn test_detect_simd() {
let _ = detect_simd();
}
}