libsql-search 0.1.6 → 0.2.1
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/dist/index.cjs +425 -66
- package/dist/index.d.ts +46 -4
- package/dist/index.esm.js +422 -67
- package/docs/API.md +103 -0
- package/docs/PROVIDERS.md +66 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4,7 +4,15 @@ var promises = require('fs/promises');
|
|
|
4
4
|
var path = require('path');
|
|
5
5
|
var matter = require('gray-matter');
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const DEFAULT_DIMENSIONS = 768;
|
|
8
|
+
const DEFAULT_MAX_LENGTH = 8e3;
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
|
+
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
+
const GEMINI_MODEL = "text-embedding-004";
|
|
12
|
+
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
|
+
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
|
+
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
15
|
+
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
8
16
|
function getEnvironmentVariable(name) {
|
|
9
17
|
const runtime = globalThis;
|
|
10
18
|
const nodeValue = runtime.process?.env?.[name];
|
|
@@ -17,80 +25,421 @@ function getEnvironmentVariable(name) {
|
|
|
17
25
|
return void 0;
|
|
18
26
|
}
|
|
19
27
|
}
|
|
20
|
-
async function getLocalEmbeddingModel() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
providerCache.local = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
|
|
25
|
-
console.log("Local model loaded successfully");
|
|
28
|
+
async function getLocalEmbeddingModel(modelName) {
|
|
29
|
+
const cached = localModelCacheByModel.get(modelName);
|
|
30
|
+
if (cached?.model) {
|
|
31
|
+
return cached.model;
|
|
26
32
|
}
|
|
27
|
-
|
|
33
|
+
console.log(`Loading local embedding model (${modelName})...`);
|
|
34
|
+
const { pipeline } = await import('@xenova/transformers');
|
|
35
|
+
const model = await pipeline("feature-extraction", modelName);
|
|
36
|
+
localModelCacheByModel.set(modelName, { model });
|
|
37
|
+
console.log("Local model loaded successfully");
|
|
38
|
+
return model;
|
|
28
39
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
function getPositiveInteger(value, optionName) {
|
|
41
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
42
|
+
throw new Error(`Invalid ${optionName}: expected a positive integer`);
|
|
43
|
+
}
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function getTimeoutMs(value) {
|
|
47
|
+
return getPositiveInteger(value ?? DEFAULT_TIMEOUT_MS, "timeoutMs");
|
|
48
|
+
}
|
|
49
|
+
function resolveProviderName(provider) {
|
|
50
|
+
switch (provider ?? "local") {
|
|
38
51
|
case "local":
|
|
39
|
-
return generateLocalEmbedding(truncated, dimensions);
|
|
40
52
|
case "gemini":
|
|
41
|
-
return generateGeminiEmbedding(truncated, apiKey);
|
|
42
53
|
case "openai":
|
|
43
|
-
return
|
|
54
|
+
return provider ?? "local";
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function resolveIntent(intent) {
|
|
60
|
+
switch (intent ?? "document") {
|
|
61
|
+
case "document":
|
|
62
|
+
case "query":
|
|
63
|
+
return intent ?? "document";
|
|
44
64
|
default:
|
|
45
|
-
throw new Error(`Unknown embedding
|
|
65
|
+
throw new Error(`Unknown embedding intent: ${String(intent)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function truncateTexts(texts, maxLength) {
|
|
69
|
+
return texts.map((text) => text.substring(0, maxLength));
|
|
70
|
+
}
|
|
71
|
+
function getOpenAIModel(dimensions) {
|
|
72
|
+
return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
|
|
73
|
+
}
|
|
74
|
+
function hasOwnProperty(value, property) {
|
|
75
|
+
return Object.prototype.hasOwnProperty.call(value, property);
|
|
76
|
+
}
|
|
77
|
+
function redactErrorText(value, exactSecrets = []) {
|
|
78
|
+
let raw = value instanceof Error ? value.message : String(value);
|
|
79
|
+
for (const secret of exactSecrets) {
|
|
80
|
+
if (secret.length > 0) {
|
|
81
|
+
raw = raw.split(secret).join("[redacted]");
|
|
82
|
+
}
|
|
46
83
|
}
|
|
84
|
+
return raw.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/([?&](?:api[_-]?key|key|token)=)[^&\s]+/gi, "$1[redacted]").replace(/(authorization\s*[:=]\s*)[^\s,}]+/gi, "$1[redacted]").replace(/https?:\/\/[^\s)]+/gi, (match) => {
|
|
85
|
+
try {
|
|
86
|
+
const url = new URL(match);
|
|
87
|
+
return `${url.origin}${url.pathname}`;
|
|
88
|
+
} catch {
|
|
89
|
+
return "[redacted-url]";
|
|
90
|
+
}
|
|
91
|
+
}).slice(0, 300);
|
|
47
92
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
93
|
+
function providerError(provider, message, cause, exactSecrets = []) {
|
|
94
|
+
const safeCause = cause === void 0 ? "" : `: ${redactErrorText(cause, exactSecrets).trim()}`;
|
|
95
|
+
return new Error(`${provider} embedding error: ${message}${safeCause}`);
|
|
96
|
+
}
|
|
97
|
+
function getResponseHeader(response, name) {
|
|
98
|
+
return response.headers.get(name) ?? void 0;
|
|
99
|
+
}
|
|
100
|
+
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
101
|
+
if (parentSignal?.aborted) {
|
|
102
|
+
throw providerError(provider, `${operation} was aborted`);
|
|
103
|
+
}
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
let abortError;
|
|
106
|
+
let rejectAbort = () => {
|
|
107
|
+
};
|
|
108
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
109
|
+
rejectAbort = reject;
|
|
53
110
|
});
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
const abort = (error) => {
|
|
112
|
+
if (abortError) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
abortError = error;
|
|
116
|
+
controller.abort();
|
|
117
|
+
rejectAbort(error);
|
|
118
|
+
};
|
|
119
|
+
const onParentAbort = () => abort(providerError(provider, `${operation} was aborted`));
|
|
120
|
+
const timeout = setTimeout(() => {
|
|
121
|
+
abort(providerError(provider, `${operation} timed out after ${timeoutMs}ms`));
|
|
122
|
+
}, timeoutMs);
|
|
123
|
+
parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
124
|
+
const operationPromise = Promise.resolve().then(() => run(controller.signal));
|
|
125
|
+
operationPromise.catch(() => void 0);
|
|
126
|
+
try {
|
|
127
|
+
return await Promise.race([operationPromise, abortPromise]);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error === abortError) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
throw providerError(provider, `${operation} failed`, error, exactSecrets);
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider) {
|
|
139
|
+
if (items.length !== expectedCount) {
|
|
140
|
+
throw providerError(
|
|
141
|
+
provider,
|
|
142
|
+
`expected ${expectedCount} embedding result(s), received ${items.length}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const hasIndexedItems = items.some((item) => !Array.isArray(item) && hasOwnProperty(item, "index"));
|
|
146
|
+
const ordered = hasIndexedItems ? reorderIndexedEmbeddings(items, expectedCount, provider) : items.map((item) => Array.isArray(item) ? item : item.embedding);
|
|
147
|
+
return ordered.map(
|
|
148
|
+
(embedding, itemIndex) => validateEmbeddingVector(embedding, expectedDimensions, provider, itemIndex)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
function reorderIndexedEmbeddings(items, expectedCount, provider) {
|
|
152
|
+
const ordered = new Array(expectedCount);
|
|
153
|
+
const seen = /* @__PURE__ */ new Set();
|
|
154
|
+
for (const item of items) {
|
|
155
|
+
if (Array.isArray(item) || !hasOwnProperty(item, "index")) {
|
|
156
|
+
throw providerError(provider, "provider returned a partially indexed embedding batch");
|
|
157
|
+
}
|
|
158
|
+
if (typeof item.index !== "number" || !Number.isInteger(item.index) || item.index < 0 || item.index >= expectedCount) {
|
|
159
|
+
throw providerError(provider, `provider returned invalid embedding index ${String(item.index)}`);
|
|
160
|
+
}
|
|
161
|
+
if (seen.has(item.index)) {
|
|
162
|
+
throw providerError(provider, `provider returned duplicate embedding index ${item.index}`);
|
|
163
|
+
}
|
|
164
|
+
seen.add(item.index);
|
|
165
|
+
ordered[item.index] = item.embedding;
|
|
166
|
+
}
|
|
167
|
+
if (seen.size !== expectedCount) {
|
|
168
|
+
throw providerError(provider, "provider returned non-contiguous embedding indices");
|
|
169
|
+
}
|
|
170
|
+
return ordered;
|
|
171
|
+
}
|
|
172
|
+
function validateEmbeddingVector(embedding, expectedDimensions, provider, itemIndex) {
|
|
173
|
+
if (!Array.isArray(embedding)) {
|
|
174
|
+
throw providerError(provider, `embedding ${itemIndex} is not an array`);
|
|
175
|
+
}
|
|
176
|
+
if (embedding.length !== expectedDimensions) {
|
|
177
|
+
throw providerError(
|
|
178
|
+
provider,
|
|
179
|
+
`embedding ${itemIndex} has ${embedding.length} dimensions, expected ${expectedDimensions}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
for (let i = 0; i < embedding.length; i++) {
|
|
183
|
+
const value = embedding[i];
|
|
184
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
185
|
+
throw providerError(provider, `embedding ${itemIndex} contains a non-finite value at dimension ${i}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return embedding;
|
|
189
|
+
}
|
|
190
|
+
function throwIfAborted(provider, operation, signal) {
|
|
191
|
+
if (signal.aborted) {
|
|
192
|
+
throw providerError(provider, `${operation} was aborted`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function embedSequentially(provider, operation, texts, signal, embedOne) {
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const text of texts) {
|
|
198
|
+
throwIfAborted(provider, operation, signal);
|
|
199
|
+
const embedding = await embedOne(text, signal);
|
|
200
|
+
throwIfAborted(provider, operation, signal);
|
|
201
|
+
results.push(embedding);
|
|
202
|
+
}
|
|
203
|
+
return results;
|
|
204
|
+
}
|
|
205
|
+
function createProviderMetadata(provider, dimensions) {
|
|
206
|
+
switch (provider) {
|
|
207
|
+
case "local":
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
name: "local",
|
|
210
|
+
model: LOCAL_MODEL,
|
|
211
|
+
dimensions,
|
|
212
|
+
batch: Object.freeze({ mode: "sequential" })
|
|
213
|
+
});
|
|
214
|
+
case "gemini":
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
name: "gemini",
|
|
217
|
+
model: GEMINI_MODEL,
|
|
218
|
+
dimensions: DEFAULT_DIMENSIONS,
|
|
219
|
+
batch: Object.freeze({ mode: "sequential" })
|
|
220
|
+
});
|
|
221
|
+
case "openai":
|
|
222
|
+
return Object.freeze({
|
|
223
|
+
name: "openai",
|
|
224
|
+
model: getOpenAIModel(dimensions),
|
|
225
|
+
dimensions,
|
|
226
|
+
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function getEffectiveDimensions(provider, dimensions) {
|
|
231
|
+
if (provider === "gemini") {
|
|
232
|
+
return DEFAULT_DIMENSIONS;
|
|
233
|
+
}
|
|
234
|
+
return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
|
|
235
|
+
}
|
|
236
|
+
function assertBatchSize(metadata, count) {
|
|
237
|
+
if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
|
|
238
|
+
throw providerError(
|
|
239
|
+
metadata.name,
|
|
240
|
+
`batch size ${count} exceeds maximum ${metadata.batch.maxSize}`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function createEmbeddingBatchResult(metadata, intent, embeddings) {
|
|
245
|
+
return Object.freeze({
|
|
246
|
+
embeddings,
|
|
247
|
+
provider: metadata.name,
|
|
248
|
+
model: metadata.model,
|
|
249
|
+
dimensions: metadata.dimensions,
|
|
250
|
+
intent
|
|
87
251
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
252
|
+
}
|
|
253
|
+
class LocalEmbeddingProvider {
|
|
254
|
+
metadata;
|
|
255
|
+
#timeoutMs;
|
|
256
|
+
constructor(metadata, timeoutMs) {
|
|
257
|
+
this.metadata = metadata;
|
|
258
|
+
this.#timeoutMs = timeoutMs;
|
|
259
|
+
}
|
|
260
|
+
async embed(texts, options = {}) {
|
|
261
|
+
const intent = resolveIntent(options.intent);
|
|
262
|
+
if (texts.length === 0) {
|
|
263
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
264
|
+
}
|
|
265
|
+
assertBatchSize(this.metadata, texts.length);
|
|
266
|
+
const vectors = await withTimeout(
|
|
267
|
+
"local",
|
|
268
|
+
"model inference",
|
|
269
|
+
this.#timeoutMs,
|
|
270
|
+
async (signal) => {
|
|
271
|
+
const model = await getLocalEmbeddingModel(this.metadata.model);
|
|
272
|
+
return embedSequentially("local", "model inference", texts, signal, async (text) => {
|
|
273
|
+
const output = await model(text, {
|
|
274
|
+
pooling: "mean",
|
|
275
|
+
normalize: true
|
|
276
|
+
});
|
|
277
|
+
const embedding = Array.from(output.data);
|
|
278
|
+
return padEmbedding(embedding, this.metadata.dimensions);
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
options.signal
|
|
282
|
+
);
|
|
283
|
+
return createEmbeddingBatchResult(
|
|
284
|
+
this.metadata,
|
|
285
|
+
intent,
|
|
286
|
+
validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "local")
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
class GeminiEmbeddingProvider {
|
|
291
|
+
metadata;
|
|
292
|
+
#apiKey;
|
|
293
|
+
#timeoutMs;
|
|
294
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
295
|
+
this.metadata = metadata;
|
|
296
|
+
this.#apiKey = apiKey;
|
|
297
|
+
this.#timeoutMs = timeoutMs;
|
|
298
|
+
}
|
|
299
|
+
async embed(texts, options = {}) {
|
|
300
|
+
const intent = resolveIntent(options.intent);
|
|
301
|
+
if (texts.length === 0) {
|
|
302
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
303
|
+
}
|
|
304
|
+
assertBatchSize(this.metadata, texts.length);
|
|
305
|
+
const vectors = await withTimeout(
|
|
306
|
+
"gemini",
|
|
307
|
+
"API request",
|
|
308
|
+
this.#timeoutMs,
|
|
309
|
+
async (signal) => {
|
|
310
|
+
const { GoogleGenerativeAI } = await import('@google/generative-ai');
|
|
311
|
+
const genAI = new GoogleGenerativeAI(this.#apiKey);
|
|
312
|
+
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
313
|
+
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
314
|
+
const result = await model.embedContent(text, { signal: itemSignal });
|
|
315
|
+
const values = result.embedding?.values;
|
|
316
|
+
if (!Array.isArray(values)) {
|
|
317
|
+
throw new Error("Gemini response did not include embedding values");
|
|
318
|
+
}
|
|
319
|
+
return values;
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
options.signal,
|
|
323
|
+
[this.#apiKey]
|
|
324
|
+
);
|
|
325
|
+
return createEmbeddingBatchResult(
|
|
326
|
+
this.metadata,
|
|
327
|
+
intent,
|
|
328
|
+
validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "gemini")
|
|
329
|
+
);
|
|
91
330
|
}
|
|
92
|
-
|
|
93
|
-
|
|
331
|
+
}
|
|
332
|
+
class OpenAIEmbeddingProvider {
|
|
333
|
+
metadata;
|
|
334
|
+
#apiKey;
|
|
335
|
+
#timeoutMs;
|
|
336
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
337
|
+
this.metadata = metadata;
|
|
338
|
+
this.#apiKey = apiKey;
|
|
339
|
+
this.#timeoutMs = timeoutMs;
|
|
340
|
+
}
|
|
341
|
+
async embed(texts, options = {}) {
|
|
342
|
+
const intent = resolveIntent(options.intent);
|
|
343
|
+
if (texts.length === 0) {
|
|
344
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
345
|
+
}
|
|
346
|
+
assertBatchSize(this.metadata, texts.length);
|
|
347
|
+
const items = await withTimeout(
|
|
348
|
+
"openai",
|
|
349
|
+
"API request",
|
|
350
|
+
this.#timeoutMs,
|
|
351
|
+
async (signal) => {
|
|
352
|
+
const response = await fetch(OPENAI_EMBEDDINGS_URL, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers: {
|
|
355
|
+
"Content-Type": "application/json",
|
|
356
|
+
"Authorization": `Bearer ${this.#apiKey}`
|
|
357
|
+
},
|
|
358
|
+
signal,
|
|
359
|
+
body: JSON.stringify({
|
|
360
|
+
input: texts,
|
|
361
|
+
model: this.metadata.model,
|
|
362
|
+
dimensions: this.metadata.dimensions
|
|
363
|
+
})
|
|
364
|
+
});
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
const requestId = getResponseHeader(response, "x-request-id");
|
|
367
|
+
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
368
|
+
throw new Error(context);
|
|
369
|
+
}
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
if (!Array.isArray(data.data)) {
|
|
372
|
+
throw new Error("OpenAI response did not include a data array");
|
|
373
|
+
}
|
|
374
|
+
return data.data.map((item) => {
|
|
375
|
+
const typed = item;
|
|
376
|
+
const embedding = typed.embedding;
|
|
377
|
+
if (!Array.isArray(embedding)) {
|
|
378
|
+
throw new Error("OpenAI response item did not include an embedding array");
|
|
379
|
+
}
|
|
380
|
+
const parsed = { embedding };
|
|
381
|
+
if (hasOwnProperty(typed, "index")) {
|
|
382
|
+
parsed.index = typed.index;
|
|
383
|
+
}
|
|
384
|
+
return parsed;
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
options.signal,
|
|
388
|
+
[this.#apiKey]
|
|
389
|
+
);
|
|
390
|
+
return createEmbeddingBatchResult(
|
|
391
|
+
this.metadata,
|
|
392
|
+
intent,
|
|
393
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function createEmbeddingProvider(options = {}) {
|
|
398
|
+
const provider = resolveProviderName(options.provider);
|
|
399
|
+
const metadata = getEmbeddingProviderMetadata(options);
|
|
400
|
+
const timeoutMs = getTimeoutMs(options.timeoutMs);
|
|
401
|
+
switch (provider) {
|
|
402
|
+
case "local":
|
|
403
|
+
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
404
|
+
case "gemini": {
|
|
405
|
+
const key = options.apiKey || getEnvironmentVariable("GEMINI_API_KEY");
|
|
406
|
+
if (!key) {
|
|
407
|
+
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
408
|
+
}
|
|
409
|
+
return new GeminiEmbeddingProvider(metadata, key, timeoutMs);
|
|
410
|
+
}
|
|
411
|
+
case "openai": {
|
|
412
|
+
const key = options.apiKey || getEnvironmentVariable("OPENAI_API_KEY");
|
|
413
|
+
if (!key) {
|
|
414
|
+
throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
|
|
415
|
+
}
|
|
416
|
+
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function getEmbeddingProviderMetadata(options = {}) {
|
|
421
|
+
const provider = resolveProviderName(options.provider);
|
|
422
|
+
return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions));
|
|
423
|
+
}
|
|
424
|
+
async function generateEmbeddings(texts, options = {}) {
|
|
425
|
+
const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
|
|
426
|
+
const intent = resolveIntent(options.intent);
|
|
427
|
+
if (texts.length === 0) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
430
|
+
const provider = createEmbeddingProvider(options);
|
|
431
|
+
const result = await provider.embed(truncateTexts(texts, maxLength), {
|
|
432
|
+
intent,
|
|
433
|
+
...options.signal ? { signal: options.signal } : {}
|
|
434
|
+
});
|
|
435
|
+
return result.embeddings;
|
|
436
|
+
}
|
|
437
|
+
async function generateEmbedding(text, options = {}) {
|
|
438
|
+
const [embedding] = await generateEmbeddings([text], options);
|
|
439
|
+
if (!embedding) {
|
|
440
|
+
throw new Error("Embedding provider returned no embedding");
|
|
441
|
+
}
|
|
442
|
+
return embedding;
|
|
94
443
|
}
|
|
95
444
|
function padEmbedding(embedding, targetDimensions) {
|
|
96
445
|
if (embedding.length === targetDimensions) {
|
|
@@ -211,7 +560,10 @@ async function processFile(file, embeddingOptions) {
|
|
|
211
560
|
content: markdown,
|
|
212
561
|
tags
|
|
213
562
|
});
|
|
214
|
-
const embedding = await generateEmbedding(embeddingText,
|
|
563
|
+
const embedding = await generateEmbedding(embeddingText, {
|
|
564
|
+
...embeddingOptions,
|
|
565
|
+
intent: embeddingOptions.intent ?? "document"
|
|
566
|
+
});
|
|
215
567
|
return {
|
|
216
568
|
slug,
|
|
217
569
|
title,
|
|
@@ -280,7 +632,10 @@ async function search(options) {
|
|
|
280
632
|
} = options;
|
|
281
633
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
282
634
|
const resultLimit = normalizeSearchLimit(limit);
|
|
283
|
-
const queryEmbedding = await generateEmbedding(query,
|
|
635
|
+
const queryEmbedding = await generateEmbedding(query, {
|
|
636
|
+
...embeddingOptions,
|
|
637
|
+
intent: embeddingOptions.intent ?? "query"
|
|
638
|
+
});
|
|
284
639
|
const results = await client.execute({
|
|
285
640
|
sql: `
|
|
286
641
|
SELECT
|
|
@@ -382,13 +737,17 @@ async function getFolders(client, tableName = "articles") {
|
|
|
382
737
|
return results.rows.map((row) => row.folder);
|
|
383
738
|
}
|
|
384
739
|
|
|
740
|
+
exports.createEmbeddingProvider = createEmbeddingProvider;
|
|
385
741
|
exports.createTable = createTable;
|
|
386
742
|
exports.generateEmbedding = generateEmbedding;
|
|
743
|
+
exports.generateEmbeddings = generateEmbeddings;
|
|
387
744
|
exports.getAllArticles = getAllArticles;
|
|
388
745
|
exports.getArticleBySlug = getArticleBySlug;
|
|
389
746
|
exports.getArticlesByFolder = getArticlesByFolder;
|
|
747
|
+
exports.getEmbeddingProviderMetadata = getEmbeddingProviderMetadata;
|
|
390
748
|
exports.getFolders = getFolders;
|
|
391
749
|
exports.indexContent = indexContent;
|
|
392
750
|
exports.padEmbedding = padEmbedding;
|
|
393
751
|
exports.prepareTextForEmbedding = prepareTextForEmbedding;
|
|
394
752
|
exports.search = search;
|
|
753
|
+
exports.validateEmbeddingBatch = validateEmbeddingBatch;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,56 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
2
|
|
|
3
3
|
type EmbeddingProvider = 'local' | 'gemini' | 'openai';
|
|
4
|
+
type EmbeddingIntent = 'document' | 'query';
|
|
5
|
+
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
|
+
interface EmbeddingBatchBehavior {
|
|
7
|
+
mode: EmbeddingBatchMode;
|
|
8
|
+
maxSize?: number;
|
|
9
|
+
}
|
|
10
|
+
interface EmbeddingProviderMetadata {
|
|
11
|
+
name: EmbeddingProvider;
|
|
12
|
+
model: string;
|
|
13
|
+
dimensions: number;
|
|
14
|
+
batch: EmbeddingBatchBehavior;
|
|
15
|
+
}
|
|
16
|
+
interface EmbeddingRequestOptions {
|
|
17
|
+
intent?: EmbeddingIntent;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
interface EmbeddingProviderClient {
|
|
21
|
+
readonly metadata: EmbeddingProviderMetadata;
|
|
22
|
+
embed(texts: string[], options?: EmbeddingRequestOptions): Promise<EmbeddingBatchResult>;
|
|
23
|
+
}
|
|
24
|
+
interface EmbeddingBatchResult {
|
|
25
|
+
embeddings: number[][];
|
|
26
|
+
provider: EmbeddingProvider;
|
|
27
|
+
model: string;
|
|
28
|
+
dimensions: number;
|
|
29
|
+
intent: EmbeddingIntent;
|
|
30
|
+
}
|
|
4
31
|
interface EmbeddingOptions {
|
|
5
32
|
provider?: EmbeddingProvider;
|
|
6
33
|
apiKey?: string;
|
|
7
34
|
dimensions?: number;
|
|
8
35
|
maxLength?: number;
|
|
36
|
+
intent?: EmbeddingIntent;
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
signal?: AbortSignal;
|
|
9
39
|
}
|
|
40
|
+
interface EmbeddingBatchItemResult {
|
|
41
|
+
embedding: number[];
|
|
42
|
+
index?: unknown;
|
|
43
|
+
}
|
|
44
|
+
type EmbeddingBatchItem = number[] | EmbeddingBatchItemResult;
|
|
45
|
+
declare function validateEmbeddingBatch(items: EmbeddingBatchItem[], expectedCount: number, expectedDimensions: number, provider: EmbeddingProvider): number[][];
|
|
46
|
+
declare function createEmbeddingProvider(options?: EmbeddingOptions): EmbeddingProviderClient;
|
|
47
|
+
declare function getEmbeddingProviderMetadata(options?: EmbeddingOptions): EmbeddingProviderMetadata;
|
|
48
|
+
/**
|
|
49
|
+
* Generate embeddings using the specified provider.
|
|
50
|
+
*/
|
|
51
|
+
declare function generateEmbeddings(texts: string[], options?: EmbeddingOptions): Promise<number[][]>;
|
|
10
52
|
/**
|
|
11
|
-
* Generate
|
|
53
|
+
* Generate an embedding using the specified provider.
|
|
12
54
|
*/
|
|
13
55
|
declare function generateEmbedding(text: string, options?: EmbeddingOptions): Promise<number[]>;
|
|
14
56
|
/**
|
|
@@ -23,7 +65,7 @@ declare function prepareTextForEmbedding(fields: {
|
|
|
23
65
|
description?: string;
|
|
24
66
|
content?: string;
|
|
25
67
|
tags?: string[];
|
|
26
|
-
[key: string]:
|
|
68
|
+
[key: string]: unknown;
|
|
27
69
|
}): string;
|
|
28
70
|
|
|
29
71
|
/**
|
|
@@ -126,5 +168,5 @@ declare function getArticlesByFolder(client: Client, folder: string, tableName?:
|
|
|
126
168
|
*/
|
|
127
169
|
declare function getFolders(client: Client, tableName?: string): Promise<string[]>;
|
|
128
170
|
|
|
129
|
-
export { createTable, generateEmbedding, getAllArticles, getArticleBySlug, getArticlesByFolder, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search };
|
|
130
|
-
export type { EmbeddingOptions, EmbeddingProvider, IndexedDocument, IndexerOptions, SearchOptions, SearchResult };
|
|
171
|
+
export { createEmbeddingProvider, createTable, generateEmbedding, generateEmbeddings, getAllArticles, getArticleBySlug, getArticlesByFolder, getEmbeddingProviderMetadata, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search, validateEmbeddingBatch };
|
|
172
|
+
export type { EmbeddingBatchBehavior, EmbeddingBatchItem, EmbeddingBatchItemResult, EmbeddingBatchMode, EmbeddingBatchResult, EmbeddingIntent, EmbeddingOptions, EmbeddingProvider, EmbeddingProviderClient, EmbeddingProviderMetadata, EmbeddingRequestOptions, IndexedDocument, IndexerOptions, SearchOptions, SearchResult };
|