libsql-search 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/index.cjs +169 -49
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.js +169 -49
- package/docs/API.md +11 -7
- package/docs/INDEXING.md +8 -0
- package/docs/INTEGRATIONS.md +13 -2
- package/docs/PROVIDERS.md +59 -14
- package/docs/README.md +2 -2
- package/docs/TROUBLESHOOTING.md +6 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@ Use it when you want:
|
|
|
21
21
|
- Markdown indexing from local directories with frontmatter via `gray-matter`
|
|
22
22
|
- libSQL/Turso storage and vector search
|
|
23
23
|
- Embedding providers: local `Xenova/all-MiniLM-L6-v2`, Cloudflare Workers AI
|
|
24
|
-
`@cf/baai/bge-m3`,
|
|
25
|
-
`
|
|
24
|
+
`@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
|
|
25
|
+
`gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
|
|
26
|
+
`text-embedding-3-large`
|
|
26
27
|
- npm distribution plus JSR publishing
|
|
27
28
|
|
|
28
29
|
## Install
|
|
@@ -98,7 +99,8 @@ Important behavior:
|
|
|
98
99
|
- Keep dimensions aligned across table creation, indexing, and search queries.
|
|
99
100
|
- `indexContent()` clears existing rows before rebuilding the index.
|
|
100
101
|
- `local` is the default offline provider; Cloudflare is the recommended hosted
|
|
101
|
-
option and
|
|
102
|
+
option. Cloudflare and Mistral use 1024 dimensions. Gemini defaults to 3072
|
|
103
|
+
dimensions and supports 128-3072.
|
|
102
104
|
|
|
103
105
|
## Core API
|
|
104
106
|
|
package/dist/index.cjs
CHANGED
|
@@ -8,10 +8,16 @@ const DEFAULT_DIMENSIONS = 768;
|
|
|
8
8
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
10
10
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
-
const GEMINI_MODEL = "
|
|
11
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
12
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
13
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
14
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
12
15
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
13
16
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
14
17
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
18
|
+
const MISTRAL_MODEL = "mistral-embed";
|
|
19
|
+
const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
|
|
20
|
+
const MISTRAL_DIMENSIONS = 1024;
|
|
15
21
|
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
16
22
|
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
17
23
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
@@ -53,6 +59,7 @@ function resolveProviderName(provider) {
|
|
|
53
59
|
case "local":
|
|
54
60
|
case "gemini":
|
|
55
61
|
case "openai":
|
|
62
|
+
case "mistral":
|
|
56
63
|
case "cloudflare":
|
|
57
64
|
return provider ?? "local";
|
|
58
65
|
default:
|
|
@@ -81,6 +88,9 @@ function getOptionalTrimmedCredential(value) {
|
|
|
81
88
|
function hasOwnProperty(value, property) {
|
|
82
89
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
83
90
|
}
|
|
91
|
+
function isRecord(value) {
|
|
92
|
+
return typeof value === "object" && value !== null;
|
|
93
|
+
}
|
|
84
94
|
function redactErrorText(value, exactSecrets = []) {
|
|
85
95
|
let raw = value instanceof Error ? value.message : String(value);
|
|
86
96
|
for (const secret of exactSecrets) {
|
|
@@ -114,6 +124,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
114
124
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
115
125
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
116
126
|
}
|
|
127
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
128
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
129
|
+
}
|
|
130
|
+
function parseGeminiEmbeddingResult(result) {
|
|
131
|
+
if (!Array.isArray(result.embeddings)) {
|
|
132
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
133
|
+
}
|
|
134
|
+
if (result.embeddings.length !== 1) {
|
|
135
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
136
|
+
}
|
|
137
|
+
const values = result.embeddings[0]?.values;
|
|
138
|
+
if (!Array.isArray(values)) {
|
|
139
|
+
throw new Error("Gemini response did not include embedding values");
|
|
140
|
+
}
|
|
141
|
+
return values;
|
|
142
|
+
}
|
|
117
143
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
118
144
|
if (parentSignal?.aborted) {
|
|
119
145
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -232,7 +258,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
232
258
|
return Object.freeze({
|
|
233
259
|
name: "gemini",
|
|
234
260
|
model: GEMINI_MODEL,
|
|
235
|
-
dimensions
|
|
261
|
+
dimensions,
|
|
236
262
|
batch: Object.freeze({ mode: "sequential" })
|
|
237
263
|
});
|
|
238
264
|
case "openai":
|
|
@@ -242,6 +268,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
242
268
|
dimensions,
|
|
243
269
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
244
270
|
});
|
|
271
|
+
case "mistral":
|
|
272
|
+
return Object.freeze({
|
|
273
|
+
name: "mistral",
|
|
274
|
+
model: MISTRAL_MODEL,
|
|
275
|
+
dimensions: MISTRAL_DIMENSIONS,
|
|
276
|
+
batch: Object.freeze({ mode: "native" })
|
|
277
|
+
});
|
|
245
278
|
case "cloudflare":
|
|
246
279
|
return Object.freeze({
|
|
247
280
|
name: "cloudflare",
|
|
@@ -253,7 +286,23 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
253
286
|
}
|
|
254
287
|
function getEffectiveDimensions(provider, dimensions) {
|
|
255
288
|
if (provider === "gemini") {
|
|
256
|
-
|
|
289
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
290
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
291
|
+
throw providerError(
|
|
292
|
+
"gemini",
|
|
293
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
return effectiveDimensions;
|
|
297
|
+
}
|
|
298
|
+
if (provider === "mistral") {
|
|
299
|
+
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
300
|
+
throw providerError(
|
|
301
|
+
"mistral",
|
|
302
|
+
`${MISTRAL_MODEL} returns ${MISTRAL_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return MISTRAL_DIMENSIONS;
|
|
257
306
|
}
|
|
258
307
|
if (provider === "cloudflare") {
|
|
259
308
|
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
@@ -340,16 +389,18 @@ class GeminiEmbeddingProvider {
|
|
|
340
389
|
"API request",
|
|
341
390
|
this.#timeoutMs,
|
|
342
391
|
async (signal) => {
|
|
343
|
-
const {
|
|
344
|
-
const
|
|
345
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
392
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
393
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
346
394
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
347
|
-
const result = await
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
395
|
+
const result = await client.models.embedContent({
|
|
396
|
+
model: this.metadata.model,
|
|
397
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
398
|
+
config: {
|
|
399
|
+
outputDimensionality: this.metadata.dimensions,
|
|
400
|
+
abortSignal: itemSignal
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
return parseGeminiEmbeddingResult(result);
|
|
353
404
|
});
|
|
354
405
|
},
|
|
355
406
|
options.signal,
|
|
@@ -362,6 +413,52 @@ class GeminiEmbeddingProvider {
|
|
|
362
413
|
);
|
|
363
414
|
}
|
|
364
415
|
}
|
|
416
|
+
async function requestOpenAICompatibleEmbeddings(request) {
|
|
417
|
+
const body = {
|
|
418
|
+
input: request.texts,
|
|
419
|
+
model: request.model
|
|
420
|
+
};
|
|
421
|
+
if (request.dimensions !== void 0) {
|
|
422
|
+
body.dimensions = request.dimensions;
|
|
423
|
+
}
|
|
424
|
+
if (request.encodingFormat !== void 0) {
|
|
425
|
+
body.encoding_format = request.encodingFormat;
|
|
426
|
+
}
|
|
427
|
+
const response = await fetch(request.endpoint, {
|
|
428
|
+
method: "POST",
|
|
429
|
+
headers: {
|
|
430
|
+
"Content-Type": "application/json",
|
|
431
|
+
"Authorization": `Bearer ${request.apiKey}`
|
|
432
|
+
},
|
|
433
|
+
signal: request.signal,
|
|
434
|
+
body: JSON.stringify(body)
|
|
435
|
+
});
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
const requestId = getSafeResponseHeader(response, "x-request-id");
|
|
438
|
+
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
439
|
+
throw new Error(context);
|
|
440
|
+
}
|
|
441
|
+
const data = await response.json();
|
|
442
|
+
if (!Array.isArray(data.data)) {
|
|
443
|
+
throw new Error(`${request.responseLabel} response did not include a data array`);
|
|
444
|
+
}
|
|
445
|
+
return data.data.map((item) => {
|
|
446
|
+
if (!isRecord(item)) {
|
|
447
|
+
throw new Error(`${request.responseLabel} response item was not an object`);
|
|
448
|
+
}
|
|
449
|
+
const embedding = item.embedding;
|
|
450
|
+
if (!Array.isArray(embedding)) {
|
|
451
|
+
throw new Error(`${request.responseLabel} response item did not include an embedding array`);
|
|
452
|
+
}
|
|
453
|
+
const parsed = { embedding };
|
|
454
|
+
if (hasOwnProperty(item, "index")) {
|
|
455
|
+
parsed.index = item.index;
|
|
456
|
+
} else if (request.requireIndex) {
|
|
457
|
+
throw new Error(`${request.responseLabel} response item did not include an index`);
|
|
458
|
+
}
|
|
459
|
+
return parsed;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
365
462
|
class OpenAIEmbeddingProvider {
|
|
366
463
|
metadata;
|
|
367
464
|
#apiKey;
|
|
@@ -381,42 +478,15 @@ class OpenAIEmbeddingProvider {
|
|
|
381
478
|
"openai",
|
|
382
479
|
"API request",
|
|
383
480
|
this.#timeoutMs,
|
|
384
|
-
async (signal) => {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
input: texts,
|
|
394
|
-
model: this.metadata.model,
|
|
395
|
-
dimensions: this.metadata.dimensions
|
|
396
|
-
})
|
|
397
|
-
});
|
|
398
|
-
if (!response.ok) {
|
|
399
|
-
const requestId = getResponseHeader(response, "x-request-id");
|
|
400
|
-
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
401
|
-
throw new Error(context);
|
|
402
|
-
}
|
|
403
|
-
const data = await response.json();
|
|
404
|
-
if (!Array.isArray(data.data)) {
|
|
405
|
-
throw new Error("OpenAI response did not include a data array");
|
|
406
|
-
}
|
|
407
|
-
return data.data.map((item) => {
|
|
408
|
-
const typed = item;
|
|
409
|
-
const embedding = typed.embedding;
|
|
410
|
-
if (!Array.isArray(embedding)) {
|
|
411
|
-
throw new Error("OpenAI response item did not include an embedding array");
|
|
412
|
-
}
|
|
413
|
-
const parsed = { embedding };
|
|
414
|
-
if (hasOwnProperty(typed, "index")) {
|
|
415
|
-
parsed.index = typed.index;
|
|
416
|
-
}
|
|
417
|
-
return parsed;
|
|
418
|
-
});
|
|
419
|
-
},
|
|
481
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
482
|
+
responseLabel: "OpenAI",
|
|
483
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
484
|
+
apiKey: this.#apiKey,
|
|
485
|
+
model: this.metadata.model,
|
|
486
|
+
texts,
|
|
487
|
+
signal,
|
|
488
|
+
dimensions: this.metadata.dimensions
|
|
489
|
+
}),
|
|
420
490
|
options.signal,
|
|
421
491
|
[this.#apiKey]
|
|
422
492
|
);
|
|
@@ -427,6 +497,45 @@ class OpenAIEmbeddingProvider {
|
|
|
427
497
|
);
|
|
428
498
|
}
|
|
429
499
|
}
|
|
500
|
+
class MistralEmbeddingProvider {
|
|
501
|
+
metadata;
|
|
502
|
+
#apiKey;
|
|
503
|
+
#timeoutMs;
|
|
504
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
505
|
+
this.metadata = metadata;
|
|
506
|
+
this.#apiKey = apiKey;
|
|
507
|
+
this.#timeoutMs = timeoutMs;
|
|
508
|
+
}
|
|
509
|
+
async embed(texts, options = {}) {
|
|
510
|
+
const intent = resolveIntent(options.intent);
|
|
511
|
+
if (texts.length === 0) {
|
|
512
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
513
|
+
}
|
|
514
|
+
assertBatchSize(this.metadata, texts.length);
|
|
515
|
+
const items = await withTimeout(
|
|
516
|
+
"mistral",
|
|
517
|
+
"API request",
|
|
518
|
+
this.#timeoutMs,
|
|
519
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
520
|
+
responseLabel: "Mistral",
|
|
521
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
522
|
+
apiKey: this.#apiKey,
|
|
523
|
+
model: this.metadata.model,
|
|
524
|
+
texts,
|
|
525
|
+
signal,
|
|
526
|
+
encodingFormat: "float",
|
|
527
|
+
requireIndex: true
|
|
528
|
+
}),
|
|
529
|
+
options.signal,
|
|
530
|
+
[this.#apiKey]
|
|
531
|
+
);
|
|
532
|
+
return createEmbeddingBatchResult(
|
|
533
|
+
this.metadata,
|
|
534
|
+
intent,
|
|
535
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
430
539
|
class CloudflareEmbeddingProvider {
|
|
431
540
|
metadata;
|
|
432
541
|
#accountId;
|
|
@@ -502,7 +611,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
502
611
|
case "local":
|
|
503
612
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
504
613
|
case "gemini": {
|
|
505
|
-
const key =
|
|
614
|
+
const key = getOptionalTrimmedCredential(
|
|
615
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
616
|
+
);
|
|
506
617
|
if (!key) {
|
|
507
618
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
508
619
|
}
|
|
@@ -515,6 +626,15 @@ function createEmbeddingProvider(options = {}) {
|
|
|
515
626
|
}
|
|
516
627
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
517
628
|
}
|
|
629
|
+
case "mistral": {
|
|
630
|
+
const key = getOptionalTrimmedCredential(
|
|
631
|
+
options.apiKey ?? getEnvironmentVariable("MISTRAL_API_KEY")
|
|
632
|
+
);
|
|
633
|
+
if (!key) {
|
|
634
|
+
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
635
|
+
}
|
|
636
|
+
return new MistralEmbeddingProvider(metadata, key, timeoutMs);
|
|
637
|
+
}
|
|
518
638
|
case "cloudflare": {
|
|
519
639
|
const accountId = getOptionalTrimmedCredential(
|
|
520
640
|
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
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' | 'cloudflare';
|
|
3
|
+
type EmbeddingProvider = 'local' | 'gemini' | 'openai' | 'mistral' | 'cloudflare';
|
|
4
4
|
type EmbeddingIntent = 'document' | 'query';
|
|
5
5
|
type EmbeddingBatchMode = 'native' | 'sequential';
|
|
6
6
|
interface EmbeddingBatchBehavior {
|
package/dist/index.esm.js
CHANGED
|
@@ -6,10 +6,16 @@ const DEFAULT_DIMENSIONS = 768;
|
|
|
6
6
|
const DEFAULT_MAX_LENGTH = 8e3;
|
|
7
7
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
8
8
|
const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
9
|
-
const GEMINI_MODEL = "
|
|
9
|
+
const GEMINI_MODEL = "gemini-embedding-2";
|
|
10
|
+
const GEMINI_DIMENSIONS = 3072;
|
|
11
|
+
const GEMINI_MIN_DIMENSIONS = 128;
|
|
12
|
+
const GEMINI_MAX_DIMENSIONS = 3072;
|
|
10
13
|
const OPENAI_SMALL_MODEL = "text-embedding-3-small";
|
|
11
14
|
const OPENAI_LARGE_MODEL = "text-embedding-3-large";
|
|
12
15
|
const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
|
|
16
|
+
const MISTRAL_MODEL = "mistral-embed";
|
|
17
|
+
const MISTRAL_EMBEDDINGS_URL = "https://api.mistral.ai/v1/embeddings";
|
|
18
|
+
const MISTRAL_DIMENSIONS = 1024;
|
|
13
19
|
const CLOUDFLARE_MODEL = "@cf/baai/bge-m3";
|
|
14
20
|
const CLOUDFLARE_DIMENSIONS = 1024;
|
|
15
21
|
const localModelCacheByModel = /* @__PURE__ */ new Map();
|
|
@@ -51,6 +57,7 @@ function resolveProviderName(provider) {
|
|
|
51
57
|
case "local":
|
|
52
58
|
case "gemini":
|
|
53
59
|
case "openai":
|
|
60
|
+
case "mistral":
|
|
54
61
|
case "cloudflare":
|
|
55
62
|
return provider ?? "local";
|
|
56
63
|
default:
|
|
@@ -79,6 +86,9 @@ function getOptionalTrimmedCredential(value) {
|
|
|
79
86
|
function hasOwnProperty(value, property) {
|
|
80
87
|
return Object.prototype.hasOwnProperty.call(value, property);
|
|
81
88
|
}
|
|
89
|
+
function isRecord(value) {
|
|
90
|
+
return typeof value === "object" && value !== null;
|
|
91
|
+
}
|
|
82
92
|
function redactErrorText(value, exactSecrets = []) {
|
|
83
93
|
let raw = value instanceof Error ? value.message : String(value);
|
|
84
94
|
for (const secret of exactSecrets) {
|
|
@@ -112,6 +122,22 @@ function getSafeResponseHeader(response, name) {
|
|
|
112
122
|
function createCloudflareEmbeddingsUrl(accountId) {
|
|
113
123
|
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/v1/embeddings`;
|
|
114
124
|
}
|
|
125
|
+
function formatGeminiEmbeddingContent(text, intent) {
|
|
126
|
+
return intent === "query" ? `task: search result | query: ${text}` : `title: none | text: ${text}`;
|
|
127
|
+
}
|
|
128
|
+
function parseGeminiEmbeddingResult(result) {
|
|
129
|
+
if (!Array.isArray(result.embeddings)) {
|
|
130
|
+
throw new Error("Gemini response did not include an embeddings array");
|
|
131
|
+
}
|
|
132
|
+
if (result.embeddings.length !== 1) {
|
|
133
|
+
throw new Error(`Gemini response included ${result.embeddings.length} embedding result(s) for one input`);
|
|
134
|
+
}
|
|
135
|
+
const values = result.embeddings[0]?.values;
|
|
136
|
+
if (!Array.isArray(values)) {
|
|
137
|
+
throw new Error("Gemini response did not include embedding values");
|
|
138
|
+
}
|
|
139
|
+
return values;
|
|
140
|
+
}
|
|
115
141
|
async function withTimeout(provider, operation, timeoutMs, run, parentSignal, exactSecrets = []) {
|
|
116
142
|
if (parentSignal?.aborted) {
|
|
117
143
|
throw providerError(provider, `${operation} was aborted`);
|
|
@@ -230,7 +256,7 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
230
256
|
return Object.freeze({
|
|
231
257
|
name: "gemini",
|
|
232
258
|
model: GEMINI_MODEL,
|
|
233
|
-
dimensions
|
|
259
|
+
dimensions,
|
|
234
260
|
batch: Object.freeze({ mode: "sequential" })
|
|
235
261
|
});
|
|
236
262
|
case "openai":
|
|
@@ -240,6 +266,13 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
240
266
|
dimensions,
|
|
241
267
|
batch: Object.freeze({ mode: "native", maxSize: 2048 })
|
|
242
268
|
});
|
|
269
|
+
case "mistral":
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
name: "mistral",
|
|
272
|
+
model: MISTRAL_MODEL,
|
|
273
|
+
dimensions: MISTRAL_DIMENSIONS,
|
|
274
|
+
batch: Object.freeze({ mode: "native" })
|
|
275
|
+
});
|
|
243
276
|
case "cloudflare":
|
|
244
277
|
return Object.freeze({
|
|
245
278
|
name: "cloudflare",
|
|
@@ -251,7 +284,23 @@ function createProviderMetadata(provider, dimensions) {
|
|
|
251
284
|
}
|
|
252
285
|
function getEffectiveDimensions(provider, dimensions) {
|
|
253
286
|
if (provider === "gemini") {
|
|
254
|
-
|
|
287
|
+
const effectiveDimensions = dimensions ?? GEMINI_DIMENSIONS;
|
|
288
|
+
if (!Number.isInteger(effectiveDimensions) || effectiveDimensions < GEMINI_MIN_DIMENSIONS || effectiveDimensions > GEMINI_MAX_DIMENSIONS) {
|
|
289
|
+
throw providerError(
|
|
290
|
+
"gemini",
|
|
291
|
+
`${GEMINI_MODEL} supports dimensions from ${GEMINI_MIN_DIMENSIONS} to ${GEMINI_MAX_DIMENSIONS}; received dimensions ${String(dimensions)}`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return effectiveDimensions;
|
|
295
|
+
}
|
|
296
|
+
if (provider === "mistral") {
|
|
297
|
+
if (dimensions !== void 0 && dimensions !== MISTRAL_DIMENSIONS) {
|
|
298
|
+
throw providerError(
|
|
299
|
+
"mistral",
|
|
300
|
+
`${MISTRAL_MODEL} returns ${MISTRAL_DIMENSIONS} dimensions; received dimensions ${dimensions}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return MISTRAL_DIMENSIONS;
|
|
255
304
|
}
|
|
256
305
|
if (provider === "cloudflare") {
|
|
257
306
|
if (dimensions !== void 0 && dimensions !== CLOUDFLARE_DIMENSIONS) {
|
|
@@ -338,16 +387,18 @@ class GeminiEmbeddingProvider {
|
|
|
338
387
|
"API request",
|
|
339
388
|
this.#timeoutMs,
|
|
340
389
|
async (signal) => {
|
|
341
|
-
const {
|
|
342
|
-
const
|
|
343
|
-
const model = genAI.getGenerativeModel({ model: this.metadata.model });
|
|
390
|
+
const { GoogleGenAI } = await import('@google/genai');
|
|
391
|
+
const client = new GoogleGenAI({ apiKey: this.#apiKey });
|
|
344
392
|
return embedSequentially("gemini", "API request", texts, signal, async (text, itemSignal) => {
|
|
345
|
-
const result = await
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
393
|
+
const result = await client.models.embedContent({
|
|
394
|
+
model: this.metadata.model,
|
|
395
|
+
contents: formatGeminiEmbeddingContent(text, intent),
|
|
396
|
+
config: {
|
|
397
|
+
outputDimensionality: this.metadata.dimensions,
|
|
398
|
+
abortSignal: itemSignal
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return parseGeminiEmbeddingResult(result);
|
|
351
402
|
});
|
|
352
403
|
},
|
|
353
404
|
options.signal,
|
|
@@ -360,6 +411,52 @@ class GeminiEmbeddingProvider {
|
|
|
360
411
|
);
|
|
361
412
|
}
|
|
362
413
|
}
|
|
414
|
+
async function requestOpenAICompatibleEmbeddings(request) {
|
|
415
|
+
const body = {
|
|
416
|
+
input: request.texts,
|
|
417
|
+
model: request.model
|
|
418
|
+
};
|
|
419
|
+
if (request.dimensions !== void 0) {
|
|
420
|
+
body.dimensions = request.dimensions;
|
|
421
|
+
}
|
|
422
|
+
if (request.encodingFormat !== void 0) {
|
|
423
|
+
body.encoding_format = request.encodingFormat;
|
|
424
|
+
}
|
|
425
|
+
const response = await fetch(request.endpoint, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
"Content-Type": "application/json",
|
|
429
|
+
"Authorization": `Bearer ${request.apiKey}`
|
|
430
|
+
},
|
|
431
|
+
signal: request.signal,
|
|
432
|
+
body: JSON.stringify(body)
|
|
433
|
+
});
|
|
434
|
+
if (!response.ok) {
|
|
435
|
+
const requestId = getSafeResponseHeader(response, "x-request-id");
|
|
436
|
+
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
437
|
+
throw new Error(context);
|
|
438
|
+
}
|
|
439
|
+
const data = await response.json();
|
|
440
|
+
if (!Array.isArray(data.data)) {
|
|
441
|
+
throw new Error(`${request.responseLabel} response did not include a data array`);
|
|
442
|
+
}
|
|
443
|
+
return data.data.map((item) => {
|
|
444
|
+
if (!isRecord(item)) {
|
|
445
|
+
throw new Error(`${request.responseLabel} response item was not an object`);
|
|
446
|
+
}
|
|
447
|
+
const embedding = item.embedding;
|
|
448
|
+
if (!Array.isArray(embedding)) {
|
|
449
|
+
throw new Error(`${request.responseLabel} response item did not include an embedding array`);
|
|
450
|
+
}
|
|
451
|
+
const parsed = { embedding };
|
|
452
|
+
if (hasOwnProperty(item, "index")) {
|
|
453
|
+
parsed.index = item.index;
|
|
454
|
+
} else if (request.requireIndex) {
|
|
455
|
+
throw new Error(`${request.responseLabel} response item did not include an index`);
|
|
456
|
+
}
|
|
457
|
+
return parsed;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
363
460
|
class OpenAIEmbeddingProvider {
|
|
364
461
|
metadata;
|
|
365
462
|
#apiKey;
|
|
@@ -379,42 +476,15 @@ class OpenAIEmbeddingProvider {
|
|
|
379
476
|
"openai",
|
|
380
477
|
"API request",
|
|
381
478
|
this.#timeoutMs,
|
|
382
|
-
async (signal) => {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
input: texts,
|
|
392
|
-
model: this.metadata.model,
|
|
393
|
-
dimensions: this.metadata.dimensions
|
|
394
|
-
})
|
|
395
|
-
});
|
|
396
|
-
if (!response.ok) {
|
|
397
|
-
const requestId = getResponseHeader(response, "x-request-id");
|
|
398
|
-
const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
|
|
399
|
-
throw new Error(context);
|
|
400
|
-
}
|
|
401
|
-
const data = await response.json();
|
|
402
|
-
if (!Array.isArray(data.data)) {
|
|
403
|
-
throw new Error("OpenAI response did not include a data array");
|
|
404
|
-
}
|
|
405
|
-
return data.data.map((item) => {
|
|
406
|
-
const typed = item;
|
|
407
|
-
const embedding = typed.embedding;
|
|
408
|
-
if (!Array.isArray(embedding)) {
|
|
409
|
-
throw new Error("OpenAI response item did not include an embedding array");
|
|
410
|
-
}
|
|
411
|
-
const parsed = { embedding };
|
|
412
|
-
if (hasOwnProperty(typed, "index")) {
|
|
413
|
-
parsed.index = typed.index;
|
|
414
|
-
}
|
|
415
|
-
return parsed;
|
|
416
|
-
});
|
|
417
|
-
},
|
|
479
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
480
|
+
responseLabel: "OpenAI",
|
|
481
|
+
endpoint: OPENAI_EMBEDDINGS_URL,
|
|
482
|
+
apiKey: this.#apiKey,
|
|
483
|
+
model: this.metadata.model,
|
|
484
|
+
texts,
|
|
485
|
+
signal,
|
|
486
|
+
dimensions: this.metadata.dimensions
|
|
487
|
+
}),
|
|
418
488
|
options.signal,
|
|
419
489
|
[this.#apiKey]
|
|
420
490
|
);
|
|
@@ -425,6 +495,45 @@ class OpenAIEmbeddingProvider {
|
|
|
425
495
|
);
|
|
426
496
|
}
|
|
427
497
|
}
|
|
498
|
+
class MistralEmbeddingProvider {
|
|
499
|
+
metadata;
|
|
500
|
+
#apiKey;
|
|
501
|
+
#timeoutMs;
|
|
502
|
+
constructor(metadata, apiKey, timeoutMs) {
|
|
503
|
+
this.metadata = metadata;
|
|
504
|
+
this.#apiKey = apiKey;
|
|
505
|
+
this.#timeoutMs = timeoutMs;
|
|
506
|
+
}
|
|
507
|
+
async embed(texts, options = {}) {
|
|
508
|
+
const intent = resolveIntent(options.intent);
|
|
509
|
+
if (texts.length === 0) {
|
|
510
|
+
return createEmbeddingBatchResult(this.metadata, intent, []);
|
|
511
|
+
}
|
|
512
|
+
assertBatchSize(this.metadata, texts.length);
|
|
513
|
+
const items = await withTimeout(
|
|
514
|
+
"mistral",
|
|
515
|
+
"API request",
|
|
516
|
+
this.#timeoutMs,
|
|
517
|
+
async (signal) => requestOpenAICompatibleEmbeddings({
|
|
518
|
+
responseLabel: "Mistral",
|
|
519
|
+
endpoint: MISTRAL_EMBEDDINGS_URL,
|
|
520
|
+
apiKey: this.#apiKey,
|
|
521
|
+
model: this.metadata.model,
|
|
522
|
+
texts,
|
|
523
|
+
signal,
|
|
524
|
+
encodingFormat: "float",
|
|
525
|
+
requireIndex: true
|
|
526
|
+
}),
|
|
527
|
+
options.signal,
|
|
528
|
+
[this.#apiKey]
|
|
529
|
+
);
|
|
530
|
+
return createEmbeddingBatchResult(
|
|
531
|
+
this.metadata,
|
|
532
|
+
intent,
|
|
533
|
+
validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "mistral")
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
428
537
|
class CloudflareEmbeddingProvider {
|
|
429
538
|
metadata;
|
|
430
539
|
#accountId;
|
|
@@ -500,7 +609,9 @@ function createEmbeddingProvider(options = {}) {
|
|
|
500
609
|
case "local":
|
|
501
610
|
return new LocalEmbeddingProvider(metadata, timeoutMs);
|
|
502
611
|
case "gemini": {
|
|
503
|
-
const key =
|
|
612
|
+
const key = getOptionalTrimmedCredential(
|
|
613
|
+
options.apiKey ?? getEnvironmentVariable("GEMINI_API_KEY")
|
|
614
|
+
);
|
|
504
615
|
if (!key) {
|
|
505
616
|
throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
|
|
506
617
|
}
|
|
@@ -513,6 +624,15 @@ function createEmbeddingProvider(options = {}) {
|
|
|
513
624
|
}
|
|
514
625
|
return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
|
|
515
626
|
}
|
|
627
|
+
case "mistral": {
|
|
628
|
+
const key = getOptionalTrimmedCredential(
|
|
629
|
+
options.apiKey ?? getEnvironmentVariable("MISTRAL_API_KEY")
|
|
630
|
+
);
|
|
631
|
+
if (!key) {
|
|
632
|
+
throw new Error("MISTRAL_API_KEY is required for Mistral embeddings");
|
|
633
|
+
}
|
|
634
|
+
return new MistralEmbeddingProvider(metadata, key, timeoutMs);
|
|
635
|
+
}
|
|
516
636
|
case "cloudflare": {
|
|
517
637
|
const accountId = getOptionalTrimmedCredential(
|
|
518
638
|
options.accountId ?? getEnvironmentVariable("CLOUDFLARE_ACCOUNT_ID")
|
package/docs/API.md
CHANGED
|
@@ -210,8 +210,11 @@ 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 Cloudflare, Gemini, or OpenAI client created with different
|
|
214
|
-
configuration.
|
|
213
|
+
a Cloudflare, Mistral, Gemini, or OpenAI client created with different
|
|
214
|
+
credentials or configuration.
|
|
215
|
+
|
|
216
|
+
Gemini uses `gemini-embedding-2`. Its default is 3072 dimensions, and explicit
|
|
217
|
+
Gemini dimensions must be an integer from 128 through 3072.
|
|
215
218
|
|
|
216
219
|
### `getEmbeddingProviderMetadata(options?)`
|
|
217
220
|
|
|
@@ -239,7 +242,7 @@ Provider clients return:
|
|
|
239
242
|
```ts
|
|
240
243
|
interface EmbeddingBatchResult {
|
|
241
244
|
embeddings: number[][];
|
|
242
|
-
provider: "local" | "cloudflare" | "gemini" | "openai";
|
|
245
|
+
provider: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
243
246
|
model: string;
|
|
244
247
|
dimensions: number;
|
|
245
248
|
intent: "document" | "query";
|
|
@@ -255,7 +258,7 @@ cardinality, dimensions, finite numeric values, and indexed batch ordering.
|
|
|
255
258
|
|
|
256
259
|
```ts
|
|
257
260
|
interface EmbeddingOptions {
|
|
258
|
-
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
261
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
259
262
|
apiKey?: string;
|
|
260
263
|
accountId?: string;
|
|
261
264
|
apiToken?: string;
|
|
@@ -267,9 +270,10 @@ interface EmbeddingOptions {
|
|
|
267
270
|
}
|
|
268
271
|
```
|
|
269
272
|
|
|
270
|
-
`apiKey` is used by Gemini and OpenAI.
|
|
271
|
-
`
|
|
272
|
-
`
|
|
273
|
+
`apiKey` is used by Mistral, Gemini, and OpenAI. It falls back to
|
|
274
|
+
`MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY` for those providers.
|
|
275
|
+
Cloudflare uses `accountId` and `apiToken`, which fall back to
|
|
276
|
+
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`.
|
|
273
277
|
|
|
274
278
|
### `padEmbedding(embedding, targetDimensions)`
|
|
275
279
|
|
package/docs/INDEXING.md
CHANGED
|
@@ -33,6 +33,14 @@ await indexContent({
|
|
|
33
33
|
That keeps the implementation simple, but it also means a failed rebuild can
|
|
34
34
|
leave the index partially repopulated.
|
|
35
35
|
|
|
36
|
+
Changing an embedding provider or dimension count requires a full re-embed.
|
|
37
|
+
For Gemini specifically, indexes created with the retired `text-embedding-004`
|
|
38
|
+
model must be rebuilt for `gemini-embedding-2` even when staying at 768
|
|
39
|
+
dimensions, because the model and query/document formatting both changed. If
|
|
40
|
+
you adopt Gemini's 3072-dimensional default, recreate the vector table or build
|
|
41
|
+
into a separate table first; clearing rows with `indexContent()` does not change
|
|
42
|
+
the table's `F32_BLOB` width.
|
|
43
|
+
|
|
36
44
|
## Quality Guidelines
|
|
37
45
|
|
|
38
46
|
- include descriptive frontmatter titles
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -128,8 +128,19 @@ const client = createClient({
|
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
const embeddingProvider =
|
|
131
|
-
process.env.EMBEDDING_PROVIDER as
|
|
132
|
-
|
|
131
|
+
process.env.EMBEDDING_PROVIDER as
|
|
132
|
+
| "local"
|
|
133
|
+
| "cloudflare"
|
|
134
|
+
| "mistral"
|
|
135
|
+
| "gemini"
|
|
136
|
+
| "openai"
|
|
137
|
+
| undefined;
|
|
138
|
+
const embeddingDimensions =
|
|
139
|
+
embeddingProvider === "cloudflare" || embeddingProvider === "mistral"
|
|
140
|
+
? 1024
|
|
141
|
+
: embeddingProvider === "gemini"
|
|
142
|
+
? 3072
|
|
143
|
+
: 768;
|
|
133
144
|
|
|
134
145
|
await createTable(client, "articles", embeddingDimensions);
|
|
135
146
|
|
package/docs/PROVIDERS.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Embedding Providers
|
|
2
2
|
|
|
3
|
-
`libsql-search` currently supports
|
|
3
|
+
`libsql-search` currently supports five embedding providers:
|
|
4
4
|
|
|
5
5
|
- `local`
|
|
6
6
|
- `cloudflare`
|
|
7
|
+
- `mistral`
|
|
7
8
|
- `gemini`
|
|
8
9
|
- `openai`
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ query time.
|
|
|
15
16
|
|
|
16
17
|
```ts
|
|
17
18
|
interface EmbeddingOptions {
|
|
18
|
-
provider?: "local" | "cloudflare" | "gemini" | "openai";
|
|
19
|
+
provider?: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
19
20
|
apiKey?: string;
|
|
20
21
|
accountId?: string;
|
|
21
22
|
apiToken?: string;
|
|
@@ -28,13 +29,14 @@ interface EmbeddingOptions {
|
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
- `provider` defaults to `"local"`
|
|
31
|
-
- `dimensions` defaults to `768`
|
|
32
|
+
- `dimensions` defaults to `768` for the library's default local provider.
|
|
33
|
+
Provider-specific defaults can differ; Gemini defaults to `3072`.
|
|
32
34
|
- `maxLength` defaults to `8000`
|
|
33
35
|
- `intent` can be `"document"` or `"query"`; indexing defaults to
|
|
34
36
|
`"document"` and search defaults to `"query"` unless explicitly set
|
|
35
37
|
- `timeoutMs` defaults to `30000`
|
|
36
|
-
- `apiKey` is used by Gemini and OpenAI and falls back to
|
|
37
|
-
`OPENAI_API_KEY`
|
|
38
|
+
- `apiKey` is used by Mistral, Gemini, and OpenAI and falls back to
|
|
39
|
+
`MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY`
|
|
38
40
|
- `accountId` and `apiToken` are used by Cloudflare and fall back to
|
|
39
41
|
`CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`
|
|
40
42
|
|
|
@@ -44,7 +46,7 @@ Each provider exposes immutable metadata:
|
|
|
44
46
|
|
|
45
47
|
```ts
|
|
46
48
|
interface EmbeddingProviderMetadata {
|
|
47
|
-
name: "local" | "cloudflare" | "gemini" | "openai";
|
|
49
|
+
name: "local" | "cloudflare" | "mistral" | "gemini" | "openai";
|
|
48
50
|
model: string;
|
|
49
51
|
dimensions: number;
|
|
50
52
|
batch: {
|
|
@@ -83,9 +85,9 @@ Lower-level provider clients return an `EmbeddingBatchResult` with the validated
|
|
|
83
85
|
vectors plus provider, model, dimensions, and intent. The compatibility helpers
|
|
84
86
|
`generateEmbedding()` and `generateEmbeddings()` return only arrays.
|
|
85
87
|
|
|
86
|
-
Cloudflare, Gemini, and OpenAI clients are scoped to their current
|
|
87
|
-
are not cached globally across different credentials or
|
|
88
|
-
local Xenova model can be cached by model name.
|
|
88
|
+
Cloudflare, Mistral, Gemini, and OpenAI clients are scoped to their current
|
|
89
|
+
options. They are not cached globally across different credentials or
|
|
90
|
+
configurations. The local Xenova model can be cached by model name.
|
|
89
91
|
|
|
90
92
|
Hosted provider failures are reported with bounded provider/status/request-id
|
|
91
93
|
context and without raw upstream bodies, credentials, Authorization headers, or
|
|
@@ -144,26 +146,60 @@ Behavior:
|
|
|
144
146
|
- Cloudflare does not accept custom dimensions in this provider; use
|
|
145
147
|
`createTable(client, "articles", 1024)` for Cloudflare-backed indexes
|
|
146
148
|
|
|
149
|
+
## Mistral
|
|
150
|
+
|
|
151
|
+
Provider value: `mistral`
|
|
152
|
+
|
|
153
|
+
Mistral uses the hosted `mistral-embed` model through
|
|
154
|
+
`https://api.mistral.ai/v1/embeddings`.
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
embeddingOptions: {
|
|
158
|
+
provider: "mistral",
|
|
159
|
+
apiKey: process.env.MISTRAL_API_KEY,
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Behavior:
|
|
164
|
+
|
|
165
|
+
- if `apiKey` is omitted, the library reads `MISTRAL_API_KEY`
|
|
166
|
+
- blank Mistral credentials are treated as missing
|
|
167
|
+
- `mistral-embed` returns 1024 dimensions
|
|
168
|
+
- metadata reports `mistral-embed` and 1024 dimensions without requiring
|
|
169
|
+
credentials
|
|
170
|
+
- batch metadata is `{ mode: "native" }`
|
|
171
|
+
- request bodies send `encoding_format: "float"`
|
|
172
|
+
- response items are reordered by provider-supplied index before being returned
|
|
173
|
+
- Mistral does not accept custom dimensions in this provider; use
|
|
174
|
+
`createTable(client, "articles", 1024)` for Mistral-backed indexes
|
|
175
|
+
|
|
147
176
|
## Gemini
|
|
148
177
|
|
|
149
178
|
Provider value: `gemini`
|
|
150
179
|
|
|
151
|
-
Gemini uses Google `
|
|
180
|
+
Gemini uses Google `gemini-embedding-2` through `@google/genai`.
|
|
152
181
|
|
|
153
182
|
```ts
|
|
154
183
|
embeddingOptions: {
|
|
155
184
|
provider: "gemini",
|
|
156
185
|
apiKey: process.env.GEMINI_API_KEY,
|
|
186
|
+
dimensions: 3072,
|
|
157
187
|
}
|
|
158
188
|
```
|
|
159
189
|
|
|
160
190
|
Behavior:
|
|
161
191
|
|
|
162
192
|
- if `apiKey` is omitted, the library reads `GEMINI_API_KEY`
|
|
163
|
-
- Gemini
|
|
164
|
-
-
|
|
193
|
+
- blank Gemini credentials are treated as missing
|
|
194
|
+
- Gemini defaults to 3072 dimensions
|
|
195
|
+
- explicit Gemini dimensions must be integers from 128 through 3072
|
|
196
|
+
- 768, 1536, and 3072 are recommended practical sizes
|
|
197
|
+
- metadata reports `gemini-embedding-2` and the effective dimensions
|
|
165
198
|
- batch metadata is `{ mode: "sequential" }`
|
|
166
|
-
- the
|
|
199
|
+
- the library sends one SDK request per input and verifies one vector per input
|
|
200
|
+
- document inputs are formatted as `title: none | text: ...`
|
|
201
|
+
- query inputs are formatted as `task: search result | query: ...`
|
|
202
|
+
- the current implementation does not expose custom model selection
|
|
167
203
|
|
|
168
204
|
## OpenAI
|
|
169
205
|
|
|
@@ -193,10 +229,19 @@ Behavior:
|
|
|
193
229
|
|
|
194
230
|
- `local` defaults to `768`
|
|
195
231
|
- `cloudflare` is fixed at `1024`
|
|
232
|
+
- `mistral` is fixed at `1024`
|
|
196
233
|
- local embeddings are padded from 384 to your target size
|
|
197
|
-
- Gemini
|
|
234
|
+
- Gemini defaults to `3072` and accepts explicit dimensions from `128` through
|
|
235
|
+
`3072`; use `768`, `1536`, or `3072` unless you have a specific reason
|
|
198
236
|
- OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
|
|
199
237
|
value you explicitly set
|
|
200
238
|
|
|
201
239
|
If you switch provider or dimensions for an existing table, recreate the table
|
|
202
240
|
or rebuild the index into a separate table so stored vectors stay consistent.
|
|
241
|
+
|
|
242
|
+
Existing Gemini indexes created with `text-embedding-004` must be fully
|
|
243
|
+
re-embedded for `gemini-embedding-2`, even if you keep `dimensions: 768`,
|
|
244
|
+
because both the model and query/document input formatting changed. If you move
|
|
245
|
+
to the new 3072-dimensional default, create a new table or recreate the vector
|
|
246
|
+
table first; `indexContent()` clears rows but does not change the `F32_BLOB`
|
|
247
|
+
width. A separate table is safer because rebuilds are not transactional.
|
package/docs/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
This directory holds the longer-form reference material for `libsql-search`.
|
|
4
4
|
Start with the page that matches the job you are doing:
|
|
5
5
|
|
|
6
|
-
- [Provider guide](./PROVIDERS.md): local, Gemini, and
|
|
7
|
-
dimensions, and API key behavior
|
|
6
|
+
- [Provider guide](./PROVIDERS.md): local, Cloudflare, Mistral, Gemini, and
|
|
7
|
+
OpenAI embedding options, dimensions, and API key behavior
|
|
8
8
|
- [API reference](./API.md): exported functions, option shapes, and result data
|
|
9
9
|
- [Integration examples](./INTEGRATIONS.md): Astro and Next.js server-side usage
|
|
10
10
|
- [Indexing and operations](./INDEXING.md): content layout, rebuild scripts,
|
package/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -9,4 +9,9 @@ Common operational checks:
|
|
|
9
9
|
- verify you called `createTable()` before indexing or searching
|
|
10
10
|
- verify the table dimension matches the embedding dimension in your code
|
|
11
11
|
- verify the same provider is used for indexing and querying
|
|
12
|
-
- verify hosted providers have `
|
|
12
|
+
- verify hosted providers have `CLOUDFLARE_ACCOUNT_ID` and
|
|
13
|
+
`CLOUDFLARE_API_TOKEN`, `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or
|
|
14
|
+
`OPENAI_API_KEY` available as required by the selected provider
|
|
15
|
+
- after upgrading an existing Gemini index, fully re-embed with
|
|
16
|
+
`gemini-embedding-2`; for 3072-dimensional Gemini indexes, recreate the table
|
|
17
|
+
or use a new table name before rebuilding
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libsql-search",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@10.34.5",
|
|
@@ -66,9 +66,6 @@
|
|
|
66
66
|
"@xenova/transformers": "^2.17.2",
|
|
67
67
|
"gray-matter": "^4.0.3"
|
|
68
68
|
},
|
|
69
|
-
"optionalDependencies": {
|
|
70
|
-
"@google/generative-ai": "^0.24.1"
|
|
71
|
-
},
|
|
72
69
|
"devDependencies": {
|
|
73
70
|
"@libsql/client": "^0.15.15",
|
|
74
71
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
@@ -82,5 +79,8 @@
|
|
|
82
79
|
"tslib": "^2.8.1",
|
|
83
80
|
"typescript": "^5.9.3",
|
|
84
81
|
"vitest": "^4.1.11"
|
|
82
|
+
},
|
|
83
|
+
"optionalDependencies": {
|
|
84
|
+
"@google/genai": "2.18.0"
|
|
85
85
|
}
|
|
86
86
|
}
|