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