claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
299 lines • 11.7 kB
JavaScript
/**
* Vector Database Module
*
* Provides optional ruvector WASM-accelerated vector operations for:
* - Semantic similarity search
* - HNSW indexing (150x faster)
* - Embedding generation
*
* Gracefully degrades when ruvector is not installed.
*
* Created with love by ruv.io
*/
import os from 'node:os';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
// ============================================================================
// Fallback Implementation (when ruvector not available)
// ============================================================================
class FallbackVectorDB {
vectors = new Map();
dimensions;
constructor(dimensions) {
this.dimensions = dimensions;
}
insert(embedding, id, metadata) {
this.vectors.set(id, { embedding, metadata });
}
search(query, k = 10) {
const results = [];
for (const [id, { embedding, metadata }] of this.vectors) {
const score = cosineSimilarity(query, embedding);
results.push({ id, score, metadata });
}
return results
.sort((a, b) => b.score - a.score)
.slice(0, k);
}
remove(id) {
return this.vectors.delete(id);
}
size() {
return this.vectors.size;
}
clear() {
this.vectors.clear();
}
}
/**
* Compute cosine similarity between two vectors
*/
function cosineSimilarity(a, b) {
if (a.length !== b.length) {
throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
}
let dotProduct = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dotProduct / denom;
}
/**
* Whether the hash-embedding one-time warning has been emitted
*/
let hashEmbeddingWarned = false;
/**
* Generate a simple hash-based embedding (fallback when ruvector not available)
* WARNING: Produces deterministic vectors with NO semantic meaning.
*/
function generateHashEmbedding(text, dimensions = 768) {
if (!hashEmbeddingWarned) {
hashEmbeddingWarned = true;
console.warn('[vector-db] Using hash-based pseudo-embeddings (no semantic similarity). ' +
'Install ruvector or @claude-flow/embeddings for real ML embeddings.');
}
const embedding = new Float32Array(dimensions);
const normalized = text.toLowerCase().trim();
// Simple hash function
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
hash = ((hash << 5) - hash) + normalized.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
// Generate pseudo-random embedding based on hash
for (let i = 0; i < dimensions; i++) {
embedding[i] = Math.sin(hash * (i + 1) * 0.001) * 0.5 + 0.5;
}
// Normalize
let norm = 0;
for (let i = 0; i < dimensions; i++) {
norm += embedding[i] * embedding[i];
}
norm = Math.sqrt(norm);
for (let i = 0; i < dimensions; i++) {
embedding[i] /= norm;
}
return embedding;
}
// ============================================================================
// Module State
// ============================================================================
let ruvectorModule = null;
let loadAttempted = false;
let isAvailable = false;
// ============================================================================
// Public API
// ============================================================================
/**
* Attempt to load the ruvector module
* Returns true if successfully loaded, false otherwise
*/
export async function loadRuVector() {
if (loadAttempted) {
return isAvailable;
}
loadAttempted = true;
try {
// Dynamic import to handle missing dependency gracefully
const ruvector = await import('ruvector').catch(() => null);
// ruvector exports VectorDB class, not createVectorDB function
if (ruvector && (typeof ruvector.VectorDB === 'function' || typeof ruvector.VectorDb === 'function')) {
// Create adapter module that matches our expected interface
const VectorDBClass = ruvector.VectorDB || ruvector.VectorDb;
ruvectorModule = {
createVectorDB: async (dimensions) => {
// Build the HNSW graph with explicit, high-recall parameters instead
// of relying on the native layer's undocumented defaults. Measured on
// a 384-dim clustered corpus (scripts/benchmark-intelligence.mjs):
// - sparser graphs (e.g. m=16/efConstruction=64) are faster but
// drop recall@10 to ~0.68 at N=20k — unacceptable for a memory
// store, so we use the standard high-recall HNSW setting
// m=32/efConstruction=200, which keeps recall@10 ~0.99 while still
// scaling sub-linearly (log-graph traversal) above the crossover.
// These map onto ruvector's DbOptions.hnsw { m, efConstruction }.
//
// storagePath: the native ruvector DB takes an exclusive file lock on
// its storage path. Without an explicit path it defaults to a shared
// file in the CWD (e.g. agentdb.rvf), which is frequently already
// held by a running daemon/MCP server. When the lock cannot be
// acquired the constructor THROWS, and createVectorDB() below
// silently degrades to the O(n) brute-force FallbackVectorDB — so the
// "HNSW" index is never actually used. Giving each transient index a
// unique path avoids that contention so the real HNSW graph is built.
const db = new VectorDBClass({
dimensions,
hnswConfig: { m: 32, efConstruction: 200 },
storagePath: path.join(os.tmpdir(), `ruvector-${process.pid}-${randomUUID()}.rvf`),
});
// Wrap ruvector's VectorDB to match our interface
return {
insert: (embedding, id, metadata) => {
db.insert({ id, vector: embedding, metadata });
},
search: async (query, k = 10) => {
const results = await db.search({ vector: query, k });
return results.map((r) => ({
id: r.id,
score: r.score,
metadata: r.metadata,
}));
},
remove: (id) => {
db.delete(id);
return true;
},
size: async () => {
const len = await db.len();
return len;
},
clear: () => {
// Not directly supported - would need to recreate
},
};
},
generateEmbedding: (text, dimensions = 768) => {
// ruvector may not have this - use fallback
return generateHashEmbedding(text, dimensions);
},
cosineSimilarity: (a, b) => {
return cosineSimilarity(a, b);
},
isWASMAccelerated: () => {
return ruvector.isWasm?.() ?? false;
},
};
isAvailable = true;
return true;
}
}
catch {
// Silently fail - ruvector is optional
}
isAvailable = false;
return false;
}
/**
* Check if ruvector is available
*/
export function isRuVectorAvailable() {
return isAvailable;
}
/**
* Check if WASM acceleration is enabled
*/
export function isWASMAccelerated() {
if (ruvectorModule && typeof ruvectorModule.isWASMAccelerated === 'function') {
return ruvectorModule.isWASMAccelerated();
}
return false;
}
/**
* Create a vector database
* Uses ruvector HNSW if available, falls back to brute-force search
*/
export async function createVectorDB(dimensions = 768) {
await loadRuVector();
if (ruvectorModule && typeof ruvectorModule.createVectorDB === 'function') {
try {
return await ruvectorModule.createVectorDB(dimensions);
}
catch (err) {
// Fall back to simple implementation, but make the degradation VISIBLE.
// A silent fall-through here turns HNSW into O(n) brute force without any
// signal — exactly the failure mode that masked the lock-contention bug.
console.warn(`[vector-db] ruvector HNSW init failed, falling back to brute-force search: ${err?.message ?? err}`);
}
}
return new FallbackVectorDB(dimensions);
}
/**
* Generate an embedding for text
* Uses ruvector if available, falls back to hash-based embedding
*
* @returns The embedding vector. When using hash fallback, the returned
* Float32Array will have a `_warning` property (non-enumerable)
* indicating it lacks semantic meaning.
*/
export function generateEmbedding(text, dimensions = 768) {
if (ruvectorModule && typeof ruvectorModule.generateEmbedding === 'function') {
try {
return ruvectorModule.generateEmbedding(text, dimensions);
}
catch {
// Fall back to hash-based embedding
}
}
const embedding = generateHashEmbedding(text, dimensions);
// Tag the result so consumers can detect it came from hash fallback
Object.defineProperty(embedding, '_warning', {
value: 'hash-based pseudo-embedding — no semantic similarity',
enumerable: false,
configurable: true,
});
return embedding;
}
/**
* Compute cosine similarity between two vectors
*/
export function computeSimilarity(a, b) {
if (ruvectorModule && typeof ruvectorModule.cosineSimilarity === 'function') {
try {
return ruvectorModule.cosineSimilarity(a, b);
}
catch {
// Fall back to JS implementation
}
}
return cosineSimilarity(a, b);
}
/**
* Get status information about the ruvector module
*/
export function getStatus() {
if (!isAvailable) {
return {
available: false,
wasmAccelerated: false,
backend: 'fallback',
};
}
// HONESTY (audit docs/reviews/intelligence-system-audit-2026-05-29.md):
// ruvector's `isWasm()` does NOT mean "WASM-accelerated" — there is no WASM
// HNSW build in this stack. `isWasm()===true` means the do-nothing STUB is
// active (search() returns []), i.e. native NAPI failed to load. So
// wasmAccelerated===false is the HEALTHY state (native NAPI is the fastest
// backend available). We label the stub honestly so a regression into it is
// visible instead of being mistaken for a faster mode.
const stubActive = isWASMAccelerated();
return {
available: true,
wasmAccelerated: stubActive,
backend: stubActive ? 'ruvector-stub-search-disabled' : 'ruvector-native',
};
}
//# sourceMappingURL=vector-db.js.map