| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | export const MODELS = { |
| | |
| | 'all-MiniLM-L6-v2': { |
| | name: 'all-MiniLM-L6-v2', |
| | dimension: 384, |
| | maxLength: 256, |
| | size: '23MB', |
| | description: 'Fast, general-purpose embeddings', |
| | model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2/resolve/main/tokenizer.json', |
| | }, |
| | 'all-MiniLM-L12-v2': { |
| | name: 'all-MiniLM-L12-v2', |
| | dimension: 384, |
| | maxLength: 256, |
| | size: '33MB', |
| | description: 'Better quality, balanced speed', |
| | model: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/sentence-transformers/all-MiniLM-L12-v2/resolve/main/tokenizer.json', |
| | }, |
| |
|
| | |
| | 'bge-small-en-v1.5': { |
| | name: 'bge-small-en-v1.5', |
| | dimension: 384, |
| | maxLength: 512, |
| | size: '33MB', |
| | description: 'State-of-the-art small model', |
| | model: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/BAAI/bge-small-en-v1.5/resolve/main/tokenizer.json', |
| | }, |
| | 'bge-base-en-v1.5': { |
| | name: 'bge-base-en-v1.5', |
| | dimension: 768, |
| | maxLength: 512, |
| | size: '110MB', |
| | description: 'Best overall quality', |
| | model: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/BAAI/bge-base-en-v1.5/resolve/main/tokenizer.json', |
| | }, |
| |
|
| | |
| | 'e5-small-v2': { |
| | name: 'e5-small-v2', |
| | dimension: 384, |
| | maxLength: 512, |
| | size: '33MB', |
| | description: 'Excellent for search & retrieval', |
| | model: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/intfloat/e5-small-v2/resolve/main/tokenizer.json', |
| | }, |
| |
|
| | |
| | 'gte-small': { |
| | name: 'gte-small', |
| | dimension: 384, |
| | maxLength: 512, |
| | size: '33MB', |
| | description: 'Good multilingual support', |
| | model: 'https://huggingface.co/thenlper/gte-small/resolve/main/onnx/model.onnx', |
| | tokenizer: 'https://huggingface.co/thenlper/gte-small/resolve/main/tokenizer.json', |
| | }, |
| | }; |
| |
|
| | |
| | |
| | |
| | export const DEFAULT_MODEL = 'all-MiniLM-L6-v2'; |
| |
|
| | |
| | |
| | |
| | export class ModelLoader { |
| | constructor(options = {}) { |
| | this.cache = options.cache ?? true; |
| | this.cacheStorage = options.cacheStorage ?? 'ruvector-models'; |
| | this.onProgress = options.onProgress ?? null; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async loadModel(modelName = DEFAULT_MODEL) { |
| | const modelConfig = MODELS[modelName]; |
| | if (!modelConfig) { |
| | throw new Error(`Unknown model: ${modelName}. Available: ${Object.keys(MODELS).join(', ')}`); |
| | } |
| |
|
| | console.log(`Loading model: ${modelConfig.name} (${modelConfig.size})`); |
| |
|
| | const [modelBytes, tokenizerJson] = await Promise.all([ |
| | this.fetchWithCache(modelConfig.model, `${modelName}-model.onnx`, 'arraybuffer'), |
| | this.fetchWithCache(modelConfig.tokenizer, `${modelName}-tokenizer.json`, 'text'), |
| | ]); |
| |
|
| | return { |
| | modelBytes: new Uint8Array(modelBytes), |
| | tokenizerJson, |
| | config: modelConfig, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async loadFromUrls(modelUrl, tokenizerUrl) { |
| | const [modelBytes, tokenizerJson] = await Promise.all([ |
| | this.fetchWithCache(modelUrl, null, 'arraybuffer'), |
| | this.fetchWithCache(tokenizerUrl, null, 'text'), |
| | ]); |
| |
|
| | return { |
| | modelBytes: new Uint8Array(modelBytes), |
| | tokenizerJson, |
| | }; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | async loadFromFiles(modelPath, tokenizerPath) { |
| | |
| | if (typeof process !== 'undefined' && process.versions?.node) { |
| | const fs = await import('fs/promises'); |
| | const [modelBytes, tokenizerJson] = await Promise.all([ |
| | fs.readFile(modelPath), |
| | fs.readFile(tokenizerPath, 'utf8'), |
| | ]); |
| | return { |
| | modelBytes: new Uint8Array(modelBytes), |
| | tokenizerJson, |
| | }; |
| | } |
| | throw new Error('loadFromFiles is only available in Node.js'); |
| | } |
| |
|
| | |
| | |
| | |
| | async fetchWithCache(url, cacheKey, responseType) { |
| | |
| | if (this.cache && typeof caches !== 'undefined' && cacheKey) { |
| | try { |
| | const cache = await caches.open(this.cacheStorage); |
| | const cached = await cache.match(cacheKey); |
| | if (cached) { |
| | console.log(` Cache hit: ${cacheKey}`); |
| | return responseType === 'arraybuffer' |
| | ? await cached.arrayBuffer() |
| | : await cached.text(); |
| | } |
| | } catch (e) { |
| | |
| | } |
| | } |
| |
|
| | |
| | console.log(` Downloading: ${url}`); |
| | const response = await this.fetchWithProgress(url); |
| |
|
| | if (!response.ok) { |
| | throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); |
| | } |
| |
|
| | |
| | const responseClone = response.clone(); |
| |
|
| | |
| | if (this.cache && typeof caches !== 'undefined' && cacheKey) { |
| | try { |
| | const cache = await caches.open(this.cacheStorage); |
| | await cache.put(cacheKey, responseClone); |
| | } catch (e) { |
| | |
| | } |
| | } |
| |
|
| | return responseType === 'arraybuffer' |
| | ? await response.arrayBuffer() |
| | : await response.text(); |
| | } |
| |
|
| | |
| | |
| | |
| | async fetchWithProgress(url) { |
| | const response = await fetch(url); |
| |
|
| | if (!this.onProgress || !response.body) { |
| | return response; |
| | } |
| |
|
| | const contentLength = response.headers.get('content-length'); |
| | if (!contentLength) { |
| | return response; |
| | } |
| |
|
| | const total = parseInt(contentLength, 10); |
| | let loaded = 0; |
| |
|
| | const reader = response.body.getReader(); |
| | const chunks = []; |
| |
|
| | while (true) { |
| | const { done, value } = await reader.read(); |
| | if (done) break; |
| |
|
| | chunks.push(value); |
| | loaded += value.length; |
| |
|
| | this.onProgress({ |
| | loaded, |
| | total, |
| | percent: Math.round((loaded / total) * 100), |
| | }); |
| | } |
| |
|
| | const body = new Uint8Array(loaded); |
| | let position = 0; |
| | for (const chunk of chunks) { |
| | body.set(chunk, position); |
| | position += chunk.length; |
| | } |
| |
|
| | return new Response(body, { |
| | headers: response.headers, |
| | status: response.status, |
| | statusText: response.statusText, |
| | }); |
| | } |
| |
|
| | |
| | |
| | |
| | async clearCache() { |
| | if (typeof caches !== 'undefined') { |
| | await caches.delete(this.cacheStorage); |
| | console.log('Model cache cleared'); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | static listModels() { |
| | return Object.entries(MODELS).map(([key, config]) => ({ |
| | id: key, |
| | ...config, |
| | })); |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function createEmbedder(modelName = DEFAULT_MODEL, wasmModule = null) { |
| | |
| | if (!wasmModule) { |
| | wasmModule = await import('./pkg/ruvector_onnx_embeddings_wasm.js'); |
| | await wasmModule.default(); |
| | } |
| |
|
| | const loader = new ModelLoader(); |
| | const { modelBytes, tokenizerJson, config } = await loader.loadModel(modelName); |
| |
|
| | const embedderConfig = new wasmModule.WasmEmbedderConfig() |
| | .setMaxLength(config.maxLength) |
| | .setNormalize(true) |
| | .setPooling(0); |
| |
|
| | const embedder = wasmModule.WasmEmbedder.withConfig( |
| | modelBytes, |
| | tokenizerJson, |
| | embedderConfig |
| | ); |
| |
|
| | return embedder; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function embed(text, modelName = DEFAULT_MODEL) { |
| | const embedder = await createEmbedder(modelName); |
| |
|
| | if (Array.isArray(text)) { |
| | return embedder.embedBatch(text); |
| | } |
| | return embedder.embedOne(text); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function similarity(text1, text2, modelName = DEFAULT_MODEL) { |
| | const embedder = await createEmbedder(modelName); |
| | return embedder.similarity(text1, text2); |
| | } |
| |
|
| | export default { |
| | MODELS, |
| | DEFAULT_MODEL, |
| | ModelLoader, |
| | createEmbedder, |
| | embed, |
| | similarity, |
| | }; |
| |
|