UNPKG

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

310 lines 10.6 kB
/** * DiskANN Vector Search Backend * * SSD-friendly approximate nearest neighbor search using Vamana graph. * Falls back gracefully to HNSW (@ruvector/router VectorDb) or * pure-JS cosine similarity when DiskANN is unavailable. * * @module v3/cli/ruvector/diskann-backend */ // ===== Lazy loading ===== let diskannInstance = null; let diskannAvailable = null; let activeBackend = 'cosine-js'; /** * Check if @ruvector/diskann is available */ export async function isDiskAnnAvailable() { if (diskannAvailable !== null) return diskannAvailable; try { const { createRequire } = await import('module'); const require2 = createRequire(import.meta.url); const mod = require2('@ruvector/diskann'); diskannAvailable = typeof mod.DiskAnn === 'function'; return diskannAvailable; } catch { diskannAvailable = false; return false; } } /** * Create or get a DiskANN index instance */ export async function getDiskAnnIndex(config) { if (diskannInstance) return { index: diskannInstance, backend: activeBackend }; // Try DiskANN first if (await isDiskAnnAvailable()) { try { const { createRequire } = await import('module'); const require2 = createRequire(import.meta.url); const { DiskAnn } = require2('@ruvector/diskann'); const index = new DiskAnn({ dim: config.dim, maxDegree: config.maxDegree ?? 64, buildBeam: config.buildBeam ?? 128, searchBeam: config.searchBeam ?? 64, alpha: config.alpha ?? 1.2, pqSubspaces: config.pqSubspaces ?? 0, storagePath: config.storagePath, }); diskannInstance = index; activeBackend = 'diskann'; return { index, backend: 'diskann' }; } catch { // Fall through } } // Try HNSW (@ruvector/router VectorDb) as fallback try { const { createRequire } = await import('module'); const require2 = createRequire(import.meta.url); const router = require2('@ruvector/router'); if (router.VectorDb && router.DistanceMetric) { const index = new router.VectorDb({ dimensions: config.dim, distanceMetric: router.DistanceMetric.Cosine, hnswM: 16, hnswEfConstruction: 200, hnswEfSearch: 100, }); diskannInstance = index; activeBackend = 'hnsw'; return { index, backend: 'hnsw' }; } } catch { // Fall through } // Pure JS fallback const jsIndex = createJsFallbackIndex(config.dim); diskannInstance = jsIndex; activeBackend = 'cosine-js'; return { index: jsIndex, backend: 'cosine-js' }; } /** * Get the active backend name */ export function getActiveBackend() { return activeBackend; } /** * Reset the index (for testing) */ export function resetIndex() { diskannInstance = null; diskannAvailable = null; activeBackend = 'cosine-js'; } // ===== Unified search interface ===== /** * Insert a vector into the active backend */ export async function insertVector(id, vector, config = { dim: 384 }) { const { index, backend } = await getDiskAnnIndex(config); if (backend === 'diskann') { index.insert(id, vector); } else if (backend === 'hnsw') { index.insert(id, vector); } else { index.insert(id, vector); } return { backend }; } /** * Build the index (required for DiskANN before search) */ export async function buildIndex(config = { dim: 384 }) { const { index, backend } = await getDiskAnnIndex(config); if (backend === 'diskann' && typeof index.build === 'function') { index.build(); } // HNSW and JS fallback don't need explicit build } /** * Search for k nearest neighbors */ export async function searchVectors(query, k, config = { dim: 384 }) { const { index, backend } = await getDiskAnnIndex(config); if (backend === 'diskann') { const results = index.search(query, k); return results.map((r) => ({ id: r.id, distance: r.distance, score: 1 / (1 + r.distance), // Convert L2 distance to similarity })); } if (backend === 'hnsw') { const results = index.search(query, k); return results.map((r) => ({ id: r.id, distance: r.score, // VectorDb returns distance as 'score' score: 1 / (1 + r.score), })); } // JS fallback return index.search(query, k); } // ===== Pure JS fallback ===== function createJsFallbackIndex(dim) { const vectors = new Map(); return { insert(id, vector) { vectors.set(id, new Float32Array(vector)); }, search(query, k) { const results = []; for (const [id, vec] of vectors) { let dot = 0, normQ = 0, normV = 0; for (let i = 0; i < dim; i++) { dot += query[i] * vec[i]; normQ += query[i] * query[i]; normV += vec[i] * vec[i]; } const cosine = dot / (Math.sqrt(normQ) * Math.sqrt(normV) || 1); const distance = 1 - cosine; results.push({ id, distance, score: cosine }); } return results.sort((a, b) => a.distance - b.distance).slice(0, k); }, count() { return vectors.size; }, delete(id) { return vectors.delete(id); }, build() { }, }; } /** * Run a benchmark comparing available backends */ export async function benchmark(opts = {}) { const dim = opts.dim ?? 384; const n = opts.vectorCount ?? 1000; const k = opts.k ?? 10; const queryCount = opts.queries ?? 100; const results = []; // Generate random vectors const vectors = []; for (let i = 0; i < n; i++) { const v = new Float32Array(dim); for (let j = 0; j < dim; j++) v[j] = Math.random() * 2 - 1; // Normalize let norm = 0; for (let j = 0; j < dim; j++) norm += v[j] * v[j]; norm = Math.sqrt(norm); for (let j = 0; j < dim; j++) v[j] /= norm; vectors.push([`vec-${i}`, v]); } // Generate query vectors const queries = []; for (let i = 0; i < queryCount; i++) { const q = new Float32Array(dim); for (let j = 0; j < dim; j++) q[j] = Math.random() * 2 - 1; let norm = 0; for (let j = 0; j < dim; j++) norm += q[j] * q[j]; norm = Math.sqrt(norm); for (let j = 0; j < dim; j++) q[j] /= norm; queries.push(q); } // Brute force ground truth function bruteForceSearch(query) { const scores = []; for (const [id, vec] of vectors) { let dist = 0; for (let j = 0; j < dim; j++) { const d = query[j] - vec[j]; dist += d * d; } scores.push({ id, dist }); } scores.sort((a, b) => a.dist - b.dist); return scores.slice(0, k).map(s => s.id); } const groundTruth = queries.map(q => bruteForceSearch(q)); // Test each available backend for (const backendName of ['diskann', 'hnsw', 'cosine-js']) { resetIndex(); try { let index; const memBefore = process.memoryUsage().heapUsed; if (backendName === 'diskann') { if (!(await isDiskAnnAvailable())) continue; const { createRequire } = await import('module'); const require2 = createRequire(import.meta.url); const { DiskAnn } = require2('@ruvector/diskann'); index = new DiskAnn({ dim, maxDegree: 64, buildBeam: 128, searchBeam: 64 }); } else if (backendName === 'hnsw') { try { const { createRequire } = await import('module'); const require2 = createRequire(import.meta.url); const router = require2('@ruvector/router'); if (!router.VectorDb) continue; index = new router.VectorDb({ dimensions: dim, distanceMetric: router.DistanceMetric.Cosine }); } catch { continue; } } else { index = createJsFallbackIndex(dim); } // Insert const insertStart = performance.now(); for (const [id, vec] of vectors) { index.insert(id, vec); } const insertTime = performance.now() - insertStart; // Build const buildStart = performance.now(); if (typeof index.build === 'function') index.build(); const buildTime = performance.now() - buildStart; // Search const searchStart = performance.now(); const searchResults = []; for (const q of queries) { const r = index.search(q, k); searchResults.push(r.map((x) => x.id)); } const searchTime = performance.now() - searchStart; // Recall vs ground truth let totalRecall = 0; for (let i = 0; i < queryCount; i++) { const truth = new Set(groundTruth[i]); const found = searchResults[i].filter(id => truth.has(id)).length; totalRecall += found / k; } const recall = totalRecall / queryCount; const memAfter = process.memoryUsage().heapUsed; results.push({ backend: backendName, dim, vectorCount: n, insertTimeMs: Math.round(insertTime * 100) / 100, buildTimeMs: Math.round(buildTime * 100) / 100, searchTimeMs: Math.round(searchTime * 100) / 100, searchesPerSecond: Math.round(queryCount / (searchTime / 1000)), recall: Math.round(recall * 1000) / 1000, memoryMB: Math.round((memAfter - memBefore) / 1024 / 1024 * 100) / 100, }); } catch { // Backend failed, skip } } resetIndex(); return results; } //# sourceMappingURL=diskann-backend.js.map