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 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`, Cloudflare Workers AI
24
- `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
25
- `text-embedding-004`, and OpenAI `text-embedding-3-small` /
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, "articles", 768);
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; Cloudflare is the recommended hosted
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 DEFAULT_DIMENSIONS = 768;
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 = "text-embedding-004";
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
- async function getLocalEmbeddingModel(modelName) {
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?.model) {
36
- return cached.model;
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: DEFAULT_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
- return DEFAULT_DIMENSIONS;
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 ?? DEFAULT_DIMENSIONS, "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
- const embedding = Array.from(output.data);
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 { GoogleGenerativeAI } = await import('@google/generative-ai');
367
- const genAI = new GoogleGenerativeAI(this.#apiKey);
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 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;
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 = options.apiKey || getEnvironmentVariable("GEMINI_API_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 = 768) {
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 DEFAULT_DIMENSIONS = 768;
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 = "text-embedding-004";
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
- async function getLocalEmbeddingModel(modelName) {
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?.model) {
34
- return cached.model;
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: DEFAULT_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
- return DEFAULT_DIMENSIONS;
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 ?? DEFAULT_DIMENSIONS, "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
- const embedding = Array.from(output.data);
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 { GoogleGenerativeAI } = await import('@google/generative-ai');
365
- const genAI = new GoogleGenerativeAI(this.#apiKey);
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 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;
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 = options.apiKey || getEnvironmentVariable("GEMINI_API_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 = 768) {
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, "articles", 768);
45
+ await createTable(client);
46
46
  ```
47
47
 
48
48
  Defaults:
49
49
 
50
50
  - `tableName`: `"articles"`
51
- - `dimensions`: `768`
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
@@ -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
- const embeddingDimensions =
139
- embeddingProvider === "cloudflare" || embeddingProvider === "mistral"
140
- ? 1024
141
- : 768;
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 `768`
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 Xenova model can be cached by model name.
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
- `@xenova/transformers`.
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 `libsql-search` pads or truncates to your
112
- requested size
113
- - metadata reports the requested output dimensions
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 machine
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 backward compatibility and offline use
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 `text-embedding-004`.
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 returns 768 dimensions natively
192
- - metadata reports `text-embedding-004` and 768 dimensions
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 current implementation does not expose model selection
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` defaults to `768`
234
+ - `local` is fixed at `384`
223
235
  - `cloudflare` is fixed at `1024`
224
236
  - `mistral` is fixed at `1024`
225
- - local embeddings are padded from 384 to your target size
226
- - Gemini stays at 768
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 depend on `sharp`. If you see an install error
4
- mentioning `sharp`, it is coming from another dependency in your application or
5
- toolchain.
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 in environments that also use
8
- `libsql-search`, and it is easy to misattribute the failure to this package.
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 `@xenova/transformers`
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
@@ -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.4.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
- "@xenova/transformers": "^2.17.2",
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
  }