libsql-search 0.1.5 → 0.2.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/dist/index.esm.js CHANGED
@@ -2,7 +2,15 @@ import { readdir, readFile } from 'fs/promises';
2
2
  import { join, extname, relative, dirname } from 'path';
3
3
  import matter from 'gray-matter';
4
4
 
5
- const providerCache = {};
5
+ const DEFAULT_DIMENSIONS = 768;
6
+ const DEFAULT_MAX_LENGTH = 8e3;
7
+ const DEFAULT_TIMEOUT_MS = 3e4;
8
+ const LOCAL_MODEL = "Xenova/all-MiniLM-L6-v2";
9
+ const GEMINI_MODEL = "text-embedding-004";
10
+ const OPENAI_SMALL_MODEL = "text-embedding-3-small";
11
+ const OPENAI_LARGE_MODEL = "text-embedding-3-large";
12
+ const OPENAI_EMBEDDINGS_URL = "https://api.openai.com/v1/embeddings";
13
+ const localModelCacheByModel = /* @__PURE__ */ new Map();
6
14
  function getEnvironmentVariable(name) {
7
15
  const runtime = globalThis;
8
16
  const nodeValue = runtime.process?.env?.[name];
@@ -15,80 +23,403 @@ function getEnvironmentVariable(name) {
15
23
  return void 0;
16
24
  }
17
25
  }
18
- async function getLocalEmbeddingModel() {
19
- if (!providerCache.local) {
20
- console.log("Loading local embedding model (Xenova/all-MiniLM-L6-v2)...");
21
- const { pipeline } = await import('@xenova/transformers');
22
- providerCache.local = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
23
- console.log("Local model loaded successfully");
26
+ async function getLocalEmbeddingModel(modelName) {
27
+ const cached = localModelCacheByModel.get(modelName);
28
+ if (cached?.model) {
29
+ return cached.model;
24
30
  }
25
- return providerCache.local;
31
+ console.log(`Loading local embedding model (${modelName})...`);
32
+ const { pipeline } = await import('@xenova/transformers');
33
+ const model = await pipeline("feature-extraction", modelName);
34
+ localModelCacheByModel.set(modelName, { model });
35
+ console.log("Local model loaded successfully");
36
+ return model;
26
37
  }
27
- async function generateEmbedding(text, options = {}) {
28
- const {
29
- provider = "local",
30
- apiKey,
31
- dimensions = 768,
32
- maxLength = 8e3
33
- } = options;
34
- const truncated = text.substring(0, maxLength);
35
- switch (provider) {
38
+ function getPositiveInteger(value, optionName) {
39
+ if (!Number.isInteger(value) || value <= 0) {
40
+ throw new Error(`Invalid ${optionName}: expected a positive integer`);
41
+ }
42
+ return value;
43
+ }
44
+ function getTimeoutMs(value) {
45
+ return getPositiveInteger(value ?? DEFAULT_TIMEOUT_MS, "timeoutMs");
46
+ }
47
+ function resolveProviderName(provider) {
48
+ switch (provider ?? "local") {
36
49
  case "local":
37
- return generateLocalEmbedding(truncated, dimensions);
38
50
  case "gemini":
39
- return generateGeminiEmbedding(truncated, apiKey);
40
51
  case "openai":
41
- return generateOpenAIEmbedding(truncated, apiKey, dimensions);
52
+ return provider ?? "local";
42
53
  default:
43
- throw new Error(`Unknown embedding provider: ${provider}`);
54
+ throw new Error(`Unknown embedding provider: ${String(provider)}`);
55
+ }
56
+ }
57
+ function resolveIntent(intent) {
58
+ switch (intent ?? "document") {
59
+ case "document":
60
+ case "query":
61
+ return intent ?? "document";
62
+ default:
63
+ throw new Error(`Unknown embedding intent: ${String(intent)}`);
64
+ }
65
+ }
66
+ function truncateTexts(texts, maxLength) {
67
+ return texts.map((text) => text.substring(0, maxLength));
68
+ }
69
+ function getOpenAIModel(dimensions) {
70
+ return dimensions <= 1536 ? OPENAI_SMALL_MODEL : OPENAI_LARGE_MODEL;
71
+ }
72
+ function hasOwnProperty(value, property) {
73
+ return Object.prototype.hasOwnProperty.call(value, property);
74
+ }
75
+ function redactErrorText(value) {
76
+ const raw = value instanceof Error ? value.message : String(value);
77
+ return raw.replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]").replace(/([?&](?:api[_-]?key|key|token)=)[^&\s]+/gi, "$1[redacted]").replace(/(authorization\s*[:=]\s*)[^\s,}]+/gi, "$1[redacted]").replace(/https?:\/\/[^\s)]+/gi, (match) => {
78
+ try {
79
+ const url = new URL(match);
80
+ return `${url.origin}${url.pathname}`;
81
+ } catch {
82
+ return "[redacted-url]";
83
+ }
84
+ }).slice(0, 300);
85
+ }
86
+ function providerError(provider, message, cause) {
87
+ const safeCause = cause === void 0 ? "" : `: ${redactErrorText(cause).trim()}`;
88
+ return new Error(`${provider} embedding error: ${message}${safeCause}`);
89
+ }
90
+ function getResponseHeader(response, name) {
91
+ return response.headers.get(name) ?? void 0;
92
+ }
93
+ async function withTimeout(provider, operation, timeoutMs, run, parentSignal) {
94
+ if (parentSignal?.aborted) {
95
+ throw providerError(provider, `${operation} was aborted`);
96
+ }
97
+ const controller = new AbortController();
98
+ let abortError;
99
+ let rejectAbort = () => {
100
+ };
101
+ const abortPromise = new Promise((_resolve, reject) => {
102
+ rejectAbort = reject;
103
+ });
104
+ const abort = (error) => {
105
+ if (abortError) {
106
+ return;
107
+ }
108
+ abortError = error;
109
+ controller.abort();
110
+ rejectAbort(error);
111
+ };
112
+ const onParentAbort = () => abort(providerError(provider, `${operation} was aborted`));
113
+ const timeout = setTimeout(() => {
114
+ abort(providerError(provider, `${operation} timed out after ${timeoutMs}ms`));
115
+ }, timeoutMs);
116
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
117
+ const operationPromise = Promise.resolve().then(() => run(controller.signal));
118
+ operationPromise.catch(() => void 0);
119
+ try {
120
+ return await Promise.race([operationPromise, abortPromise]);
121
+ } catch (error) {
122
+ if (error === abortError) {
123
+ throw error;
124
+ }
125
+ throw providerError(provider, `${operation} failed`, error);
126
+ } finally {
127
+ clearTimeout(timeout);
128
+ parentSignal?.removeEventListener("abort", onParentAbort);
129
+ }
130
+ }
131
+ function validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider) {
132
+ if (items.length !== expectedCount) {
133
+ throw providerError(
134
+ provider,
135
+ `expected ${expectedCount} embedding result(s), received ${items.length}`
136
+ );
137
+ }
138
+ const hasIndexedItems = items.some((item) => !Array.isArray(item) && hasOwnProperty(item, "index"));
139
+ const ordered = hasIndexedItems ? reorderIndexedEmbeddings(items, expectedCount, provider) : items.map((item) => Array.isArray(item) ? item : item.embedding);
140
+ return ordered.map(
141
+ (embedding, itemIndex) => validateEmbeddingVector(embedding, expectedDimensions, provider, itemIndex)
142
+ );
143
+ }
144
+ function reorderIndexedEmbeddings(items, expectedCount, provider) {
145
+ const ordered = new Array(expectedCount);
146
+ const seen = /* @__PURE__ */ new Set();
147
+ for (const item of items) {
148
+ if (Array.isArray(item) || !hasOwnProperty(item, "index")) {
149
+ throw providerError(provider, "provider returned a partially indexed embedding batch");
150
+ }
151
+ if (typeof item.index !== "number" || !Number.isInteger(item.index) || item.index < 0 || item.index >= expectedCount) {
152
+ throw providerError(provider, `provider returned invalid embedding index ${String(item.index)}`);
153
+ }
154
+ if (seen.has(item.index)) {
155
+ throw providerError(provider, `provider returned duplicate embedding index ${item.index}`);
156
+ }
157
+ seen.add(item.index);
158
+ ordered[item.index] = item.embedding;
159
+ }
160
+ if (seen.size !== expectedCount) {
161
+ throw providerError(provider, "provider returned non-contiguous embedding indices");
162
+ }
163
+ return ordered;
164
+ }
165
+ function validateEmbeddingVector(embedding, expectedDimensions, provider, itemIndex) {
166
+ if (!Array.isArray(embedding)) {
167
+ throw providerError(provider, `embedding ${itemIndex} is not an array`);
168
+ }
169
+ if (embedding.length !== expectedDimensions) {
170
+ throw providerError(
171
+ provider,
172
+ `embedding ${itemIndex} has ${embedding.length} dimensions, expected ${expectedDimensions}`
173
+ );
174
+ }
175
+ for (let i = 0; i < embedding.length; i++) {
176
+ const value = embedding[i];
177
+ if (typeof value !== "number" || !Number.isFinite(value)) {
178
+ throw providerError(provider, `embedding ${itemIndex} contains a non-finite value at dimension ${i}`);
179
+ }
180
+ }
181
+ return embedding;
182
+ }
183
+ function createProviderMetadata(provider, dimensions) {
184
+ switch (provider) {
185
+ case "local":
186
+ return Object.freeze({
187
+ name: "local",
188
+ model: LOCAL_MODEL,
189
+ dimensions,
190
+ batch: Object.freeze({ mode: "sequential" })
191
+ });
192
+ case "gemini":
193
+ return Object.freeze({
194
+ name: "gemini",
195
+ model: GEMINI_MODEL,
196
+ dimensions: DEFAULT_DIMENSIONS,
197
+ batch: Object.freeze({ mode: "sequential" })
198
+ });
199
+ case "openai":
200
+ return Object.freeze({
201
+ name: "openai",
202
+ model: getOpenAIModel(dimensions),
203
+ dimensions,
204
+ batch: Object.freeze({ mode: "native", maxSize: 2048 })
205
+ });
44
206
  }
45
207
  }
46
- async function generateLocalEmbedding(text, targetDimensions) {
47
- const model = await getLocalEmbeddingModel();
48
- const output = await model(text, {
49
- pooling: "mean",
50
- normalize: true
208
+ function getEffectiveDimensions(provider, dimensions) {
209
+ if (provider === "gemini") {
210
+ return DEFAULT_DIMENSIONS;
211
+ }
212
+ return getPositiveInteger(dimensions ?? DEFAULT_DIMENSIONS, "dimensions");
213
+ }
214
+ function assertBatchSize(metadata, count) {
215
+ if (metadata.batch.maxSize !== void 0 && count > metadata.batch.maxSize) {
216
+ throw providerError(
217
+ metadata.name,
218
+ `batch size ${count} exceeds maximum ${metadata.batch.maxSize}`
219
+ );
220
+ }
221
+ }
222
+ function createEmbeddingBatchResult(metadata, intent, embeddings) {
223
+ return Object.freeze({
224
+ embeddings,
225
+ provider: metadata.name,
226
+ model: metadata.model,
227
+ dimensions: metadata.dimensions,
228
+ intent
51
229
  });
52
- const embedding = Array.from(output.data);
53
- return padEmbedding(embedding, targetDimensions);
54
- }
55
- async function generateGeminiEmbedding(text, apiKey) {
56
- const key = apiKey || getEnvironmentVariable("GEMINI_API_KEY");
57
- if (!key) {
58
- throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
59
- }
60
- if (!providerCache.gemini) {
61
- const { GoogleGenerativeAI } = await import('@google/generative-ai');
62
- const genAI = new GoogleGenerativeAI(key);
63
- providerCache.gemini = genAI.getGenerativeModel({ model: "text-embedding-004" });
64
- }
65
- const result = await providerCache.gemini.embedContent(text);
66
- return result.embedding.values;
67
- }
68
- async function generateOpenAIEmbedding(text, apiKey, dimensions = 1536) {
69
- const key = apiKey || getEnvironmentVariable("OPENAI_API_KEY");
70
- if (!key) {
71
- throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
72
- }
73
- const model = dimensions <= 1536 ? "text-embedding-3-small" : "text-embedding-3-large";
74
- const response = await fetch("https://api.openai.com/v1/embeddings", {
75
- method: "POST",
76
- headers: {
77
- "Content-Type": "application/json",
78
- "Authorization": `Bearer ${key}`
79
- },
80
- body: JSON.stringify({
81
- input: text,
82
- model,
83
- dimensions
84
- })
230
+ }
231
+ class LocalEmbeddingProvider {
232
+ metadata;
233
+ #timeoutMs;
234
+ constructor(metadata, timeoutMs) {
235
+ this.metadata = metadata;
236
+ this.#timeoutMs = timeoutMs;
237
+ }
238
+ async embed(texts, options = {}) {
239
+ const intent = resolveIntent(options.intent);
240
+ if (texts.length === 0) {
241
+ return createEmbeddingBatchResult(this.metadata, intent, []);
242
+ }
243
+ assertBatchSize(this.metadata, texts.length);
244
+ const vectors = await withTimeout(
245
+ "local",
246
+ "model inference",
247
+ this.#timeoutMs,
248
+ async () => {
249
+ const model = await getLocalEmbeddingModel(this.metadata.model);
250
+ const results = [];
251
+ for (const text of texts) {
252
+ const output = await model(text, {
253
+ pooling: "mean",
254
+ normalize: true
255
+ });
256
+ const embedding = Array.from(output.data);
257
+ results.push(padEmbedding(embedding, this.metadata.dimensions));
258
+ }
259
+ return results;
260
+ },
261
+ options.signal
262
+ );
263
+ return createEmbeddingBatchResult(
264
+ this.metadata,
265
+ intent,
266
+ validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "local")
267
+ );
268
+ }
269
+ }
270
+ class GeminiEmbeddingProvider {
271
+ metadata;
272
+ #apiKey;
273
+ #timeoutMs;
274
+ constructor(metadata, apiKey, timeoutMs) {
275
+ this.metadata = metadata;
276
+ this.#apiKey = apiKey;
277
+ this.#timeoutMs = timeoutMs;
278
+ }
279
+ async embed(texts, options = {}) {
280
+ const intent = resolveIntent(options.intent);
281
+ if (texts.length === 0) {
282
+ return createEmbeddingBatchResult(this.metadata, intent, []);
283
+ }
284
+ assertBatchSize(this.metadata, texts.length);
285
+ const vectors = await withTimeout(
286
+ "gemini",
287
+ "API request",
288
+ this.#timeoutMs,
289
+ async () => {
290
+ const { GoogleGenerativeAI } = await import('@google/generative-ai');
291
+ const genAI = new GoogleGenerativeAI(this.#apiKey);
292
+ const model = genAI.getGenerativeModel({ model: this.metadata.model });
293
+ const results = [];
294
+ for (const text of texts) {
295
+ const result = await model.embedContent(text);
296
+ const values = result.embedding?.values;
297
+ if (!Array.isArray(values)) {
298
+ throw new Error("Gemini response did not include embedding values");
299
+ }
300
+ results.push(values);
301
+ }
302
+ return results;
303
+ },
304
+ options.signal
305
+ );
306
+ return createEmbeddingBatchResult(
307
+ this.metadata,
308
+ intent,
309
+ validateEmbeddingBatch(vectors, texts.length, this.metadata.dimensions, "gemini")
310
+ );
311
+ }
312
+ }
313
+ class OpenAIEmbeddingProvider {
314
+ metadata;
315
+ #apiKey;
316
+ #timeoutMs;
317
+ constructor(metadata, apiKey, timeoutMs) {
318
+ this.metadata = metadata;
319
+ this.#apiKey = apiKey;
320
+ this.#timeoutMs = timeoutMs;
321
+ }
322
+ async embed(texts, options = {}) {
323
+ const intent = resolveIntent(options.intent);
324
+ if (texts.length === 0) {
325
+ return createEmbeddingBatchResult(this.metadata, intent, []);
326
+ }
327
+ assertBatchSize(this.metadata, texts.length);
328
+ const items = await withTimeout(
329
+ "openai",
330
+ "API request",
331
+ this.#timeoutMs,
332
+ async (signal) => {
333
+ const response = await fetch(OPENAI_EMBEDDINGS_URL, {
334
+ method: "POST",
335
+ headers: {
336
+ "Content-Type": "application/json",
337
+ "Authorization": `Bearer ${this.#apiKey}`
338
+ },
339
+ signal,
340
+ body: JSON.stringify({
341
+ input: texts,
342
+ model: this.metadata.model,
343
+ dimensions: this.metadata.dimensions
344
+ })
345
+ });
346
+ if (!response.ok) {
347
+ const requestId = getResponseHeader(response, "x-request-id");
348
+ const context = requestId ? ` status ${response.status}, request ${requestId}` : ` status ${response.status}`;
349
+ throw new Error(context);
350
+ }
351
+ const data = await response.json();
352
+ if (!Array.isArray(data.data)) {
353
+ throw new Error("OpenAI response did not include a data array");
354
+ }
355
+ return data.data.map((item) => {
356
+ const typed = item;
357
+ const embedding = typed.embedding;
358
+ if (!Array.isArray(embedding)) {
359
+ throw new Error("OpenAI response item did not include an embedding array");
360
+ }
361
+ const parsed = { embedding };
362
+ if (hasOwnProperty(typed, "index")) {
363
+ parsed.index = typed.index;
364
+ }
365
+ return parsed;
366
+ });
367
+ },
368
+ options.signal
369
+ );
370
+ return createEmbeddingBatchResult(
371
+ this.metadata,
372
+ intent,
373
+ validateEmbeddingBatch(items, texts.length, this.metadata.dimensions, "openai")
374
+ );
375
+ }
376
+ }
377
+ function createEmbeddingProvider(options = {}) {
378
+ const provider = resolveProviderName(options.provider);
379
+ const metadata = getEmbeddingProviderMetadata(options);
380
+ const timeoutMs = getTimeoutMs(options.timeoutMs);
381
+ switch (provider) {
382
+ case "local":
383
+ return new LocalEmbeddingProvider(metadata, timeoutMs);
384
+ case "gemini": {
385
+ const key = options.apiKey || getEnvironmentVariable("GEMINI_API_KEY");
386
+ if (!key) {
387
+ throw new Error("GEMINI_API_KEY is required for Gemini embeddings");
388
+ }
389
+ return new GeminiEmbeddingProvider(metadata, key, timeoutMs);
390
+ }
391
+ case "openai": {
392
+ const key = options.apiKey || getEnvironmentVariable("OPENAI_API_KEY");
393
+ if (!key) {
394
+ throw new Error("OPENAI_API_KEY is required for OpenAI embeddings");
395
+ }
396
+ return new OpenAIEmbeddingProvider(metadata, key, timeoutMs);
397
+ }
398
+ }
399
+ }
400
+ function getEmbeddingProviderMetadata(options = {}) {
401
+ const provider = resolveProviderName(options.provider);
402
+ return createProviderMetadata(provider, getEffectiveDimensions(provider, options.dimensions));
403
+ }
404
+ async function generateEmbeddings(texts, options = {}) {
405
+ const maxLength = getPositiveInteger(options.maxLength ?? DEFAULT_MAX_LENGTH, "maxLength");
406
+ const intent = resolveIntent(options.intent);
407
+ if (texts.length === 0) {
408
+ return [];
409
+ }
410
+ const provider = createEmbeddingProvider(options);
411
+ const result = await provider.embed(truncateTexts(texts, maxLength), {
412
+ intent,
413
+ ...options.signal ? { signal: options.signal } : {}
85
414
  });
86
- if (!response.ok) {
87
- const error = await response.text();
88
- throw new Error(`OpenAI API error: ${error}`);
415
+ return result.embeddings;
416
+ }
417
+ async function generateEmbedding(text, options = {}) {
418
+ const [embedding] = await generateEmbeddings([text], options);
419
+ if (!embedding) {
420
+ throw new Error("Embedding provider returned no embedding");
89
421
  }
90
- const data = await response.json();
91
- return data.data[0].embedding;
422
+ return embedding;
92
423
  }
93
424
  function padEmbedding(embedding, targetDimensions) {
94
425
  if (embedding.length === targetDimensions) {
@@ -112,6 +443,34 @@ function prepareTextForEmbedding(fields) {
112
443
  return parts.filter(Boolean).join("\n\n");
113
444
  }
114
445
 
446
+ const SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
447
+ const DEFAULT_SEARCH_LIMIT = 10;
448
+ const MAX_SEARCH_LIMIT = 100;
449
+ function validateSqlIdentifier(identifier, name = "identifier") {
450
+ if (typeof identifier !== "string" || !SQL_IDENTIFIER_PATTERN.test(identifier)) {
451
+ throw new Error(
452
+ `Invalid SQL ${name}: expected an ASCII identifier matching ${SQL_IDENTIFIER_PATTERN.toString()}`
453
+ );
454
+ }
455
+ return identifier;
456
+ }
457
+ function quoteSqlIdentifier(identifier, name) {
458
+ const validated = validateSqlIdentifier(identifier, name);
459
+ return `"${validated.replace(/"/g, '""')}"`;
460
+ }
461
+ function normalizeSearchLimit(limit = DEFAULT_SEARCH_LIMIT) {
462
+ if (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_LIMIT) {
463
+ throw new Error(`Invalid search limit: expected an integer from 1 to ${MAX_SEARCH_LIMIT}`);
464
+ }
465
+ return limit;
466
+ }
467
+ function normalizeVectorDimensions(dimensions) {
468
+ if (typeof dimensions !== "number" || !Number.isFinite(dimensions) || !Number.isInteger(dimensions) || dimensions < 1) {
469
+ throw new Error("Invalid vector dimensions: expected a positive integer");
470
+ }
471
+ return dimensions;
472
+ }
473
+
115
474
  async function indexContent(options) {
116
475
  const {
117
476
  client,
@@ -122,12 +481,13 @@ async function indexContent(options) {
122
481
  tableName = "articles",
123
482
  onProgress
124
483
  } = options;
484
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
125
485
  const files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
126
486
  if (files.length === 0) {
127
487
  console.warn(`No files found in ${contentPath}`);
128
488
  return { success: 0, failed: 0, total: 0 };
129
489
  }
130
- await client.execute(`DELETE FROM ${tableName}`);
490
+ await client.execute(`DELETE FROM ${quotedTableName}`);
131
491
  let success = 0;
132
492
  let failed = 0;
133
493
  for (let i = 0; i < files.length; i++) {
@@ -137,7 +497,7 @@ async function indexContent(options) {
137
497
  }
138
498
  try {
139
499
  const document = await processFile(file, embeddingOptions);
140
- await insertDocument(client, document, tableName);
500
+ await insertDocument(client, document, quotedTableName);
141
501
  success++;
142
502
  } catch (error) {
143
503
  console.error(`Failed to index ${file.relativePath}:`, error);
@@ -180,7 +540,10 @@ async function processFile(file, embeddingOptions) {
180
540
  content: markdown,
181
541
  tags
182
542
  });
183
- const embedding = await generateEmbedding(embeddingText, embeddingOptions);
543
+ const embedding = await generateEmbedding(embeddingText, {
544
+ ...embeddingOptions,
545
+ intent: embeddingOptions.intent ?? "document"
546
+ });
184
547
  return {
185
548
  slug,
186
549
  title,
@@ -191,9 +554,9 @@ async function processFile(file, embeddingOptions) {
191
554
  metadata: frontMatter
192
555
  };
193
556
  }
194
- async function insertDocument(client, document, tableName) {
557
+ async function insertDocument(client, document, quotedTableName) {
195
558
  await client.execute({
196
- sql: `INSERT INTO ${tableName}
559
+ sql: `INSERT INTO ${quotedTableName}
197
560
  (slug, title, content, folder, tags, embedding, created_at, updated_at)
198
561
  VALUES (?, ?, ?, ?, ?, vector(?), datetime('now'), datetime('now'))`,
199
562
  args: [
@@ -207,30 +570,35 @@ async function insertDocument(client, document, tableName) {
207
570
  });
208
571
  }
209
572
  async function createTable(client, tableName = "articles", dimensions = 768) {
573
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
574
+ const vectorDimensions = normalizeVectorDimensions(dimensions);
575
+ const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
576
+ const quotedFolderIndexName = quoteSqlIdentifier(`${tableName}_folder_idx`, "folder index name");
577
+ const quotedSlugIndexName = quoteSqlIdentifier(`${tableName}_slug_idx`, "slug index name");
210
578
  await client.execute(`
211
- CREATE TABLE IF NOT EXISTS ${tableName} (
579
+ CREATE TABLE IF NOT EXISTS ${quotedTableName} (
212
580
  id INTEGER PRIMARY KEY AUTOINCREMENT,
213
581
  slug TEXT UNIQUE NOT NULL,
214
582
  title TEXT NOT NULL,
215
583
  content TEXT NOT NULL,
216
584
  folder TEXT NOT NULL DEFAULT 'root',
217
585
  tags TEXT DEFAULT '[]',
218
- embedding F32_BLOB(${dimensions}),
586
+ embedding F32_BLOB(${vectorDimensions}),
219
587
  created_at TEXT NOT NULL,
220
588
  updated_at TEXT NOT NULL
221
589
  )
222
590
  `);
223
591
  await client.execute(`
224
- CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx
225
- ON ${tableName}(libsql_vector_idx(embedding))
592
+ CREATE INDEX IF NOT EXISTS ${quotedEmbeddingIndexName}
593
+ ON ${quotedTableName}(libsql_vector_idx(embedding))
226
594
  `);
227
595
  await client.execute(`
228
- CREATE INDEX IF NOT EXISTS ${tableName}_folder_idx
229
- ON ${tableName}(folder)
596
+ CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
597
+ ON ${quotedTableName}(folder)
230
598
  `);
231
599
  await client.execute(`
232
- CREATE INDEX IF NOT EXISTS ${tableName}_slug_idx
233
- ON ${tableName}(slug)
600
+ CREATE INDEX IF NOT EXISTS ${quotedSlugIndexName}
601
+ ON ${quotedTableName}(slug)
234
602
  `);
235
603
  }
236
604
 
@@ -242,7 +610,12 @@ async function search(options) {
242
610
  tableName = "articles",
243
611
  embeddingOptions = {}
244
612
  } = options;
245
- const queryEmbedding = await generateEmbedding(query, embeddingOptions);
613
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
614
+ const resultLimit = normalizeSearchLimit(limit);
615
+ const queryEmbedding = await generateEmbedding(query, {
616
+ ...embeddingOptions,
617
+ intent: embeddingOptions.intent ?? "query"
618
+ });
246
619
  const results = await client.execute({
247
620
  sql: `
248
621
  SELECT
@@ -254,12 +627,12 @@ async function search(options) {
254
627
  tags,
255
628
  created_at,
256
629
  vector_distance_cos(embedding, vector(?)) as distance
257
- FROM ${tableName}
630
+ FROM ${quotedTableName}
258
631
  WHERE embedding IS NOT NULL
259
632
  ORDER BY distance
260
633
  LIMIT ?
261
634
  `,
262
- args: [JSON.stringify(queryEmbedding), limit]
635
+ args: [JSON.stringify(queryEmbedding), resultLimit]
263
636
  });
264
637
  return results.rows.map((row) => ({
265
638
  id: row.id,
@@ -273,9 +646,10 @@ async function search(options) {
273
646
  }));
274
647
  }
275
648
  async function getAllArticles(client, tableName = "articles") {
649
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
276
650
  const results = await client.execute(`
277
651
  SELECT id, slug, title, folder, tags, created_at, updated_at
278
- FROM ${tableName}
652
+ FROM ${quotedTableName}
279
653
  ORDER BY title
280
654
  `);
281
655
  return results.rows.map((row) => ({
@@ -289,10 +663,11 @@ async function getAllArticles(client, tableName = "articles") {
289
663
  }));
290
664
  }
291
665
  async function getArticleBySlug(client, slug, tableName = "articles") {
666
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
292
667
  const results = await client.execute({
293
668
  sql: `
294
669
  SELECT id, slug, title, content, folder, tags, created_at, updated_at
295
- FROM ${tableName}
670
+ FROM ${quotedTableName}
296
671
  WHERE slug = ?
297
672
  LIMIT 1
298
673
  `,
@@ -314,10 +689,11 @@ async function getArticleBySlug(client, slug, tableName = "articles") {
314
689
  };
315
690
  }
316
691
  async function getArticlesByFolder(client, folder, tableName = "articles") {
692
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
317
693
  const results = await client.execute({
318
694
  sql: `
319
695
  SELECT id, slug, title, folder, tags
320
- FROM ${tableName}
696
+ FROM ${quotedTableName}
321
697
  WHERE folder = ?
322
698
  ORDER BY title
323
699
  `,
@@ -332,12 +708,13 @@ async function getArticlesByFolder(client, folder, tableName = "articles") {
332
708
  }));
333
709
  }
334
710
  async function getFolders(client, tableName = "articles") {
711
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
335
712
  const results = await client.execute(`
336
713
  SELECT DISTINCT folder
337
- FROM ${tableName}
714
+ FROM ${quotedTableName}
338
715
  ORDER BY folder
339
716
  `);
340
717
  return results.rows.map((row) => row.folder);
341
718
  }
342
719
 
343
- export { createTable, generateEmbedding, getAllArticles, getArticleBySlug, getArticlesByFolder, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search };
720
+ export { createEmbeddingProvider, createTable, generateEmbedding, generateEmbeddings, getAllArticles, getArticleBySlug, getArticlesByFolder, getEmbeddingProviderMetadata, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search, validateEmbeddingBatch };