Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions crates/ruvector-router-core/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,11 @@ impl Storage {

// Read from database
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(VECTORS_TABLE)?;
let table = match read_txn.open_table(VECTORS_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => return Err(e.into()),
};

if let Some(bytes) = table.get(id)? {
let (vector, _): (Vec<f32>, usize) =
Expand All @@ -201,7 +205,11 @@ impl Storage {
/// Get metadata for a vector
pub fn get_metadata(&self, id: &str) -> Result<Option<HashMap<String, serde_json::Value>>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(METADATA_TABLE)?;
let table = match read_txn.open_table(METADATA_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(None),
Err(e) => return Err(e.into()),
};

if let Some(bytes) = table.get(id)? {
let metadata: HashMap<String, serde_json::Value> =
Expand Down Expand Up @@ -241,7 +249,11 @@ impl Storage {
/// Get all vector IDs
pub fn get_all_ids(&self) -> Result<Vec<String>> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(VECTORS_TABLE)?;
let table = match read_txn.open_table(VECTORS_TABLE) {
Ok(t) => t,
Err(redb::TableError::TableDoesNotExist(_)) => return Ok(Vec::new()),
Err(e) => return Err(e.into()),
};

let mut ids = Vec::new();
let iter = table.iter()?;
Expand All @@ -256,8 +268,11 @@ impl Storage {
/// Count total vectors
pub fn count(&self) -> Result<usize> {
let read_txn = self.db.begin_read()?;
let table = read_txn.open_table(VECTORS_TABLE)?;
Ok(table.len()? as usize)
match read_txn.open_table(VECTORS_TABLE) {
Ok(table) => Ok(table.len()? as usize),
Err(redb::TableError::TableDoesNotExist(_)) => Ok(0),
Err(e) => Err(e.into()),
}
}

/// Store index data
Expand Down
9 changes: 9 additions & 0 deletions crates/ruvector-router-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ use ruvector_router_core::{
VectorEntry as CoreVectorEntry,
};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;

static INSTANCE_COUNTER: AtomicU64 = AtomicU64::new(0);

#[napi]
pub enum DistanceMetric {
Euclidean,
Expand Down Expand Up @@ -74,6 +77,12 @@ impl VectorDB {

if let Some(path) = options.storage_path {
builder = builder.storage_path(path);
} else {
// Use a unique temp path per instance to avoid file lock conflicts
let id = INSTANCE_COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let tmp = std::env::temp_dir().join(format!("ruvector-{}-{}.db", pid, id));
builder = builder.storage_path(tmp);
}

let db = builder
Expand Down
2 changes: 1 addition & 1 deletion npm/packages/router-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruvector/router-darwin-arm64",
"version": "0.1.27",
"version": "0.1.28",
"description": "macOS ARM64 (Apple Silicon) native bindings for @ruvector/router",
"main": "ruvector-router.darwin-arm64.node",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion npm/packages/router-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruvector/router-darwin-x64",
"version": "0.1.27",
"version": "0.1.28",
"description": "macOS x64 native bindings for @ruvector/router",
"main": "ruvector-router.darwin-x64.node",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion npm/packages/router-linux-arm64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruvector/router-linux-arm64-gnu",
"version": "0.1.27",
"version": "0.1.28",
"description": "Linux ARM64 (glibc) native bindings for @ruvector/router",
"main": "ruvector-router.linux-arm64-gnu.node",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion npm/packages/router-linux-x64-gnu/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruvector/router-linux-x64-gnu",
"version": "0.1.27",
"version": "0.1.28",
"description": "Linux x64 (glibc) native bindings for @ruvector/router",
"main": "ruvector-router.linux-x64-gnu.node",
"files": [
Expand Down
2 changes: 1 addition & 1 deletion npm/packages/router-win32-x64-msvc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ruvector/router-win32-x64-msvc",
"version": "0.1.27",
"version": "0.1.28",
"description": "Windows x64 native bindings for @ruvector/router",
"main": "ruvector-router.win32-x64-msvc.node",
"files": [
Expand Down
32 changes: 28 additions & 4 deletions npm/packages/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ class SemanticRouter {
'manhattan': native.DistanceMetric.Manhattan
};

this._metric = config.metric || 'cosine';
this._db = new native.VectorDb({
dimensions: config.dimension,
distanceMetric: metricMap[config.metric] || native.DistanceMetric.Cosine,
distanceMetric: metricMap[this._metric] || native.DistanceMetric.Cosine,
hnswM: config.m || 16,
hnswEfConstruction: config.efConstruction || 200,
hnswEfSearch: config.efSearch || 100
Expand Down Expand Up @@ -160,7 +161,7 @@ class SemanticRouter {
embedding: null
});

// Compute embedding if we have an embedder
// Compute embedding if we have an embedder, otherwise require explicit embedding
if (this._embedder && !intent.embedding) {
// Compute centroid from all utterances
const embeddings = await Promise.all(
Expand All @@ -183,6 +184,10 @@ class SemanticRouter {
: new Float32Array(intent.embedding);
this._intents.get(intent.name).embedding = vector;
this._db.insert(intent.name, vector);
} else {
throw new Error(
`Intent "${intent.name}" requires either an embedder (setEmbedder) or a pre-computed embedding`
);
}
}

Expand Down Expand Up @@ -223,12 +228,24 @@ class SemanticRouter {
const results = this._db.search(embedding, k);

return results
.filter(r => r.score >= this._threshold)
.map(r => {
// Convert distance to similarity based on metric
let similarity;
if (this._metric === 'cosine') {
similarity = 1 - r.score; // cosine distance = 1 - cosine_similarity
} else if (this._metric === 'dot') {
similarity = -r.score; // dot distance = -dot_product
} else {
similarity = 1 / (1 + r.score); // euclidean/manhattan: inverse distance
}
return { ...r, similarity };
})
.filter(r => r.similarity >= this._threshold)
.map(r => {
const intentInfo = this._intents.get(r.id);
return {
intent: r.id,
score: r.score,
score: r.similarity,
metadata: intentInfo ? intentInfo.metadata : {}
};
});
Expand Down Expand Up @@ -321,6 +338,13 @@ class SemanticRouter {
const content = await fs.readFile(filePath, 'utf8');
const data = JSON.parse(content);

if (data.dimension && data.dimension !== this._dimension) {
throw new Error(
`Dimension mismatch: router has ${this._dimension} but saved state has ${data.dimension}. ` +
`Create a new SemanticRouter with dimension: ${data.dimension} before loading.`
);
}

this.clear();
this._threshold = data.threshold || 0.7;

Expand Down
10 changes: 5 additions & 5 deletions npm/packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
"@napi-rs/cli": "^2.18.0"
},
"optionalDependencies": {
"@ruvector/router-linux-x64-gnu": "0.1.27",
"@ruvector/router-linux-arm64-gnu": "0.1.27",
"@ruvector/router-darwin-x64": "0.1.27",
"@ruvector/router-darwin-arm64": "0.1.27",
"@ruvector/router-win32-x64-msvc": "0.1.27"
"@ruvector/router-linux-x64-gnu": "0.1.28",
"@ruvector/router-linux-arm64-gnu": "0.1.28",
"@ruvector/router-darwin-x64": "0.1.28",
"@ruvector/router-darwin-arm64": "0.1.28",
"@ruvector/router-win32-x64-msvc": "0.1.28"
},
"publishConfig": {
"access": "public"
Expand Down
27 changes: 20 additions & 7 deletions npm/packages/ruvector/src/core/router-wrapper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,38 @@ export declare class SemanticRouter {
private inner;
private routes;
constructor(options?: {
dimensions?: number;
dimension?: number;
threshold?: number;
});
/**
* Add a route with example utterances
* Set the embedder function for converting text to vectors.
* Required before match() can accept string input.
*/
addRoute(name: string, utterances: string[], metadata?: Record<string, any>): void;
setEmbedder(embedder: (text: string) => Promise<Float32Array>): void;
/**
* Add a route with example utterances (sync, requires pre-computed embedding)
*/
addRoute(name: string, utterances: string[], metadata?: Record<string, any>, embedding?: Float32Array | number[]): void;
/**
* Add a route with automatic embedding computation (requires setEmbedder)
*/
addRouteAsync(name: string, utterances: string[], metadata?: Record<string, any>): Promise<void>;
/**
* Add multiple routes at once
*/
addRoutes(routes: Route[]): void;
/**
* Match input to best route
* Match input to best route (async, accepts string if embedder is set, or Float32Array)
*/
match(input: string | Float32Array): Promise<RouteMatch | null>;
/**
* Get top-k route matches (async)
*/
match(input: string): RouteMatch | null;
matchTopK(input: string | Float32Array, k?: number): Promise<RouteMatch[]>;
/**
* Get top-k route matches
* Match with a pre-computed embedding (synchronous)
*/
matchTopK(input: string, k?: number): RouteMatch[];
matchWithEmbedding(embedding: Float32Array, k?: number): RouteMatch[];
/**
* Get all registered routes
*/
Expand Down
70 changes: 52 additions & 18 deletions npm/packages/ruvector/src/core/router-wrapper.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading