libsql-search 0.4.0 → 0.6.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/README.md +7 -8
- package/dist/index.cjs +116 -28
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +116 -28
- package/docs/API.md +12 -2
- package/docs/INDEXING.md +14 -2
- package/docs/INTEGRATIONS.md +10 -6
- package/docs/PROVIDERS.md +46 -16
- package/docs/TROUBLESHOOTING-SHARP.md +7 -6
- package/docs/TROUBLESHOOTING.md +5 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ Use it when you want:
|
|
|
20
20
|
|
|
21
21
|
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
|
-
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`,
|
|
24
|
-
`@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
|
-
`
|
|
23
|
+
- Embedding providers: local Hugging Face `Xenova/all-MiniLM-L6-v2`,
|
|
24
|
+
Cloudflare Workers AI `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
|
+
`gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
|
|
26
26
|
`text-embedding-3-large`
|
|
27
27
|
- npm distribution plus JSR publishing
|
|
28
28
|
|
|
@@ -65,14 +65,13 @@ const client = createClient({
|
|
|
65
65
|
authToken: "your-auth-token",
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
await createTable(client
|
|
68
|
+
await createTable(client);
|
|
69
69
|
|
|
70
70
|
await indexContent({
|
|
71
71
|
client,
|
|
72
72
|
contentPath: "./content",
|
|
73
73
|
embeddingOptions: {
|
|
74
74
|
provider: "local",
|
|
75
|
-
dimensions: 768,
|
|
76
75
|
},
|
|
77
76
|
});
|
|
78
77
|
|
|
@@ -82,7 +81,6 @@ const results = await search({
|
|
|
82
81
|
limit: 5,
|
|
83
82
|
embeddingOptions: {
|
|
84
83
|
provider: "local",
|
|
85
|
-
dimensions: 768,
|
|
86
84
|
},
|
|
87
85
|
});
|
|
88
86
|
|
|
@@ -98,8 +96,9 @@ Important behavior:
|
|
|
98
96
|
- Call `createTable()` before indexing or searching.
|
|
99
97
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
100
98
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
101
|
-
- `local` is the default offline provider
|
|
102
|
-
option. Cloudflare and Mistral use 1024 dimensions.
|
|
99
|
+
- `local` is the default offline provider and uses 384 dimensions. Cloudflare is
|
|
100
|
+
the recommended hosted option. Cloudflare and Mistral use 1024 dimensions.
|
|
101
|
+
Gemini defaults to 3072 dimensions and supports 128-3072.
|
|
103
102
|
|
|
104
103
|
## Core API
|
|
105
104
|
|
package/dist/index.cjs
CHANGED
|
@@ -4,11 +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 OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
8
|
+
const LOCAL_DIMENSIONS = 384;
|
|
8
9
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
11
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
-
const GEMINI_MODEL = "
|
|
12
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
13
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
14
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
15
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
12
16
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
17
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
18
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
@@ -30,17 +34,66 @@ function getEnvironmentVariable(name) {
|
|
|
30
34
|
return void 0;
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
|
-
|
|
37
|
+
function deletePendingLocalModelCache(modelName, entry) {
|
|
38
|
+
if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
|
|
39
|
+
localModelCacheByModel.delete(modelName);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function getLocalEmbeddingModel(modelName, signal) {
|
|
34
43
|
const cached = localModelCacheByModel.get(modelName);
|
|
35
|
-
if (cached
|
|
36
|
-
|
|
44
|
+
if (cached) {
|
|
45
|
+
cached.waiters++;
|
|
46
|
+
try {
|
|
47
|
+
return await waitForLocalEmbeddingModel(modelName, cached, signal);
|
|
48
|
+
} finally {
|
|
49
|
+
cached.waiters--;
|
|
50
|
+
deletePendingLocalModelCache(modelName, cached);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const modelPromise = (async () => {
|
|
54
|
+
console.log(`Loading local embedding model (${modelName})...`);
|
|
55
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
56
|
+
const model = await pipeline("feature-extraction", modelName);
|
|
57
|
+
console.log("Local model loaded successfully");
|
|
58
|
+
return model;
|
|
59
|
+
})();
|
|
60
|
+
const entry = {
|
|
61
|
+
promise: modelPromise,
|
|
62
|
+
settled: false,
|
|
63
|
+
waiters: 1
|
|
64
|
+
};
|
|
65
|
+
localModelCacheByModel.set(modelName, entry);
|
|
66
|
+
modelPromise.then(() => {
|
|
67
|
+
entry.settled = true;
|
|
68
|
+
}).catch(() => {
|
|
69
|
+
localModelCacheByModel.delete(modelName);
|
|
70
|
+
});
|
|
71
|
+
try {
|
|
72
|
+
return await waitForLocalEmbeddingModel(modelName, entry, signal);
|
|
73
|
+
} finally {
|
|
74
|
+
entry.waiters--;
|
|
75
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function waitForLocalEmbeddingModel(modelName, entry, signal) {
|
|
79
|
+
if (signal.aborted) {
|
|
80
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
81
|
+
throw providerError("local", "model inference was aborted");
|
|
82
|
+
}
|
|
83
|
+
let rejectAbort = () => {
|
|
84
|
+
};
|
|
85
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
86
|
+
rejectAbort = reject;
|
|
87
|
+
});
|
|
88
|
+
const onAbort = () => {
|
|
89
|
+
rejectAbort(providerError("local", "model inference was aborted"));
|
|
90
|
+
};
|
|
91
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
92
|
+
try {
|
|
93
|
+
return await Promise.race([entry.promise, abortPromise]);
|
|
94
|
+
} finally {
|
|
95
|
+
signal.removeEventListener("abort", onAbort);
|
|
37
96
|
}
|
|
38
|
-
console.log(`Loading local embedding model (${modelName})...`);
|
|
39
|
-
const { pipeline } = await import('@xenova/transformers');
|
|
40
|
-
const model = await pipeline("feature-extraction", modelName);
|
|
41
|
-
localModelCacheByModel.set(modelName, { model });
|
|
42
|
-
console.log("Local model loaded successfully");
|
|
43
|
-
return model;
|
|
44
97
|
}
|
|
45
98
|
function getPositiveInteger(value, optionName) {
|
|
46
99
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -121,6 +174,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
121
174
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
122
175
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
123
176
|
}
|
|
177
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
178
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
179
|
+
}
|
|
180
|
+
function parseGeminiEmbeddingResult(result) {
|
|
181
|
+
if (!Array.isArray(result.embeddings)) {
|
|
182
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
183
|
+
}
|
|
184
|
+
if (result.embeddings.length !== 1) {
|
|
185
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
186
|
+
}
|
|
187
|
+
const values = result.embeddings[0]?.values;
|
|
188
|
+
if (!Array.isArray(values)) {
|
|
189
|
+
throw new Error("Gemini response did not include embedding values");
|
|
190
|
+
}
|
|
191
|
+
return values;
|
|
192
|
+
}
|
|
124
193
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
125
194
|
if (parentSignal?.aborted) {
|
|
126
195
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -239,7 +308,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
239
308
|
return Object.freeze({
|
|
240
309
|
name: "gemini",
|
|
241
310
|
model: GEMINI_MODEL,
|
|
242
|
-
dimensions
|
|
311
|
+
dimensions,
|
|
243
312
|
batch: Object.freeze({ mode: "sequential" })
|
|
244
313
|
});
|
|
245
314
|
case "openai":
|
|
@@ -266,8 +335,24 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
266
335
|
}
|
|
267
336
|
}
|
|
268
337
|
function getEffectiveDimensions(provider, dimensions) {
|
|
338
|
+
if (provider === "local") {
|
|
339
|
+
if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
|
|
340
|
+
throw providerError(
|
|
341
|
+
"local",
|
|
342
|
+
`${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return LOCAL_DIMENSIONS;
|
|
346
|
+
}
|
|
269
347
|
if (provider === "gemini") {
|
|
270
|
-
|
|
348
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
349
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
350
|
+
throw providerError(
|
|
351
|
+
"gemini",
|
|
352
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return effectiveDimensions;
|
|
271
356
|
}
|
|
272
357
|
if (provider === "mistral") {
|
|
273
358
|
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
@@ -287,7 +372,7 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
287
372
|
}
|
|
288
373
|
return CLOUDFLARE_DIMENSIONS;
|
|
289
374
|
}
|
|
290
|
-
return getPositiveInteger(dimensions ??
|
|
375
|
+
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
291
376
|
}
|
|
292
377
|
function assertBatchSize(metadata, count) {
|
|
293
378
|
if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
|
|
@@ -324,14 +409,13 @@ class LocalEmbeddingProvider {
|
|
|
324
409
|
"model inference",
|
|
325
410
|
this.#timeoutMs,
|
|
326
411
|
async (signal) => {
|
|
327
|
-
const model = await getLocalEmbeddingModel(this.metadata.model);
|
|
412
|
+
const model = await getLocalEmbeddingModel(this.metadata.model, signal);
|
|
328
413
|
return embedSequentially("local", "model inference", texts, signal, async (text) => {
|
|
329
414
|
const output = await model(text, {
|
|
330
415
|
pooling: "mean",
|
|
331
416
|
normalize: true
|
|
332
417
|
});
|
|
333
|
-
|
|
334
|
-
return padEmbedding(embedding, this.metadata.dimensions);
|
|
418
|
+
return Array.from(output.data);
|
|
335
419
|
});
|
|
336
420
|
},
|
|
337
421
|
options.signal
|
|
@@ -363,16 +447,18 @@ class GeminiEmbeddingProvider {
|
|
|
363
447
|
"API request",
|
|
364
448
|
this.#timeoutMs,
|
|
365
449
|
async (signal) => {
|
|
366
|
-
const {
|
|
367
|
-
const
|
|
368
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
450
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
451
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
369
452
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
370
|
-
const result = await
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
453
|
+
const result = await client.models.embedContent({
|
|
454
|
+
model: this.metadata.model,
|
|
455
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
456
|
+
config: {
|
|
457
|
+
outputDimensionality: this.metadata.dimensions,
|
|
458
|
+
abortSignal: itemSignal
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
return parseGeminiEmbeddingResult(result);
|
|
376
462
|
});
|
|
377
463
|
},
|
|
378
464
|
options.signal,
|
|
@@ -583,7 +669,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
583
669
|
case "local":
|
|
584
670
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
585
671
|
case "gemini": {
|
|
586
|
-
const key =
|
|
672
|
+
const key = getOptionalTrimmedCredential(
|
|
673
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
674
|
+
);
|
|
587
675
|
if (!key) {
|
|
588
676
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
589
677
|
}
|
|
@@ -794,7 +882,7 @@ async function insertDocument(client, document, quotedTableName) {
|
|
|
794
882
|
]
|
|
795
883
|
});
|
|
796
884
|
}
|
|
797
|
-
async function createTable(client, tableName = "articles", dimensions =
|
|
885
|
+
async function createTable(client, tableName = "articles", dimensions = 384) {
|
|
798
886
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
799
887
|
const vectorDimensions = normalizeVectorDimensions(dimensions);
|
|
800
888
|
const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Multi-provider embedding generation
|
|
5
|
+
* Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral, and Cloudflare Workers AI
|
|
6
|
+
*/
|
|
3
7
|
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
|
|
4
8
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
9
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
package/dist/index.esm.js
CHANGED
|
@@ -2,11 +2,15 @@ import { readdir, readFile } from 'fs/promises';
|
|
|
2
2
|
import { join, extname, relative, dirname } from 'path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const OPENAI_DEFAULT_DIMENSIONS = 768;
|
|
6
|
+
const LOCAL_DIMENSIONS = 384;
|
|
6
7
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
7
8
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
8
9
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
9
|
-
const GEMINI_MODEL = "
|
|
10
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
11
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
12
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
13
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
10
14
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
11
15
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
12
16
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
@@ -28,17 +32,66 @@ function getEnvironmentVariable(name) {
|
|
|
28
32
|
return void 0;
|
|
29
33
|
}
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
function deletePendingLocalModelCache(modelName, entry) {
|
|
36
|
+
if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
|
|
37
|
+
localModelCacheByModel.delete(modelName);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function getLocalEmbeddingModel(modelName, signal) {
|
|
32
41
|
const cached = localModelCacheByModel.get(modelName);
|
|
33
|
-
if (cached
|
|
34
|
-
|
|
42
|
+
if (cached) {
|
|
43
|
+
cached.waiters++;
|
|
44
|
+
try {
|
|
45
|
+
return await waitForLocalEmbeddingModel(modelName, cached, signal);
|
|
46
|
+
} finally {
|
|
47
|
+
cached.waiters--;
|
|
48
|
+
deletePendingLocalModelCache(modelName, cached);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const modelPromise = (async () => {
|
|
52
|
+
console.log(`Loading local embedding model (${modelName})...`);
|
|
53
|
+
const { pipeline } = await import('@huggingface/transformers');
|
|
54
|
+
const model = await pipeline("feature-extraction", modelName);
|
|
55
|
+
console.log("Local model loaded successfully");
|
|
56
|
+
return model;
|
|
57
|
+
})();
|
|
58
|
+
const entry = {
|
|
59
|
+
promise: modelPromise,
|
|
60
|
+
settled: false,
|
|
61
|
+
waiters: 1
|
|
62
|
+
};
|
|
63
|
+
localModelCacheByModel.set(modelName, entry);
|
|
64
|
+
modelPromise.then(() => {
|
|
65
|
+
entry.settled = true;
|
|
66
|
+
}).catch(() => {
|
|
67
|
+
localModelCacheByModel.delete(modelName);
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
return await waitForLocalEmbeddingModel(modelName, entry, signal);
|
|
71
|
+
} finally {
|
|
72
|
+
entry.waiters--;
|
|
73
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function waitForLocalEmbeddingModel(modelName, entry, signal) {
|
|
77
|
+
if (signal.aborted) {
|
|
78
|
+
deletePendingLocalModelCache(modelName, entry);
|
|
79
|
+
throw providerError("local", "model inference was aborted");
|
|
80
|
+
}
|
|
81
|
+
let rejectAbort = () => {
|
|
82
|
+
};
|
|
83
|
+
const abortPromise = new Promise((_resolve, reject) => {
|
|
84
|
+
rejectAbort = reject;
|
|
85
|
+
});
|
|
86
|
+
const onAbort = () => {
|
|
87
|
+
rejectAbort(providerError("local", "model inference was aborted"));
|
|
88
|
+
};
|
|
89
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
90
|
+
try {
|
|
91
|
+
return await Promise.race([entry.promise, abortPromise]);
|
|
92
|
+
} finally {
|
|
93
|
+
signal.removeEventListener("abort", onAbort);
|
|
35
94
|
}
|
|
36
|
-
console.log(`Loading local embedding model (${modelName})...`);
|
|
37
|
-
const { pipeline } = await import('@xenova/transformers');
|
|
38
|
-
const model = await pipeline("feature-extraction", modelName);
|
|
39
|
-
localModelCacheByModel.set(modelName, { model });
|
|
40
|
-
console.log("Local model loaded successfully");
|
|
41
|
-
return model;
|
|
42
95
|
}
|
|
43
96
|
function getPositiveInteger(value, optionName) {
|
|
44
97
|
if (!Number.isInteger(value) || value <= 0) {
|
|
@@ -119,6 +172,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
119
172
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
120
173
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
121
174
|
}
|
|
175
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
176
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
177
|
+
}
|
|
178
|
+
function parseGeminiEmbeddingResult(result) {
|
|
179
|
+
if (!Array.isArray(result.embeddings)) {
|
|
180
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
181
|
+
}
|
|
182
|
+
if (result.embeddings.length !== 1) {
|
|
183
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
184
|
+
}
|
|
185
|
+
const values = result.embeddings[0]?.values;
|
|
186
|
+
if (!Array.isArray(values)) {
|
|
187
|
+
throw new Error("Gemini response did not include embedding values");
|
|
188
|
+
}
|
|
189
|
+
return values;
|
|
190
|
+
}
|
|
122
191
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
123
192
|
if (parentSignal?.aborted) {
|
|
124
193
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -237,7 +306,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
237
306
|
return Object.freeze({
|
|
238
307
|
name: "gemini",
|
|
239
308
|
model: GEMINI_MODEL,
|
|
240
|
-
dimensions
|
|
309
|
+
dimensions,
|
|
241
310
|
batch: Object.freeze({ mode: "sequential" })
|
|
242
311
|
});
|
|
243
312
|
case "openai":
|
|
@@ -264,8 +333,24 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
264
333
|
}
|
|
265
334
|
}
|
|
266
335
|
function getEffectiveDimensions(provider, dimensions) {
|
|
336
|
+
if (provider === "local") {
|
|
337
|
+
if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
|
|
338
|
+
throw providerError(
|
|
339
|
+
"local",
|
|
340
|
+
`${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return LOCAL_DIMENSIONS;
|
|
344
|
+
}
|
|
267
345
|
if (provider === "gemini") {
|
|
268
|
-
|
|
346
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
347
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
348
|
+
throw providerError(
|
|
349
|
+
"gemini",
|
|
350
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
return effectiveDimensions;
|
|
269
354
|
}
|
|
270
355
|
if (provider === "mistral") {
|
|
271
356
|
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
@@ -285,7 +370,7 @@ function getEffectiveDimensions(provider, dimensions) {
|
|
|
285
370
|
}
|
|
286
371
|
return CLOUDFLARE_DIMENSIONS;
|
|
287
372
|
}
|
|
288
|
-
return getPositiveInteger(dimensions ??
|
|
373
|
+
return getPositiveInteger(dimensions ?? OPENAI_DEFAULT_DIMENSIONS, "dimensions");
|
|
289
374
|
}
|
|
290
375
|
function assertBatchSize(metadata, count) {
|
|
291
376
|
if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
|
|
@@ -322,14 +407,13 @@ class LocalEmbeddingProvider {
|
|
|
322
407
|
"model inference",
|
|
323
408
|
this.#timeoutMs,
|
|
324
409
|
async (signal) => {
|
|
325
|
-
const model = await getLocalEmbeddingModel(this.metadata.model);
|
|
410
|
+
const model = await getLocalEmbeddingModel(this.metadata.model, signal);
|
|
326
411
|
return embedSequentially("local", "model inference", texts, signal, async (text) => {
|
|
327
412
|
const output = await model(text, {
|
|
328
413
|
pooling: "mean",
|
|
329
414
|
normalize: true
|
|
330
415
|
});
|
|
331
|
-
|
|
332
|
-
return padEmbedding(embedding, this.metadata.dimensions);
|
|
416
|
+
return Array.from(output.data);
|
|
333
417
|
});
|
|
334
418
|
},
|
|
335
419
|
options.signal
|
|
@@ -361,16 +445,18 @@ class GeminiEmbeddingProvider {
|
|
|
361
445
|
"API request",
|
|
362
446
|
this.#timeoutMs,
|
|
363
447
|
async (signal) => {
|
|
364
|
-
const {
|
|
365
|
-
const
|
|
366
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
448
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
449
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
367
450
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
451
|
+
const result = await client.models.embedContent({
|
|
452
|
+
model: this.metadata.model,
|
|
453
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
454
|
+
config: {
|
|
455
|
+
outputDimensionality: this.metadata.dimensions,
|
|
456
|
+
abortSignal: itemSignal
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
return parseGeminiEmbeddingResult(result);
|
|
374
460
|
});
|
|
375
461
|
},
|
|
376
462
|
options.signal,
|
|
@@ -581,7 +667,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
581
667
|
case "local":
|
|
582
668
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
583
669
|
case "gemini": {
|
|
584
|
-
const key =
|
|
670
|
+
const key = getOptionalTrimmedCredential(
|
|
671
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
672
|
+
);
|
|
585
673
|
if (!key) {
|
|
586
674
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
587
675
|
}
|
|
@@ -792,7 +880,7 @@ async function insertDocument(client, document, quotedTableName) {
|
|
|
792
880
|
]
|
|
793
881
|
});
|
|
794
882
|
}
|
|
795
|
-
async function createTable(client, tableName = "articles", dimensions =
|
|
883
|
+
async function createTable(client, tableName = "articles", dimensions = 384) {
|
|
796
884
|
const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
|
|
797
885
|
const vectorDimensions = normalizeVectorDimensions(dimensions);
|
|
798
886
|
const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
|
package/docs/API.md
CHANGED
|
@@ -42,13 +42,13 @@ It also exports these types:
|
|
|
42
42
|
Creates the table and supporting indexes used by search.
|
|
43
43
|
|
|
44
44
|
```ts
|
|
45
|
-
await createTable(client
|
|
45
|
+
await createTable(client);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Defaults:
|
|
49
49
|
|
|
50
50
|
- `tableName`: `"articles"`
|
|
51
|
-
- `dimensions`: `
|
|
51
|
+
- `dimensions`: `384`
|
|
52
52
|
|
|
53
53
|
`tableName` must be an ASCII SQLite identifier matching
|
|
54
54
|
`[A-Za-z_][A-Za-z0-9_]*`. Valid identifiers are quoted internally, so reserved
|
|
@@ -213,6 +213,13 @@ Hosted provider clients are scoped to their options. The library does not reuse
|
|
|
213
213
|
a Cloudflare, Mistral, Gemini, or OpenAI client created with different
|
|
214
214
|
credentials or configuration.
|
|
215
215
|
|
|
216
|
+
Gemini uses `gemini-embedding-2`. Its default is 3072 dimensions, and explicit
|
|
217
|
+
Gemini dimensions must be an integer from 128 through 3072.
|
|
218
|
+
|
|
219
|
+
Local embeddings use `Xenova/all-MiniLM-L6-v2` through
|
|
220
|
+
`@huggingface/transformers` and are fixed at the model's native 384 dimensions.
|
|
221
|
+
Passing any other local dimension is rejected before the runtime is loaded.
|
|
222
|
+
|
|
216
223
|
### `getEmbeddingProviderMetadata(options?)`
|
|
217
224
|
|
|
218
225
|
Returns the same metadata exposed by `createEmbeddingProvider(options).metadata`
|
|
@@ -276,6 +283,9 @@ Cloudflare uses `accountId` and `apiToken`, which fall back to
|
|
|
276
283
|
|
|
277
284
|
Pads or truncates an embedding array to the requested length.
|
|
278
285
|
|
|
286
|
+
This helper remains exported for callers that used it directly. The local
|
|
287
|
+
provider does not use it; local vectors are validated at native 384 dimensions.
|
|
288
|
+
|
|
279
289
|
### `prepareTextForEmbedding(fields)`
|
|
280
290
|
|
|
281
291
|
Combines title, description, tags, and content into the text sent to the
|
package/docs/INDEXING.md
CHANGED
|
@@ -25,7 +25,6 @@ await indexContent({
|
|
|
25
25
|
tableName: "articles",
|
|
26
26
|
embeddingOptions: {
|
|
27
27
|
provider: "local",
|
|
28
|
-
dimensions: 768,
|
|
29
28
|
},
|
|
30
29
|
});
|
|
31
30
|
```
|
|
@@ -33,6 +32,19 @@ await indexContent({
|
|
|
33
32
|
That keeps the implementation simple, but it also means a failed rebuild can
|
|
34
33
|
leave the index partially repopulated.
|
|
35
34
|
|
|
35
|
+
Changing an embedding provider or dimension count requires a full re-embed.
|
|
36
|
+
For existing local indexes created with the older padded-local behavior, create
|
|
37
|
+
or recreate a 384-dimensional table before rebuilding. Those older local
|
|
38
|
+
768-dimensional tables stored 384 model values followed by zero padding;
|
|
39
|
+
`indexContent()` clears rows but does not change the table's `F32_BLOB` width.
|
|
40
|
+
|
|
41
|
+
For Gemini specifically, indexes created with the retired `text-embedding-004`
|
|
42
|
+
model must be rebuilt for `gemini-embedding-2` even when staying at 768
|
|
43
|
+
dimensions, because the model and query/document formatting both changed. If
|
|
44
|
+
you adopt Gemini's 3072-dimensional default, recreate the vector table or build
|
|
45
|
+
into a separate table first; clearing rows with `indexContent()` does not change
|
|
46
|
+
the table's `F32_BLOB` width.
|
|
47
|
+
|
|
36
48
|
## Quality Guidelines
|
|
37
49
|
|
|
38
50
|
- include descriptive frontmatter titles
|
|
@@ -64,7 +76,7 @@ database calls or embedding generation.
|
|
|
64
76
|
|
|
65
77
|
## Runtime Notes
|
|
66
78
|
|
|
67
|
-
- local embeddings may download a model on the first run
|
|
79
|
+
- local embeddings may download and cache a model on the first run
|
|
68
80
|
- Node users need `@libsql/client` installed alongside the package
|
|
69
81
|
- the repository validates both the npm package build and `deno check`, but the
|
|
70
82
|
indexing flow itself still depends on filesystem access
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -26,7 +26,6 @@ export const POST: APIRoute = async ({ request }) => {
|
|
|
26
26
|
limit,
|
|
27
27
|
embeddingOptions: {
|
|
28
28
|
provider: "local",
|
|
29
|
-
dimensions: 768,
|
|
30
29
|
},
|
|
31
30
|
});
|
|
32
31
|
|
|
@@ -79,7 +78,6 @@ export async function POST(request: NextRequest) {
|
|
|
79
78
|
limit,
|
|
80
79
|
embeddingOptions: {
|
|
81
80
|
provider: "local",
|
|
82
|
-
dimensions: 768,
|
|
83
81
|
},
|
|
84
82
|
});
|
|
85
83
|
|
|
@@ -135,10 +133,16 @@ const embeddingProvider =
|
|
|
135
133
|
| "gemini"
|
|
136
134
|
| "openai"
|
|
137
135
|
| undefined;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
|
|
137
|
+
const dimensionsByProvider = {
|
|
138
|
+
local: 384,
|
|
139
|
+
cloudflare: 1024,
|
|
140
|
+
mistral: 1024,
|
|
141
|
+
gemini: 3072,
|
|
142
|
+
openai: 1536,
|
|
143
|
+
} as const;
|
|
144
|
+
|
|
145
|
+
const embeddingDimensions = dimensionsByProvider[embeddingProvider ?? "local"];
|
|
142
146
|
|
|
143
147
|
await createTable(client, "articles", embeddingDimensions);
|
|
144
148
|
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -29,7 +29,9 @@ interface EmbeddingOptions {
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- `provider` defaults to `"local"`
|
|
32
|
-
- `dimensions` defaults to `
|
|
32
|
+
- `dimensions` defaults to `384` for the library's default local provider.
|
|
33
|
+
Provider-specific defaults can differ; Cloudflare and Mistral use `1024`,
|
|
34
|
+
and Gemini defaults to `3072`.
|
|
33
35
|
- `maxLength` defaults to `8000`
|
|
34
36
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
35
37
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
@@ -86,7 +88,8 @@ vectors plus provider, model, dimensions, and intent. The compatibility helpers
|
|
|
86
88
|
|
|
87
89
|
Cloudflare, Mistral, Gemini, and OpenAI clients are scoped to their current
|
|
88
90
|
options. They are not cached globally across different credentials or
|
|
89
|
-
configurations. The local
|
|
91
|
+
configurations. The local Hugging Face Transformers pipeline is loaded lazily
|
|
92
|
+
and cached by model name.
|
|
90
93
|
|
|
91
94
|
Hosted provider failures are reported with bounded provider/status/request-id
|
|
92
95
|
context and without raw upstream bodies, credentials, Authorization headers, or
|
|
@@ -97,24 +100,26 @@ full URLs with query strings.
|
|
|
97
100
|
Provider value: `local`
|
|
98
101
|
|
|
99
102
|
The local provider loads `Xenova/all-MiniLM-L6-v2` through
|
|
100
|
-
`@
|
|
103
|
+
`@huggingface/transformers`.
|
|
101
104
|
|
|
102
105
|
```ts
|
|
103
106
|
embeddingOptions: {
|
|
104
107
|
provider: "local",
|
|
105
|
-
dimensions: 768,
|
|
106
108
|
}
|
|
107
109
|
```
|
|
108
110
|
|
|
109
111
|
Notes:
|
|
110
112
|
|
|
111
|
-
- the model emits 384 dimensions and
|
|
112
|
-
|
|
113
|
-
-
|
|
113
|
+
- the model emits 384 dimensions, and local vectors are validated at exactly
|
|
114
|
+
384 finite numbers
|
|
115
|
+
- `dimensions: 384` is accepted explicitly; any other local dimension is
|
|
116
|
+
rejected before the runtime is imported or loaded
|
|
117
|
+
- metadata reports 384 dimensions
|
|
114
118
|
- batch metadata is `{ mode: "sequential" }`
|
|
115
|
-
- the first run downloads the model and can take longer on a fresh
|
|
119
|
+
- the first run downloads and caches the model and can take longer on a fresh
|
|
120
|
+
machine
|
|
116
121
|
- no API key is required
|
|
117
|
-
- this remains the default provider for
|
|
122
|
+
- this remains the default provider for offline use
|
|
118
123
|
|
|
119
124
|
## Cloudflare Workers AI
|
|
120
125
|
|
|
@@ -176,22 +181,29 @@ Behavior:
|
|
|
176
181
|
|
|
177
182
|
Provider value: `gemini`
|
|
178
183
|
|
|
179
|
-
Gemini uses Google `
|
|
184
|
+
Gemini uses Google `gemini-embedding-2` through `@google/genai`.
|
|
180
185
|
|
|
181
186
|
```ts
|
|
182
187
|
embeddingOptions: {
|
|
183
188
|
provider: "gemini",
|
|
184
189
|
apiKey: process.env.GEMINI_API_KEY,
|
|
190
|
+
dimensions: 3072,
|
|
185
191
|
}
|
|
186
192
|
```
|
|
187
193
|
|
|
188
194
|
Behavior:
|
|
189
195
|
|
|
190
196
|
- if `apiKey` is omitted, the library reads `GEMINI_API_KEY`
|
|
191
|
-
- Gemini
|
|
192
|
-
-
|
|
197
|
+
- blank Gemini credentials are treated as missing
|
|
198
|
+
- Gemini defaults to 3072 dimensions
|
|
199
|
+
- explicit Gemini dimensions must be integers from 128 through 3072
|
|
200
|
+
- 768, 1536, and 3072 are recommended practical sizes
|
|
201
|
+
- metadata reports `gemini-embedding-2` and the effective dimensions
|
|
193
202
|
- batch metadata is `{ mode: "sequential" }`
|
|
194
|
-
- the
|
|
203
|
+
- the library sends one SDK request per input and verifies one vector per input
|
|
204
|
+
- document inputs are formatted as `title: none | text: ...`
|
|
205
|
+
- query inputs are formatted as `task: search result | query: ...`
|
|
206
|
+
- the current implementation does not expose custom model selection
|
|
195
207
|
|
|
196
208
|
## OpenAI
|
|
197
209
|
|
|
@@ -219,13 +231,31 @@ Behavior:
|
|
|
219
231
|
|
|
220
232
|
## Dimension Guidelines
|
|
221
233
|
|
|
222
|
-
- `local`
|
|
234
|
+
- `local` is fixed at `384`
|
|
223
235
|
- `cloudflare` is fixed at `1024`
|
|
224
236
|
- `mistral` is fixed at `1024`
|
|
225
|
-
-
|
|
226
|
-
|
|
237
|
+
- Gemini defaults to `3072` and accepts explicit dimensions from `128` through
|
|
238
|
+
`3072`; use `768`, `1536`, or `3072` unless you have a specific reason
|
|
227
239
|
- OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
|
|
228
240
|
value you explicitly set
|
|
229
241
|
|
|
230
242
|
If you switch provider or dimensions for an existing table, recreate the table
|
|
231
243
|
or rebuild the index into a separate table so stored vectors stay consistent.
|
|
244
|
+
|
|
245
|
+
Existing Gemini indexes created with `text-embedding-004` must be fully
|
|
246
|
+
re-embedded for `gemini-embedding-2`, even if you keep `dimensions: 768`,
|
|
247
|
+
because both the model and query/document input formatting changed. If you move
|
|
248
|
+
to the new 3072-dimensional default, create a new table or recreate the vector
|
|
249
|
+
table first; `indexContent()` clears rows but does not change the `F32_BLOB`
|
|
250
|
+
width. A separate table is safer because rebuilds are not transactional.
|
|
251
|
+
|
|
252
|
+
Existing local indexes created with the older padded-local behavior usually have
|
|
253
|
+
`F32_BLOB(768)` rows containing the 384 model values followed by zero padding.
|
|
254
|
+
The current local contract stores the native 384-dimensional model output. To
|
|
255
|
+
migrate, create or recreate a `F32_BLOB(384)` table and run a full re-index
|
|
256
|
+
before querying it. Using the same model ID avoids an intentional model-space
|
|
257
|
+
change, but bit-identical vectors are not promised across runtime, model
|
|
258
|
+
revision, dtype, pooling, or normalization changes; validate search quality and
|
|
259
|
+
re-index when those details change.
|
|
260
|
+
|
|
261
|
+
Routine unit tests mock the local runtime and do not download the model.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Troubleshooting: Transitive `sharp` Install Errors
|
|
2
2
|
|
|
3
|
-
`libsql-search` does not directly
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
`libsql-search` does not directly import `sharp`, but local embeddings use
|
|
4
|
+
`@huggingface/transformers`, which currently brings in `sharp` as a transitive
|
|
5
|
+
runtime dependency. If you see an install error mentioning `sharp`, it is
|
|
6
|
+
usually a native-package install or approval issue.
|
|
6
7
|
|
|
7
|
-
This page exists because the error can show up
|
|
8
|
-
`libsql-search
|
|
8
|
+
This page exists because the error can show up before your application reaches
|
|
9
|
+
any `libsql-search` code.
|
|
9
10
|
|
|
10
11
|
## Typical Error
|
|
11
12
|
|
|
@@ -48,7 +49,7 @@ in `pnpm-workspace.yaml` with `onlyBuiltDependencies`.
|
|
|
48
49
|
|
|
49
50
|
## Relation To `libsql-search`
|
|
50
51
|
|
|
51
|
-
- local embeddings use `@
|
|
52
|
+
- local embeddings use `@huggingface/transformers`
|
|
52
53
|
- the first local embedding run may download a model at runtime
|
|
53
54
|
- that runtime model download is separate from a pnpm native-module install
|
|
54
55
|
failure
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -12,3 +12,8 @@ Common operational checks:
|
|
|
12
12
|
- verify hosted providers have `CLOUDFLARE_ACCOUNT_ID` and
|
|
13
13
|
`CLOUDFLARE_API_TOKEN`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or
|
|
14
14
|
`OPENAI_API_KEY` available as required by the selected provider
|
|
15
|
+
- after upgrading an existing Gemini index, fully re-embed with
|
|
16
|
+
`gemini-embedding-2`; for 3072-dimensional Gemini indexes, recreate the table
|
|
17
|
+
or use a new table name before rebuilding
|
|
18
|
+
- after upgrading an existing local 768-dimensional padded index, create or
|
|
19
|
+
recreate a 384-dimensional table and fully re-index before querying it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libsql-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.34.5",
|
|
@@ -63,12 +63,9 @@
|
|
|
63
63
|
"@libsql/client": "^0.15.0"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@
|
|
66
|
+
"@huggingface/transformers": "4.2.0",
|
|
67
67
|
"gray-matter": "^4.0.3"
|
|
68
68
|
},
|
|
69
|
-
"optionalDependencies": {
|
|
70
|
-
"@google/generative-ai": "^0.24.1"
|
|
71
|
-
},
|
|
72
69
|
"devDependencies": {
|
|
73
70
|
"@libsql/client": "^0.15.15",
|
|
74
71
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
@@ -82,5 +79,8 @@
|
|
|
82
79
|
"tslib": "^2.8.1",
|
|
83
80
|
"typescript": "^5.9.3",
|
|
84
81
|
"vitest": "^4.1.11"
|
|
82
|
+
},
|
|
83
|
+
"optionalDependencies": {
|
|
84
|
+
"@google/genai": "2.18.0"
|
|
85
85
|
}
|
|
86
86
|
}
|