embrix 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/dist/index.cjs +708 -0
- package/dist/index.d.cts +547 -0
- package/dist/index.d.ts +547 -0
- package/dist/index.js +676 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
21
|
+
|
|
22
|
+
// src/models.ts
|
|
23
|
+
function isValidModel(model) {
|
|
24
|
+
return Object.values(EmbeddingModel).includes(model);
|
|
25
|
+
}
|
|
26
|
+
function getModelConfig(model) {
|
|
27
|
+
const config = MODEL_CONFIG[model];
|
|
28
|
+
if (!config) {
|
|
29
|
+
throw new Error(`Unknown model: ${model}. Valid models: ${Object.values(EmbeddingModel).join(", ")}`);
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
function getSupportedModels() {
|
|
34
|
+
return Object.values(EmbeddingModel);
|
|
35
|
+
}
|
|
36
|
+
var EmbeddingModel, MODEL_CONFIG;
|
|
37
|
+
var init_models = __esm({
|
|
38
|
+
"src/models.ts"() {
|
|
39
|
+
"use strict";
|
|
40
|
+
EmbeddingModel = /* @__PURE__ */ ((EmbeddingModel3) => {
|
|
41
|
+
EmbeddingModel3["MiniLM"] = "minilm";
|
|
42
|
+
EmbeddingModel3["BGE"] = "bge";
|
|
43
|
+
return EmbeddingModel3;
|
|
44
|
+
})(EmbeddingModel || {});
|
|
45
|
+
MODEL_CONFIG = {
|
|
46
|
+
["minilm" /* MiniLM */]: {
|
|
47
|
+
hfPath: "Xenova/all-MiniLM-L6-v2",
|
|
48
|
+
dimension: 384,
|
|
49
|
+
name: "all-MiniLM-L6-v2",
|
|
50
|
+
description: "Fast and efficient sentence transformer model",
|
|
51
|
+
maxLength: 256
|
|
52
|
+
},
|
|
53
|
+
["bge" /* BGE */]: {
|
|
54
|
+
hfPath: "Xenova/bge-small-en-v1.5",
|
|
55
|
+
dimension: 384,
|
|
56
|
+
name: "bge-small-en-v1.5",
|
|
57
|
+
description: "High quality English embeddings from BAAI",
|
|
58
|
+
maxLength: 512
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// src/loader.ts
|
|
65
|
+
var loader_exports = {};
|
|
66
|
+
__export(loader_exports, {
|
|
67
|
+
clearModelCache: () => clearModelCache,
|
|
68
|
+
getLoadedModels: () => getLoadedModels,
|
|
69
|
+
isModelLoaded: () => isModelLoaded,
|
|
70
|
+
loadModel: () => loadModel,
|
|
71
|
+
preloadAllModels: () => preloadAllModels,
|
|
72
|
+
preloadModel: () => preloadModel
|
|
73
|
+
});
|
|
74
|
+
import { pipeline, env } from "@xenova/transformers";
|
|
75
|
+
async function loadModel(model) {
|
|
76
|
+
if (modelCache.has(model)) {
|
|
77
|
+
return modelCache.get(model);
|
|
78
|
+
}
|
|
79
|
+
if (loadingPromises.has(model)) {
|
|
80
|
+
return loadingPromises.get(model);
|
|
81
|
+
}
|
|
82
|
+
const config = MODEL_CONFIG[model];
|
|
83
|
+
const loadingPromise = pipeline("feature-extraction", config.hfPath, {
|
|
84
|
+
// Use default quantized model for smaller download
|
|
85
|
+
quantized: true
|
|
86
|
+
}).then((embedder) => {
|
|
87
|
+
modelCache.set(model, embedder);
|
|
88
|
+
loadingPromises.delete(model);
|
|
89
|
+
return embedder;
|
|
90
|
+
}).catch((error) => {
|
|
91
|
+
loadingPromises.delete(model);
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to load model ${config.name} (${config.hfPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
loadingPromises.set(model, loadingPromise);
|
|
97
|
+
return loadingPromise;
|
|
98
|
+
}
|
|
99
|
+
function isModelLoaded(model) {
|
|
100
|
+
return modelCache.has(model);
|
|
101
|
+
}
|
|
102
|
+
function clearModelCache() {
|
|
103
|
+
modelCache.clear();
|
|
104
|
+
loadingPromises.clear();
|
|
105
|
+
}
|
|
106
|
+
function getLoadedModels() {
|
|
107
|
+
return Array.from(modelCache.keys());
|
|
108
|
+
}
|
|
109
|
+
async function preloadModel(model) {
|
|
110
|
+
await loadModel(model);
|
|
111
|
+
}
|
|
112
|
+
async function preloadAllModels() {
|
|
113
|
+
const models = Object.values(EmbeddingModel);
|
|
114
|
+
await Promise.all(models.map((model) => loadModel(model)));
|
|
115
|
+
}
|
|
116
|
+
var modelCache, loadingPromises;
|
|
117
|
+
var init_loader = __esm({
|
|
118
|
+
"src/loader.ts"() {
|
|
119
|
+
"use strict";
|
|
120
|
+
init_models();
|
|
121
|
+
env.allowLocalModels = false;
|
|
122
|
+
modelCache = /* @__PURE__ */ new Map();
|
|
123
|
+
loadingPromises = /* @__PURE__ */ new Map();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/index.ts
|
|
128
|
+
init_models();
|
|
129
|
+
init_loader();
|
|
130
|
+
|
|
131
|
+
// src/embedder.ts
|
|
132
|
+
init_loader();
|
|
133
|
+
init_models();
|
|
134
|
+
var DEFAULT_OPTIONS = {
|
|
135
|
+
normalize: true,
|
|
136
|
+
pooling: "mean"
|
|
137
|
+
};
|
|
138
|
+
var Embedder = class {
|
|
139
|
+
/**
|
|
140
|
+
* Create a new Embedder instance.
|
|
141
|
+
*
|
|
142
|
+
* @param model - The embedding model to use
|
|
143
|
+
* @throws Error if the model is not supported
|
|
144
|
+
*/
|
|
145
|
+
constructor(model) {
|
|
146
|
+
this.model = model;
|
|
147
|
+
this.config = getModelConfig(model);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get the dimension of embeddings produced by this model.
|
|
151
|
+
*
|
|
152
|
+
* @returns The embedding dimension
|
|
153
|
+
*/
|
|
154
|
+
get dimension() {
|
|
155
|
+
return this.config.dimension;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Get the model name.
|
|
159
|
+
*
|
|
160
|
+
* @returns The human-readable model name
|
|
161
|
+
*/
|
|
162
|
+
get modelName() {
|
|
163
|
+
return this.config.name;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Get the model enum value.
|
|
167
|
+
*
|
|
168
|
+
* @returns The EmbeddingModel enum value
|
|
169
|
+
*/
|
|
170
|
+
get modelType() {
|
|
171
|
+
return this.model;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Generate an embedding for a single text.
|
|
175
|
+
*
|
|
176
|
+
* @param text - The text to embed
|
|
177
|
+
* @param options - Optional embedding configuration
|
|
178
|
+
* @returns Promise resolving to the embedding vector
|
|
179
|
+
* @throws Error if embedding generation fails
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* const embedder = new Embedder(EmbeddingModel.MiniLM);
|
|
184
|
+
* const vector = await embedder.embed("Hello world");
|
|
185
|
+
* console.log(vector.length); // 384
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
async embed(text, options) {
|
|
189
|
+
if (!text || typeof text !== "string") {
|
|
190
|
+
throw new Error("Input text must be a non-empty string");
|
|
191
|
+
}
|
|
192
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
193
|
+
const pipeline2 = await loadModel(this.model);
|
|
194
|
+
try {
|
|
195
|
+
const output = await pipeline2(text, {
|
|
196
|
+
pooling: opts.pooling,
|
|
197
|
+
normalize: opts.normalize
|
|
198
|
+
});
|
|
199
|
+
const vector = this.tensorToFloat32(output);
|
|
200
|
+
this.validateDimension(vector);
|
|
201
|
+
return vector;
|
|
202
|
+
} catch (error) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Failed to generate embedding: ${error instanceof Error ? error.message : String(error)}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Generate embeddings for multiple texts.
|
|
210
|
+
*
|
|
211
|
+
* This method is more efficient than calling embed() multiple times
|
|
212
|
+
* as it processes all texts in a single batch.
|
|
213
|
+
*
|
|
214
|
+
* @param texts - Array of texts to embed
|
|
215
|
+
* @param options - Optional embedding configuration
|
|
216
|
+
* @returns Promise resolving to array of embedding vectors
|
|
217
|
+
* @throws Error if embedding generation fails
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* const embedder = new Embedder(EmbeddingModel.MiniLM);
|
|
222
|
+
* const vectors = await embedder.embedBatch(["Hello", "World"]);
|
|
223
|
+
* console.log(vectors.length); // 2
|
|
224
|
+
* console.log(vectors[0].length); // 384
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
async embedBatch(texts, options) {
|
|
228
|
+
if (!Array.isArray(texts) || texts.length === 0) {
|
|
229
|
+
throw new Error("Input must be a non-empty array of strings");
|
|
230
|
+
}
|
|
231
|
+
for (let i = 0; i < texts.length; i++) {
|
|
232
|
+
if (!texts[i] || typeof texts[i] !== "string") {
|
|
233
|
+
throw new Error(`Invalid text at index ${i}: must be a non-empty string`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
237
|
+
const pipeline2 = await loadModel(this.model);
|
|
238
|
+
try {
|
|
239
|
+
const output = await pipeline2(texts, {
|
|
240
|
+
pooling: opts.pooling,
|
|
241
|
+
normalize: opts.normalize
|
|
242
|
+
});
|
|
243
|
+
const vectors = this.extractBatchVectors(output);
|
|
244
|
+
for (const vector of vectors) {
|
|
245
|
+
this.validateDimension(vector);
|
|
246
|
+
}
|
|
247
|
+
return vectors;
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Failed to generate batch embeddings: ${error instanceof Error ? error.message : String(error)}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Generate an embedding with full result metadata.
|
|
256
|
+
*
|
|
257
|
+
* @param text - The text to embed
|
|
258
|
+
* @param options - Optional embedding configuration
|
|
259
|
+
* @returns Promise resolving to embedding result with metadata
|
|
260
|
+
*/
|
|
261
|
+
async embedWithMetadata(text, options) {
|
|
262
|
+
const embedding = await this.embed(text, options);
|
|
263
|
+
return {
|
|
264
|
+
embedding,
|
|
265
|
+
model: this.model,
|
|
266
|
+
dimension: this.dimension
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if the model for this embedder is loaded.
|
|
271
|
+
*
|
|
272
|
+
* @returns true if the model is loaded and ready
|
|
273
|
+
*/
|
|
274
|
+
isReady() {
|
|
275
|
+
const { isModelLoaded: isModelLoaded2 } = (init_loader(), __toCommonJS(loader_exports));
|
|
276
|
+
return isModelLoaded2(this.model);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Convert a Tensor to Float32Array.
|
|
280
|
+
*
|
|
281
|
+
* Handles different tensor data types and shapes.
|
|
282
|
+
*/
|
|
283
|
+
tensorToFloat32(tensor) {
|
|
284
|
+
const data = tensor.data;
|
|
285
|
+
if (data instanceof Float32Array) {
|
|
286
|
+
return data;
|
|
287
|
+
}
|
|
288
|
+
return new Float32Array(data);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract vectors from batch output tensor.
|
|
292
|
+
*
|
|
293
|
+
* Handles the tensor shape from batch processing.
|
|
294
|
+
*/
|
|
295
|
+
extractBatchVectors(output) {
|
|
296
|
+
const data = output.data;
|
|
297
|
+
const dims = output.dims;
|
|
298
|
+
const flatData = data instanceof Float32Array ? data : new Float32Array(data);
|
|
299
|
+
if (dims.length === 2) {
|
|
300
|
+
const batchSize = dims[0];
|
|
301
|
+
const hiddenDim = dims[1];
|
|
302
|
+
const vectors = [];
|
|
303
|
+
for (let i = 0; i < batchSize; i++) {
|
|
304
|
+
const start = i * hiddenDim;
|
|
305
|
+
const end = start + hiddenDim;
|
|
306
|
+
vectors.push(flatData.slice(start, end));
|
|
307
|
+
}
|
|
308
|
+
return vectors;
|
|
309
|
+
} else if (dims.length === 1) {
|
|
310
|
+
return [this.tensorToFloat32(output)];
|
|
311
|
+
} else if (dims.length === 3) {
|
|
312
|
+
const batchSize = dims[0];
|
|
313
|
+
const seqLen = dims[1];
|
|
314
|
+
const hiddenDim = dims[2];
|
|
315
|
+
const vectors = [];
|
|
316
|
+
for (let b = 0; b < batchSize; b++) {
|
|
317
|
+
const vec = new Float32Array(hiddenDim);
|
|
318
|
+
for (let s = 0; s < seqLen; s++) {
|
|
319
|
+
for (let h = 0; h < hiddenDim; h++) {
|
|
320
|
+
const idx = b * seqLen * hiddenDim + s * hiddenDim + h;
|
|
321
|
+
vec[h] += flatData[idx] / seqLen;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
vectors.push(vec);
|
|
325
|
+
}
|
|
326
|
+
return vectors;
|
|
327
|
+
}
|
|
328
|
+
return [this.tensorToFloat32(output)];
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Validate that an embedding has the expected dimension.
|
|
332
|
+
*
|
|
333
|
+
* @param vector - The embedding vector to validate
|
|
334
|
+
* @throws Error if dimension doesn't match expected
|
|
335
|
+
*/
|
|
336
|
+
validateDimension(vector) {
|
|
337
|
+
const expected = this.config.dimension;
|
|
338
|
+
if (vector.length !== expected) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Embedding dimension mismatch: expected ${expected}, got ${vector.length}. This may indicate a model loading issue or incompatible model.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function createEmbedder(model) {
|
|
346
|
+
return new Embedder(model);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/similarity.ts
|
|
350
|
+
function dotProduct(a, b) {
|
|
351
|
+
validateEqualLength(a, b);
|
|
352
|
+
let sum = 0;
|
|
353
|
+
for (let i = 0; i < a.length; i++) {
|
|
354
|
+
sum += a[i] * b[i];
|
|
355
|
+
}
|
|
356
|
+
return sum;
|
|
357
|
+
}
|
|
358
|
+
function cosineSimilarity(a, b) {
|
|
359
|
+
validateEqualLength(a, b);
|
|
360
|
+
let dotSum = 0;
|
|
361
|
+
let normA = 0;
|
|
362
|
+
let normB = 0;
|
|
363
|
+
for (let i = 0; i < a.length; i++) {
|
|
364
|
+
const aVal = a[i];
|
|
365
|
+
const bVal = b[i];
|
|
366
|
+
dotSum += aVal * bVal;
|
|
367
|
+
normA += aVal * aVal;
|
|
368
|
+
normB += bVal * bVal;
|
|
369
|
+
}
|
|
370
|
+
const magnitude2 = Math.sqrt(normA * normB);
|
|
371
|
+
if (magnitude2 === 0) {
|
|
372
|
+
throw new Error("Cannot compute cosine similarity: zero magnitude vector");
|
|
373
|
+
}
|
|
374
|
+
return dotSum / magnitude2;
|
|
375
|
+
}
|
|
376
|
+
function euclideanDistance(a, b) {
|
|
377
|
+
validateEqualLength(a, b);
|
|
378
|
+
let sumSquared = 0;
|
|
379
|
+
for (let i = 0; i < a.length; i++) {
|
|
380
|
+
const diff = a[i] - b[i];
|
|
381
|
+
sumSquared += diff * diff;
|
|
382
|
+
}
|
|
383
|
+
return Math.sqrt(sumSquared);
|
|
384
|
+
}
|
|
385
|
+
function euclideanDistanceSquared(a, b) {
|
|
386
|
+
validateEqualLength(a, b);
|
|
387
|
+
let sumSquared = 0;
|
|
388
|
+
for (let i = 0; i < a.length; i++) {
|
|
389
|
+
const diff = a[i] - b[i];
|
|
390
|
+
sumSquared += diff * diff;
|
|
391
|
+
}
|
|
392
|
+
return sumSquared;
|
|
393
|
+
}
|
|
394
|
+
function manhattanDistance(a, b) {
|
|
395
|
+
validateEqualLength(a, b);
|
|
396
|
+
let sum = 0;
|
|
397
|
+
for (let i = 0; i < a.length; i++) {
|
|
398
|
+
sum += Math.abs(a[i] - b[i]);
|
|
399
|
+
}
|
|
400
|
+
return sum;
|
|
401
|
+
}
|
|
402
|
+
function magnitude(vector) {
|
|
403
|
+
let sum = 0;
|
|
404
|
+
for (let i = 0; i < vector.length; i++) {
|
|
405
|
+
const val = vector[i];
|
|
406
|
+
sum += val * val;
|
|
407
|
+
}
|
|
408
|
+
return Math.sqrt(sum);
|
|
409
|
+
}
|
|
410
|
+
function normalize(vector) {
|
|
411
|
+
const mag = magnitude(vector);
|
|
412
|
+
if (mag === 0) {
|
|
413
|
+
throw new Error("Cannot normalize zero magnitude vector");
|
|
414
|
+
}
|
|
415
|
+
const result = new Float32Array(vector.length);
|
|
416
|
+
for (let i = 0; i < vector.length; i++) {
|
|
417
|
+
result[i] = vector[i] / mag;
|
|
418
|
+
}
|
|
419
|
+
return result;
|
|
420
|
+
}
|
|
421
|
+
function findMostSimilar(query, candidates) {
|
|
422
|
+
if (candidates.length === 0) {
|
|
423
|
+
throw new Error("Candidates array cannot be empty");
|
|
424
|
+
}
|
|
425
|
+
let bestIndex = 0;
|
|
426
|
+
let bestSimilarity = -Infinity;
|
|
427
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
428
|
+
const similarity = cosineSimilarity(query, candidates[i]);
|
|
429
|
+
if (similarity > bestSimilarity) {
|
|
430
|
+
bestSimilarity = similarity;
|
|
431
|
+
bestIndex = i;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { index: bestIndex, similarity: bestSimilarity };
|
|
435
|
+
}
|
|
436
|
+
function findKMostSimilar(query, candidates, k) {
|
|
437
|
+
if (candidates.length === 0) {
|
|
438
|
+
throw new Error("Candidates array cannot be empty");
|
|
439
|
+
}
|
|
440
|
+
const similarities = candidates.map((candidate, index) => ({
|
|
441
|
+
index,
|
|
442
|
+
similarity: cosineSimilarity(query, candidate)
|
|
443
|
+
}));
|
|
444
|
+
similarities.sort((a, b) => b.similarity - a.similarity);
|
|
445
|
+
return similarities.slice(0, Math.min(k, similarities.length));
|
|
446
|
+
}
|
|
447
|
+
function validateEqualLength(a, b) {
|
|
448
|
+
if (a.length !== b.length) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`Vector length mismatch: ${a.length} vs ${b.length}. All similarity functions require vectors of equal length.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/benchmark.ts
|
|
456
|
+
init_models();
|
|
457
|
+
var DEFAULT_OPTIONS2 = {
|
|
458
|
+
batchSize: 100,
|
|
459
|
+
sampleText: "This is a sample text for benchmarking embedding generation performance.",
|
|
460
|
+
verbose: true
|
|
461
|
+
};
|
|
462
|
+
function generateSampleTexts(count) {
|
|
463
|
+
const templates = [
|
|
464
|
+
"The quick brown fox jumps over the lazy dog.",
|
|
465
|
+
"Machine learning models can generate embeddings for text.",
|
|
466
|
+
"Natural language processing enables computers to understand human language.",
|
|
467
|
+
"Vector embeddings capture semantic meaning in dense numerical representations.",
|
|
468
|
+
"Transformers have revolutionized the field of natural language processing."
|
|
469
|
+
];
|
|
470
|
+
const texts = [];
|
|
471
|
+
for (let i = 0; i < count; i++) {
|
|
472
|
+
const template = templates[i % templates.length];
|
|
473
|
+
texts.push(`${template} (sample ${i + 1})`);
|
|
474
|
+
}
|
|
475
|
+
return texts;
|
|
476
|
+
}
|
|
477
|
+
async function measureTime(fn) {
|
|
478
|
+
const start = process.hrtime.bigint();
|
|
479
|
+
const result = await fn();
|
|
480
|
+
const end = process.hrtime.bigint();
|
|
481
|
+
const durationNs = Number(end - start);
|
|
482
|
+
const durationMs = durationNs / 1e6;
|
|
483
|
+
return { durationMs, result };
|
|
484
|
+
}
|
|
485
|
+
async function runBenchmark(model, options) {
|
|
486
|
+
const opts = { ...DEFAULT_OPTIONS2, ...options };
|
|
487
|
+
const embedder = new Embedder(model);
|
|
488
|
+
const suiteStart = process.hrtime.bigint();
|
|
489
|
+
if (opts.verbose) {
|
|
490
|
+
console.log(`
|
|
491
|
+
${"=".repeat(60)}`);
|
|
492
|
+
console.log(`Benchmark: ${embedder.modelName}`);
|
|
493
|
+
console.log(`Model: ${model}`);
|
|
494
|
+
console.log(`Dimension: ${embedder.dimension}`);
|
|
495
|
+
console.log(`${"=".repeat(60)}
|
|
496
|
+
`);
|
|
497
|
+
}
|
|
498
|
+
if (opts.verbose) {
|
|
499
|
+
console.log("Running cold start benchmark...");
|
|
500
|
+
}
|
|
501
|
+
const coldStartResult = await measureTime(
|
|
502
|
+
() => embedder.embed(opts.sampleText)
|
|
503
|
+
);
|
|
504
|
+
const coldStart = {
|
|
505
|
+
operation: "Cold Start (first embed)",
|
|
506
|
+
durationMs: coldStartResult.durationMs,
|
|
507
|
+
embeddingCount: 1,
|
|
508
|
+
avgTimePerEmbeddingMs: coldStartResult.durationMs,
|
|
509
|
+
throughput: 1e3 / coldStartResult.durationMs
|
|
510
|
+
};
|
|
511
|
+
if (opts.verbose) {
|
|
512
|
+
console.log(` Duration: ${coldStart.durationMs.toFixed(2)}ms`);
|
|
513
|
+
}
|
|
514
|
+
if (opts.verbose) {
|
|
515
|
+
console.log("\nRunning warm start benchmark...");
|
|
516
|
+
}
|
|
517
|
+
const warmStartResult = await measureTime(
|
|
518
|
+
() => embedder.embed(opts.sampleText)
|
|
519
|
+
);
|
|
520
|
+
const warmStart = {
|
|
521
|
+
operation: "Warm Start (second embed)",
|
|
522
|
+
durationMs: warmStartResult.durationMs,
|
|
523
|
+
embeddingCount: 1,
|
|
524
|
+
avgTimePerEmbeddingMs: warmStartResult.durationMs,
|
|
525
|
+
throughput: 1e3 / warmStartResult.durationMs
|
|
526
|
+
};
|
|
527
|
+
if (opts.verbose) {
|
|
528
|
+
console.log(` Duration: ${warmStart.durationMs.toFixed(2)}ms`);
|
|
529
|
+
console.log(` Speedup: ${(coldStart.durationMs / warmStart.durationMs).toFixed(2)}x faster than cold start`);
|
|
530
|
+
}
|
|
531
|
+
if (opts.verbose) {
|
|
532
|
+
console.log(`
|
|
533
|
+
Running batch benchmark (${opts.batchSize} texts)...`);
|
|
534
|
+
}
|
|
535
|
+
const sampleTexts = generateSampleTexts(opts.batchSize);
|
|
536
|
+
const batchResult = await measureTime(
|
|
537
|
+
() => embedder.embedBatch(sampleTexts)
|
|
538
|
+
);
|
|
539
|
+
const batchBenchmark = {
|
|
540
|
+
operation: `Batch Embed (${opts.batchSize} texts)`,
|
|
541
|
+
durationMs: batchResult.durationMs,
|
|
542
|
+
embeddingCount: opts.batchSize,
|
|
543
|
+
avgTimePerEmbeddingMs: batchResult.durationMs / opts.batchSize,
|
|
544
|
+
throughput: opts.batchSize * 1e3 / batchResult.durationMs
|
|
545
|
+
};
|
|
546
|
+
if (opts.verbose) {
|
|
547
|
+
console.log(` Total duration: ${batchBenchmark.durationMs.toFixed(2)}ms`);
|
|
548
|
+
console.log(` Avg per embedding: ${batchBenchmark.avgTimePerEmbeddingMs.toFixed(2)}ms`);
|
|
549
|
+
console.log(` Throughput: ${batchBenchmark.throughput.toFixed(2)} embeddings/sec`);
|
|
550
|
+
}
|
|
551
|
+
const suiteEnd = process.hrtime.bigint();
|
|
552
|
+
const totalDurationMs = Number(suiteEnd - suiteStart) / 1e6;
|
|
553
|
+
if (opts.verbose) {
|
|
554
|
+
console.log(`
|
|
555
|
+
${"-".repeat(60)}`);
|
|
556
|
+
console.log("Summary:");
|
|
557
|
+
console.log(` Total benchmark time: ${totalDurationMs.toFixed(2)}ms`);
|
|
558
|
+
console.log(` Cold start overhead: ${(coldStart.durationMs - warmStart.durationMs).toFixed(2)}ms`);
|
|
559
|
+
console.log(`${"-".repeat(60)}
|
|
560
|
+
`);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
model,
|
|
564
|
+
modelName: embedder.modelName,
|
|
565
|
+
dimension: embedder.dimension,
|
|
566
|
+
coldStart,
|
|
567
|
+
warmStart,
|
|
568
|
+
batchBenchmark,
|
|
569
|
+
totalDurationMs,
|
|
570
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async function runAllBenchmarks(options) {
|
|
574
|
+
const models = Object.values(EmbeddingModel);
|
|
575
|
+
const results = [];
|
|
576
|
+
for (const model of models) {
|
|
577
|
+
const result = await runBenchmark(model, options);
|
|
578
|
+
results.push(result);
|
|
579
|
+
}
|
|
580
|
+
return results;
|
|
581
|
+
}
|
|
582
|
+
function formatBenchmarkResult(result) {
|
|
583
|
+
const lines = [
|
|
584
|
+
"=".repeat(60),
|
|
585
|
+
`Benchmark Results: ${result.modelName}`,
|
|
586
|
+
"=".repeat(60),
|
|
587
|
+
"",
|
|
588
|
+
`Model: ${result.model}`,
|
|
589
|
+
`Dimension: ${result.dimension}`,
|
|
590
|
+
`Timestamp: ${result.timestamp}`,
|
|
591
|
+
"",
|
|
592
|
+
"-".repeat(60),
|
|
593
|
+
"Performance Metrics:",
|
|
594
|
+
"-".repeat(60),
|
|
595
|
+
"",
|
|
596
|
+
`Cold Start (first embed):`,
|
|
597
|
+
` Duration: ${result.coldStart.durationMs.toFixed(2)}ms`,
|
|
598
|
+
` Throughput: ${result.coldStart.throughput.toFixed(2)} embeddings/sec`,
|
|
599
|
+
"",
|
|
600
|
+
`Warm Start (subsequent embed):`,
|
|
601
|
+
` Duration: ${result.warmStart.durationMs.toFixed(2)}ms`,
|
|
602
|
+
` Throughput: ${result.warmStart.throughput.toFixed(2)} embeddings/sec`,
|
|
603
|
+
"",
|
|
604
|
+
`Batch Embed (${result.batchBenchmark.embeddingCount} texts):`,
|
|
605
|
+
` Total Duration: ${result.batchBenchmark.durationMs.toFixed(2)}ms`,
|
|
606
|
+
` Avg per Embedding: ${result.batchBenchmark.avgTimePerEmbeddingMs.toFixed(2)}ms`,
|
|
607
|
+
` Throughput: ${result.batchBenchmark.throughput.toFixed(2)} embeddings/sec`,
|
|
608
|
+
"",
|
|
609
|
+
"-".repeat(60),
|
|
610
|
+
`Total Benchmark Time: ${result.totalDurationMs.toFixed(2)}ms`,
|
|
611
|
+
"=".repeat(60)
|
|
612
|
+
];
|
|
613
|
+
return lines.join("\n");
|
|
614
|
+
}
|
|
615
|
+
function compareBenchmarks(result1, result2) {
|
|
616
|
+
const lines = [
|
|
617
|
+
"\n" + "=".repeat(60),
|
|
618
|
+
"Benchmark Comparison",
|
|
619
|
+
"=".repeat(60),
|
|
620
|
+
"",
|
|
621
|
+
`Model 1: ${result1.modelName}`,
|
|
622
|
+
`Model 2: ${result2.modelName}`,
|
|
623
|
+
"",
|
|
624
|
+
"-".repeat(60),
|
|
625
|
+
"Cold Start Comparison:",
|
|
626
|
+
"-".repeat(60),
|
|
627
|
+
` ${result1.modelName}: ${result1.coldStart.durationMs.toFixed(2)}ms`,
|
|
628
|
+
` ${result2.modelName}: ${result2.coldStart.durationMs.toFixed(2)}ms`,
|
|
629
|
+
` Difference: ${Math.abs(result1.coldStart.durationMs - result2.coldStart.durationMs).toFixed(2)}ms`,
|
|
630
|
+
"",
|
|
631
|
+
"-".repeat(60),
|
|
632
|
+
"Warm Start Comparison:",
|
|
633
|
+
"-".repeat(60),
|
|
634
|
+
` ${result1.modelName}: ${result1.warmStart.durationMs.toFixed(2)}ms`,
|
|
635
|
+
` ${result2.modelName}: ${result2.warmStart.durationMs.toFixed(2)}ms`,
|
|
636
|
+
` Difference: ${Math.abs(result1.warmStart.durationMs - result2.warmStart.durationMs).toFixed(2)}ms`,
|
|
637
|
+
"",
|
|
638
|
+
"-".repeat(60),
|
|
639
|
+
"Batch Throughput Comparison:",
|
|
640
|
+
"-".repeat(60),
|
|
641
|
+
` ${result1.modelName}: ${result1.batchBenchmark.throughput.toFixed(2)} embeddings/sec`,
|
|
642
|
+
` ${result2.modelName}: ${result2.batchBenchmark.throughput.toFixed(2)} embeddings/sec`,
|
|
643
|
+
` Difference: ${Math.abs(result1.batchBenchmark.throughput - result2.batchBenchmark.throughput).toFixed(2)} embeddings/sec`,
|
|
644
|
+
"",
|
|
645
|
+
"=".repeat(60)
|
|
646
|
+
];
|
|
647
|
+
return lines.join("\n");
|
|
648
|
+
}
|
|
649
|
+
export {
|
|
650
|
+
Embedder,
|
|
651
|
+
EmbeddingModel,
|
|
652
|
+
MODEL_CONFIG,
|
|
653
|
+
clearModelCache,
|
|
654
|
+
compareBenchmarks,
|
|
655
|
+
cosineSimilarity,
|
|
656
|
+
createEmbedder,
|
|
657
|
+
dotProduct,
|
|
658
|
+
euclideanDistance,
|
|
659
|
+
euclideanDistanceSquared,
|
|
660
|
+
findKMostSimilar,
|
|
661
|
+
findMostSimilar,
|
|
662
|
+
formatBenchmarkResult,
|
|
663
|
+
getLoadedModels,
|
|
664
|
+
getModelConfig,
|
|
665
|
+
getSupportedModels,
|
|
666
|
+
isModelLoaded,
|
|
667
|
+
isValidModel,
|
|
668
|
+
loadModel,
|
|
669
|
+
magnitude,
|
|
670
|
+
manhattanDistance,
|
|
671
|
+
normalize,
|
|
672
|
+
preloadAllModels,
|
|
673
|
+
preloadModel,
|
|
674
|
+
runAllBenchmarks,
|
|
675
|
+
runBenchmark
|
|
676
|
+
};
|