libsql-search 0.10.0 → 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 +18 -15
- package/dist/index.cjs +21 -128
- package/dist/index.d.ts +10 -10
- package/dist/index.esm.js +21 -128
- package/docs/API.md +22 -17
- package/docs/INDEXING.md +13 -7
- package/docs/INTEGRATIONS.md +18 -24
- package/docs/MIGRATIONS.md +10 -10
- package/docs/PROVIDERS.md +3 -23
- package/docs/README.md +1 -1
- package/docs/TESTING.md +13 -12
- package/docs/TROUBLESHOOTING.md +0 -6
- package/docs/TURSO.md +10 -3
- package/package.json +1 -2
- package/docs/TROUBLESHOOTING-SHARP.md +0 -65
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
|
|
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
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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);
|
|
@@ -1017,11 +904,13 @@ async function buildDocument(file, embeddingOptions) {
|
|
|
1017
904
|
return { ok: false, stage: "parse", error: toError(error) };
|
|
1018
905
|
}
|
|
1019
906
|
let embedding;
|
|
907
|
+
let serializedEmbedding;
|
|
1020
908
|
try {
|
|
1021
909
|
embedding = await generateEmbedding(parsed.embeddingText, {
|
|
1022
910
|
...embeddingOptions,
|
|
1023
911
|
intent: embeddingOptions.intent ?? "document"
|
|
1024
912
|
});
|
|
913
|
+
serializedEmbedding = JSON.stringify(embedding);
|
|
1025
914
|
} catch (error) {
|
|
1026
915
|
return { ok: false, stage: "embed", error: toError(error) };
|
|
1027
916
|
}
|
|
@@ -1035,6 +924,7 @@ async function buildDocument(file, embeddingOptions) {
|
|
|
1035
924
|
tags: parsed.tags,
|
|
1036
925
|
serializedTags: parsed.serializedTags,
|
|
1037
926
|
embedding,
|
|
927
|
+
serializedEmbedding,
|
|
1038
928
|
metadata: parsed.metadata
|
|
1039
929
|
}
|
|
1040
930
|
};
|
|
@@ -1119,7 +1009,7 @@ function createInsertStatement(document, quotedTableName) {
|
|
|
1119
1009
|
document.content,
|
|
1120
1010
|
document.folder,
|
|
1121
1011
|
document.serializedTags,
|
|
1122
|
-
|
|
1012
|
+
document.serializedEmbedding
|
|
1123
1013
|
]
|
|
1124
1014
|
};
|
|
1125
1015
|
}
|
|
@@ -1175,7 +1065,7 @@ async function search(options) {
|
|
|
1175
1065
|
query,
|
|
1176
1066
|
limit = 10,
|
|
1177
1067
|
tableName = "articles",
|
|
1178
|
-
embeddingOptions
|
|
1068
|
+
embeddingOptions,
|
|
1179
1069
|
candidates,
|
|
1180
1070
|
exact = false
|
|
1181
1071
|
} = options;
|
|
@@ -1187,6 +1077,9 @@ async function search(options) {
|
|
|
1187
1077
|
"embedding index name"
|
|
1188
1078
|
);
|
|
1189
1079
|
const candidateCount = normalizeSearchCandidates(candidates, resultLimit);
|
|
1080
|
+
if (embeddingOptions === void 0) {
|
|
1081
|
+
throw new TypeError("embeddingOptions is required");
|
|
1082
|
+
}
|
|
1190
1083
|
const queryEmbedding = await generateEmbedding(query, {
|
|
1191
1084
|
...embeddingOptions,
|
|
1192
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
|
|
6
|
-
*
|
|
5
|
+
* Supports Gemini, OpenAI, Mistral, Cloudflare Workers AI, and custom
|
|
6
|
+
* OpenAI-compatible endpoints.
|
|
7
7
|
*/
|
|
8
|
-
type EmbeddingProvider = '
|
|
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
|
|
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
|
|
57
|
-
declare function getEmbeddingProviderMetadata(options
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
@@ -1015,11 +902,13 @@ async function buildDocument(file, embeddingOptions) {
|
|
|
1015
902
|
return { ok: false, stage: "parse", error: toError(error) };
|
|
1016
903
|
}
|
|
1017
904
|
let embedding;
|
|
905
|
+
let serializedEmbedding;
|
|
1018
906
|
try {
|
|
1019
907
|
embedding = await generateEmbedding(parsed.embeddingText, {
|
|
1020
908
|
...embeddingOptions,
|
|
1021
909
|
intent: embeddingOptions.intent ?? "document"
|
|
1022
910
|
});
|
|
911
|
+
serializedEmbedding = JSON.stringify(embedding);
|
|
1023
912
|
} catch (error) {
|
|
1024
913
|
return { ok: false, stage: "embed", error: toError(error) };
|
|
1025
914
|
}
|
|
@@ -1033,6 +922,7 @@ async function buildDocument(file, embeddingOptions) {
|
|
|
1033
922
|
tags: parsed.tags,
|
|
1034
923
|
serializedTags: parsed.serializedTags,
|
|
1035
924
|
embedding,
|
|
925
|
+
serializedEmbedding,
|
|
1036
926
|
metadata: parsed.metadata
|
|
1037
927
|
}
|
|
1038
928
|
};
|
|
@@ -1117,7 +1007,7 @@ function createInsertStatement(document, quotedTableName) {
|
|
|
1117
1007
|
document.content,
|
|
1118
1008
|
document.folder,
|
|
1119
1009
|
document.serializedTags,
|
|
1120
|
-
|
|
1010
|
+
document.serializedEmbedding
|
|
1121
1011
|
]
|
|
1122
1012
|
};
|
|
1123
1013
|
}
|
|
@@ -1173,7 +1063,7 @@ async function search(options) {
|
|
|
1173
1063
|
query,
|
|
1174
1064
|
limit = 10,
|
|
1175
1065
|
tableName = "articles",
|
|
1176
|
-
embeddingOptions
|
|
1066
|
+
embeddingOptions,
|
|
1177
1067
|
candidates,
|
|
1178
1068
|
exact = false
|
|
1179
1069
|
} = options;
|
|
@@ -1185,6 +1075,9 @@ async function search(options) {
|
|
|
1185
1075
|
"embedding index name"
|
|
1186
1076
|
);
|
|
1187
1077
|
const candidateCount = normalizeSearchCandidates(candidates, resultLimit);
|
|
1078
|
+
if (embeddingOptions === void 0) {
|
|
1079
|
+
throw new TypeError("embeddingOptions is required");
|
|
1080
|
+
}
|
|
1188
1081
|
const queryEmbedding = await generateEmbedding(query, {
|
|
1189
1082
|
...embeddingOptions,
|
|
1190
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
|
|
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({
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
-
|
|
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
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -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: "
|
|
133
|
+
tableName: "articles_tei_1024",
|
|
141
134
|
embeddingOptions: {
|
|
142
|
-
provider: "
|
|
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, "
|
|
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", "
|
|
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: "
|
|
191
|
+
tableName: "articles_tei_1024",
|
|
195
192
|
embeddingOptions: {
|
|
196
|
-
provider: "
|
|
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, "
|
|
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, "
|
|
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 ?? "
|
|
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
|
|
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.
|
package/docs/MIGRATIONS.md
CHANGED
|
@@ -27,7 +27,7 @@ References:
|
|
|
27
27
|
|
|
28
28
|
In practice, this means:
|
|
29
29
|
|
|
30
|
-
- `384
|
|
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, "
|
|
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 "
|
|
94
|
-
ON "
|
|
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
|
|
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
|
|
114
|
+
### Legacy In-Process Embeddings To An External Service
|
|
115
115
|
|
|
116
|
-
|
|
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, "
|
|
121
|
+
await createTable(client, "articles_bge_1024", 1024);
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
Reindex into `
|
|
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
|
|
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`
|
|
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
|
|
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/
|
|
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
|
-
##
|
|
14
|
+
## Shared Embedding Service Mock
|
|
15
15
|
|
|
16
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
embeddingServiceMock,
|
|
21
|
+
resetEmbeddingServiceMock,
|
|
22
|
+
TEST_EMBEDDING_OPTIONS,
|
|
23
|
+
} from "./embedding-service.mock.js";
|
|
23
24
|
|
|
24
25
|
beforeEach(() => {
|
|
25
|
-
|
|
26
|
+
resetEmbeddingServiceMock();
|
|
26
27
|
});
|
|
27
28
|
```
|
|
28
29
|
|
|
29
|
-
The
|
|
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
|
|
34
|
-
-
|
|
35
|
-
-
|
|
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`
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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)
|