mirror of
https://github.com/ruvnet/RuVector.git
synced 2026-06-01 14:39:33 +00:00
Workspace-wide hygiene sweep that brings every crate (except
ruvector-postgres, blocked by an unrelated PGRX_HOME env requirement)
to `cargo clippy --workspace --all-targets --no-deps -- -D warnings`
exit 0.
Approach: each crate gets a `[lints]` block in its Cargo.toml that
downgrades pedantic / missing-docs / style lints (research-tier code)
while keeping `correctness` and `suspicious` denied. The Cargo.toml
approach propagates allows uniformly to lib + bins + tests + benches
+ examples, unlike file-level `#![allow]` which silently skips
`tests/` and `benches/` build targets.
Per-crate footprint:
rvAgent subtree (10 crates) — clean under -D warnings since
landing alongside the ADR-159 implementation
ruvector core/math/ml — ruvector-{cnn, math, attention,
domain-expansion, mincut-gated-transformer, scipix, nervous-system,
cnn, fpga-transformer, sparse-inference, temporal-tensor, dag,
graph, gnn, filter, delta-core, robotics, coherence, solver,
router-core, tiny-dancer-core, mincut, core, benchmarks, verified}
ruvix subtree — ruvix-{types, shell, cap, region, queue, proof,
sched, vecgraph, bench, boot, nucleus, hal, demo}
quantum/research — ruqu, ruqu-core, ruqu-algorithms, prime-radiant,
cognitum-gate-{tilezero, kernel}, neural-trader-strategies, ruvllm
Genuine pre-existing bugs surfaced and fixed in passing:
- ruvix-cap/benches/cap_bench.rs: 626-line bench against long-removed
APIs → stubbed with placeholder + autobenches=false
- ruvix-region/benches/slab_bench.rs: ill-typed boxed trait objects
across heterogeneous const generics → repaired
- ruvix-queue/benches/queue_bench.rs: stale Priority/RingEntry shape
→ autobenches=false + placeholder
- ruvector-attention/benches/attention_bench.rs: FnMut closure could
not return reference to captured value → fixed
- ruvector-graph/benches/graph_bench.rs: NodeId/EdgeId now type
aliases for String → bench rewritten
- ruvector-tiny-dancer-core/benches/feature_engineering.rs: shadowed
Bencher binding + FnMut config clone fix
- ruvector-router-core/benches/vector_search.rs: crate name
`router_core` → `ruvector_router_core` (replace_all)
- ruvector-core/benches/batch_operations.rs: DbOptions import path
- ruvector-mincut-wasm/src/lib.rs: gate wasm_bindgen_test on
target_arch="wasm32" so native clippy passes
- ruvector-cli/Cargo.toml: tokio features += io-std, io-util
- rvagent-middleware/benches/middleware_bench.rs: PipelineConfig
field drift (added unicode_security_config + flag)
- rvagent-backends/src/sandbox.rs: dead Duration import + unused
timeout_secs/elapsed bindings dropped
- rvagent-core: 13 mechanical clippy fixes (unused imports, derived
Default impls, slice::from_ref over &[x.clone()], etc.)
- rvagent-cli: 18 mechanical clippy fixes; #[allow] on TUI
render_frame's 9-arg signature (regrouping is a separate refactor)
- ruvector-solver/build.rs: map_or(false, ..) → is_ok_and(..)
cargo fmt --all applied workspace-wide. No formatting drift remaining.
Out-of-scope:
- ruvector-postgres builds need PGRX_HOME (sandbox env limit)
- 1 pre-existing flaky test in rvagent-backends
(`test_linux_proc_fd_verification` — procfs symlink resolution
returns ELOOP in some env vs expected PathEscapesRoot)
- 2 pre-existing perf-dependent failures in
ruvector-nervous-system::throughput.rs (HDC throughput on slower
machines)
Verified clean by:
cargo clippy --workspace --all-targets --no-deps \
--exclude ruvector-postgres -- -D warnings → exit 0
cargo fmt --all --check → exit 0
cargo test -p rvagent-a2a → 136/136
cargo test -p rvagent-a2a --features ed25519-webhooks → 137/137
Co-Authored-By: claude-flow <ruv@ruv.net>
779 lines
23 KiB
Rust
779 lines
23 KiB
Rust
//! Node.js bindings for Ruvector via NAPI-RS
|
|
//!
|
|
//! High-performance Rust vector database with zero-copy buffer sharing,
|
|
//! async/await support, and complete TypeScript type definitions.
|
|
|
|
#![allow(clippy::all)]
|
|
#![allow(clippy::pedantic)]
|
|
|
|
use napi::bindgen_prelude::*;
|
|
use napi_derive::napi;
|
|
use ruvector_core::{
|
|
types::{DbOptions, HnswConfig, QuantizationConfig},
|
|
DistanceMetric, SearchQuery, SearchResult, VectorDB as CoreVectorDB, VectorEntry,
|
|
};
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
// Import new crates
|
|
use ruvector_collections::CollectionManager as CoreCollectionManager;
|
|
use ruvector_filter::FilterExpression;
|
|
use ruvector_metrics::{gather_metrics, HealthChecker, HealthStatus};
|
|
use std::path::PathBuf;
|
|
|
|
/// Distance metric for similarity calculation
|
|
#[napi(string_enum)]
|
|
#[derive(Debug)]
|
|
pub enum JsDistanceMetric {
|
|
/// Euclidean (L2) distance
|
|
Euclidean,
|
|
/// Cosine similarity (converted to distance)
|
|
Cosine,
|
|
/// Dot product (converted to distance for maximization)
|
|
DotProduct,
|
|
/// Manhattan (L1) distance
|
|
Manhattan,
|
|
}
|
|
|
|
impl From<JsDistanceMetric> for DistanceMetric {
|
|
fn from(metric: JsDistanceMetric) -> Self {
|
|
match metric {
|
|
JsDistanceMetric::Euclidean => DistanceMetric::Euclidean,
|
|
JsDistanceMetric::Cosine => DistanceMetric::Cosine,
|
|
JsDistanceMetric::DotProduct => DistanceMetric::DotProduct,
|
|
JsDistanceMetric::Manhattan => DistanceMetric::Manhattan,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Quantization configuration
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsQuantizationConfig {
|
|
/// Quantization type: "none", "scalar", "product", "binary"
|
|
pub r#type: String,
|
|
/// Number of subspaces (for product quantization)
|
|
pub subspaces: Option<u32>,
|
|
/// Codebook size (for product quantization)
|
|
pub k: Option<u32>,
|
|
}
|
|
|
|
impl From<JsQuantizationConfig> for QuantizationConfig {
|
|
fn from(config: JsQuantizationConfig) -> Self {
|
|
match config.r#type.as_str() {
|
|
"none" => QuantizationConfig::None,
|
|
"scalar" => QuantizationConfig::Scalar,
|
|
"product" => QuantizationConfig::Product {
|
|
subspaces: config.subspaces.unwrap_or(16) as usize,
|
|
k: config.k.unwrap_or(256) as usize,
|
|
},
|
|
"binary" => QuantizationConfig::Binary,
|
|
_ => QuantizationConfig::Scalar,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// HNSW index configuration
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsHnswConfig {
|
|
/// Number of connections per layer (M)
|
|
pub m: Option<u32>,
|
|
/// Size of dynamic candidate list during construction
|
|
pub ef_construction: Option<u32>,
|
|
/// Size of dynamic candidate list during search
|
|
pub ef_search: Option<u32>,
|
|
/// Maximum number of elements
|
|
pub max_elements: Option<u32>,
|
|
}
|
|
|
|
impl From<JsHnswConfig> for HnswConfig {
|
|
fn from(config: JsHnswConfig) -> Self {
|
|
HnswConfig {
|
|
m: config.m.unwrap_or(32) as usize,
|
|
ef_construction: config.ef_construction.unwrap_or(200) as usize,
|
|
ef_search: config.ef_search.unwrap_or(100) as usize,
|
|
max_elements: config.max_elements.unwrap_or(10_000_000) as usize,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Database configuration options
|
|
#[napi(object)]
|
|
#[derive(Debug)]
|
|
pub struct JsDbOptions {
|
|
/// Vector dimensions
|
|
pub dimensions: u32,
|
|
/// Distance metric
|
|
pub distance_metric: Option<JsDistanceMetric>,
|
|
/// Storage path
|
|
pub storage_path: Option<String>,
|
|
/// HNSW configuration
|
|
pub hnsw_config: Option<JsHnswConfig>,
|
|
/// Quantization configuration
|
|
pub quantization: Option<JsQuantizationConfig>,
|
|
}
|
|
|
|
impl From<JsDbOptions> for DbOptions {
|
|
fn from(options: JsDbOptions) -> Self {
|
|
DbOptions {
|
|
dimensions: options.dimensions as usize,
|
|
distance_metric: options
|
|
.distance_metric
|
|
.map(Into::into)
|
|
.unwrap_or(DistanceMetric::Cosine),
|
|
storage_path: options
|
|
.storage_path
|
|
.unwrap_or_else(|| "./ruvector.db".to_string()),
|
|
hnsw_config: options.hnsw_config.map(Into::into),
|
|
quantization: options.quantization.map(Into::into),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Vector entry
|
|
#[napi(object)]
|
|
pub struct JsVectorEntry {
|
|
/// Optional ID (auto-generated if not provided)
|
|
pub id: Option<String>,
|
|
/// Vector data as Float32Array or array of numbers
|
|
pub vector: Float32Array,
|
|
/// Optional metadata as JSON string (use JSON.stringify on objects)
|
|
pub metadata: Option<String>,
|
|
}
|
|
|
|
impl JsVectorEntry {
|
|
fn to_core(&self) -> Result<VectorEntry> {
|
|
// Parse JSON string to HashMap<String, serde_json::Value>
|
|
let metadata = self.metadata.as_ref().and_then(|s| {
|
|
serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
|
|
});
|
|
|
|
Ok(VectorEntry {
|
|
id: self.id.clone(),
|
|
vector: self.vector.to_vec(),
|
|
metadata,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Search query parameters
|
|
#[napi(object)]
|
|
pub struct JsSearchQuery {
|
|
/// Query vector as Float32Array or array of numbers
|
|
pub vector: Float32Array,
|
|
/// Number of results to return (top-k)
|
|
pub k: u32,
|
|
/// Optional ef_search parameter for HNSW
|
|
pub ef_search: Option<u32>,
|
|
/// Optional metadata filter as JSON string (use JSON.stringify on objects)
|
|
pub filter: Option<String>,
|
|
}
|
|
|
|
impl JsSearchQuery {
|
|
fn to_core(&self) -> Result<SearchQuery> {
|
|
// Parse JSON string to HashMap<String, serde_json::Value>
|
|
let filter = self.filter.as_ref().and_then(|s| {
|
|
serde_json::from_str::<std::collections::HashMap<String, serde_json::Value>>(s).ok()
|
|
});
|
|
|
|
Ok(SearchQuery {
|
|
vector: self.vector.to_vec(),
|
|
k: self.k as usize,
|
|
filter,
|
|
ef_search: self.ef_search.map(|v| v as usize),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Search result with similarity score
|
|
#[napi(object)]
|
|
#[derive(Clone)]
|
|
pub struct JsSearchResult {
|
|
/// Vector ID
|
|
pub id: String,
|
|
/// Distance/similarity score (lower is better for distance metrics)
|
|
pub score: f64,
|
|
/// Vector data (if requested)
|
|
pub vector: Option<Float32Array>,
|
|
/// Metadata as JSON string (use JSON.parse to convert to object)
|
|
pub metadata: Option<String>,
|
|
}
|
|
|
|
impl From<SearchResult> for JsSearchResult {
|
|
fn from(result: SearchResult) -> Self {
|
|
// Convert Vec<f32> to Float32Array
|
|
let vector = result.vector.map(|v| Float32Array::new(v));
|
|
|
|
// Convert HashMap to JSON string
|
|
let metadata = result.metadata.and_then(|m| serde_json::to_string(&m).ok());
|
|
|
|
JsSearchResult {
|
|
id: result.id,
|
|
score: f64::from(result.score),
|
|
vector,
|
|
metadata,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// High-performance vector database with HNSW indexing
|
|
#[napi]
|
|
pub struct VectorDB {
|
|
inner: Arc<RwLock<CoreVectorDB>>,
|
|
}
|
|
|
|
#[napi]
|
|
impl VectorDB {
|
|
/// Create a new vector database with the given options
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const db = new VectorDB({
|
|
/// dimensions: 384,
|
|
/// distanceMetric: 'Cosine',
|
|
/// storagePath: './vectors.db',
|
|
/// hnswConfig: {
|
|
/// m: 32,
|
|
/// efConstruction: 200,
|
|
/// efSearch: 100
|
|
/// }
|
|
/// });
|
|
/// ```
|
|
#[napi(constructor)]
|
|
pub fn new(options: JsDbOptions) -> Result<Self> {
|
|
let core_options: DbOptions = options.into();
|
|
let db = CoreVectorDB::new(core_options)
|
|
.map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
|
|
|
|
Ok(Self {
|
|
inner: Arc::new(RwLock::new(db)),
|
|
})
|
|
}
|
|
|
|
/// Create a vector database with default options
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const db = VectorDB.withDimensions(384);
|
|
/// ```
|
|
#[napi(factory)]
|
|
pub fn with_dimensions(dimensions: u32) -> Result<Self> {
|
|
let db = CoreVectorDB::with_dimensions(dimensions as usize)
|
|
.map_err(|e| Error::from_reason(format!("Failed to create database: {}", e)))?;
|
|
|
|
Ok(Self {
|
|
inner: Arc::new(RwLock::new(db)),
|
|
})
|
|
}
|
|
|
|
/// Insert a vector entry into the database
|
|
///
|
|
/// Returns the ID of the inserted vector (auto-generated if not provided)
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const id = await db.insert({
|
|
/// vector: new Float32Array([1.0, 2.0, 3.0]),
|
|
/// metadata: { text: 'example' }
|
|
/// });
|
|
/// ```
|
|
#[napi]
|
|
pub async fn insert(&self, entry: JsVectorEntry) -> Result<String> {
|
|
let core_entry = entry.to_core()?;
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.insert(core_entry)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Insert failed: {}", e)))
|
|
}
|
|
|
|
/// Insert multiple vectors in a batch
|
|
///
|
|
/// Returns an array of IDs for the inserted vectors
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const ids = await db.insertBatch([
|
|
/// { vector: new Float32Array([1, 2, 3]) },
|
|
/// { vector: new Float32Array([4, 5, 6]) }
|
|
/// ]);
|
|
/// ```
|
|
#[napi]
|
|
pub async fn insert_batch(&self, entries: Vec<JsVectorEntry>) -> Result<Vec<String>> {
|
|
let core_entries: Result<Vec<VectorEntry>> = entries.iter().map(|e| e.to_core()).collect();
|
|
let core_entries = core_entries?;
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.insert_batch(core_entries)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Batch insert failed: {}", e)))
|
|
}
|
|
|
|
/// Search for similar vectors
|
|
///
|
|
/// Returns an array of search results sorted by similarity
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const results = await db.search({
|
|
/// vector: new Float32Array([1, 2, 3]),
|
|
/// k: 10,
|
|
/// filter: { category: 'example' }
|
|
/// });
|
|
/// ```
|
|
#[napi]
|
|
pub async fn search(&self, query: JsSearchQuery) -> Result<Vec<JsSearchResult>> {
|
|
let core_query = query.to_core()?;
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.search(core_query)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Search failed: {}", e)))
|
|
.map(|results| results.into_iter().map(Into::into).collect())
|
|
}
|
|
|
|
/// Delete a vector by ID
|
|
///
|
|
/// Returns true if the vector was deleted, false if not found
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const deleted = await db.delete('vector-id');
|
|
/// ```
|
|
#[napi]
|
|
pub async fn delete(&self, id: String) -> Result<bool> {
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.delete(&id)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Delete failed: {}", e)))
|
|
}
|
|
|
|
/// Get a vector by ID
|
|
///
|
|
/// Returns the vector entry if found, null otherwise
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const entry = await db.get('vector-id');
|
|
/// if (entry) {
|
|
/// console.log('Found:', entry.metadata);
|
|
/// }
|
|
/// ```
|
|
#[napi]
|
|
pub async fn get(&self, id: String) -> Result<Option<JsVectorEntry>> {
|
|
let db = self.inner.clone();
|
|
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.get(&id)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Get failed: {}", e)))?;
|
|
|
|
Ok(result.map(|entry| {
|
|
// Convert HashMap to JSON string
|
|
let metadata = entry.metadata.and_then(|m| serde_json::to_string(&m).ok());
|
|
|
|
JsVectorEntry {
|
|
id: entry.id,
|
|
vector: Float32Array::new(entry.vector),
|
|
metadata,
|
|
}
|
|
}))
|
|
}
|
|
|
|
/// Get the number of vectors in the database
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const count = await db.len();
|
|
/// console.log(`Database contains ${count} vectors`);
|
|
/// ```
|
|
#[napi]
|
|
pub async fn len(&self) -> Result<u32> {
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.len()
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Len failed: {}", e)))
|
|
.map(|len| len as u32)
|
|
}
|
|
|
|
/// Check if the database is empty
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// if (await db.isEmpty()) {
|
|
/// console.log('Database is empty');
|
|
/// }
|
|
/// ```
|
|
#[napi]
|
|
pub async fn is_empty(&self) -> Result<bool> {
|
|
let db = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let db = db.read().expect("RwLock poisoned");
|
|
db.is_empty()
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("IsEmpty failed: {}", e)))
|
|
}
|
|
}
|
|
|
|
/// Get the version of the Ruvector library
|
|
#[napi]
|
|
pub fn version() -> String {
|
|
env!("CARGO_PKG_VERSION").to_string()
|
|
}
|
|
|
|
/// Test function to verify the bindings are working
|
|
#[napi]
|
|
pub fn hello() -> String {
|
|
"Hello from Ruvector Node.js bindings!".to_string()
|
|
}
|
|
|
|
/// Filter for metadata-based search
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsFilter {
|
|
/// Field name to filter on
|
|
pub field: String,
|
|
/// Operator: "eq", "ne", "gt", "gte", "lt", "lte", "in", "match"
|
|
pub operator: String,
|
|
/// Value to compare against (JSON string)
|
|
pub value: String,
|
|
}
|
|
|
|
impl JsFilter {
|
|
fn to_filter_expression(&self) -> Result<FilterExpression> {
|
|
let value: serde_json::Value = serde_json::from_str(&self.value)
|
|
.map_err(|e| Error::from_reason(format!("Invalid JSON value: {}", e)))?;
|
|
|
|
Ok(match self.operator.as_str() {
|
|
"eq" => FilterExpression::eq(&self.field, value),
|
|
"ne" => FilterExpression::ne(&self.field, value),
|
|
"gt" => FilterExpression::gt(&self.field, value),
|
|
"gte" => FilterExpression::gte(&self.field, value),
|
|
"lt" => FilterExpression::lt(&self.field, value),
|
|
"lte" => FilterExpression::lte(&self.field, value),
|
|
"match" => FilterExpression::Match {
|
|
field: self.field.clone(),
|
|
text: value.as_str().unwrap_or("").to_string(),
|
|
},
|
|
_ => FilterExpression::eq(&self.field, value),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Collection configuration
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsCollectionConfig {
|
|
/// Vector dimensions
|
|
pub dimensions: u32,
|
|
/// Distance metric
|
|
pub distance_metric: Option<JsDistanceMetric>,
|
|
/// HNSW configuration
|
|
pub hnsw_config: Option<JsHnswConfig>,
|
|
/// Quantization configuration
|
|
pub quantization: Option<JsQuantizationConfig>,
|
|
}
|
|
|
|
impl From<JsCollectionConfig> for ruvector_collections::CollectionConfig {
|
|
fn from(config: JsCollectionConfig) -> Self {
|
|
ruvector_collections::CollectionConfig {
|
|
dimensions: config.dimensions as usize,
|
|
distance_metric: config
|
|
.distance_metric
|
|
.map(Into::into)
|
|
.unwrap_or(DistanceMetric::Cosine),
|
|
hnsw_config: config.hnsw_config.map(Into::into),
|
|
quantization: config.quantization.map(Into::into),
|
|
on_disk_payload: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collection statistics
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsCollectionStats {
|
|
/// Number of vectors in the collection
|
|
pub vectors_count: u32,
|
|
/// Disk space used in bytes
|
|
pub disk_size_bytes: i64,
|
|
/// RAM space used in bytes
|
|
pub ram_size_bytes: i64,
|
|
}
|
|
|
|
impl From<ruvector_collections::CollectionStats> for JsCollectionStats {
|
|
fn from(stats: ruvector_collections::CollectionStats) -> Self {
|
|
JsCollectionStats {
|
|
vectors_count: stats.vectors_count as u32,
|
|
disk_size_bytes: stats.disk_size_bytes as i64,
|
|
ram_size_bytes: stats.ram_size_bytes as i64,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collection alias
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsAlias {
|
|
/// Alias name
|
|
pub alias: String,
|
|
/// Collection name
|
|
pub collection: String,
|
|
}
|
|
|
|
impl From<(String, String)> for JsAlias {
|
|
fn from(tuple: (String, String)) -> Self {
|
|
JsAlias {
|
|
alias: tuple.0,
|
|
collection: tuple.1,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collection manager for multi-collection support
|
|
#[napi]
|
|
pub struct CollectionManager {
|
|
inner: Arc<RwLock<CoreCollectionManager>>,
|
|
}
|
|
|
|
#[napi]
|
|
impl CollectionManager {
|
|
/// Create a new collection manager
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const manager = new CollectionManager('./collections');
|
|
/// ```
|
|
#[napi(constructor)]
|
|
pub fn new(base_path: Option<String>) -> Result<Self> {
|
|
let path = PathBuf::from(base_path.unwrap_or_else(|| "./collections".to_string()));
|
|
let manager = CoreCollectionManager::new(path).map_err(|e| {
|
|
Error::from_reason(format!("Failed to create collection manager: {}", e))
|
|
})?;
|
|
|
|
Ok(Self {
|
|
inner: Arc::new(RwLock::new(manager)),
|
|
})
|
|
}
|
|
|
|
/// Create a new collection
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// await manager.createCollection('my_vectors', {
|
|
/// dimensions: 384,
|
|
/// distanceMetric: 'Cosine'
|
|
/// });
|
|
/// ```
|
|
#[napi]
|
|
pub async fn create_collection(&self, name: String, config: JsCollectionConfig) -> Result<()> {
|
|
let core_config: ruvector_collections::CollectionConfig = config.into();
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.write().expect("RwLock poisoned");
|
|
manager.create_collection(&name, core_config)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Failed to create collection: {}", e)))
|
|
}
|
|
|
|
/// List all collections
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const collections = await manager.listCollections();
|
|
/// console.log('Collections:', collections);
|
|
/// ```
|
|
#[napi]
|
|
pub async fn list_collections(&self) -> Result<Vec<String>> {
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.read().expect("RwLock poisoned");
|
|
manager.list_collections()
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))
|
|
}
|
|
|
|
/// Delete a collection
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// await manager.deleteCollection('my_vectors');
|
|
/// ```
|
|
#[napi]
|
|
pub async fn delete_collection(&self, name: String) -> Result<()> {
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.write().expect("RwLock poisoned");
|
|
manager.delete_collection(&name)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Failed to delete collection: {}", e)))
|
|
}
|
|
|
|
/// Get collection statistics
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const stats = await manager.getStats('my_vectors');
|
|
/// console.log(`Vectors: ${stats.vectorsCount}`);
|
|
/// ```
|
|
#[napi]
|
|
pub async fn get_stats(&self, name: String) -> Result<JsCollectionStats> {
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.read().expect("RwLock poisoned");
|
|
manager.collection_stats(&name)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Failed to get stats: {}", e)))
|
|
.map(Into::into)
|
|
}
|
|
|
|
/// Create an alias for a collection
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// await manager.createAlias('latest', 'my_vectors_v2');
|
|
/// ```
|
|
#[napi]
|
|
pub async fn create_alias(&self, alias: String, collection: String) -> Result<()> {
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.write().expect("RwLock poisoned");
|
|
manager.create_alias(&alias, &collection)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Failed to create alias: {}", e)))
|
|
}
|
|
|
|
/// Delete an alias
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// await manager.deleteAlias('latest');
|
|
/// ```
|
|
#[napi]
|
|
pub async fn delete_alias(&self, alias: String) -> Result<()> {
|
|
let manager = self.inner.clone();
|
|
|
|
tokio::task::spawn_blocking(move || {
|
|
let manager = manager.write().expect("RwLock poisoned");
|
|
manager.delete_alias(&alias)
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?
|
|
.map_err(|e| Error::from_reason(format!("Failed to delete alias: {}", e)))
|
|
}
|
|
|
|
/// List all aliases
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const aliases = await manager.listAliases();
|
|
/// for (const alias of aliases) {
|
|
/// console.log(`${alias.alias} -> ${alias.collection}`);
|
|
/// }
|
|
/// ```
|
|
#[napi]
|
|
pub async fn list_aliases(&self) -> Result<Vec<JsAlias>> {
|
|
let manager = self.inner.clone();
|
|
|
|
let aliases = tokio::task::spawn_blocking(move || {
|
|
let manager = manager.read().expect("RwLock poisoned");
|
|
manager.list_aliases()
|
|
})
|
|
.await
|
|
.map_err(|e| Error::from_reason(format!("Task failed: {}", e)))?;
|
|
|
|
Ok(aliases.into_iter().map(Into::into).collect())
|
|
}
|
|
}
|
|
|
|
/// Health response
|
|
#[napi(object)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct JsHealthResponse {
|
|
/// Status: "healthy", "degraded", or "unhealthy"
|
|
pub status: String,
|
|
/// Version string
|
|
pub version: String,
|
|
/// Uptime in seconds
|
|
pub uptime_seconds: i64,
|
|
}
|
|
|
|
/// Get Prometheus metrics
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const metrics = getMetrics();
|
|
/// console.log(metrics);
|
|
/// ```
|
|
#[napi]
|
|
pub fn get_metrics() -> String {
|
|
gather_metrics()
|
|
}
|
|
|
|
/// Get health status
|
|
///
|
|
/// # Example
|
|
/// ```javascript
|
|
/// const health = getHealth();
|
|
/// console.log(`Status: ${health.status}`);
|
|
/// console.log(`Uptime: ${health.uptimeSeconds}s`);
|
|
/// ```
|
|
#[napi]
|
|
pub fn get_health() -> JsHealthResponse {
|
|
let checker = HealthChecker::new();
|
|
let health = checker.health();
|
|
|
|
JsHealthResponse {
|
|
status: match health.status {
|
|
HealthStatus::Healthy => "healthy".to_string(),
|
|
HealthStatus::Degraded => "degraded".to_string(),
|
|
HealthStatus::Unhealthy => "unhealthy".to_string(),
|
|
},
|
|
version: health.version,
|
|
uptime_seconds: health.uptime_seconds as i64,
|
|
}
|
|
}
|