libsql-search 0.10.1 → 0.11.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
@@ -9,7 +9,7 @@
9
9
 
10
10
  Use it when you want:
11
11
 
12
- - one indexing/search API across local and hosted embedding providers
12
+ - one indexing/search API across external embedding providers
13
13
  - direct control over vector dimensions, table names, and deployment shape
14
14
  - a lightweight library instead of a hosted search product
15
15
 
@@ -45,7 +45,7 @@ Note for `0.17.x`: the client no longer exports `./package.json`, so `require("@
45
45
 
46
46
  ## Quick Start
47
47
 
48
- This example uses the default local provider. Local embeddings run in-process after the initial model download and cache warmup; they are not automatically air-gapped.
48
+ This example uses a separately deployed OpenAI-compatible embedding service. `libsql-search` never loads or hosts an embedding model in-process.
49
49
 
50
50
  ```ts
51
51
  import { createClient } from "@libsql/client";
@@ -56,25 +56,29 @@ const client = createClient({
56
56
  authToken: "your-auth-token",
57
57
  });
58
58
 
59
- await createTable(client, "articles_local_384", 384);
59
+ const embeddingOptions = {
60
+ provider: "openai-compatible" as const,
61
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
62
+ apiKey: process.env.EMBEDDING_API_KEY,
63
+ model: "bge-large-en-v1.5",
64
+ dimensions: 1024,
65
+ };
66
+
67
+ await createTable(client, "articles_bge_1024", 1024);
60
68
 
61
69
  await indexContent({
62
70
  client,
63
71
  contentPath: "./content",
64
- tableName: "articles_local_384",
65
- embeddingOptions: {
66
- provider: "local",
67
- },
72
+ tableName: "articles_bge_1024",
73
+ embeddingOptions,
68
74
  });
69
75
 
70
76
  const results = await search({
71
77
  client,
72
78
  query: "how do I deploy my docs site",
73
- tableName: "articles_local_384",
79
+ tableName: "articles_bge_1024",
74
80
  limit: 5,
75
- embeddingOptions: {
76
- provider: "local",
77
- },
81
+ embeddingOptions,
78
82
  });
79
83
 
80
84
  console.log(results.map((result) => ({
@@ -91,7 +95,7 @@ Important behavior:
91
95
  - `indexContent()` embeds every document before it touches the database, then replaces the table in one transaction, so a failed rebuild leaves the previous index intact.
92
96
  - `indexContent()` throws `IndexingError` when a file fails; pass `failurePolicy: "skip"` to rebuild from the remaining files.
93
97
  - `indexContent()` throws `IndexingError` when no source files are found; pass `allowEmptyIndex: true` to intentionally empty the index.
94
- - Hosted providers send indexed and queried text to external services and may incur provider charges.
98
+ - Every provider sends indexed and queried text to an external service; review that service's privacy, retention, and pricing terms.
95
99
 
96
100
  ## Search Accuracy And Performance
97
101
 
@@ -103,10 +107,10 @@ Two options control the trade-off:
103
107
 
104
108
  ```ts
105
109
  // Widen the index probe to raise recall (default: max(limit * 4, 32))
106
- await search({ client, query, limit: 10, candidates: 200 });
110
+ await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });
107
111
 
108
112
  // Bypass the index entirely: exact, but linear in table size
109
- await search({ client, query, exact: true });
113
+ await search({ client, query, embeddingOptions, exact: true });
110
114
  ```
111
115
 
112
116
  `exact: true` is the only way to guarantee exactness. Use it for small corpora, for correctness checks against the index path, and for tables that have no vector index.
@@ -117,7 +121,6 @@ Requirements: `vector_top_k()` and `libsql_vector_idx()` need a libSQL build wit
117
121
 
118
122
  Built-in providers:
119
123
 
120
- - `local` with `Xenova/all-MiniLM-L6-v2` at 384 dimensions
121
124
  - `cloudflare` with `@cf/baai/bge-m3` at 1024 dimensions
122
125
  - `mistral` with `mistral-embed` at 1024 dimensions
123
126
  - `gemini` with `gemini-embedding-2` at 128-3072 dimensions, default 3072
package/dist/index.cjs CHANGED
@@ -6,10 +6,8 @@ var matter = require('gray-matter');
6
6
 
7
7
  const OPENAI_DEFAULT_DIMENSIONS = 768;
8
8
  const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
9
- const LOCAL_DIMENSIONS = 384;
10
9
  const DEFAULT_MAX_LENGTH = 8e3;
11
10
  const DEFAULT_TIMEOUT_MS = 3e4;
12
- const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
13
11
  const GEMINI_MODEL = "gemini-embedding-2";
14
12
  const GEMINI_DIMENSIONS = 3072;
15
13
  const GEMINI_MIN_DIMENSIONS = 128;
@@ -22,7 +20,6 @@ const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
22
20
  const MISTRAL_DIMENSIONS = 1024;
23
21
  const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
24
22
  const CLOUDFLARE_DIMENSIONS = 1024;
25
- const localModelCacheByModel = /* @__PURE__ */ new Map();
26
23
  function getEnvironmentVariable(name) {
27
24
  const runtime = globalThis;
28
25
  const nodeValue = runtime.process?.env?.[name];
@@ -35,67 +32,6 @@ function getEnvironmentVariable(name) {
35
32
  return void 0;
36
33
  }
37
34
  }
38
- function deletePendingLocalModelCache(modelName, entry) {
39
- if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
40
- localModelCacheByModel.delete(modelName);
41
- }
42
- }
43
- async function getLocalEmbeddingModel(modelName, signal) {
44
- const cached = localModelCacheByModel.get(modelName);
45
- if (cached) {
46
- cached.waiters++;
47
- try {
48
- return await waitForLocalEmbeddingModel(modelName, cached, signal);
49
- } finally {
50
- cached.waiters--;
51
- deletePendingLocalModelCache(modelName, cached);
52
- }
53
- }
54
- const modelPromise = (async () => {
55
- console.log(`Loading local embedding model (${modelName})...`);
56
- const { pipeline } = await import('@huggingface/transformers');
57
- const model = await pipeline("feature-extraction", modelName);
58
- console.log("Local model loaded successfully");
59
- return model;
60
- })();
61
- const entry = {
62
- promise: modelPromise,
63
- settled: false,
64
- waiters: 1
65
- };
66
- localModelCacheByModel.set(modelName, entry);
67
- modelPromise.then(() => {
68
- entry.settled = true;
69
- }).catch(() => {
70
- localModelCacheByModel.delete(modelName);
71
- });
72
- try {
73
- return await waitForLocalEmbeddingModel(modelName, entry, signal);
74
- } finally {
75
- entry.waiters--;
76
- deletePendingLocalModelCache(modelName, entry);
77
- }
78
- }
79
- async function waitForLocalEmbeddingModel(modelName, entry, signal) {
80
- if (signal.aborted) {
81
- deletePendingLocalModelCache(modelName, entry);
82
- throw providerError("local", "model inference was aborted");
83
- }
84
- let rejectAbort = () => {
85
- };
86
- const abortPromise = new Promise((_resolve, reject) => {
87
- rejectAbort = reject;
88
- });
89
- const onAbort = () => {
90
- rejectAbort(providerError("local", "model inference was aborted"));
91
- };
92
- signal.addEventListener("abort", onAbort, { once: true });
93
- try {
94
- return await Promise.race([entry.promise, abortPromise]);
95
- } finally {
96
- signal.removeEventListener("abort", onAbort);
97
- }
98
- }
99
35
  function getPositiveInteger(value, optionName) {
100
36
  if (!Number.isInteger(value) || value <= 0) {
101
37
  throw new Error(`Invalid ${optionName}: expected a positive integer`);
@@ -106,15 +42,17 @@ function getTimeoutMs(value) {
106
42
  return getPositiveInteger(value ?? DEFAULT_TIMEOUT_MS, "timeoutMs");
107
43
  }
108
44
  function resolveProviderName(provider) {
109
- switch (provider ?? "local") {
110
- case "local":
45
+ switch (provider) {
111
46
  case "gemini":
112
47
  case "openai":
113
48
  case "mistral":
114
49
  case "cloudflare":
115
50
  case "openai-compatible":
116
- return provider ?? "local";
51
+ return provider;
117
52
  default:
53
+ if (provider === void 0) {
54
+ throw new Error("Embedding provider is required");
55
+ }
118
56
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
119
57
  }
120
58
  }
@@ -326,13 +264,6 @@ function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
326
264
  }
327
265
  function createProviderMetadata(provider, dimensions, model) {
328
266
  switch (provider) {
329
- case "local":
330
- return Object.freeze({
331
- name: "local",
332
- model: LOCAL_MODEL,
333
- dimensions,
334
- batch: Object.freeze({ mode: "sequential" })
335
- });
336
267
  case "gemini":
337
268
  return Object.freeze({
338
269
  name: "gemini",
@@ -371,15 +302,6 @@ function createProviderMetadata(provider, dimensions, model) {
371
302
  }
372
303
  }
373
304
  function getEffectiveDimensions(provider, dimensions) {
374
- if (provider === "local") {
375
- if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
376
- throw providerError(
377
- "local",
378
- `${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
379
- );
380
- }
381
- return LOCAL_DIMENSIONS;
382
- }
383
305
  if (provider === "gemini") {
384
306
  const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
385
307
  if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
@@ -440,42 +362,6 @@ function createEmbeddingBatchResult(metadata, intent, embeddings) {
440
362
  intent
441
363
  });
442
364
  }
443
- class LocalEmbeddingProvider {
444
- metadata;
445
- #timeoutMs;
446
- constructor(metadata, timeoutMs) {
447
- this.metadata = metadata;
448
- this.#timeoutMs = timeoutMs;
449
- }
450
- async embed(texts, options = {}) {
451
- const intent = resolveIntent(options.intent);
452
- if (texts.length === 0) {
453
- return createEmbeddingBatchResult(this.metadata, intent, []);
454
- }
455
- assertBatchSize(this.metadata, texts.length);
456
- const vectors = await withTimeout(
457
- "local",
458
- "model inference",
459
- this.#timeoutMs,
460
- async (signal) => {
461
- const model = await getLocalEmbeddingModel(this.metadata.model, signal);
462
- return embedSequentially("local", "model inference", texts, signal, async (text) => {
463
- const output = await model(text, {
464
- pooling: "mean",
465
- normalize: true
466
- });
467
- return Array.from(output.data);
468
- });
469
- },
470
- options.signal
471
- );
472
- return createEmbeddingBatchResult(
473
- this.metadata,
474
- intent,
475
- validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "local")
476
- );
477
- }
478
- }
479
365
  class GeminiEmbeddingProvider {
480
366
  metadata;
481
367
  #apiKey;
@@ -628,13 +514,11 @@ class OpenAICompatibleEmbeddingProvider {
628
514
  );
629
515
  }
630
516
  }
631
- function createEmbeddingProvider(options = {}) {
517
+ function createEmbeddingProvider(options) {
632
518
  const provider = resolveProviderName(options.provider);
633
519
  const metadata = getEmbeddingProviderMetadata(options);
634
520
  const timeoutMs = getTimeoutMs(options.timeoutMs);
635
521
  switch (provider) {
636
- case "local":
637
- return new LocalEmbeddingProvider(metadata, timeoutMs);
638
522
  case "gemini": {
639
523
  const key = getOptionalTrimmedCredential(
640
524
  options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
@@ -733,7 +617,7 @@ function createEmbeddingProvider(options = {}) {
733
617
  }
734
618
  }
735
619
  }
736
- function getEmbeddingProviderMetadata(options = {}) {
620
+ function getEmbeddingProviderMetadata(options) {
737
621
  const provider = resolveProviderName(options.provider);
738
622
  if (provider === "openai-compatible") {
739
623
  normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
@@ -743,7 +627,7 @@ function getEmbeddingProviderMetadata(options = {}) {
743
627
  }
744
628
  return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
745
629
  }
746
- async function generateEmbeddings(texts, options = {}) {
630
+ async function generateEmbeddings(texts, options) {
747
631
  const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
748
632
  const intent = resolveIntent(options.intent);
749
633
  if (texts.length === 0) {
@@ -756,7 +640,7 @@ async function generateEmbeddings(texts, options = {}) {
756
640
  });
757
641
  return result.embeddings;
758
642
  }
759
- async function generateEmbedding(text, options = {}) {
643
+ async function generateEmbedding(text, options) {
760
644
  const [embedding] = await generateEmbeddings([text], options);
761
645
  if (!embedding) {
762
646
  throw new Error("Embedding provider returned no embedding");
@@ -889,7 +773,7 @@ async function indexContent(options) {
889
773
  const {
890
774
  client,
891
775
  contentPath,
892
- embeddingOptions = {},
776
+ embeddingOptions,
893
777
  fileExtensions = [".md", ".markdown"],
894
778
  exclude = ["node_modules", ".git", "dist", "build"],
895
779
  tableName = "articles",
@@ -899,6 +783,9 @@ async function indexContent(options) {
899
783
  } = options;
900
784
  const database = resolveDatabase(client);
901
785
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
786
+ if (embeddingOptions === void 0) {
787
+ throw new TypeError("embeddingOptions is required");
788
+ }
902
789
  let files;
903
790
  try {
904
791
  files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
@@ -1178,7 +1065,7 @@ async function search(options) {
1178
1065
  query,
1179
1066
  limit = 10,
1180
1067
  tableName = "articles",
1181
- embeddingOptions = {},
1068
+ embeddingOptions,
1182
1069
  candidates,
1183
1070
  exact = false
1184
1071
  } = options;
@@ -1190,6 +1077,9 @@ async function search(options) {
1190
1077
  "embedding index name"
1191
1078
  );
1192
1079
  const candidateCount = normalizeSearchCandidates(candidates, resultLimit);
1080
+ if (embeddingOptions === void 0) {
1081
+ throw new TypeError("embeddingOptions is required");
1082
+ }
1193
1083
  const queryEmbedding = await generateEmbedding(query, {
1194
1084
  ...embeddingOptions,
1195
1085
  intent: embeddingOptions.intent ?? "query"
package/dist/index.d.ts CHANGED
@@ -2,10 +2,10 @@ import { Client } from '@libsql/client';
2
2
 
3
3
  /**
4
4
  * Multi-provider embedding generation
5
- * Supports local Hugging Face Transformers, Gemini, OpenAI, Mistral,
6
- * Cloudflare Workers AI, and custom OpenAI-compatible endpoints
5
+ * Supports Gemini, OpenAI, Mistral, Cloudflare Workers AI, and custom
6
+ * OpenAI-compatible endpoints.
7
7
  */
8
- type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
8
+ type EmbeddingProvider = 'gemini' | 'openai' | 'mistral' | 'cloudflare' | 'openai-compatible';
9
9
  type EmbeddingIntent = 'document' | 'query';
10
10
  type EmbeddingBatchMode = 'native' | 'sequential';
11
11
  interface EmbeddingBatchBehavior {
@@ -34,7 +34,7 @@ interface EmbeddingBatchResult {
34
34
  intent: EmbeddingIntent;
35
35
  }
36
36
  interface EmbeddingOptions {
37
- provider?: EmbeddingProvider;
37
+ provider: EmbeddingProvider;
38
38
  apiKey?: string;
39
39
  accountId?: string;
40
40
  apiToken?: string;
@@ -53,16 +53,16 @@ interface EmbeddingBatchItemResult {
53
53
  }
54
54
  type EmbeddingBatchItem = number[] | EmbeddingBatchItemResult;
55
55
  declare function validateEmbeddingBatch(items: EmbeddingBatchItem[], expectedCount: number, expectedDimensions: number, provider: EmbeddingProvider): number[][];
56
- declare function createEmbeddingProvider(options?: EmbeddingOptions): EmbeddingProviderClient;
57
- declare function getEmbeddingProviderMetadata(options?: EmbeddingOptions): EmbeddingProviderMetadata;
56
+ declare function createEmbeddingProvider(options: EmbeddingOptions): EmbeddingProviderClient;
57
+ declare function getEmbeddingProviderMetadata(options: EmbeddingOptions): EmbeddingProviderMetadata;
58
58
  /**
59
59
  * Generate embeddings using the specified provider.
60
60
  */
61
- declare function generateEmbeddings(texts: string[], options?: EmbeddingOptions): Promise<number[][]>;
61
+ declare function generateEmbeddings(texts: string[], options: EmbeddingOptions): Promise<number[][]>;
62
62
  /**
63
63
  * Generate an embedding using the specified provider.
64
64
  */
65
- declare function generateEmbedding(text: string, options?: EmbeddingOptions): Promise<number[]>;
65
+ declare function generateEmbedding(text: string, options: EmbeddingOptions): Promise<number[]>;
66
66
  /**
67
67
  * Pad or truncate embedding to target dimensions
68
68
  */
@@ -189,7 +189,7 @@ interface IndexerOptions {
189
189
  */
190
190
  client: DatabaseClient;
191
191
  contentPath: string;
192
- embeddingOptions?: EmbeddingOptions;
192
+ embeddingOptions: EmbeddingOptions;
193
193
  fileExtensions?: string[];
194
194
  exclude?: string[];
195
195
  tableName?: string;
@@ -282,7 +282,7 @@ interface SearchOptions {
282
282
  query: string;
283
283
  limit?: number;
284
284
  tableName?: string;
285
- embeddingOptions?: EmbeddingOptions;
285
+ embeddingOptions: EmbeddingOptions;
286
286
  /**
287
287
  * How many candidates to pull from the vector index before the exact
288
288
  * re-rank. Must be an integer from `limit` through {@link MAX_SEARCH_CANDIDATES}.
package/dist/index.esm.js CHANGED
@@ -4,10 +4,8 @@ import matter from 'gray-matter';
4
4
 
5
5
  const OPENAI_DEFAULT_DIMENSIONS = 768;
6
6
  const OPENAI_COMPATIBLE_DEFAULT_BATCH_SIZE = 32;
7
- const LOCAL_DIMENSIONS = 384;
8
7
  const DEFAULT_MAX_LENGTH = 8e3;
9
8
  const DEFAULT_TIMEOUT_MS = 3e4;
10
- const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
11
9
  const GEMINI_MODEL = "gemini-embedding-2";
12
10
  const GEMINI_DIMENSIONS = 3072;
13
11
  const GEMINI_MIN_DIMENSIONS = 128;
@@ -20,7 +18,6 @@ const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
20
18
  const MISTRAL_DIMENSIONS = 1024;
21
19
  const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
22
20
  const CLOUDFLARE_DIMENSIONS = 1024;
23
- const localModelCacheByModel = /* @__PURE__ */ new Map();
24
21
  function getEnvironmentVariable(name) {
25
22
  const runtime = globalThis;
26
23
  const nodeValue = runtime.process?.env?.[name];
@@ -33,67 +30,6 @@ function getEnvironmentVariable(name) {
33
30
  return void 0;
34
31
  }
35
32
  }
36
- function deletePendingLocalModelCache(modelName, entry) {
37
- if (!entry.settled && entry.waiters === 0 && localModelCacheByModel.get(modelName) === entry) {
38
- localModelCacheByModel.delete(modelName);
39
- }
40
- }
41
- async function getLocalEmbeddingModel(modelName, signal) {
42
- const cached = localModelCacheByModel.get(modelName);
43
- if (cached) {
44
- cached.waiters++;
45
- try {
46
- return await waitForLocalEmbeddingModel(modelName, cached, signal);
47
- } finally {
48
- cached.waiters--;
49
- deletePendingLocalModelCache(modelName, cached);
50
- }
51
- }
52
- const modelPromise = (async () => {
53
- console.log(`Loading local embedding model (${modelName})...`);
54
- const { pipeline } = await import('@huggingface/transformers');
55
- const model = await pipeline("feature-extraction", modelName);
56
- console.log("Local model loaded successfully");
57
- return model;
58
- })();
59
- const entry = {
60
- promise: modelPromise,
61
- settled: false,
62
- waiters: 1
63
- };
64
- localModelCacheByModel.set(modelName, entry);
65
- modelPromise.then(() => {
66
- entry.settled = true;
67
- }).catch(() => {
68
- localModelCacheByModel.delete(modelName);
69
- });
70
- try {
71
- return await waitForLocalEmbeddingModel(modelName, entry, signal);
72
- } finally {
73
- entry.waiters--;
74
- deletePendingLocalModelCache(modelName, entry);
75
- }
76
- }
77
- async function waitForLocalEmbeddingModel(modelName, entry, signal) {
78
- if (signal.aborted) {
79
- deletePendingLocalModelCache(modelName, entry);
80
- throw providerError("local", "model inference was aborted");
81
- }
82
- let rejectAbort = () => {
83
- };
84
- const abortPromise = new Promise((_resolve, reject) => {
85
- rejectAbort = reject;
86
- });
87
- const onAbort = () => {
88
- rejectAbort(providerError("local", "model inference was aborted"));
89
- };
90
- signal.addEventListener("abort", onAbort, { once: true });
91
- try {
92
- return await Promise.race([entry.promise, abortPromise]);
93
- } finally {
94
- signal.removeEventListener("abort", onAbort);
95
- }
96
- }
97
33
  function getPositiveInteger(value, optionName) {
98
34
  if (!Number.isInteger(value) || value <= 0) {
99
35
  throw new Error(`Invalid ${optionName}: expected a positive integer`);
@@ -104,15 +40,17 @@ function getTimeoutMs(value) {
104
40
  return getPositiveInteger(value ?? DEFAULT_TIMEOUT_MS, "timeoutMs");
105
41
  }
106
42
  function resolveProviderName(provider) {
107
- switch (provider ?? "local") {
108
- case "local":
43
+ switch (provider) {
109
44
  case "gemini":
110
45
  case "openai":
111
46
  case "mistral":
112
47
  case "cloudflare":
113
48
  case "openai-compatible":
114
- return provider ?? "local";
49
+ return provider;
115
50
  default:
51
+ if (provider === void 0) {
52
+ throw new Error("Embedding provider is required");
53
+ }
116
54
  throw new Error(`Unknown embedding provider: ${String(provider)}`);
117
55
  }
118
56
  }
@@ -324,13 +262,6 @@ function normalizeOpenAICompatibleEmbeddingsUrl(baseUrl) {
324
262
  }
325
263
  function createProviderMetadata(provider, dimensions, model) {
326
264
  switch (provider) {
327
- case "local":
328
- return Object.freeze({
329
- name: "local",
330
- model: LOCAL_MODEL,
331
- dimensions,
332
- batch: Object.freeze({ mode: "sequential" })
333
- });
334
265
  case "gemini":
335
266
  return Object.freeze({
336
267
  name: "gemini",
@@ -369,15 +300,6 @@ function createProviderMetadata(provider, dimensions, model) {
369
300
  }
370
301
  }
371
302
  function getEffectiveDimensions(provider, dimensions) {
372
- if (provider === "local") {
373
- if (dimensions !== void 0 && dimensions !== LOCAL_DIMENSIONS) {
374
- throw providerError(
375
- "local",
376
- `${LOCAL_MODEL} returns ${LOCAL_DIMENSIONS} dimensions; received dimensions ${String(dimensions)}`
377
- );
378
- }
379
- return LOCAL_DIMENSIONS;
380
- }
381
303
  if (provider === "gemini") {
382
304
  const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
383
305
  if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
@@ -438,42 +360,6 @@ function createEmbeddingBatchResult(metadata, intent, embeddings) {
438
360
  intent
439
361
  });
440
362
  }
441
- class LocalEmbeddingProvider {
442
- metadata;
443
- #timeoutMs;
444
- constructor(metadata, timeoutMs) {
445
- this.metadata = metadata;
446
- this.#timeoutMs = timeoutMs;
447
- }
448
- async embed(texts, options = {}) {
449
- const intent = resolveIntent(options.intent);
450
- if (texts.length === 0) {
451
- return createEmbeddingBatchResult(this.metadata, intent, []);
452
- }
453
- assertBatchSize(this.metadata, texts.length);
454
- const vectors = await withTimeout(
455
- "local",
456
- "model inference",
457
- this.#timeoutMs,
458
- async (signal) => {
459
- const model = await getLocalEmbeddingModel(this.metadata.model, signal);
460
- return embedSequentially("local", "model inference", texts, signal, async (text) => {
461
- const output = await model(text, {
462
- pooling: "mean",
463
- normalize: true
464
- });
465
- return Array.from(output.data);
466
- });
467
- },
468
- options.signal
469
- );
470
- return createEmbeddingBatchResult(
471
- this.metadata,
472
- intent,
473
- validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "local")
474
- );
475
- }
476
- }
477
363
  class GeminiEmbeddingProvider {
478
364
  metadata;
479
365
  #apiKey;
@@ -626,13 +512,11 @@ class OpenAICompatibleEmbeddingProvider {
626
512
  );
627
513
  }
628
514
  }
629
- function createEmbeddingProvider(options = {}) {
515
+ function createEmbeddingProvider(options) {
630
516
  const provider = resolveProviderName(options.provider);
631
517
  const metadata = getEmbeddingProviderMetadata(options);
632
518
  const timeoutMs = getTimeoutMs(options.timeoutMs);
633
519
  switch (provider) {
634
- case "local":
635
- return new LocalEmbeddingProvider(metadata, timeoutMs);
636
520
  case "gemini": {
637
521
  const key = getOptionalTrimmedCredential(
638
522
  options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
@@ -731,7 +615,7 @@ function createEmbeddingProvider(options = {}) {
731
615
  }
732
616
  }
733
617
  }
734
- function getEmbeddingProviderMetadata(options = {}) {
618
+ function getEmbeddingProviderMetadata(options) {
735
619
  const provider = resolveProviderName(options.provider);
736
620
  if (provider === "openai-compatible") {
737
621
  normalizeOpenAICompatibleEmbeddingsUrl(getRequiredTrimmedString(options.baseUrl, "baseUrl"));
@@ -741,7 +625,7 @@ function getEmbeddingProviderMetadata(options = {}) {
741
625
  }
742
626
  return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions), options.model);
743
627
  }
744
- async function generateEmbeddings(texts, options = {}) {
628
+ async function generateEmbeddings(texts, options) {
745
629
  const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
746
630
  const intent = resolveIntent(options.intent);
747
631
  if (texts.length === 0) {
@@ -754,7 +638,7 @@ async function generateEmbeddings(texts, options = {}) {
754
638
  });
755
639
  return result.embeddings;
756
640
  }
757
- async function generateEmbedding(text, options = {}) {
641
+ async function generateEmbedding(text, options) {
758
642
  const [embedding] = await generateEmbeddings([text], options);
759
643
  if (!embedding) {
760
644
  throw new Error("Embedding provider returned no embedding");
@@ -887,7 +771,7 @@ async function indexContent(options) {
887
771
  const {
888
772
  client,
889
773
  contentPath,
890
- embeddingOptions = {},
774
+ embeddingOptions,
891
775
  fileExtensions = [".md", ".markdown"],
892
776
  exclude = ["node_modules", ".git", "dist", "build"],
893
777
  tableName = "articles",
@@ -897,6 +781,9 @@ async function indexContent(options) {
897
781
  } = options;
898
782
  const database = resolveDatabase(client);
899
783
  const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
784
+ if (embeddingOptions === void 0) {
785
+ throw new TypeError("embeddingOptions is required");
786
+ }
900
787
  let files;
901
788
  try {
902
789
  files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
@@ -1176,7 +1063,7 @@ async function search(options) {
1176
1063
  query,
1177
1064
  limit = 10,
1178
1065
  tableName = "articles",
1179
- embeddingOptions = {},
1066
+ embeddingOptions,
1180
1067
  candidates,
1181
1068
  exact = false
1182
1069
  } = options;
@@ -1188,6 +1075,9 @@ async function search(options) {
1188
1075
  "embedding index name"
1189
1076
  );
1190
1077
  const candidateCount = normalizeSearchCandidates(candidates, resultLimit);
1078
+ if (embeddingOptions === void 0) {
1079
+ throw new TypeError("embeddingOptions is required");
1080
+ }
1191
1081
  const queryEmbedding = await generateEmbedding(query, {
1192
1082
  ...embeddingOptions,
1193
1083
  intent: embeddingOptions.intent ?? "query"
package/docs/API.md CHANGED
@@ -113,7 +113,7 @@ Indexes Markdown files from a directory on disk.
113
113
  interface IndexerOptions {
114
114
  client: Client | DatabaseAdapter;
115
115
  contentPath: string;
116
- embeddingOptions?: EmbeddingOptions;
116
+ embeddingOptions: EmbeddingOptions;
117
117
  fileExtensions?: string[];
118
118
  exclude?: string[];
119
119
  tableName?: string;
@@ -187,7 +187,16 @@ class IndexingError extends Error {
187
187
  import { indexContent, IndexingError } from "libsql-search";
188
188
 
189
189
  try {
190
- await indexContent({ client, contentPath: "./content" });
190
+ await indexContent({
191
+ client,
192
+ contentPath: "./content",
193
+ embeddingOptions: {
194
+ provider: "openai-compatible",
195
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
196
+ model: "bge-large-en-v1.5",
197
+ dimensions: 1024,
198
+ },
199
+ });
191
200
  } catch (error) {
192
201
  if (error instanceof IndexingError) {
193
202
  console.error(error.phase, error.failures);
@@ -212,7 +221,7 @@ interface SearchOptions {
212
221
  query: string;
213
222
  limit?: number;
214
223
  tableName?: string;
215
- embeddingOptions?: EmbeddingOptions;
224
+ embeddingOptions: EmbeddingOptions;
216
225
  candidates?: number;
217
226
  exact?: boolean;
218
227
  }
@@ -246,7 +255,7 @@ Controls how many rows the index returns for the exact re-rank. It must be an in
246
255
 
247
256
  ```ts
248
257
  // Trade query cost for recall on a large corpus
249
- const results = await search({ client, query, limit: 10, candidates: 200 });
258
+ const results = await search({ client, query, embeddingOptions, limit: 10, candidates: 200 });
250
259
  ```
251
260
 
252
261
  `candidates` has no effect when `exact` is `true` — that path scans every row — but it is **still validated**. `search({ exact: true, limit: 10, candidates: 5 })` throws, exactly as it would on the index path. Validity does not depend on which path a call happens to take.
@@ -262,7 +271,7 @@ Related exported constants:
262
271
  Set `exact: true` to bypass the index and score every row in the table.
263
272
 
264
273
  ```ts
265
- const results = await search({ client, query, exact: true });
274
+ const results = await search({ client, query, embeddingOptions, exact: true });
266
275
  ```
267
276
 
268
277
  This is the guaranteed-exact path: it computes `vector_distance_cos` for every row with a non-`NULL` embedding, sorts by `(distance, id)`, and trims to `limit`. Cost grows linearly with table size, so it is intended for small corpora, correctness checks against the index path, and tables that have no vector index.
@@ -330,8 +339,7 @@ All retrieval helpers validate `tableName` before executing SQL, and all of them
330
339
 
331
340
  ```ts
332
341
  interface EmbeddingOptions {
333
- provider?:
334
- | "local"
342
+ provider:
335
343
  | "cloudflare"
336
344
  | "mistral"
337
345
  | "gemini"
@@ -353,7 +361,7 @@ interface EmbeddingOptions {
353
361
 
354
362
  Important option rules:
355
363
 
356
- - `provider` defaults to `local`
364
+ - `provider` is required; every provider is an external service
357
365
  - `maxLength` defaults to `8000`
358
366
  - `timeoutMs` defaults to `30000`
359
367
  - `model` is only used by `openai-compatible`
@@ -364,7 +372,6 @@ Important option rules:
364
372
 
365
373
  Dimension rules:
366
374
 
367
- - local: fixed `384`
368
375
  - Cloudflare: fixed `1024`
369
376
  - Mistral: fixed `1024`
370
377
  - Gemini: default `3072`, allowed integer range `128-3072`
@@ -373,7 +380,7 @@ Dimension rules:
373
380
 
374
381
  See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.
375
382
 
376
- ### `generateEmbedding(text, options?)`
383
+ ### `generateEmbedding(text, options)`
377
384
 
378
385
  Generates one embedding vector.
379
386
 
@@ -385,15 +392,15 @@ const embedding = await generateEmbedding("deploy docs", {
385
392
  });
386
393
  ```
387
394
 
388
- ### `generateEmbeddings(texts, options?)`
395
+ ### `generateEmbeddings(texts, options)`
389
396
 
390
397
  Generates an ordered batch of embeddings.
391
398
 
392
- - empty batches return `[]` without loading the local model or making a hosted call
399
+ - empty batches return `[]` without configuring credentials or making a provider call
393
400
  - OpenAI batches above `2048` inputs are rejected before network work
394
401
  - `openai-compatible` batches are chunked sequentially according to `batchSize`
395
402
 
396
- ### `createEmbeddingProvider(options?)`
403
+ ### `createEmbeddingProvider(options)`
397
404
 
398
405
  Creates a provider client with immutable metadata and an `embed(texts, options?)` method.
399
406
 
@@ -413,7 +420,7 @@ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers
413
420
 
414
421
  Hosted provider clients are scoped to their current options. The library does not reuse a Cloudflare, Mistral, Gemini, or OpenAI client across different credential sets or configurations.
415
422
 
416
- ### `getEmbeddingProviderMetadata(options?)`
423
+ ### `getEmbeddingProviderMetadata(options)`
417
424
 
418
425
  Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.
419
426
 
@@ -422,7 +429,6 @@ Metadata shape:
422
429
  ```ts
423
430
  interface EmbeddingProviderMetadata {
424
431
  name:
425
- | "local"
426
432
  | "cloudflare"
427
433
  | "mistral"
428
434
  | "gemini"
@@ -451,7 +457,6 @@ Batch interpretation:
451
457
  interface EmbeddingBatchResult {
452
458
  embeddings: number[][];
453
459
  provider:
454
- | "local"
455
460
  | "cloudflare"
456
461
  | "mistral"
457
462
  | "gemini"
@@ -474,7 +479,7 @@ Validates provider responses before they reach the database:
474
479
 
475
480
  ### `padEmbedding(embedding, targetDimensions)`
476
481
 
477
- Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows, but the current local provider uses its native `384` dimensions rather than padding by default.
482
+ Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows; provider adapters otherwise validate and preserve the vectors returned by their external service.
478
483
 
479
484
  ### `prepareTextForEmbedding(fields)`
480
485
 
package/docs/INDEXING.md CHANGED
@@ -18,13 +18,18 @@ The slug is derived from the file path relative to `contentPath`.
18
18
  `indexContent()` replaces the whole target table:
19
19
 
20
20
  ```ts
21
+ const embeddingOptions = {
22
+ provider: "openai-compatible" as const,
23
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
24
+ model: "bge-large-en-v1.5",
25
+ dimensions: 1024,
26
+ };
27
+
21
28
  await indexContent({
22
29
  client,
23
30
  contentPath: "./content",
24
- tableName: "articles_local_384",
25
- embeddingOptions: {
26
- provider: "local",
27
- },
31
+ tableName: "articles_bge_1024",
32
+ embeddingOptions,
28
33
  });
29
34
  ```
30
35
 
@@ -69,7 +74,7 @@ Both are governed by `failurePolicy` like any other build failure, so they abort
69
74
  import { indexContent, IndexingError } from "libsql-search";
70
75
 
71
76
  try {
72
- await indexContent({ client, contentPath: "./content" });
77
+ await indexContent({ client, contentPath: "./content", embeddingOptions });
73
78
  } catch (error) {
74
79
  if (error instanceof IndexingError) {
75
80
  for (const failure of error.failures) {
@@ -87,6 +92,7 @@ By default one bad file aborts the whole rebuild. To index everything that can b
87
92
  const result = await indexContent({
88
93
  client,
89
94
  contentPath: "./content",
95
+ embeddingOptions,
90
96
  failurePolicy: "skip",
91
97
  });
92
98
 
@@ -105,6 +111,7 @@ An empty source directory throws by default, because silently leaving stale rows
105
111
  await indexContent({
106
112
  client,
107
113
  contentPath: "./content",
114
+ embeddingOptions,
108
115
  allowEmptyIndex: true,
109
116
  });
110
117
  ```
@@ -173,7 +180,6 @@ Many projects wire indexing into a dedicated script and call it before their sit
173
180
 
174
181
  ## Runtime Notes
175
182
 
176
- - local embeddings may download and cache a model on the first run
177
183
  - Node users need `@libsql/client` installed alongside the package, at `^0.15.0 || ^0.17.0`; the packaged build is smoke-tested against both arms (`0.15.15` and `0.17.4`), which covers table and vector-index creation. `batch()` rollback behaves identically on both at the contract level, though its error text differs — see [Version differences](./TROUBLESHOOTING.md#libsqlclient-version-differences). Upgrading the client is not a prerequisite for upgrading this package. Deno/JSR users are not covered by that range and should pin the client themselves — see [Install](../README.md#install)
178
- - hosted providers send indexed or queried text to external services
184
+ - all embedding providers send indexed or queried text to external services; this library never loads a model in-process
179
185
  - the repository validates package build and `deno check`, but indexing still depends on filesystem access
@@ -55,13 +55,6 @@ These presets keep credentials out of the source file while making dimensions an
55
55
 
56
56
  ```ts
57
57
  const providerPresets = {
58
- local: {
59
- tableName: "articles_local_384",
60
- dimensions: 384,
61
- embeddingOptions: {
62
- provider: "local" as const,
63
- },
64
- },
65
58
  cloudflare: {
66
59
  tableName: "articles_cf_bgem3_1024",
67
60
  dimensions: 1024,
@@ -137,9 +130,13 @@ export const POST: APIRoute = async ({ request }) => {
137
130
  client,
138
131
  query,
139
132
  limit,
140
- tableName: "articles_local_384",
133
+ tableName: "articles_tei_1024",
141
134
  embeddingOptions: {
142
- provider: "local",
135
+ provider: "openai-compatible",
136
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
137
+ model: process.env.EMBEDDING_MODEL!,
138
+ dimensions: 1024,
139
+ apiKey: process.env.EMBEDDING_API_KEY,
143
140
  intent: "query",
144
141
  },
145
142
  });
@@ -162,14 +159,14 @@ const client = createClient({
162
159
  });
163
160
 
164
161
  export async function getStaticPaths() {
165
- const articles = await getAllArticles(client, "articles_local_384");
162
+ const articles = await getAllArticles(client, "articles_tei_1024");
166
163
 
167
164
  return articles.map((article) => ({
168
165
  params: { slug: article.slug },
169
166
  }));
170
167
  }
171
168
 
172
- const article = await getArticleBySlug(client, "guides/getting-started", "articles_local_384");
169
+ const article = await getArticleBySlug(client, "guides/getting-started", "articles_tei_1024");
173
170
  ```
174
171
 
175
172
  ## Next.js Route Handler
@@ -191,9 +188,13 @@ export async function POST(request: NextRequest) {
191
188
  client,
192
189
  query,
193
190
  limit,
194
- tableName: "articles_local_384",
191
+ tableName: "articles_tei_1024",
195
192
  embeddingOptions: {
196
- provider: "local",
193
+ provider: "openai-compatible",
194
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
195
+ model: process.env.EMBEDDING_MODEL!,
196
+ dimensions: 1024,
197
+ apiKey: process.env.EMBEDDING_API_KEY,
197
198
  intent: "query",
198
199
  },
199
200
  });
@@ -214,7 +215,7 @@ const client = createClient({
214
215
  });
215
216
 
216
217
  export async function generateStaticParams() {
217
- const articles = await getAllArticles(client, "articles_local_384");
218
+ const articles = await getAllArticles(client, "articles_tei_1024");
218
219
 
219
220
  return articles.map((article) => ({
220
221
  slug: article.slug,
@@ -223,7 +224,7 @@ export async function generateStaticParams() {
223
224
 
224
225
  export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
225
226
  const { slug } = await params;
226
- const article = await getArticleBySlug(client, slug, "articles_local_384");
227
+ const article = await getArticleBySlug(client, slug, "articles_tei_1024");
227
228
 
228
229
  return <article>{article?.title}</article>;
229
230
  }
@@ -241,13 +242,6 @@ const client = createClient({
241
242
  });
242
243
 
243
244
  const providerPresets = {
244
- local: {
245
- tableName: "articles_local_384",
246
- dimensions: 384,
247
- embeddingOptions: {
248
- provider: "local" as const,
249
- },
250
- },
251
245
  cloudflare: {
252
246
  tableName: "articles_cf_bgem3_1024",
253
247
  dimensions: 1024,
@@ -299,7 +293,7 @@ const providerPresets = {
299
293
  },
300
294
  } as const;
301
295
 
302
- const provider = process.env.EMBEDDING_PROVIDER ?? "local";
296
+ const provider = process.env.EMBEDDING_PROVIDER ?? "openai-compatible";
303
297
 
304
298
  if (!(provider in providerPresets)) {
305
299
  throw new Error(
@@ -349,4 +343,4 @@ await generateEmbeddings(["doc one", "doc two"], {
349
343
  });
350
344
  ```
351
345
 
352
- The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for local model mocks, Gemini SDK mocks, and validation-before-network assertions.
346
+ The repository test suite should not require real provider credentials. See [Testing guidance](./TESTING.md) for embedding-service mocks, Gemini SDK mocks, and validation-before-network assertions.
@@ -27,7 +27,7 @@ References:
27
27
 
28
28
  In practice, this means:
29
29
 
30
- - `384 local` and `1024 Mistral` can never share a table
30
+ - a legacy `384` embedding space and `1024 Mistral` can never share a table
31
31
  - `1024 Cloudflare` and `1024 Mistral` still need separate rebuilds because equal width does not make the vectors compatible
32
32
  - a custom endpoint change at the same width still needs a new table because the model or serving stack may have changed
33
33
 
@@ -84,14 +84,14 @@ Re-running `createTable()` with the table's existing width is the fix. It is ide
84
84
 
85
85
  ```ts
86
86
  // Same name and same width as the existing table
87
- await createTable(client, "articles_local_384", 384);
87
+ await createTable(client, "articles_legacy_384", 384);
88
88
  ```
89
89
 
90
90
  Equivalently, in SQL:
91
91
 
92
92
  ```sql
93
- CREATE INDEX IF NOT EXISTS "articles_local_384_embedding_idx"
94
- ON "articles_local_384"(libsql_vector_idx(embedding));
93
+ CREATE INDEX IF NOT EXISTS "articles_legacy_384_embedding_idx"
94
+ ON "articles_legacy_384"(libsql_vector_idx(embedding));
95
95
  ```
96
96
 
97
97
  Reindexing does not create the index; `indexContent()` only replaces rows. Any new table created by `createTable()` as part of a migration already has it, so this applies only to pre-existing tables you are carrying forward. Until the index exists, `search({ ..., exact: true })` keeps queries working on the exact full-scan path.
@@ -100,7 +100,7 @@ Reindexing does not create the index; `indexContent()` only replaces rows. Any n
100
100
 
101
101
  | From | To | Why a rebuild is required | Recommended table move |
102
102
  | --- | --- | --- | --- |
103
- | Legacy padded local `768` | Native local `384` | Old tables stored `384` model values plus zero padding; current local provider is a native `384`-dimension space | Build into `articles_local_384`, validate, then retire the legacy table |
103
+ | Legacy in-process Transformers.js index | Any external provider | The runtime and provider were removed; every replacement service has its own embedding space | Build a parallel table named for the external provider/model, validate, then retire the legacy table |
104
104
  | Any `768` space | Any `1024` space | Width changes from `F32_BLOB(768)` to `F32_BLOB(1024)` | Create a new `1024` table and reindex |
105
105
  | Cloudflare `1024` | Mistral `1024` | Width stays the same, but provider/model space changes | Use a parallel `1024` table such as `articles_mistral_1024` |
106
106
  | Mistral `1024` | Cloudflare `1024` | Same reason in reverse | Use a parallel `1024` table such as `articles_cf_bgem3_1024` |
@@ -111,21 +111,21 @@ Reindexing does not create the index; `indexContent()` only replaces rows. Any n
111
111
 
112
112
  ## Scenario Notes
113
113
 
114
- ### Legacy Local `768` To Native Local `384`
114
+ ### Legacy In-Process Embeddings To An External Service
115
115
 
116
- Earlier local migrations sometimes relied on zero padding to fit a `768`-wide table. The current local adapter emits the model's native `384` dimensions and rejects any other local dimension count.
116
+ Versions that exposed the in-process Transformers.js provider produced a separate embedding space that this release can no longer query. Choose an external provider or separately deployed OpenAI-compatible service and rebuild every vector into a new table.
117
117
 
118
118
  Safe path:
119
119
 
120
120
  ```ts
121
- await createTable(client, "articles_local_384", 384);
121
+ await createTable(client, "articles_bge_1024", 1024);
122
122
  ```
123
123
 
124
- Reindex into `articles_local_384`; do not keep writing new local vectors into the legacy padded table.
124
+ Reindex into `articles_bge_1024` with the external service configuration; do not mix its vectors with the legacy table.
125
125
 
126
126
  ### `768` To `1024`
127
127
 
128
- Any move from `768` dimensions to `1024` dimensions changes the schema width. Examples include a legacy local table moving to Cloudflare or Mistral.
128
+ Any move from `768` dimensions to `1024` dimensions changes the schema width. Examples include a legacy table moving to Cloudflare, Mistral, or a 1024-dimensional OpenAI-compatible service.
129
129
 
130
130
  ```ts
131
131
  await createTable(client, "articles_mistral_1024", 1024);
package/docs/PROVIDERS.md CHANGED
@@ -2,9 +2,8 @@
2
2
 
3
3
  Use this page to choose an embedding provider, confirm the table width it needs, and understand what crosses a network boundary.
4
4
 
5
- `libsql-search` supports these provider values:
5
+ `libsql-search` only talks to external embedding services; it never loads or hosts an embedding model in-process. It supports these provider values:
6
6
 
7
- - `local`
8
7
  - `cloudflare`
9
8
  - `mistral`
10
9
  - `gemini`
@@ -17,8 +16,7 @@ All providers share the same `EmbeddingOptions` surface:
17
16
 
18
17
  ```ts
19
18
  interface EmbeddingOptions {
20
- provider?:
21
- | "local"
19
+ provider:
22
20
  | "cloudflare"
23
21
  | "mistral"
24
22
  | "gemini"
@@ -40,7 +38,7 @@ interface EmbeddingOptions {
40
38
 
41
39
  Shared defaults and rules:
42
40
 
43
- - `provider` defaults to `local`
41
+ - `provider` is required; there is no implicit embedding runtime or service
44
42
  - `maxLength` defaults to `8000` code units
45
43
  - `timeoutMs` defaults to `30000`
46
44
  - `indexContent()` defaults to `intent: "document"`
@@ -59,7 +57,6 @@ The `model` option is only used by `openai-compatible`.
59
57
 
60
58
  | Provider | Literal | Upstream model used by this adapter | Dimensions | Credentials | Batching | Network and privacy boundary | Cost and table planning |
61
59
  | --- | --- | --- | --- | --- | --- | --- | --- |
62
- | Local | `local` | `Xenova/all-MiniLM-L6-v2` | Fixed `384` | None | Sequential in-process | No hosted API call. First use may download model artifacts and cache them locally. | No hosted API bill. Table must be `F32_BLOB(384)`. |
63
60
  | Cloudflare Workers AI | `cloudflare` | `@cf/baai/bge-m3` | Fixed `1024` | `accountId` and `apiToken`, or `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` | Native batch in one request | Indexed and queried text is sent to Cloudflare. | Check Cloudflare pricing before large rebuilds. Table must be `F32_BLOB(1024)`. |
64
61
  | Mistral | `mistral` | `mistral-embed` | Fixed `1024` | `apiKey`, or `MISTRAL_API_KEY` | Native batch in one request | Indexed and queried text is sent to Mistral. | Check Mistral pricing before rebuilds. Table must be `F32_BLOB(1024)`. |
65
62
  | Gemini | `gemini` | `gemini-embedding-2` | Default `3072`; allowed integers `128-3072` | `apiKey`, or `GEMINI_API_KEY` | Sequential SDK request per input | Indexed and queried text is sent to Google. The adapter currently rewrites payload text by intent. | Check Gemini pricing before rebuilds. Table width must match the chosen dimension count exactly. |
@@ -68,23 +65,6 @@ The `model` option is only used by `openai-compatible`.
68
65
 
69
66
  ## Provider Notes
70
67
 
71
- ### Local
72
-
73
- ```ts
74
- embeddingOptions: {
75
- provider: "local",
76
- }
77
- ```
78
-
79
- - fixed at `384` dimensions
80
- - rejects any other `dimensions` value before loading the runtime
81
- - uses `@huggingface/transformers` lazily and caches the local pipeline by model name
82
-
83
- References:
84
-
85
- - [Transformers.js in Node.js](https://huggingface.co/docs/transformers.js/en/tutorials/node)
86
- - [Transformers.js environment and cache controls](https://huggingface.co/docs/transformers.js/en/api/env)
87
-
88
68
  ### Cloudflare Workers AI
89
69
 
90
70
  ```ts
package/docs/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory holds the longer-form reference material for `libsql-search`.
4
4
 
5
- - [Provider selection and configuration](./PROVIDERS.md): compare local, hosted, and custom embedding providers before you build an index
5
+ - [Provider selection and configuration](./PROVIDERS.md): compare external and custom embedding providers before you build an index
6
6
  - [Integration examples](./INTEGRATIONS.md): reusable provider flow plus Astro and Next.js examples
7
7
  - [Migration and reindexing guide](./MIGRATIONS.md): table-width changes, provider/model swaps, and safe cutovers
8
8
  - [API reference](./API.md): exported functions, option shapes, and result data
package/docs/TESTING.md CHANGED
@@ -9,30 +9,31 @@ Routine unit tests and CI should not make live embedding-provider calls and shou
9
9
  - validate option handling and response parsing with mocks first
10
10
  - assert failures happen before network calls when configuration is invalid
11
11
 
12
- The current test suite follows that pattern in `tests/embeddings.test.ts` and [`tests/huggingface-transformers.mock.ts`](../tests/huggingface-transformers.mock.ts).
12
+ The current test suite follows that pattern in `tests/embeddings.test.ts` and [`tests/embedding-service.mock.ts`](../tests/embedding-service.mock.ts).
13
13
 
14
- ## Local Provider Mocks
14
+ ## Shared Embedding Service Mock
15
15
 
16
- The local provider should use a lightweight Transformers.js mock instead of downloading the real model during routine tests.
16
+ Indexer, search, and database tests use a deterministic OpenAI-compatible service mock. This keeps the tests on the same external-service boundary as production without making network calls.
17
17
 
18
18
  ```ts
19
19
  import {
20
- huggingFaceTransformersMock,
21
- resetHuggingFaceTransformersMock,
22
- } from "./huggingface-transformers.mock.js";
20
+ embeddingServiceMock,
21
+ resetEmbeddingServiceMock,
22
+ TEST_EMBEDDING_OPTIONS,
23
+ } from "./embedding-service.mock.js";
23
24
 
24
25
  beforeEach(() => {
25
- resetHuggingFaceTransformersMock();
26
+ resetEmbeddingServiceMock();
26
27
  });
27
28
  ```
28
29
 
29
- The repository source file is `huggingface-transformers.mock.ts`. The example keeps the `.js` import suffix because this repo's ESM TypeScript source uses explicit `.js` relative imports that resolve after compilation.
30
+ The example keeps the `.js` import suffix because this repo's ESM TypeScript source uses explicit `.js` relative imports that resolve after compilation.
30
31
 
31
32
  Test the contract you care about:
32
33
 
33
- - the library requests `Xenova/all-MiniLM-L6-v2`
34
- - the call uses `pooling: "mean"` and `normalize: true`
35
- - non-`384` local dimensions fail before runtime loading
34
+ - indexing and querying use the same endpoint, model, and dimensions
35
+ - provider failures remain classified as build-stage failures
36
+ - queued deterministic vectors exercise exact ranking and tie behavior
36
37
 
37
38
  ## HTTP Provider Mocks
38
39
 
@@ -116,9 +117,9 @@ Apply the same pattern to `GEMINI_API_KEY`, `MISTRAL_API_KEY`, `CLOUDFLARE_ACCOU
116
117
  Prefer tests that prove bad inputs fail locally:
117
118
 
118
119
  - unknown provider
120
+ - missing provider
119
121
  - missing provider credentials
120
122
  - blank credentials where trimming is expected
121
- - invalid local dimensions
122
123
  - invalid Gemini dimensions
123
124
  - invalid `openai-compatible` `baseUrl`
124
125
  - invalid `openai-compatible` `batchSize`
@@ -1,9 +1,5 @@
1
1
  # Troubleshooting
2
2
 
3
- Use the page that matches the failure mode:
4
-
5
- - [Sharp native module issues](./TROUBLESHOOTING-SHARP.md)
6
-
7
3
  Common operational checks:
8
4
 
9
5
  - verify you called `createTable()` before indexing or searching
@@ -15,8 +11,6 @@ Common operational checks:
15
11
  - after upgrading an existing Gemini index, fully re-embed with
16
12
  `gemini-embedding-2`; for 3072-dimensional Gemini indexes, recreate the table
17
13
  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
20
14
  - if `search()` reports that the `<tableName>_embedding_idx` vector index could
21
15
  not be used, the table has no embedding vector index: re-run `createTable()`
22
16
  with the table's existing name and width to add it without touching rows, or
package/docs/TURSO.md CHANGED
@@ -44,19 +44,26 @@ import { createTable, indexContent, search } from "libsql-search";
44
44
  const database = await connect("./local.db");
45
45
  const client = tursoAdapter(database);
46
46
 
47
- await createTable(client, "articles", 384);
47
+ const embeddingOptions = {
48
+ provider: "openai-compatible" as const,
49
+ baseUrl: process.env.EMBEDDING_BASE_URL!,
50
+ model: "bge-large-en-v1.5",
51
+ dimensions: 1024,
52
+ };
53
+
54
+ await createTable(client, "articles", 1024);
48
55
 
49
56
  await indexContent({
50
57
  client,
51
58
  contentPath: "./content",
52
- embeddingOptions: { provider: "local" },
59
+ embeddingOptions,
53
60
  });
54
61
 
55
62
  const results = await search({
56
63
  client,
57
64
  query: "how do I deploy my docs site",
58
65
  limit: 5,
59
- embeddingOptions: { provider: "local" },
66
+ embeddingOptions,
60
67
  });
61
68
  ```
62
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.10.1",
3
+ "version": "0.11.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",
@@ -75,7 +75,6 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@huggingface/transformers": "4.2.0",
79
78
  "gray-matter": "^4.0.3"
80
79
  },
81
80
  "devDependencies": {
@@ -1,65 +0,0 @@
1
- # Troubleshooting: Transitive `sharp` Install Errors
2
-
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.
7
-
8
- This page exists because the error can show up before your application reaches
9
- any `libsql-search` code.
10
-
11
- ## Typical Error
12
-
13
- ```text
14
- Cannot find module '../build/Release/sharp-*.node'
15
- ```
16
-
17
- Or:
18
-
19
- ```text
20
- Error: Something went wrong installing the "sharp" module
21
- ```
22
-
23
- ## Why It Happens
24
-
25
- With pnpm, native packages may need explicit build-script approval. If the
26
- relevant install script is blocked, the native binary is never downloaded or
27
- built.
28
-
29
- ## What To Do
30
-
31
- First inspect which build scripts pnpm blocked:
32
-
33
- ```bash
34
- pnpm ignored-builds
35
- ```
36
-
37
- Then approve the package that is actually failing and reinstall:
38
-
39
- ```bash
40
- pnpm approve-builds
41
- pnpm install
42
- ```
43
-
44
- In the interactive `pnpm approve-builds` prompt, select `sharp` if that is the
45
- package reporting the native-module failure.
46
-
47
- For a committed repository-level fix, you can also allow the package explicitly
48
- in `pnpm-workspace.yaml` with `onlyBuiltDependencies`.
49
-
50
- ## Relation To `libsql-search`
51
-
52
- - local embeddings use `@huggingface/transformers`
53
- - the first local embedding run may download a model at runtime
54
- - that runtime model download is separate from a pnpm native-module install
55
- failure
56
-
57
- ## Verification
58
-
59
- After reinstalling, rerun the command that originally failed. If your app uses
60
- `sharp` directly, verify that import in your own project context.
61
-
62
- ## Additional Resources
63
-
64
- - [pnpm approve-builds](https://pnpm.io/10.x/cli/approve-builds)
65
- - [Sharp installation docs](https://sharp.pixelplumbing.com/install)