libsql-search 0.4.0 → 0.5.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 +3 -2
- package/dist/index.cjs +43 -13
- package/dist/index.esm.js +43 -13
- package/docs/API.md +3 -0
- package/docs/INDEXING.md +8 -0
- package/docs/INTEGRATIONS.md +2 -0
- package/docs/PROVIDERS.md +22 -6
- package/docs/TROUBLESHOOTING.md +3 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Use it when you want:
|
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
23
|
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
|
|
24
24
|
`@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
|
-
`
|
|
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
|
|
|
@@ -99,7 +99,8 @@ Important behavior:
|
|
|
99
99
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
100
100
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
101
101
|
- `local` is the default offline provider; Cloudflare is the recommended hosted
|
|
102
|
-
option. Cloudflare and Mistral use 1024 dimensions.
|
|
102
|
+
option. Cloudflare and Mistral use 1024 dimensions. Gemini defaults to 3072
|
|
103
|
+
dimensions and supports 128-3072.
|
|
103
104
|
|
|
104
105
|
## Core API
|
|
105
106
|
|
package/dist/index.cjs
CHANGED
|
@@ -8,7 +8,10 @@ const DEFAULT_DIMENSIONS = 768;
|
|
|
8
8
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
10
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
-
const GEMINI_MODEL = "
|
|
11
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
12
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
13
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
14
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
12
15
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
16
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
17
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
@@ -121,6 +124,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
121
124
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
122
125
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
123
126
|
}
|
|
127
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
128
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
129
|
+
}
|
|
130
|
+
function parseGeminiEmbeddingResult(result) {
|
|
131
|
+
if (!Array.isArray(result.embeddings)) {
|
|
132
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
133
|
+
}
|
|
134
|
+
if (result.embeddings.length !== 1) {
|
|
135
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
136
|
+
}
|
|
137
|
+
const values = result.embeddings[0]?.values;
|
|
138
|
+
if (!Array.isArray(values)) {
|
|
139
|
+
throw new Error("Gemini response did not include embedding values");
|
|
140
|
+
}
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
124
143
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
125
144
|
if (parentSignal?.aborted) {
|
|
126
145
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -239,7 +258,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
239
258
|
return Object.freeze({
|
|
240
259
|
name: "gemini",
|
|
241
260
|
model: GEMINI_MODEL,
|
|
242
|
-
dimensions
|
|
261
|
+
dimensions,
|
|
243
262
|
batch: Object.freeze({ mode: "sequential" })
|
|
244
263
|
});
|
|
245
264
|
case "openai":
|
|
@@ -267,7 +286,14 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
267
286
|
}
|
|
268
287
|
function getEffectiveDimensions(provider, dimensions) {
|
|
269
288
|
if (provider === "gemini") {
|
|
270
|
-
|
|
289
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
290
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
291
|
+
throw providerError(
|
|
292
|
+
"gemini",
|
|
293
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return effectiveDimensions;
|
|
271
297
|
}
|
|
272
298
|
if (provider === "mistral") {
|
|
273
299
|
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
@@ -363,16 +389,18 @@ class GeminiEmbeddingProvider {
|
|
|
363
389
|
"API request",
|
|
364
390
|
this.#timeoutMs,
|
|
365
391
|
async (signal) => {
|
|
366
|
-
const {
|
|
367
|
-
const
|
|
368
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
392
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
393
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
369
394
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
370
|
-
const result = await
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
395
|
+
const result = await client.models.embedContent({
|
|
396
|
+
model: this.metadata.model,
|
|
397
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
398
|
+
config: {
|
|
399
|
+
outputDimensionality: this.metadata.dimensions,
|
|
400
|
+
abortSignal: itemSignal
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return parseGeminiEmbeddingResult(result);
|
|
376
404
|
});
|
|
377
405
|
},
|
|
378
406
|
options.signal,
|
|
@@ -583,7 +611,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
583
611
|
case "local":
|
|
584
612
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
585
613
|
case "gemini": {
|
|
586
|
-
const key =
|
|
614
|
+
const key = getOptionalTrimmedCredential(
|
|
615
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
616
|
+
);
|
|
587
617
|
if (!key) {
|
|
588
618
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
589
619
|
}
|
package/dist/index.esm.js
CHANGED
|
@@ -6,7 +6,10 @@ const DEFAULT_DIMENSIONS = 768;
|
|
|
6
6
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
8
8
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
9
|
-
const GEMINI_MODEL = "
|
|
9
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
10
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
11
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
12
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
10
13
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
11
14
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
12
15
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
@@ -119,6 +122,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
119
122
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
120
123
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
121
124
|
}
|
|
125
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
126
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
127
|
+
}
|
|
128
|
+
function parseGeminiEmbeddingResult(result) {
|
|
129
|
+
if (!Array.isArray(result.embeddings)) {
|
|
130
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
131
|
+
}
|
|
132
|
+
if (result.embeddings.length !== 1) {
|
|
133
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
134
|
+
}
|
|
135
|
+
const values = result.embeddings[0]?.values;
|
|
136
|
+
if (!Array.isArray(values)) {
|
|
137
|
+
throw new Error("Gemini response did not include embedding values");
|
|
138
|
+
}
|
|
139
|
+
return values;
|
|
140
|
+
}
|
|
122
141
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
123
142
|
if (parentSignal?.aborted) {
|
|
124
143
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -237,7 +256,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
237
256
|
return Object.freeze({
|
|
238
257
|
name: "gemini",
|
|
239
258
|
model: GEMINI_MODEL,
|
|
240
|
-
dimensions
|
|
259
|
+
dimensions,
|
|
241
260
|
batch: Object.freeze({ mode: "sequential" })
|
|
242
261
|
});
|
|
243
262
|
case "openai":
|
|
@@ -265,7 +284,14 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
265
284
|
}
|
|
266
285
|
function getEffectiveDimensions(provider, dimensions) {
|
|
267
286
|
if (provider === "gemini") {
|
|
268
|
-
|
|
287
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
288
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
289
|
+
throw providerError(
|
|
290
|
+
"gemini",
|
|
291
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return effectiveDimensions;
|
|
269
295
|
}
|
|
270
296
|
if (provider === "mistral") {
|
|
271
297
|
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
@@ -361,16 +387,18 @@ class GeminiEmbeddingProvider {
|
|
|
361
387
|
"API request",
|
|
362
388
|
this.#timeoutMs,
|
|
363
389
|
async (signal) => {
|
|
364
|
-
const {
|
|
365
|
-
const
|
|
366
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
390
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
391
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
367
392
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
368
|
-
const result = await
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
393
|
+
const result = await client.models.embedContent({
|
|
394
|
+
model: this.metadata.model,
|
|
395
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
396
|
+
config: {
|
|
397
|
+
outputDimensionality: this.metadata.dimensions,
|
|
398
|
+
abortSignal: itemSignal
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return parseGeminiEmbeddingResult(result);
|
|
374
402
|
});
|
|
375
403
|
},
|
|
376
404
|
options.signal,
|
|
@@ -581,7 +609,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
581
609
|
case "local":
|
|
582
610
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
583
611
|
case "gemini": {
|
|
584
|
-
const key =
|
|
612
|
+
const key = getOptionalTrimmedCredential(
|
|
613
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
614
|
+
);
|
|
585
615
|
if (!key) {
|
|
586
616
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
587
617
|
}
|
package/docs/API.md
CHANGED
|
@@ -213,6 +213,9 @@ 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
|
+
|
|
216
219
|
### `getEmbeddingProviderMetadata(options?)`
|
|
217
220
|
|
|
218
221
|
Returns the same metadata exposed by `createEmbeddingProvider(options).metadata`
|
package/docs/INDEXING.md
CHANGED
|
@@ -33,6 +33,14 @@ await indexContent({
|
|
|
33
33
|
That keeps the implementation simple, but it also means a failed rebuild can
|
|
34
34
|
leave the index partially repopulated.
|
|
35
35
|
|
|
36
|
+
Changing an embedding provider or dimension count requires a full re-embed.
|
|
37
|
+
For Gemini specifically, indexes created with the retired `text-embedding-004`
|
|
38
|
+
model must be rebuilt for `gemini-embedding-2` even when staying at 768
|
|
39
|
+
dimensions, because the model and query/document formatting both changed. If
|
|
40
|
+
you adopt Gemini's 3072-dimensional default, recreate the vector table or build
|
|
41
|
+
into a separate table first; clearing rows with `indexContent()` does not change
|
|
42
|
+
the table's `F32_BLOB` width.
|
|
43
|
+
|
|
36
44
|
## Quality Guidelines
|
|
37
45
|
|
|
38
46
|
- include descriptive frontmatter titles
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -138,6 +138,8 @@ const embeddingProvider =
|
|
|
138
138
|
const embeddingDimensions =
|
|
139
139
|
embeddingProvider === "cloudflare" || embeddingProvider === "mistral"
|
|
140
140
|
? 1024
|
|
141
|
+
: embeddingProvider === "gemini"
|
|
142
|
+
? 3072
|
|
141
143
|
: 768;
|
|
142
144
|
|
|
143
145
|
await createTable(client, "articles", embeddingDimensions);
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -29,7 +29,8 @@ interface EmbeddingOptions {
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
- `provider` defaults to `"local"`
|
|
32
|
-
- `dimensions` defaults to `768`
|
|
32
|
+
- `dimensions` defaults to `768` for the library's default local provider.
|
|
33
|
+
Provider-specific defaults can differ; Gemini defaults to `3072`.
|
|
33
34
|
- `maxLength` defaults to `8000`
|
|
34
35
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
35
36
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
@@ -176,22 +177,29 @@ Behavior:
|
|
|
176
177
|
|
|
177
178
|
Provider value: `gemini`
|
|
178
179
|
|
|
179
|
-
Gemini uses Google `
|
|
180
|
+
Gemini uses Google `gemini-embedding-2` through `@google/genai`.
|
|
180
181
|
|
|
181
182
|
```ts
|
|
182
183
|
embeddingOptions: {
|
|
183
184
|
provider: "gemini",
|
|
184
185
|
apiKey: process.env.GEMINI_API_KEY,
|
|
186
|
+
dimensions: 3072,
|
|
185
187
|
}
|
|
186
188
|
```
|
|
187
189
|
|
|
188
190
|
Behavior:
|
|
189
191
|
|
|
190
192
|
- if `apiKey` is omitted, the library reads `GEMINI_API_KEY`
|
|
191
|
-
- Gemini
|
|
192
|
-
-
|
|
193
|
+
- blank Gemini credentials are treated as missing
|
|
194
|
+
- Gemini defaults to 3072 dimensions
|
|
195
|
+
- explicit Gemini dimensions must be integers from 128 through 3072
|
|
196
|
+
- 768, 1536, and 3072 are recommended practical sizes
|
|
197
|
+
- metadata reports `gemini-embedding-2` and the effective dimensions
|
|
193
198
|
- batch metadata is `{ mode: "sequential" }`
|
|
194
|
-
- the
|
|
199
|
+
- the library sends one SDK request per input and verifies one vector per input
|
|
200
|
+
- document inputs are formatted as `title: none | text: ...`
|
|
201
|
+
- query inputs are formatted as `task: search result | query: ...`
|
|
202
|
+
- the current implementation does not expose custom model selection
|
|
195
203
|
|
|
196
204
|
## OpenAI
|
|
197
205
|
|
|
@@ -223,9 +231,17 @@ Behavior:
|
|
|
223
231
|
- `cloudflare` is fixed at `1024`
|
|
224
232
|
- `mistral` is fixed at `1024`
|
|
225
233
|
- local embeddings are padded from 384 to your target size
|
|
226
|
-
- Gemini
|
|
234
|
+
- Gemini defaults to `3072` and accepts explicit dimensions from `128` through
|
|
235
|
+
`3072`; use `768`, `1536`, or `3072` unless you have a specific reason
|
|
227
236
|
- OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
|
|
228
237
|
value you explicitly set
|
|
229
238
|
|
|
230
239
|
If you switch provider or dimensions for an existing table, recreate the table
|
|
231
240
|
or rebuild the index into a separate table so stored vectors stay consistent.
|
|
241
|
+
|
|
242
|
+
Existing Gemini indexes created with `text-embedding-004` must be fully
|
|
243
|
+
re-embedded for `gemini-embedding-2`, even if you keep `dimensions: 768`,
|
|
244
|
+
because both the model and query/document input formatting changed. If you move
|
|
245
|
+
to the new 3072-dimensional default, create a new table or recreate the vector
|
|
246
|
+
table first; `indexContent()` clears rows but does not change the `F32_BLOB`
|
|
247
|
+
width. A separate table is safer because rebuilds are not transactional.
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -12,3 +12,6 @@ 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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libsql-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|
|
@@ -66,9 +66,6 @@
|
|
|
66
66
|
"@xenova/transformers": "^2.17.2",
|
|
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
|
}
|