libsql-search 0.1.3 → 0.1.5
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 +79 -431
- package/dist/index.cjs +354 -0
- package/dist/index.d.ts +0 -4
- package/dist/index.esm.js +249 -295
- package/docs/API.md +167 -0
- package/docs/INDEXING.md +68 -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 +31 -19
- package/dist/index.js +0 -400
package/dist/index.esm.js
CHANGED
|
@@ -1,244 +1,213 @@
|
|
|
1
|
-
import { pipeline } from '@xenova/transformers';
|
|
2
1
|
import { readdir, readFile } from 'fs/promises';
|
|
3
2
|
import { join, extname, relative, dirname } from 'path';
|
|
4
3
|
import matter from 'gray-matter';
|
|
5
4
|
|
|
6
|
-
/**
|
|
7
|
-
* Multi-provider embedding generation
|
|
8
|
-
* Supports local (Xenova), Gemini, and OpenAI
|
|
9
|
-
*/
|
|
10
5
|
const providerCache = {};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
function getEnvironmentVariable(name) {
|
|
7
|
+
const runtime = globalThis;
|
|
8
|
+
const nodeValue = runtime.process?.env?.[name];
|
|
9
|
+
if (nodeValue) {
|
|
10
|
+
return nodeValue;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return runtime.Deno?.env?.get?.(name);
|
|
14
|
+
} catch {
|
|
15
|
+
return void 0;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
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");
|
|
24
|
+
}
|
|
25
|
+
return providerCache.local;
|
|
26
|
+
}
|
|
14
27
|
async function generateEmbedding(text, options = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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) {
|
|
36
|
+
case "local":
|
|
37
|
+
return generateLocalEmbedding(truncated, dimensions);
|
|
38
|
+
case "gemini":
|
|
39
|
+
return generateGeminiEmbedding(truncated, apiKey);
|
|
40
|
+
case "openai":
|
|
41
|
+
return generateOpenAIEmbedding(truncated, apiKey, dimensions);
|
|
42
|
+
default:
|
|
43
|
+
throw new Error(`Unknown embedding provider: ${provider}`);
|
|
44
|
+
}
|
|
27
45
|
}
|
|
28
|
-
/**
|
|
29
|
-
* Generate embeddings using local model (Xenova/all-MiniLM-L6-v2)
|
|
30
|
-
* Returns 384 dimensions, padded to target dimensions
|
|
31
|
-
*/
|
|
32
46
|
async function generateLocalEmbedding(text, targetDimensions) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
normalize: true
|
|
41
|
-
});
|
|
42
|
-
const embedding = Array.from(output.data);
|
|
43
|
-
return padEmbedding(embedding, targetDimensions);
|
|
47
|
+
const model = await getLocalEmbeddingModel();
|
|
48
|
+
const output = await model(text, {
|
|
49
|
+
pooling: "mean",
|
|
50
|
+
normalize: true
|
|
51
|
+
});
|
|
52
|
+
const embedding = Array.from(output.data);
|
|
53
|
+
return padEmbedding(embedding, targetDimensions);
|
|
44
54
|
}
|
|
45
|
-
/**
|
|
46
|
-
* Generate embeddings using Google Gemini API
|
|
47
|
-
* Returns 768 dimensions natively
|
|
48
|
-
*/
|
|
49
55
|
async function generateGeminiEmbedding(text, apiKey) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return result.embedding.values;
|
|
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;
|
|
62
67
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Generate embeddings using OpenAI API
|
|
65
|
-
* Supports text-embedding-3-small (1536 dims) and text-embedding-3-large (3072 dims)
|
|
66
|
-
*/
|
|
67
68
|
async function generateOpenAIEmbedding(text, apiKey, dimensions = 1536) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
})
|
|
85
|
+
});
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
const error = await response.text();
|
|
88
|
+
throw new Error(`OpenAI API error: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
return data.data[0].embedding;
|
|
91
92
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Pad or truncate embedding to target dimensions
|
|
94
|
-
*/
|
|
95
93
|
function padEmbedding(embedding, targetDimensions) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
94
|
+
if (embedding.length === targetDimensions) {
|
|
95
|
+
return embedding;
|
|
96
|
+
}
|
|
97
|
+
if (embedding.length > targetDimensions) {
|
|
98
|
+
return embedding.slice(0, targetDimensions);
|
|
99
|
+
}
|
|
100
|
+
const padded = new Array(targetDimensions).fill(0);
|
|
101
|
+
padded.splice(0, embedding.length, ...embedding);
|
|
102
|
+
return padded;
|
|
105
103
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Prepare text for embedding by combining multiple fields
|
|
108
|
-
*/
|
|
109
104
|
function prepareTextForEmbedding(fields) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (fields.content)
|
|
119
|
-
parts.push(fields.content);
|
|
120
|
-
return parts.filter(Boolean).join('\n\n');
|
|
105
|
+
const parts = [];
|
|
106
|
+
if (fields.title) parts.push(fields.title);
|
|
107
|
+
if (fields.description) parts.push(fields.description);
|
|
108
|
+
if (fields.tags && fields.tags.length > 0) {
|
|
109
|
+
parts.push(`Tags: ${fields.tags.join(", ")}`);
|
|
110
|
+
}
|
|
111
|
+
if (fields.content) parts.push(fields.content);
|
|
112
|
+
return parts.filter(Boolean).join("\n\n");
|
|
121
113
|
}
|
|
122
114
|
|
|
123
|
-
/**
|
|
124
|
-
* Content indexer for markdown and other formats
|
|
125
|
-
*/
|
|
126
|
-
/**
|
|
127
|
-
* Index markdown content from a directory
|
|
128
|
-
*/
|
|
129
115
|
async function indexContent(options) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
116
|
+
const {
|
|
117
|
+
client,
|
|
118
|
+
contentPath,
|
|
119
|
+
embeddingOptions = {},
|
|
120
|
+
fileExtensions = [".md", ".markdown"],
|
|
121
|
+
exclude = ["node_modules", ".git", "dist", "build"],
|
|
122
|
+
tableName = "articles",
|
|
123
|
+
onProgress
|
|
124
|
+
} = options;
|
|
125
|
+
const files = await findFiles(contentPath, contentPath, fileExtensions, exclude);
|
|
126
|
+
if (files.length === 0) {
|
|
127
|
+
console.warn(`No files found in ${contentPath}`);
|
|
128
|
+
return { success: 0, failed: 0, total: 0 };
|
|
129
|
+
}
|
|
130
|
+
await client.execute(`DELETE FROM ${tableName}`);
|
|
131
|
+
let success = 0;
|
|
132
|
+
let failed = 0;
|
|
133
|
+
for (let i = 0; i < files.length; i++) {
|
|
134
|
+
const file = files[i];
|
|
135
|
+
if (onProgress) {
|
|
136
|
+
onProgress(i + 1, files.length, file.relativePath);
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (onProgress) {
|
|
145
|
-
onProgress(i + 1, files.length, file.relativePath);
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
const document = await processFile(file, embeddingOptions);
|
|
149
|
-
await insertDocument(client, document, tableName);
|
|
150
|
-
success++;
|
|
151
|
-
}
|
|
152
|
-
catch (error) {
|
|
153
|
-
console.error(`Failed to index ${file.relativePath}:`, error);
|
|
154
|
-
failed++;
|
|
155
|
-
}
|
|
138
|
+
try {
|
|
139
|
+
const document = await processFile(file, embeddingOptions);
|
|
140
|
+
await insertDocument(client, document, tableName);
|
|
141
|
+
success++;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`Failed to index ${file.relativePath}:`, error);
|
|
144
|
+
failed++;
|
|
156
145
|
}
|
|
157
|
-
|
|
146
|
+
}
|
|
147
|
+
return { success, failed, total: files.length };
|
|
158
148
|
}
|
|
159
|
-
/**
|
|
160
|
-
* Find all files matching extensions
|
|
161
|
-
*/
|
|
162
149
|
async function findFiles(dir, baseDir, extensions, exclude) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
}
|
|
150
|
+
const files = [];
|
|
151
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
const fullPath = join(dir, entry.name);
|
|
154
|
+
if (entry.isDirectory()) {
|
|
155
|
+
if (!entry.name.startsWith(".") && !exclude.includes(entry.name)) {
|
|
156
|
+
const subFiles = await findFiles(fullPath, baseDir, extensions, exclude);
|
|
157
|
+
files.push(...subFiles);
|
|
158
|
+
}
|
|
159
|
+
} else if (extensions.includes(extname(entry.name))) {
|
|
160
|
+
const relativePath = relative(baseDir, fullPath);
|
|
161
|
+
const folder = dirname(relativePath);
|
|
162
|
+
files.push({
|
|
163
|
+
fullPath,
|
|
164
|
+
relativePath,
|
|
165
|
+
folder: folder === "." ? "root" : folder
|
|
166
|
+
});
|
|
182
167
|
}
|
|
183
|
-
|
|
168
|
+
}
|
|
169
|
+
return files;
|
|
184
170
|
}
|
|
185
|
-
/**
|
|
186
|
-
* Process a single file into an indexed document
|
|
187
|
-
*/
|
|
188
171
|
async function processFile(file, embeddingOptions) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
slug,
|
|
211
|
-
title,
|
|
212
|
-
content: markdown,
|
|
213
|
-
folder: file.folder,
|
|
214
|
-
tags,
|
|
215
|
-
embedding,
|
|
216
|
-
metadata: frontMatter
|
|
217
|
-
};
|
|
172
|
+
const content = await readFile(file.fullPath, "utf-8");
|
|
173
|
+
const { data: frontMatter, content: markdown } = matter(content);
|
|
174
|
+
const slug = file.relativePath.replace(/\.(md|markdown)$/, "").replace(/\\/g, "/");
|
|
175
|
+
const title = frontMatter.title || file.relativePath.split("/").pop()?.replace(/\.(md|markdown)$/, "").replace(/-/g, " ") || "Untitled";
|
|
176
|
+
const tags = Array.isArray(frontMatter.tags) ? frontMatter.tags : [];
|
|
177
|
+
const embeddingText = prepareTextForEmbedding({
|
|
178
|
+
title,
|
|
179
|
+
description: frontMatter.description,
|
|
180
|
+
content: markdown,
|
|
181
|
+
tags
|
|
182
|
+
});
|
|
183
|
+
const embedding = await generateEmbedding(embeddingText, embeddingOptions);
|
|
184
|
+
return {
|
|
185
|
+
slug,
|
|
186
|
+
title,
|
|
187
|
+
content: markdown,
|
|
188
|
+
folder: file.folder,
|
|
189
|
+
tags,
|
|
190
|
+
embedding,
|
|
191
|
+
metadata: frontMatter
|
|
192
|
+
};
|
|
218
193
|
}
|
|
219
|
-
/**
|
|
220
|
-
* Insert document into database
|
|
221
|
-
*/
|
|
222
194
|
async function insertDocument(client, document, tableName) {
|
|
223
|
-
|
|
224
|
-
|
|
195
|
+
await client.execute({
|
|
196
|
+
sql: `INSERT INTO ${tableName}
|
|
225
197
|
(slug, title, content, folder, tags, embedding, created_at, updated_at)
|
|
226
198
|
VALUES (?, ?, ?, ?, ?, vector(?), datetime('now'), datetime('now'))`,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
199
|
+
args: [
|
|
200
|
+
document.slug,
|
|
201
|
+
document.title,
|
|
202
|
+
document.content,
|
|
203
|
+
document.folder,
|
|
204
|
+
JSON.stringify(document.tags),
|
|
205
|
+
JSON.stringify(document.embedding)
|
|
206
|
+
]
|
|
207
|
+
});
|
|
236
208
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*/
|
|
240
|
-
async function createTable(client, tableName = 'articles', dimensions = 768) {
|
|
241
|
-
await client.execute(`
|
|
209
|
+
async function createTable(client, tableName = "articles", dimensions = 768) {
|
|
210
|
+
await client.execute(`
|
|
242
211
|
CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
243
212
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
244
213
|
slug TEXT UNIQUE NOT NULL,
|
|
@@ -251,33 +220,31 @@ async function createTable(client, tableName = 'articles', dimensions = 768) {
|
|
|
251
220
|
updated_at TEXT NOT NULL
|
|
252
221
|
)
|
|
253
222
|
`);
|
|
254
|
-
|
|
223
|
+
await client.execute(`
|
|
255
224
|
CREATE INDEX IF NOT EXISTS ${tableName}_embedding_idx
|
|
256
225
|
ON ${tableName}(libsql_vector_idx(embedding))
|
|
257
226
|
`);
|
|
258
|
-
|
|
227
|
+
await client.execute(`
|
|
259
228
|
CREATE INDEX IF NOT EXISTS ${tableName}_folder_idx
|
|
260
229
|
ON ${tableName}(folder)
|
|
261
230
|
`);
|
|
262
|
-
|
|
231
|
+
await client.execute(`
|
|
263
232
|
CREATE INDEX IF NOT EXISTS ${tableName}_slug_idx
|
|
264
233
|
ON ${tableName}(slug)
|
|
265
234
|
`);
|
|
266
235
|
}
|
|
267
236
|
|
|
268
|
-
/**
|
|
269
|
-
* Vector search functionality
|
|
270
|
-
*/
|
|
271
|
-
/**
|
|
272
|
-
* Perform semantic search using vector similarity
|
|
273
|
-
*/
|
|
274
237
|
async function search(options) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
238
|
+
const {
|
|
239
|
+
client,
|
|
240
|
+
query,
|
|
241
|
+
limit = 10,
|
|
242
|
+
tableName = "articles",
|
|
243
|
+
embeddingOptions = {}
|
|
244
|
+
} = options;
|
|
245
|
+
const queryEmbedding = await generateEmbedding(query, embeddingOptions);
|
|
246
|
+
const results = await client.execute({
|
|
247
|
+
sql: `
|
|
281
248
|
SELECT
|
|
282
249
|
id,
|
|
283
250
|
slug,
|
|
@@ -292,98 +259,85 @@ async function search(options) {
|
|
|
292
259
|
ORDER BY distance
|
|
293
260
|
LIMIT ?
|
|
294
261
|
`,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}));
|
|
262
|
+
args: [JSON.stringify(queryEmbedding), limit]
|
|
263
|
+
});
|
|
264
|
+
return results.rows.map((row) => ({
|
|
265
|
+
id: row.id,
|
|
266
|
+
slug: row.slug,
|
|
267
|
+
title: row.title,
|
|
268
|
+
content: row.content,
|
|
269
|
+
folder: row.folder,
|
|
270
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
271
|
+
distance: row.distance,
|
|
272
|
+
created_at: row.created_at
|
|
273
|
+
}));
|
|
308
274
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
*/
|
|
312
|
-
async function getAllArticles(client, tableName = 'articles') {
|
|
313
|
-
const results = await client.execute(`
|
|
275
|
+
async function getAllArticles(client, tableName = "articles") {
|
|
276
|
+
const results = await client.execute(`
|
|
314
277
|
SELECT id, slug, title, folder, tags, created_at, updated_at
|
|
315
278
|
FROM ${tableName}
|
|
316
279
|
ORDER BY title
|
|
317
280
|
`);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
281
|
+
return results.rows.map((row) => ({
|
|
282
|
+
id: row.id,
|
|
283
|
+
slug: row.slug,
|
|
284
|
+
title: row.title,
|
|
285
|
+
folder: row.folder,
|
|
286
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
287
|
+
created_at: row.created_at,
|
|
288
|
+
updated_at: row.updated_at
|
|
289
|
+
}));
|
|
327
290
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
async function getArticleBySlug(client, slug, tableName = 'articles') {
|
|
332
|
-
const results = await client.execute({
|
|
333
|
-
sql: `
|
|
291
|
+
async function getArticleBySlug(client, slug, tableName = "articles") {
|
|
292
|
+
const results = await client.execute({
|
|
293
|
+
sql: `
|
|
334
294
|
SELECT id, slug, title, content, folder, tags, created_at, updated_at
|
|
335
295
|
FROM ${tableName}
|
|
336
296
|
WHERE slug = ?
|
|
337
297
|
LIMIT 1
|
|
338
298
|
`,
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
299
|
+
args: [slug]
|
|
300
|
+
});
|
|
301
|
+
if (results.rows.length === 0) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const row = results.rows[0];
|
|
305
|
+
return {
|
|
306
|
+
id: row.id,
|
|
307
|
+
slug: row.slug,
|
|
308
|
+
title: row.title,
|
|
309
|
+
content: row.content,
|
|
310
|
+
folder: row.folder,
|
|
311
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
312
|
+
created_at: row.created_at,
|
|
313
|
+
updated_at: row.updated_at
|
|
314
|
+
};
|
|
355
315
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
async function getArticlesByFolder(client, folder, tableName = 'articles') {
|
|
360
|
-
const results = await client.execute({
|
|
361
|
-
sql: `
|
|
316
|
+
async function getArticlesByFolder(client, folder, tableName = "articles") {
|
|
317
|
+
const results = await client.execute({
|
|
318
|
+
sql: `
|
|
362
319
|
SELECT id, slug, title, folder, tags
|
|
363
320
|
FROM ${tableName}
|
|
364
321
|
WHERE folder = ?
|
|
365
322
|
ORDER BY title
|
|
366
323
|
`,
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
324
|
+
args: [folder]
|
|
325
|
+
});
|
|
326
|
+
return results.rows.map((row) => ({
|
|
327
|
+
id: row.id,
|
|
328
|
+
slug: row.slug,
|
|
329
|
+
title: row.title,
|
|
330
|
+
folder: row.folder,
|
|
331
|
+
tags: JSON.parse(row.tags || "[]")
|
|
332
|
+
}));
|
|
376
333
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
*/
|
|
380
|
-
async function getFolders(client, tableName = 'articles') {
|
|
381
|
-
const results = await client.execute(`
|
|
334
|
+
async function getFolders(client, tableName = "articles") {
|
|
335
|
+
const results = await client.execute(`
|
|
382
336
|
SELECT DISTINCT folder
|
|
383
337
|
FROM ${tableName}
|
|
384
338
|
ORDER BY folder
|
|
385
339
|
`);
|
|
386
|
-
|
|
340
|
+
return results.rows.map((row) => row.folder);
|
|
387
341
|
}
|
|
388
342
|
|
|
389
343
|
export { createTable, generateEmbedding, getAllArticles, getArticleBySlug, getArticlesByFolder, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search };
|