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 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
- `text-embedding-004`, and OpenAI `text-embedding-3-small` /
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 = "text-embedding-004";
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: DEFAULT_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
- return DEFAULT_DIMENSIONS;
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 { GoogleGenerativeAI } = await import('@google/generative-ai');
367
- const genAI = new GoogleGenerativeAI(this.#apiKey);
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 model.embedContent(text, { signal: itemSignal });
371
- const values = result.embedding?.values;
372
- if (!Array.isArray(values)) {
373
- throw new Error("Gemini response did not include embedding values");
374
- }
375
- return values;
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 = options.apiKey || getEnvironmentVariable("GEMINI_API_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 = "text-embedding-004";
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: DEFAULT_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
- return DEFAULT_DIMENSIONS;
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 { GoogleGenerativeAI } = await import('@google/generative-ai');
365
- const genAI = new GoogleGenerativeAI(this.#apiKey);
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 model.embedContent(text, { signal: itemSignal });
369
- const values = result.embedding?.values;
370
- if (!Array.isArray(values)) {
371
- throw new Error("Gemini response did not include embedding values");
372
- }
373
- return values;
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 = options.apiKey || getEnvironmentVariable("GEMINI_API_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
@@ -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 `text-embedding-004`.
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 returns 768 dimensions natively
192
- - metadata reports `text-embedding-004` and 768 dimensions
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 current implementation does not expose model selection
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 stays at 768
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.
@@ -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.4.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
  }