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/README.md +75 -433
- package/dist/index.cjs +58 -18
- package/dist/index.esm.js +58 -18
- package/docs/API.md +180 -0
- package/docs/INDEXING.md +70 -0
- package/docs/INTEGRATIONS.md +143 -0
- package/docs/PROVIDERS.md +100 -0
- package/docs/README.md +16 -0
- package/docs/RELEASING.md +91 -0
- package/docs/TROUBLESHOOTING-SHARP.md +64 -0
- package/docs/TROUBLESHOOTING.md +12 -0
- package/package.json +3 -2
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 ${
|
|
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,
|
|
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,
|
|
225
|
+
async function insertDocument(client, document, quotedTableName) {
|
|
197
226
|
await client.execute({
|
|
198
|
-
sql: `INSERT INTO ${
|
|
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 ${
|
|
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(${
|
|
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 ${
|
|
227
|
-
ON ${
|
|
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 ${
|
|
231
|
-
ON ${
|
|
264
|
+
CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
|
|
265
|
+
ON ${quotedTableName}(folder)
|
|
232
266
|
`);
|
|
233
267
|
await client.execute(`
|
|
234
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
235
|
-
ON ${
|
|
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 ${
|
|
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),
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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,
|
|
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,
|
|
223
|
+
async function insertDocument(client, document, quotedTableName) {
|
|
195
224
|
await client.execute({
|
|
196
|
-
sql: `INSERT INTO ${
|
|
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 ${
|
|
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(${
|
|
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 ${
|
|
225
|
-
ON ${
|
|
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 ${
|
|
229
|
-
ON ${
|
|
262
|
+
CREATE INDEX IF NOT EXISTS ${quotedFolderIndexName}
|
|
263
|
+
ON ${quotedTableName}(folder)
|
|
230
264
|
`);
|
|
231
265
|
await client.execute(`
|
|
232
|
-
CREATE INDEX IF NOT EXISTS ${
|
|
233
|
-
ON ${
|
|
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 ${
|
|
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),
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
+
```
|
package/docs/INDEXING.md
ADDED
|
@@ -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
|