libsql-search 0.2.3 → 0.3.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 +6 -4
- package/dist/index.cjs +115 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +115 -0
- package/docs/API.md +10 -3
- package/docs/INTEGRATIONS.md +7 -3
- package/docs/PROVIDERS.md +45 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,16 +13,16 @@ Use it when you want:
|
|
|
13
13
|
|
|
14
14
|
- a small TypeScript library instead of a hosted search product
|
|
15
15
|
- one search index shared across static-site builds and app routes
|
|
16
|
-
- local or
|
|
16
|
+
- local or hosted embeddings behind the same indexing/search API
|
|
17
17
|
- direct control over table names, dimensions, content shape, and deployment
|
|
18
18
|
|
|
19
19
|
## What It Supports
|
|
20
20
|
|
|
21
21
|
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
|
-
- Embedding providers
|
|
24
|
-
|
|
25
|
-
`text-embedding-3-small`
|
|
23
|
+
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
|
|
24
|
+
`@cf/baai/bge-m3`, Google Gemini `text-embedding-004`, and OpenAI
|
|
25
|
+
`text-embedding-3-small` / `text-embedding-3-large`
|
|
26
26
|
- npm distribution plus JSR publishing
|
|
27
27
|
|
|
28
28
|
## Install
|
|
@@ -97,6 +97,8 @@ Important behavior:
|
|
|
97
97
|
- Call `createTable()` before indexing or searching.
|
|
98
98
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
99
99
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
100
|
+
- `local` is the default offline provider; Cloudflare is the recommended hosted
|
|
101
|
+
option and uses 1024 dimensions.
|
|
100
102
|
|
|
101
103
|
## Core API
|
|
102
104
|
|
package/dist/index.cjs
CHANGED
|
@@ -12,6 +12,8 @@ const GEMINI_MODEL = "text-embedding-004";
|
|
|
12
12
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
13
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
14
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
15
|
+
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
16
|
+
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
15
17
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
16
18
|
function getEnvironmentVariable(name) {
|
|
17
19
|
const runtime = globalThis;
|
|
@@ -51,6 +53,7 @@ function resolveProviderName(provider) {
|
|
|
51
53
|
case "local":
|
|
52
54
|
case "gemini":
|
|
53
55
|
case "openai":
|
|
56
|
+
case "cloudflare":
|
|
54
57
|
return provider ?? "local";
|
|
55
58
|
default:
|
|
56
59
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -71,6 +74,10 @@ function truncateTexts(texts, maxLength) {
|
|
|
71
74
|
function getOpenAIModel(dimensions) {
|
|
72
75
|
return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
|
|
73
76
|
}
|
|
77
|
+
function getOptionalTrimmedCredential(value) {
|
|
78
|
+
const trimmed = value?.trim();
|
|
79
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
80
|
+
}
|
|
74
81
|
function hasOwnProperty(value, property) {
|
|
75
82
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
76
83
|
}
|
|
@@ -97,6 +104,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
|
|
|
97
104
|
function getResponseHeader(response, name) {
|
|
98
105
|
return response.headers.get(name) ?? void 0;
|
|
99
106
|
}
|
|
107
|
+
function getSafeResponseHeader(response, name) {
|
|
108
|
+
const value = getResponseHeader(response, name)?.trim();
|
|
109
|
+
if (!value) {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
|
|
113
|
+
}
|
|
114
|
+
function createCloudflareEmbeddingsUrl(accountId) {
|
|
115
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
116
|
+
}
|
|
100
117
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
101
118
|
if (parentSignal?.aborted) {
|
|
102
119
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -225,12 +242,28 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
225
242
|
dimensions,
|
|
226
243
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
227
244
|
});
|
|
245
|
+
case "cloudflare":
|
|
246
|
+
return Object.freeze({
|
|
247
|
+
name: "cloudflare",
|
|
248
|
+
model: CLOUDFLARE_MODEL,
|
|
249
|
+
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
250
|
+
batch: Object.freeze({ mode: "native" })
|
|
251
|
+
});
|
|
228
252
|
}
|
|
229
253
|
}
|
|
230
254
|
function getEffectiveDimensions(provider, dimensions) {
|
|
231
255
|
if (provider === "gemini") {
|
|
232
256
|
return DEFAULT_DIMENSIONS;
|
|
233
257
|
}
|
|
258
|
+
if (provider === "cloudflare") {
|
|
259
|
+
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
260
|
+
throw providerError(
|
|
261
|
+
"cloudflare",
|
|
262
|
+
`@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return CLOUDFLARE_DIMENSIONS;
|
|
266
|
+
}
|
|
234
267
|
return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
|
|
235
268
|
}
|
|
236
269
|
function assertBatchSize(metadata, count) {
|
|
@@ -394,6 +427,73 @@ class OpenAIEmbeddingProvider {
|
|
|
394
427
|
);
|
|
395
428
|
}
|
|
396
429
|
}
|
|
430
|
+
class CloudflareEmbeddingProvider {
|
|
431
|
+
metadata;
|
|
432
|
+
#accountId;
|
|
433
|
+
#apiToken;
|
|
434
|
+
#timeoutMs;
|
|
435
|
+
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
436
|
+
this.metadata = metadata;
|
|
437
|
+
this.#accountId = accountId;
|
|
438
|
+
this.#apiToken = apiToken;
|
|
439
|
+
this.#timeoutMs = timeoutMs;
|
|
440
|
+
}
|
|
441
|
+
async embed(texts, options = {}) {
|
|
442
|
+
const intent = resolveIntent(options.intent);
|
|
443
|
+
if (texts.length === 0) {
|
|
444
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
445
|
+
}
|
|
446
|
+
assertBatchSize(this.metadata, texts.length);
|
|
447
|
+
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
448
|
+
const items = await withTimeout(
|
|
449
|
+
"cloudflare",
|
|
450
|
+
"API request",
|
|
451
|
+
this.#timeoutMs,
|
|
452
|
+
async (signal) => {
|
|
453
|
+
const response = await fetch(endpoint, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: {
|
|
456
|
+
"Content-Type": "application/json",
|
|
457
|
+
"Authorization": `Bearer ${this.#apiToken}`
|
|
458
|
+
},
|
|
459
|
+
signal,
|
|
460
|
+
body: JSON.stringify({
|
|
461
|
+
model: this.metadata.model,
|
|
462
|
+
input: texts
|
|
463
|
+
})
|
|
464
|
+
});
|
|
465
|
+
if (!response.ok) {
|
|
466
|
+
const cfRay = getSafeResponseHeader(response, "cf-ray");
|
|
467
|
+
const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
|
|
468
|
+
throw new Error(context);
|
|
469
|
+
}
|
|
470
|
+
const data = await response.json();
|
|
471
|
+
if (!Array.isArray(data.data)) {
|
|
472
|
+
throw new Error("Cloudflare response did not include a data array");
|
|
473
|
+
}
|
|
474
|
+
return data.data.map((item) => {
|
|
475
|
+
const typed = item;
|
|
476
|
+
const embedding = typed.embedding;
|
|
477
|
+
if (!Array.isArray(embedding)) {
|
|
478
|
+
throw new Error("Cloudflare response item did not include an embedding array");
|
|
479
|
+
}
|
|
480
|
+
const parsed = { embedding };
|
|
481
|
+
if (hasOwnProperty(typed, "index")) {
|
|
482
|
+
parsed.index = typed.index;
|
|
483
|
+
}
|
|
484
|
+
return parsed;
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
options.signal,
|
|
488
|
+
[this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
|
|
489
|
+
);
|
|
490
|
+
return createEmbeddingBatchResult(
|
|
491
|
+
this.metadata,
|
|
492
|
+
intent,
|
|
493
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
397
497
|
function createEmbeddingProvider(options = {}) {
|
|
398
498
|
const provider = resolveProviderName(options.provider);
|
|
399
499
|
const metadata = getEmbeddingProviderMetadata(options);
|
|
@@ -415,6 +515,21 @@ function createEmbeddingProvider(options = {}) {
|
|
|
415
515
|
}
|
|
416
516
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
417
517
|
}
|
|
518
|
+
case "cloudflare": {
|
|
519
|
+
const accountId = getOptionalTrimmedCredential(
|
|
520
|
+
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
|
521
|
+
);
|
|
522
|
+
const apiToken = getOptionalTrimmedCredential(
|
|
523
|
+
options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
|
|
524
|
+
);
|
|
525
|
+
if (!accountId) {
|
|
526
|
+
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
|
|
527
|
+
}
|
|
528
|
+
if (!apiToken) {
|
|
529
|
+
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
530
|
+
}
|
|
531
|
+
return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
|
|
532
|
+
}
|
|
418
533
|
}
|
|
419
534
|
}
|
|
420
535
|
function getEmbeddingProviderMetadata(options = {}) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client } from '@libsql/client';
|
|
2
2
|
|
|
3
|
-
type EmbeddingProvider = 'local' | 'gemini' | 'openai';
|
|
3
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'cloudflare';
|
|
4
4
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
5
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
6
|
interface EmbeddingBatchBehavior {
|
|
@@ -31,6 +31,8 @@ interface EmbeddingBatchResult {
|
|
|
31
31
|
interface EmbeddingOptions {
|
|
32
32
|
provider?: EmbeddingProvider;
|
|
33
33
|
apiKey?: string;
|
|
34
|
+
accountId?: string;
|
|
35
|
+
apiToken?: string;
|
|
34
36
|
dimensions?: number;
|
|
35
37
|
maxLength?: number;
|
|
36
38
|
intent?: EmbeddingIntent;
|
package/dist/index.esm.js
CHANGED
|
@@ -10,6 +10,8 @@ const GEMINI_MODEL = "text-embedding-004";
|
|
|
10
10
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
11
11
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
12
12
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
13
|
+
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
14
|
+
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
13
15
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
14
16
|
function getEnvironmentVariable(name) {
|
|
15
17
|
const runtime = globalThis;
|
|
@@ -49,6 +51,7 @@ function resolveProviderName(provider) {
|
|
|
49
51
|
case "local":
|
|
50
52
|
case "gemini":
|
|
51
53
|
case "openai":
|
|
54
|
+
case "cloudflare":
|
|
52
55
|
return provider ?? "local";
|
|
53
56
|
default:
|
|
54
57
|
throw new Error(`Unknown embedding provider: ${String(provider)}`);
|
|
@@ -69,6 +72,10 @@ function truncateTexts(texts, maxLength) {
|
|
|
69
72
|
function getOpenAIModel(dimensions) {
|
|
70
73
|
return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
|
|
71
74
|
}
|
|
75
|
+
function getOptionalTrimmedCredential(value) {
|
|
76
|
+
const trimmed = value?.trim();
|
|
77
|
+
return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
|
|
78
|
+
}
|
|
72
79
|
function hasOwnProperty(value, property) {
|
|
73
80
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
74
81
|
}
|
|
@@ -95,6 +102,16 @@ function providerError(provider, message, cause, exactSecrets = []) {
|
|
|
95
102
|
function getResponseHeader(response, name) {
|
|
96
103
|
return response.headers.get(name) ?? void 0;
|
|
97
104
|
}
|
|
105
|
+
function getSafeResponseHeader(response, name) {
|
|
106
|
+
const value = getResponseHeader(response, name)?.trim();
|
|
107
|
+
if (!value) {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
return value.replace(/[^A-Za-z0-9._:-]/g, "").slice(0, 128);
|
|
111
|
+
}
|
|
112
|
+
function createCloudflareEmbeddingsUrl(accountId) {
|
|
113
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
114
|
+
}
|
|
98
115
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
99
116
|
if (parentSignal?.aborted) {
|
|
100
117
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -223,12 +240,28 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
223
240
|
dimensions,
|
|
224
241
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
225
242
|
});
|
|
243
|
+
case "cloudflare":
|
|
244
|
+
return Object.freeze({
|
|
245
|
+
name: "cloudflare",
|
|
246
|
+
model: CLOUDFLARE_MODEL,
|
|
247
|
+
dimensions: CLOUDFLARE_DIMENSIONS,
|
|
248
|
+
batch: Object.freeze({ mode: "native" })
|
|
249
|
+
});
|
|
226
250
|
}
|
|
227
251
|
}
|
|
228
252
|
function getEffectiveDimensions(provider, dimensions) {
|
|
229
253
|
if (provider === "gemini") {
|
|
230
254
|
return DEFAULT_DIMENSIONS;
|
|
231
255
|
}
|
|
256
|
+
if (provider === "cloudflare") {
|
|
257
|
+
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
258
|
+
throw providerError(
|
|
259
|
+
"cloudflare",
|
|
260
|
+
`@cf/baai/bge-m3 returns ${CLOUDFLARE_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return CLOUDFLARE_DIMENSIONS;
|
|
264
|
+
}
|
|
232
265
|
return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
|
|
233
266
|
}
|
|
234
267
|
function assertBatchSize(metadata, count) {
|
|
@@ -392,6 +425,73 @@ class OpenAIEmbeddingProvider {
|
|
|
392
425
|
);
|
|
393
426
|
}
|
|
394
427
|
}
|
|
428
|
+
class CloudflareEmbeddingProvider {
|
|
429
|
+
metadata;
|
|
430
|
+
#accountId;
|
|
431
|
+
#apiToken;
|
|
432
|
+
#timeoutMs;
|
|
433
|
+
constructor(metadata, accountId, apiToken, timeoutMs) {
|
|
434
|
+
this.metadata = metadata;
|
|
435
|
+
this.#accountId = accountId;
|
|
436
|
+
this.#apiToken = apiToken;
|
|
437
|
+
this.#timeoutMs = timeoutMs;
|
|
438
|
+
}
|
|
439
|
+
async embed(texts, options = {}) {
|
|
440
|
+
const intent = resolveIntent(options.intent);
|
|
441
|
+
if (texts.length === 0) {
|
|
442
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
443
|
+
}
|
|
444
|
+
assertBatchSize(this.metadata, texts.length);
|
|
445
|
+
const endpoint = createCloudflareEmbeddingsUrl(this.#accountId);
|
|
446
|
+
const items = await withTimeout(
|
|
447
|
+
"cloudflare",
|
|
448
|
+
"API request",
|
|
449
|
+
this.#timeoutMs,
|
|
450
|
+
async (signal) => {
|
|
451
|
+
const response = await fetch(endpoint, {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: {
|
|
454
|
+
"Content-Type": "application/json",
|
|
455
|
+
"Authorization": `Bearer ${this.#apiToken}`
|
|
456
|
+
},
|
|
457
|
+
signal,
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
model: this.metadata.model,
|
|
460
|
+
input: texts
|
|
461
|
+
})
|
|
462
|
+
});
|
|
463
|
+
if (!response.ok) {
|
|
464
|
+
const cfRay = getSafeResponseHeader(response, "cf-ray");
|
|
465
|
+
const context = cfRay ? ` status ${response.status}, cf-ray ${cfRay}` : ` status ${response.status}`;
|
|
466
|
+
throw new Error(context);
|
|
467
|
+
}
|
|
468
|
+
const data = await response.json();
|
|
469
|
+
if (!Array.isArray(data.data)) {
|
|
470
|
+
throw new Error("Cloudflare response did not include a data array");
|
|
471
|
+
}
|
|
472
|
+
return data.data.map((item) => {
|
|
473
|
+
const typed = item;
|
|
474
|
+
const embedding = typed.embedding;
|
|
475
|
+
if (!Array.isArray(embedding)) {
|
|
476
|
+
throw new Error("Cloudflare response item did not include an embedding array");
|
|
477
|
+
}
|
|
478
|
+
const parsed = { embedding };
|
|
479
|
+
if (hasOwnProperty(typed, "index")) {
|
|
480
|
+
parsed.index = typed.index;
|
|
481
|
+
}
|
|
482
|
+
return parsed;
|
|
483
|
+
});
|
|
484
|
+
},
|
|
485
|
+
options.signal,
|
|
486
|
+
[this.#apiToken, this.#accountId, encodeURIComponent(this.#accountId)]
|
|
487
|
+
);
|
|
488
|
+
return createEmbeddingBatchResult(
|
|
489
|
+
this.metadata,
|
|
490
|
+
intent,
|
|
491
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "cloudflare")
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
395
495
|
function createEmbeddingProvider(options = {}) {
|
|
396
496
|
const provider = resolveProviderName(options.provider);
|
|
397
497
|
const metadata = getEmbeddingProviderMetadata(options);
|
|
@@ -413,6 +513,21 @@ function createEmbeddingProvider(options = {}) {
|
|
|
413
513
|
}
|
|
414
514
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
415
515
|
}
|
|
516
|
+
case "cloudflare": {
|
|
517
|
+
const accountId = getOptionalTrimmedCredential(
|
|
518
|
+
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
|
519
|
+
);
|
|
520
|
+
const apiToken = getOptionalTrimmedCredential(
|
|
521
|
+
options.apiToken ?? getEnvironmentVariable("CLOUDFLARE_API_TOKEN")
|
|
522
|
+
);
|
|
523
|
+
if (!accountId) {
|
|
524
|
+
throw new Error("CLOUDFLARE_ACCOUNT_ID is required for Cloudflare embeddings");
|
|
525
|
+
}
|
|
526
|
+
if (!apiToken) {
|
|
527
|
+
throw new Error("CLOUDFLARE_API_TOKEN is required for Cloudflare embeddings");
|
|
528
|
+
}
|
|
529
|
+
return new CloudflareEmbeddingProvider(metadata, accountId, apiToken, timeoutMs);
|
|
530
|
+
}
|
|
416
531
|
}
|
|
417
532
|
}
|
|
418
533
|
function getEmbeddingProviderMetadata(options = {}) {
|
package/docs/API.md
CHANGED
|
@@ -210,7 +210,8 @@ Provider metadata includes:
|
|
|
210
210
|
- `batch.maxSize`, when the provider has a hard maximum
|
|
211
211
|
|
|
212
212
|
Hosted provider clients are scoped to their options. The library does not reuse
|
|
213
|
-
a Gemini or OpenAI client created with
|
|
213
|
+
a Cloudflare, Gemini, or OpenAI client created with different credentials or
|
|
214
|
+
configuration.
|
|
214
215
|
|
|
215
216
|
### `getEmbeddingProviderMetadata(options?)`
|
|
216
217
|
|
|
@@ -238,7 +239,7 @@ Provider clients return:
|
|
|
238
239
|
```ts
|
|
239
240
|
interface EmbeddingBatchResult {
|
|
240
241
|
embeddings: number[][];
|
|
241
|
-
provider: "local" | "gemini" | "openai";
|
|
242
|
+
provider: "local" | "cloudflare" | "gemini" | "openai";
|
|
242
243
|
model: string;
|
|
243
244
|
dimensions: number;
|
|
244
245
|
intent: "document" | "query";
|
|
@@ -254,8 +255,10 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
|
|
|
254
255
|
|
|
255
256
|
```ts
|
|
256
257
|
interface EmbeddingOptions {
|
|
257
|
-
provider?: "local" | "gemini" | "openai";
|
|
258
|
+
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
258
259
|
apiKey?: string;
|
|
260
|
+
accountId?: string;
|
|
261
|
+
apiToken?: string;
|
|
259
262
|
dimensions?: number;
|
|
260
263
|
maxLength?: number;
|
|
261
264
|
intent?: "document" | "query";
|
|
@@ -264,6 +267,10 @@ interface EmbeddingOptions {
|
|
|
264
267
|
}
|
|
265
268
|
```
|
|
266
269
|
|
|
270
|
+
`apiKey` is used by Gemini and OpenAI. Cloudflare uses `accountId` and
|
|
271
|
+
`apiToken`, which fall back to `CLOUDFLARE_ACCOUNT_ID` and
|
|
272
|
+
`CLOUDFLARE_API_TOKEN`.
|
|
273
|
+
|
|
267
274
|
### `padEmbedding(embedding, targetDimensions)`
|
|
268
275
|
|
|
269
276
|
Pads or truncates an embedding array to the requested length.
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -127,14 +127,18 @@ const client = createClient({
|
|
|
127
127
|
authToken: process.env.TURSO_AUTH_TOKEN!,
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
const embeddingProvider =
|
|
131
|
+
process.env.EMBEDDING_PROVIDER as "local" | "cloudflare" | "gemini" | "openai" | undefined;
|
|
132
|
+
const embeddingDimensions = embeddingProvider === "cloudflare" ? 1024 : 768;
|
|
133
|
+
|
|
134
|
+
await createTable(client, "articles", embeddingDimensions);
|
|
131
135
|
|
|
132
136
|
await indexContent({
|
|
133
137
|
client,
|
|
134
138
|
contentPath: "./content",
|
|
135
139
|
embeddingOptions: {
|
|
136
|
-
provider:
|
|
137
|
-
dimensions:
|
|
140
|
+
provider: embeddingProvider,
|
|
141
|
+
dimensions: embeddingDimensions,
|
|
138
142
|
},
|
|
139
143
|
});
|
|
140
144
|
```
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Embedding Providers
|
|
2
2
|
|
|
3
|
-
`libsql-search` currently supports
|
|
3
|
+
`libsql-search` currently supports four embedding providers:
|
|
4
4
|
|
|
5
5
|
- `local`
|
|
6
|
+
- `cloudflare`
|
|
6
7
|
- `gemini`
|
|
7
8
|
- `openai`
|
|
8
9
|
|
|
@@ -14,8 +15,10 @@ query time.
|
|
|
14
15
|
|
|
15
16
|
```ts
|
|
16
17
|
interface EmbeddingOptions {
|
|
17
|
-
provider?: "local" | "gemini" | "openai";
|
|
18
|
+
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
18
19
|
apiKey?: string;
|
|
20
|
+
accountId?: string;
|
|
21
|
+
apiToken?: string;
|
|
19
22
|
dimensions?: number;
|
|
20
23
|
maxLength?: number;
|
|
21
24
|
intent?: "document" | "query";
|
|
@@ -30,8 +33,10 @@ interface EmbeddingOptions {
|
|
|
30
33
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
31
34
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
32
35
|
- `timeoutMs` defaults to `30000`
|
|
33
|
-
- `apiKey` is
|
|
34
|
-
|
|
36
|
+
- `apiKey` is used by Gemini and OpenAI and falls back to `GEMINI_API_KEY` or
|
|
37
|
+
`OPENAI_API_KEY`
|
|
38
|
+
- `accountId` and `apiToken` are used by Cloudflare and fall back to
|
|
39
|
+
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
|
|
35
40
|
|
|
36
41
|
## Provider Contract
|
|
37
42
|
|
|
@@ -39,7 +44,7 @@ Each provider exposes immutable metadata:
|
|
|
39
44
|
|
|
40
45
|
```ts
|
|
41
46
|
interface EmbeddingProviderMetadata {
|
|
42
|
-
name: "local" | "gemini" | "openai";
|
|
47
|
+
name: "local" | "cloudflare" | "gemini" | "openai";
|
|
43
48
|
model: string;
|
|
44
49
|
dimensions: number;
|
|
45
50
|
batch: {
|
|
@@ -78,9 +83,9 @@ Lower-level provider clients return an `EmbeddingBatchResult` with the validated
|
|
|
78
83
|
vectors plus provider, model, dimensions, and intent. The compatibility helpers
|
|
79
84
|
`generateEmbedding()` and `generateEmbeddings()` return only arrays.
|
|
80
85
|
|
|
81
|
-
Gemini and OpenAI clients are scoped to their current options. They
|
|
82
|
-
cached globally across different credentials or configurations. The
|
|
83
|
-
model can be cached by model name.
|
|
86
|
+
Cloudflare, Gemini, and OpenAI clients are scoped to their current options. They
|
|
87
|
+
are not cached globally across different credentials or configurations. The
|
|
88
|
+
local Xenova model can be cached by model name.
|
|
84
89
|
|
|
85
90
|
Hosted provider failures are reported with bounded provider/status/request-id
|
|
86
91
|
context and without raw upstream bodies, credentials, Authorization headers, or
|
|
@@ -108,6 +113,36 @@ Notes:
|
|
|
108
113
|
- batch metadata is `{ mode: "sequential" }`
|
|
109
114
|
- the first run downloads the model and can take longer on a fresh machine
|
|
110
115
|
- no API key is required
|
|
116
|
+
- this remains the default provider for backward compatibility and offline use
|
|
117
|
+
|
|
118
|
+
## Cloudflare Workers AI
|
|
119
|
+
|
|
120
|
+
Provider value: `cloudflare`
|
|
121
|
+
|
|
122
|
+
Cloudflare is the recommended hosted provider for low-cost Markdown search.
|
|
123
|
+
It uses Workers AI `@cf/baai/bge-m3` through Cloudflare's OpenAI-compatible
|
|
124
|
+
embeddings endpoint.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
embeddingOptions: {
|
|
128
|
+
provider: "cloudflare",
|
|
129
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
|
130
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN,
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Behavior:
|
|
135
|
+
|
|
136
|
+
- if `accountId` is omitted, the library reads `CLOUDFLARE_ACCOUNT_ID`
|
|
137
|
+
- if `apiToken` is omitted, the library reads `CLOUDFLARE_API_TOKEN`
|
|
138
|
+
- blank Cloudflare credentials are treated as missing
|
|
139
|
+
- `@cf/baai/bge-m3` returns 1024 dimensions
|
|
140
|
+
- metadata reports `@cf/baai/bge-m3` and 1024 dimensions without requiring
|
|
141
|
+
credentials
|
|
142
|
+
- batch metadata is `{ mode: "native" }`
|
|
143
|
+
- response items are reordered by provider-supplied index before being returned
|
|
144
|
+
- Cloudflare does not accept custom dimensions in this provider; use
|
|
145
|
+
`createTable(client, "articles", 1024)` for Cloudflare-backed indexes
|
|
111
146
|
|
|
112
147
|
## Gemini
|
|
113
148
|
|
|
@@ -156,7 +191,8 @@ Behavior:
|
|
|
156
191
|
|
|
157
192
|
## Dimension Guidelines
|
|
158
193
|
|
|
159
|
-
- `
|
|
194
|
+
- `local` defaults to `768`
|
|
195
|
+
- `cloudflare` is fixed at `1024`
|
|
160
196
|
- local embeddings are padded from 384 to your target size
|
|
161
197
|
- Gemini stays at 768
|
|
162
198
|
- OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libsql-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@libsql/client": "^0.15.15",
|
|
74
74
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
75
75
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
76
|
-
"@types/node": "^
|
|
76
|
+
"@types/node": "^24.13.3",
|
|
77
77
|
"@vitest/coverage-v8": "4.1.11",
|
|
78
78
|
"@vitest/ui": "^4.1.11",
|
|
79
79
|
"rollup": "^4.62.5",
|