ruvector-fixed / dist /core /onnx-embedder.js
Archie
Fix dimension/dimensions bug and positional insert/search args
40d7073
"use strict";
/**
* ONNX WASM Embedder - Semantic embeddings for hooks
*
* Provides real transformer-based embeddings using all-MiniLM-L6-v2
* running in pure WASM (no native dependencies).
*
* Uses bundled ONNX WASM files from src/core/onnx/
*
* Features:
* - 384-dimensional semantic embeddings
* - Real semantic understanding (not hash-based)
* - Cached model loading (downloads from HuggingFace on first use)
* - Batch embedding support
* - Optional parallel workers for 3.8x batch speedup
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.OnnxEmbedder = void 0;
exports.isOnnxAvailable = isOnnxAvailable;
exports.initOnnxEmbedder = initOnnxEmbedder;
exports.embed = embed;
exports.embedBatch = embedBatch;
exports.similarity = similarity;
exports.cosineSimilarity = cosineSimilarity;
exports.getDimension = getDimension;
exports.isReady = isReady;
exports.getStats = getStats;
exports.shutdown = shutdown;
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const url_1 = require("url");
const module_1 = require("module");
// Set up ESM-compatible require for WASM module (fixes Windows/ESM compatibility)
// The WASM bindings use module.require for Node.js crypto, this provides a fallback
if (typeof globalThis !== 'undefined' && !globalThis.__ruvector_require) {
try {
// In ESM context, use createRequire with __filename
globalThis.__ruvector_require = (0, module_1.createRequire)(__filename);
}
catch {
// Fallback: require should be available in CommonJS
try {
globalThis.__ruvector_require = require;
}
catch {
// Neither available - WASM will fall back to crypto.getRandomValues
}
}
}
// Force native dynamic import (avoids TypeScript transpiling to require)
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const dynamicImport = new Function('specifier', 'return import(specifier)');
// Capability detection
let simdAvailable = false;
let parallelAvailable = false;
// Lazy-loaded module state
let wasmModule = null;
let embedder = null;
let parallelEmbedder = null;
let loadError = null;
let loadPromise = null;
let isInitialized = false;
let parallelEnabled = false;
let parallelThreshold = 4;
// Default model
const DEFAULT_MODEL = 'all-MiniLM-L6-v2';
/**
* Check if ONNX embedder is available (bundled files exist)
*/
function isOnnxAvailable() {
try {
const pkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm.js');
return fs.existsSync(pkgPath);
}
catch {
return false;
}
}
/**
* Check if parallel workers are available (npm package installed)
*/
async function detectParallelAvailable() {
try {
await dynamicImport('ruvector-onnx-embeddings-wasm/parallel');
parallelAvailable = true;
return true;
}
catch {
parallelAvailable = false;
return false;
}
}
/**
* Check if SIMD is available (from WASM module)
*/
function detectSimd() {
try {
if (wasmModule && typeof wasmModule.simd_available === 'function') {
simdAvailable = wasmModule.simd_available();
return simdAvailable;
}
}
catch { }
return false;
}
/**
* Try to load ParallelEmbedder from npm package (optional)
*/
async function tryInitParallel(config) {
// Skip if explicitly disabled
if (config.enableParallel === false)
return false;
// For 'auto' or true, try to initialize
try {
const parallelModule = await dynamicImport('ruvector-onnx-embeddings-wasm/parallel');
const { ParallelEmbedder } = parallelModule;
parallelEmbedder = new ParallelEmbedder({
numWorkers: config.numWorkers,
});
await parallelEmbedder.init(config.modelId || DEFAULT_MODEL);
parallelThreshold = config.parallelThreshold || 4;
parallelEnabled = true;
parallelAvailable = true;
console.error(`Parallel embedder ready: ${parallelEmbedder.numWorkers} workers, SIMD: ${simdAvailable}`);
return true;
}
catch (e) {
parallelAvailable = false;
if (config.enableParallel === true) {
// Only warn if explicitly requested
console.error(`Parallel embedder not available: ${e.message}`);
}
return false;
}
}
/**
* Initialize the ONNX embedder (downloads model if needed)
*/
async function initOnnxEmbedder(config = {}) {
if (isInitialized)
return true;
if (loadError)
throw loadError;
if (loadPromise) {
await loadPromise;
return isInitialized;
}
loadPromise = (async () => {
try {
// Paths to bundled ONNX files
const pkgPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm.js');
const loaderPath = path.join(__dirname, 'onnx', 'loader.js');
if (!fs.existsSync(pkgPath)) {
throw new Error('ONNX WASM files not bundled. The onnx/ directory is missing.');
}
// Convert paths to file:// URLs for cross-platform ESM compatibility (Windows fix)
const pkgUrl = (0, url_1.pathToFileURL)(pkgPath).href;
const loaderUrl = (0, url_1.pathToFileURL)(loaderPath).href;
// Dynamic import of bundled modules using file:// URLs
wasmModule = await dynamicImport(pkgUrl);
// Initialize WASM module (loads the .wasm file)
const wasmPath = path.join(__dirname, 'onnx', 'pkg', 'ruvector_onnx_embeddings_wasm_bg.wasm');
if (wasmModule.default && typeof wasmModule.default === 'function') {
// For bundler-style initialization, pass the wasm buffer
const wasmBytes = fs.readFileSync(wasmPath);
await wasmModule.default(wasmBytes);
}
const loaderModule = await dynamicImport(loaderUrl);
const { ModelLoader } = loaderModule;
// Create model loader with caching
const modelLoader = new ModelLoader({
cache: true,
cacheDir: config.cacheDir || path.join(process.env.HOME || '/tmp', '.ruvector', 'models'),
});
// Load model (downloads from HuggingFace on first use)
const modelId = config.modelId || DEFAULT_MODEL;
console.error(`Loading ONNX model: ${modelId}...`);
const { modelBytes, tokenizerJson, config: modelConfig } = await modelLoader.loadModel(modelId);
// Create embedder with config
const embedderConfig = new wasmModule.WasmEmbedderConfig()
.setMaxLength(config.maxLength || modelConfig.maxLength || 256)
.setNormalize(config.normalize !== false)
.setPooling(0); // Mean pooling
embedder = wasmModule.WasmEmbedder.withConfig(modelBytes, tokenizerJson, embedderConfig);
// Detect SIMD capability
detectSimd();
console.error(`ONNX embedder ready: ${embedder.dimension()}d, SIMD: ${simdAvailable}`);
isInitialized = true;
// Determine if we should use parallel workers
// - true: always enable
// - false: never enable
// - 'auto'/undefined: enable for long-running processes (MCP, servers), skip for CLI
let shouldTryParallel = false;
if (config.enableParallel === true) {
shouldTryParallel = true;
}
else if (config.enableParallel === false) {
shouldTryParallel = false;
}
else {
// Auto-detect: check if running as CLI hook or long-running process
const isCLI = process.argv[1]?.includes('cli.js') ||
process.argv[1]?.includes('bin/ruvector') ||
process.env.RUVECTOR_CLI === '1';
const isMCP = process.env.MCP_SERVER === '1' ||
process.argv.some(a => a.includes('mcp'));
const forceParallel = process.env.RUVECTOR_PARALLEL === '1';
// Enable parallel for MCP/servers or if explicitly requested, skip for CLI
shouldTryParallel = forceParallel || (isMCP && !isCLI);
}
if (shouldTryParallel) {
await tryInitParallel(config);
}
}
catch (e) {
loadError = new Error(`Failed to initialize ONNX embedder: ${e.message}`);
throw loadError;
}
})();
await loadPromise;
return isInitialized;
}
/**
* Generate embedding for text
*/
async function embed(text) {
if (!isInitialized) {
await initOnnxEmbedder();
}
if (!embedder) {
throw new Error('ONNX embedder not initialized');
}
const start = performance.now();
const embedding = embedder.embedOne(text);
const timeMs = performance.now() - start;
return {
embedding: Array.from(embedding),
dimension: embedding.length,
timeMs,
};
}
/**
* Generate embeddings for multiple texts
* Uses parallel workers automatically for batches >= parallelThreshold
*/
async function embedBatch(texts) {
if (!isInitialized) {
await initOnnxEmbedder();
}
if (!embedder) {
throw new Error('ONNX embedder not initialized');
}
const start = performance.now();
// Use parallel workers for large batches
if (parallelEnabled && parallelEmbedder && texts.length >= parallelThreshold) {
const batchResults = await parallelEmbedder.embedBatch(texts);
const totalTime = performance.now() - start;
const dimension = parallelEmbedder.dimension || 384;
return batchResults.map((emb) => ({
embedding: Array.from(emb),
dimension,
timeMs: totalTime / texts.length,
}));
}
// Sequential fallback
const batchEmbeddings = embedder.embedBatch(texts);
const totalTime = performance.now() - start;
const dimension = embedder.dimension();
const results = [];
for (let i = 0; i < texts.length; i++) {
const embedding = batchEmbeddings.slice(i * dimension, (i + 1) * dimension);
results.push({
embedding: Array.from(embedding),
dimension,
timeMs: totalTime / texts.length,
});
}
return results;
}
/**
* Calculate cosine similarity between two texts
*/
async function similarity(text1, text2) {
if (!isInitialized) {
await initOnnxEmbedder();
}
if (!embedder) {
throw new Error('ONNX embedder not initialized');
}
const start = performance.now();
const sim = embedder.similarity(text1, text2);
const timeMs = performance.now() - start;
return { similarity: sim, timeMs };
}
/**
* Calculate cosine similarity between two embeddings
*/
function cosineSimilarity(a, b) {
if (a.length !== b.length) {
throw new Error('Embeddings must have same dimension');
}
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 magnitude = Math.sqrt(normA) * Math.sqrt(normB);
return magnitude === 0 ? 0 : dotProduct / magnitude;
}
/**
* Get embedding dimension
*/
function getDimension() {
return embedder ? embedder.dimension() : 384;
}
/**
* Check if embedder is ready
*/
function isReady() {
return isInitialized;
}
/**
* Get embedder stats including SIMD and parallel capabilities
*/
function getStats() {
return {
ready: isInitialized,
dimension: embedder ? embedder.dimension() : 384,
model: DEFAULT_MODEL,
simd: simdAvailable,
parallel: parallelEnabled,
parallelWorkers: parallelEmbedder?.numWorkers || 0,
parallelThreshold,
};
}
/**
* Shutdown parallel workers (call on exit)
*/
async function shutdown() {
if (parallelEmbedder) {
await parallelEmbedder.shutdown();
parallelEmbedder = null;
parallelEnabled = false;
}
}
// Export class wrapper for compatibility
class OnnxEmbedder {
constructor(config = {}) {
this.config = config;
}
async init() {
return initOnnxEmbedder(this.config);
}
async embed(text) {
const result = await embed(text);
return result.embedding;
}
async embedBatch(texts) {
const results = await embedBatch(texts);
return results.map(r => r.embedding);
}
async similarity(text1, text2) {
const result = await similarity(text1, text2);
return result.similarity;
}
get dimension() {
return getDimension();
}
get ready() {
return isReady();
}
}
exports.OnnxEmbedder = OnnxEmbedder;
exports.default = OnnxEmbedder;