libsql-search 0.1.4 → 0.1.6

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
@@ -114,6 +114,34 @@ function prepareTextForEmbedding(fields) {
114
114
  return parts.filter(Boolean).join("\n\n");
115
115
  }
116
116
 
117
+ const SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
118
+ const DEFAULT_SEARCH_LIMIT = 10;
119
+ const MAX_SEARCH_LIMIT = 100;
120
+ function validateSqlIdentifier(identifier, name = "identifier") {
121
+ if (typeof identifier !== "string" || !SQL_IDENTIFIER_PATTERN.test(identifier)) {
122
+ throw new Error(
123
+ `Invalid SQL ${name}: expected an ASCII identifier matching ${SQL_IDENTIFIER_PATTERN.toString()}`
124
+ );
125
+ }
126
+ return identifier;
127
+ }
128
+ function quoteSqlIdentifier(identifier, name) {
129
+ const validated = validateSqlIdentifier(identifier, name);
130
+ return `"${validated.replace(/"/g, '""')}"`;
131
+ }
132
+ function normalizeSearchLimit(limit = DEFAULT_SEARCH_LIMIT) {
133
+ if (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_LIMIT) {
134
+ throw new Error(`Invalid search limit: expected an integer from 1 to ${MAX_SEARCH_LIMIT}`);
135
+ }
136
+ return limit;
137
+ }
138
+ function normalizeVectorDimensions(dimensions) {
139
+ if (typeof dimensions !== "number" || !Number.isFinite(dimensions) || !Number.isInteger(dimensions) || dimensions < 1) {
140
+ throw new Error("Invalid vector dimensions: expected a positive integer");
141
+ }
142
+ return dimensions;
143
+ }
144
+
117
145
  async function indexContent(options) {
118
146
  const {
119
147
  client,
@@ -124,12 +152,13 @@ async function indexContent(options) {
124
152
  tableName = "articles",
125
153
  onProgress
126
154
  } = options;
155
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
127
156
  const files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
128
157
  if (files.length === 0) {
129
158
  console.warn(`No files found in ${contentPath}`);
130
159
  return { success: 0, failed: 0, total: 0 };
131
160
  }
132
- await client.execute(`DELETE FROM ${tableName}`);
161
+ await client.execute(`DELETE FROM ${quotedTableName}`);
133
162
  let success = 0;
134
163
  let failed = 0;
135
164
  for (let i = 0; i < files.length; i++) {
@@ -139,7 +168,7 @@ async function indexContent(options) {
139
168
  }
140
169
  try {
141
170
  const document = await processFile(file, embeddingOptions);
142
- await insertDocument(client, document, tableName);
171
+ await insertDocument(client, document, quotedTableName);
143
172
  success++;
144
173
  } catch (error) {
145
174
  console.error(`Failed to index ${file.relativePath}:`, error);
@@ -193,9 +222,9 @@ async function processFile(file, embeddingOptions) {
193
222
  metadata: frontMatter
194
223
  };
195
224
  }
196
- async function insertDocument(client, document, tableName) {
225
+ async function insertDocument(client, document, quotedTableName) {
197
226
  await client.execute({
198
- sql: `INSERT INTO ${tableName}
227
+ sql: `INSERT INTO ${quotedTableName}
199
228
  (slug, title, content, folder, tags, embedding, created_at, updated_at)
200
229
  VALUES (?, ?, ?, ?, ?, vector(?), datetime('now'), datetime('now'))`,
201
230
  args: [
@@ -209,30 +238,35 @@ async function insertDocument(client, document, tableName) {
209
238
  });
210
239
  }
211
240
  async function createTable(client, tableName = "articles", dimensions = 768) {
241
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
242
+ const vectorDimensions = normalizeVectorDimensions(dimensions);
243
+ const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
244
+ const quotedFolderIndexName = quoteSqlIdentifier(`${tableName}_folder_idx`, "folder index name");
245
+ const quotedSlugIndexName = quoteSqlIdentifier(`${tableName}_slug_idx`, "slug index name");
212
246
  await client.execute(`
213
- CREATE TABLE IF NOT EXISTS ${tableName} (
247
+ CREATE TABLE IF NOT EXISTS ${quotedTableName} (
214
248
  id INTEGER PRIMARY KEY AUTOINCREMENT,
215
249
  slug TEXT UNIQUE NOT NULL,
216
250
  title TEXT NOT NULL,
217
251
  content TEXT NOT NULL,
218
252
  folder TEXT NOT NULL DEFAULT 'root',
219
253
  tags TEXT DEFAULT '[]',
220
- embedding F32_BLOB(${dimensions}),
254
+ embedding F32_BLOB(${vectorDimensions}),
221
255
  created_at TEXT NOT NULL,
222
256
  updated_at TEXT NOT NULL
223
257
  )
224
258
  `);
225
259
  await client.execute(`
226
- CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx
227
- ON ${tableName}(libsql_vector_idx(embedding))
260
+ CREATE INDEX IF NOT EXISTS ${quotedEmbeddingIndexName}
261
+ ON ${quotedTableName}(libsql_vector_idx(embedding))
228
262
  `);
229
263
  await client.execute(`
230
- CREATE INDEX IF NOT EXISTS ${tableName}_folder_idx
231
- ON ${tableName}(folder)
264
+ CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
265
+ ON ${quotedTableName}(folder)
232
266
  `);
233
267
  await client.execute(`
234
- CREATE INDEX IF NOT EXISTS ${tableName}_slug_idx
235
- ON ${tableName}(slug)
268
+ CREATE INDEX IF NOT EXISTS ${quotedSlugIndexName}
269
+ ON ${quotedTableName}(slug)
236
270
  `);
237
271
  }
238
272
 
@@ -244,6 +278,8 @@ async function search(options) {
244
278
  tableName = "articles",
245
279
  embeddingOptions = {}
246
280
  } = options;
281
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
282
+ const resultLimit = normalizeSearchLimit(limit);
247
283
  const queryEmbedding = await generateEmbedding(query, embeddingOptions);
248
284
  const results = await client.execute({
249
285
  sql: `
@@ -256,12 +292,12 @@ async function search(options) {
256
292
  tags,
257
293
  created_at,
258
294
  vector_distance_cos(embedding, vector(?)) as distance
259
- FROM ${tableName}
295
+ FROM ${quotedTableName}
260
296
  WHERE embedding IS NOT NULL
261
297
  ORDER BY distance
262
298
  LIMIT ?
263
299
  `,
264
- args: [JSON.stringify(queryEmbedding), limit]
300
+ args: [JSON.stringify(queryEmbedding), resultLimit]
265
301
  });
266
302
  return results.rows.map((row) => ({
267
303
  id: row.id,
@@ -275,9 +311,10 @@ async function search(options) {
275
311
  }));
276
312
  }
277
313
  async function getAllArticles(client, tableName = "articles") {
314
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
278
315
  const results = await client.execute(`
279
316
  SELECT id, slug, title, folder, tags, created_at, updated_at
280
- FROM ${tableName}
317
+ FROM ${quotedTableName}
281
318
  ORDER BY title
282
319
  `);
283
320
  return results.rows.map((row) => ({
@@ -291,10 +328,11 @@ async function getAllArticles(client, tableName = "articles") {
291
328
  }));
292
329
  }
293
330
  async function getArticleBySlug(client, slug, tableName = "articles") {
331
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
294
332
  const results = await client.execute({
295
333
  sql: `
296
334
  SELECT id, slug, title, content, folder, tags, created_at, updated_at
297
- FROM ${tableName}
335
+ FROM ${quotedTableName}
298
336
  WHERE slug = ?
299
337
  LIMIT 1
300
338
  `,
@@ -316,10 +354,11 @@ async function getArticleBySlug(client, slug, tableName = "articles") {
316
354
  };
317
355
  }
318
356
  async function getArticlesByFolder(client, folder, tableName = "articles") {
357
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
319
358
  const results = await client.execute({
320
359
  sql: `
321
360
  SELECT id, slug, title, folder, tags
322
- FROM ${tableName}
361
+ FROM ${quotedTableName}
323
362
  WHERE folder = ?
324
363
  ORDER BY title
325
364
  `,
@@ -334,9 +373,10 @@ async function getArticlesByFolder(client, folder, tableName = "articles") {
334
373
  }));
335
374
  }
336
375
  async function getFolders(client, tableName = "articles") {
376
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
337
377
  const results = await client.execute(`
338
378
  SELECT DISTINCT folder
339
- FROM ${tableName}
379
+ FROM ${quotedTableName}
340
380
  ORDER BY folder
341
381
  `);
342
382
  return results.rows.map((row) => row.folder);
package/dist/index.esm.js CHANGED
@@ -112,6 +112,34 @@ function prepareTextForEmbedding(fields) {
112
112
  return parts.filter(Boolean).join("\n\n");
113
113
  }
114
114
 
115
+ const SQL_IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
116
+ const DEFAULT_SEARCH_LIMIT = 10;
117
+ const MAX_SEARCH_LIMIT = 100;
118
+ function validateSqlIdentifier(identifier, name = "identifier") {
119
+ if (typeof identifier !== "string" || !SQL_IDENTIFIER_PATTERN.test(identifier)) {
120
+ throw new Error(
121
+ `Invalid SQL ${name}: expected an ASCII identifier matching ${SQL_IDENTIFIER_PATTERN.toString()}`
122
+ );
123
+ }
124
+ return identifier;
125
+ }
126
+ function quoteSqlIdentifier(identifier, name) {
127
+ const validated = validateSqlIdentifier(identifier, name);
128
+ return `"${validated.replace(/"/g, '""')}"`;
129
+ }
130
+ function normalizeSearchLimit(limit = DEFAULT_SEARCH_LIMIT) {
131
+ if (typeof limit !== "number" || !Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_LIMIT) {
132
+ throw new Error(`Invalid search limit: expected an integer from 1 to ${MAX_SEARCH_LIMIT}`);
133
+ }
134
+ return limit;
135
+ }
136
+ function normalizeVectorDimensions(dimensions) {
137
+ if (typeof dimensions !== "number" || !Number.isFinite(dimensions) || !Number.isInteger(dimensions) || dimensions < 1) {
138
+ throw new Error("Invalid vector dimensions: expected a positive integer");
139
+ }
140
+ return dimensions;
141
+ }
142
+
115
143
  async function indexContent(options) {
116
144
  const {
117
145
  client,
@@ -122,12 +150,13 @@ async function indexContent(options) {
122
150
  tableName = "articles",
123
151
  onProgress
124
152
  } = options;
153
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
125
154
  const files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
126
155
  if (files.length === 0) {
127
156
  console.warn(`No files found in ${contentPath}`);
128
157
  return { success: 0, failed: 0, total: 0 };
129
158
  }
130
- await client.execute(`DELETE FROM ${tableName}`);
159
+ await client.execute(`DELETE FROM ${quotedTableName}`);
131
160
  let success = 0;
132
161
  let failed = 0;
133
162
  for (let i = 0; i < files.length; i++) {
@@ -137,7 +166,7 @@ async function indexContent(options) {
137
166
  }
138
167
  try {
139
168
  const document = await processFile(file, embeddingOptions);
140
- await insertDocument(client, document, tableName);
169
+ await insertDocument(client, document, quotedTableName);
141
170
  success++;
142
171
  } catch (error) {
143
172
  console.error(`Failed to index ${file.relativePath}:`, error);
@@ -191,9 +220,9 @@ async function processFile(file, embeddingOptions) {
191
220
  metadata: frontMatter
192
221
  };
193
222
  }
194
- async function insertDocument(client, document, tableName) {
223
+ async function insertDocument(client, document, quotedTableName) {
195
224
  await client.execute({
196
- sql: `INSERT INTO ${tableName}
225
+ sql: `INSERT INTO ${quotedTableName}
197
226
  (slug, title, content, folder, tags, embedding, created_at, updated_at)
198
227
  VALUES (?, ?, ?, ?, ?, vector(?), datetime('now'), datetime('now'))`,
199
228
  args: [
@@ -207,30 +236,35 @@ async function insertDocument(client, document, tableName) {
207
236
  });
208
237
  }
209
238
  async function createTable(client, tableName = "articles", dimensions = 768) {
239
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
240
+ const vectorDimensions = normalizeVectorDimensions(dimensions);
241
+ const quotedEmbeddingIndexName = quoteSqlIdentifier(`${tableName}_embedding_idx`, "embedding index name");
242
+ const quotedFolderIndexName = quoteSqlIdentifier(`${tableName}_folder_idx`, "folder index name");
243
+ const quotedSlugIndexName = quoteSqlIdentifier(`${tableName}_slug_idx`, "slug index name");
210
244
  await client.execute(`
211
- CREATE TABLE IF NOT EXISTS ${tableName} (
245
+ CREATE TABLE IF NOT EXISTS ${quotedTableName} (
212
246
  id INTEGER PRIMARY KEY AUTOINCREMENT,
213
247
  slug TEXT UNIQUE NOT NULL,
214
248
  title TEXT NOT NULL,
215
249
  content TEXT NOT NULL,
216
250
  folder TEXT NOT NULL DEFAULT 'root',
217
251
  tags TEXT DEFAULT '[]',
218
- embedding F32_BLOB(${dimensions}),
252
+ embedding F32_BLOB(${vectorDimensions}),
219
253
  created_at TEXT NOT NULL,
220
254
  updated_at TEXT NOT NULL
221
255
  )
222
256
  `);
223
257
  await client.execute(`
224
- CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx
225
- ON ${tableName}(libsql_vector_idx(embedding))
258
+ CREATE INDEX IF NOT EXISTS ${quotedEmbeddingIndexName}
259
+ ON ${quotedTableName}(libsql_vector_idx(embedding))
226
260
  `);
227
261
  await client.execute(`
228
- CREATE INDEX IF NOT EXISTS ${tableName}_folder_idx
229
- ON ${tableName}(folder)
262
+ CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
263
+ ON ${quotedTableName}(folder)
230
264
  `);
231
265
  await client.execute(`
232
- CREATE INDEX IF NOT EXISTS ${tableName}_slug_idx
233
- ON ${tableName}(slug)
266
+ CREATE INDEX IF NOT EXISTS ${quotedSlugIndexName}
267
+ ON ${quotedTableName}(slug)
234
268
  `);
235
269
  }
236
270
 
@@ -242,6 +276,8 @@ async function search(options) {
242
276
  tableName = "articles",
243
277
  embeddingOptions = {}
244
278
  } = options;
279
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
280
+ const resultLimit = normalizeSearchLimit(limit);
245
281
  const queryEmbedding = await generateEmbedding(query, embeddingOptions);
246
282
  const results = await client.execute({
247
283
  sql: `
@@ -254,12 +290,12 @@ async function search(options) {
254
290
  tags,
255
291
  created_at,
256
292
  vector_distance_cos(embedding, vector(?)) as distance
257
- FROM ${tableName}
293
+ FROM ${quotedTableName}
258
294
  WHERE embedding IS NOT NULL
259
295
  ORDER BY distance
260
296
  LIMIT ?
261
297
  `,
262
- args: [JSON.stringify(queryEmbedding), limit]
298
+ args: [JSON.stringify(queryEmbedding), resultLimit]
263
299
  });
264
300
  return results.rows.map((row) => ({
265
301
  id: row.id,
@@ -273,9 +309,10 @@ async function search(options) {
273
309
  }));
274
310
  }
275
311
  async function getAllArticles(client, tableName = "articles") {
312
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
276
313
  const results = await client.execute(`
277
314
  SELECT id, slug, title, folder, tags, created_at, updated_at
278
- FROM ${tableName}
315
+ FROM ${quotedTableName}
279
316
  ORDER BY title
280
317
  `);
281
318
  return results.rows.map((row) => ({
@@ -289,10 +326,11 @@ async function getAllArticles(client, tableName = "articles") {
289
326
  }));
290
327
  }
291
328
  async function getArticleBySlug(client, slug, tableName = "articles") {
329
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
292
330
  const results = await client.execute({
293
331
  sql: `
294
332
  SELECT id, slug, title, content, folder, tags, created_at, updated_at
295
- FROM ${tableName}
333
+ FROM ${quotedTableName}
296
334
  WHERE slug = ?
297
335
  LIMIT 1
298
336
  `,
@@ -314,10 +352,11 @@ async function getArticleBySlug(client, slug, tableName = "articles") {
314
352
  };
315
353
  }
316
354
  async function getArticlesByFolder(client, folder, tableName = "articles") {
355
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
317
356
  const results = await client.execute({
318
357
  sql: `
319
358
  SELECT id, slug, title, folder, tags
320
- FROM ${tableName}
359
+ FROM ${quotedTableName}
321
360
  WHERE folder = ?
322
361
  ORDER BY title
323
362
  `,
@@ -332,9 +371,10 @@ async function getArticlesByFolder(client, folder, tableName = "articles") {
332
371
  }));
333
372
  }
334
373
  async function getFolders(client, tableName = "articles") {
374
+ const quotedTableName = quoteSqlIdentifier(tableName, "tableName");
335
375
  const results = await client.execute(`
336
376
  SELECT DISTINCT folder
337
- FROM ${tableName}
377
+ FROM ${quotedTableName}
338
378
  ORDER BY folder
339
379
  `);
340
380
  return results.rows.map((row) => row.folder);
package/docs/API.md ADDED
@@ -0,0 +1,180 @@
1
+ # API Reference
2
+
3
+ ## Exports
4
+
5
+ `libsql-search` exports:
6
+
7
+ - `createTable`
8
+ - `indexContent`
9
+ - `search`
10
+ - `getAllArticles`
11
+ - `getArticleBySlug`
12
+ - `getArticlesByFolder`
13
+ - `getFolders`
14
+ - `generateEmbedding`
15
+ - `padEmbedding`
16
+ - `prepareTextForEmbedding`
17
+
18
+ It also exports these types:
19
+
20
+ - `EmbeddingProvider`
21
+ - `EmbeddingOptions`
22
+ - `IndexerOptions`
23
+ - `IndexedDocument`
24
+ - `SearchOptions`
25
+ - `SearchResult`
26
+
27
+ ## `createTable(client, tableName?, dimensions?)`
28
+
29
+ Creates the table and supporting indexes used by search.
30
+
31
+ ```ts
32
+ await createTable(client, "articles", 768);
33
+ ```
34
+
35
+ Defaults:
36
+
37
+ - `tableName`: `"articles"`
38
+ - `dimensions`: `768`
39
+
40
+ `tableName` must be an ASCII SQLite identifier matching
41
+ `[A-Za-z_][A-Za-z0-9_]*`. Valid identifiers are quoted internally, so reserved
42
+ words such as `"select"` are safe to use. `dimensions` must be a positive
43
+ integer.
44
+
45
+ The created schema includes:
46
+
47
+ - `id` primary key
48
+ - `slug`
49
+ - `title`
50
+ - `content`
51
+ - `folder`
52
+ - `tags`
53
+ - `embedding`
54
+ - `created_at`
55
+ - `updated_at`
56
+
57
+ ## `indexContent(options)`
58
+
59
+ Indexes Markdown files from a directory on disk.
60
+
61
+ ```ts
62
+ interface IndexerOptions {
63
+ client: Client;
64
+ contentPath: string;
65
+ embeddingOptions?: EmbeddingOptions;
66
+ fileExtensions?: string[];
67
+ exclude?: string[];
68
+ tableName?: string;
69
+ onProgress?: (current: number, total: number, file: string) => void;
70
+ }
71
+ ```
72
+
73
+ Defaults:
74
+
75
+ - `fileExtensions`: [".md", ".markdown"]
76
+ - `exclude`: ["node_modules", ".git", "dist", "build"]
77
+ - `tableName`: `"articles"`
78
+
79
+ `tableName` follows the same identifier policy as `createTable()`.
80
+
81
+ Return shape:
82
+
83
+ ```ts
84
+ {
85
+ success: number;
86
+ failed: number;
87
+ total: number;
88
+ }
89
+ ```
90
+
91
+ Behavior notes:
92
+
93
+ - `indexContent()` deletes existing rows in the target table before rebuilding
94
+ - frontmatter `title`, `description`, and `tags` are folded into the embedding
95
+ text
96
+ - if a file has no frontmatter title, the filename becomes the title
97
+
98
+ ## `search(options)`
99
+
100
+ Generates a query embedding and performs vector similarity search.
101
+
102
+ ```ts
103
+ interface SearchOptions {
104
+ client: Client;
105
+ query: string;
106
+ limit?: number;
107
+ tableName?: string;
108
+ embeddingOptions?: EmbeddingOptions;
109
+ }
110
+ ```
111
+
112
+ Defaults:
113
+
114
+ - `limit`: `10`
115
+ - `tableName`: `"articles"`
116
+
117
+ `limit` must be an integer from `1` through `100`; invalid values are rejected
118
+ before query embedding generation. `tableName` follows the same identifier
119
+ policy as `createTable()`.
120
+
121
+ Result shape:
122
+
123
+ ```ts
124
+ interface SearchResult {
125
+ id: number;
126
+ slug: string;
127
+ title: string;
128
+ content: string;
129
+ folder: string;
130
+ tags: string[];
131
+ distance: number;
132
+ created_at: string;
133
+ }
134
+ ```
135
+
136
+ Lower `distance` values are better matches.
137
+
138
+ ## Article Retrieval Helpers
139
+
140
+ ### `getAllArticles(client, tableName?)`
141
+
142
+ Returns all indexed articles ordered by title.
143
+
144
+ ### `getArticleBySlug(client, slug, tableName?)`
145
+
146
+ Returns one article or `null`.
147
+
148
+ ### `getArticlesByFolder(client, folder, tableName?)`
149
+
150
+ Returns articles in a specific folder.
151
+
152
+ ### `getFolders(client, tableName?)`
153
+
154
+ Returns distinct folder names from the index.
155
+
156
+ All article retrieval helpers validate `tableName` before executing SQL.
157
+
158
+ ## Embedding Helpers
159
+
160
+ ### `generateEmbedding(text, options?)`
161
+
162
+ Generates an embedding for arbitrary text using the selected provider.
163
+
164
+ ### `padEmbedding(embedding, targetDimensions)`
165
+
166
+ Pads or truncates an embedding array to the requested length.
167
+
168
+ ### `prepareTextForEmbedding(fields)`
169
+
170
+ Combines title, description, tags, and content into the text sent to the
171
+ embedding model.
172
+
173
+ ```ts
174
+ const text = prepareTextForEmbedding({
175
+ title: "My Article",
176
+ description: "How semantic search works",
177
+ tags: ["search", "turso"],
178
+ content: "# Content",
179
+ });
180
+ ```
@@ -0,0 +1,70 @@
1
+ # Indexing And Operations
2
+
3
+ ## Content Shape
4
+
5
+ `indexContent()` walks a directory tree, reads Markdown files, parses
6
+ frontmatter with `gray-matter`, and stores:
7
+
8
+ - `slug`
9
+ - `title`
10
+ - `content`
11
+ - `folder`
12
+ - `tags`
13
+ - `embedding`
14
+
15
+ The slug is derived from the file path relative to `contentPath`.
16
+
17
+ ## Rebuild Behavior
18
+
19
+ `indexContent()` clears the target table before rebuilding:
20
+
21
+ ```ts
22
+ await indexContent({
23
+ client,
24
+ contentPath: "./content",
25
+ tableName: "articles",
26
+ embeddingOptions: {
27
+ provider: "local",
28
+ dimensions: 768,
29
+ },
30
+ });
31
+ ```
32
+
33
+ That keeps the implementation simple, but it also means a failed rebuild can
34
+ leave the index partially repopulated.
35
+
36
+ ## Quality Guidelines
37
+
38
+ - include descriptive frontmatter titles
39
+ - add meaningful `tags` when they help retrieval
40
+ - use the same embedding provider and dimensions at index and query time
41
+ - keep `maxLength` intentional if your content is very large
42
+ - start with a smaller search `limit` and tune from real query behavior
43
+
44
+ ## Build Integration
45
+
46
+ Many projects wire indexing into a dedicated script and call it before their
47
+ site build:
48
+
49
+ ```json
50
+ {
51
+ "scripts": {
52
+ "index": "node ./scripts/index.js",
53
+ "build": "pnpm index && astro build"
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Table Names
59
+
60
+ `tableName` must be an ASCII SQLite identifier matching
61
+ `[A-Za-z_][A-Za-z0-9_]*`. Valid names are quoted internally for table and index
62
+ SQL, so reserved words such as `"select"` work safely. Invalid names fail before
63
+ database calls or embedding generation.
64
+
65
+ ## Runtime Notes
66
+
67
+ - local embeddings may download a model on the first run
68
+ - Node users need `@libsql/client` installed alongside the package
69
+ - the repository validates both the npm package build and `deno check`, but the
70
+ indexing flow itself still depends on filesystem access