libsql-search 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 llbbl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,477 @@
1
+ # libsql-search
2
+
3
+ Semantic search for static sites using libSQL/Turso with multi-provider embeddings.
4
+
5
+ Add AI-powered vector search to your Astro, Next.js, or any static site with minimal configuration. Index markdown content, generate embeddings locally or via API, and provide lightning-fast semantic search to your users.
6
+
7
+ ## Features
8
+
9
+ - 🔍 **Semantic Search** - Find content by meaning, not just keywords
10
+ - 🌐 **Multi-Provider Embeddings** - Choose local (Xenova), Gemini, or OpenAI
11
+ - ⚡ **Edge-Ready** - Works with Turso's global edge database
12
+ - 📝 **Markdown Support** - Built-in gray-matter parsing
13
+ - 🎯 **Type-Safe** - Full TypeScript support
14
+ - 🆓 **Free Tier Friendly** - Local embeddings require no API keys
15
+
16
+ ## Installation
17
+
18
+ **npm:**
19
+ ```bash
20
+ npm install libsql-search @libsql/client
21
+ ```
22
+
23
+ **pnpm:**
24
+ ```bash
25
+ pnpm add libsql-search @libsql/client
26
+ ```
27
+
28
+ **JSR:**
29
+ ```bash
30
+ deno add @llbbl/libsql-search
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ### 1. Set Up Your Database
36
+
37
+ ```typescript
38
+ import { createClient } from '@libsql/client';
39
+ import { createTable } from 'libsql-search';
40
+
41
+ const client = createClient({
42
+ url: 'libsql://your-db.turso.io',
43
+ authToken: 'your-auth-token'
44
+ });
45
+
46
+ // Create the articles table with vector index
47
+ await createTable(client, 'articles', 768);
48
+ ```
49
+
50
+ ### 2. Index Your Content
51
+
52
+ ```typescript
53
+ import { indexContent } from 'libsql-search';
54
+
55
+ const result = await indexContent({
56
+ client,
57
+ contentPath: './content',
58
+ embeddingOptions: {
59
+ provider: 'local', // or 'gemini', 'openai'
60
+ dimensions: 768
61
+ },
62
+ onProgress: (current, total, file) => {
63
+ console.log(`[${current}/${total}] Indexing: ${file}`);
64
+ }
65
+ });
66
+
67
+ console.log(`Indexed ${result.success}/${result.total} documents`);
68
+ ```
69
+
70
+ ### 3. Search Your Content
71
+
72
+ ```typescript
73
+ import { search } from 'libsql-search';
74
+
75
+ const results = await search({
76
+ client,
77
+ query: 'how to deploy astro',
78
+ limit: 5,
79
+ embeddingOptions: {
80
+ provider: 'local'
81
+ }
82
+ });
83
+
84
+ results.forEach(result => {
85
+ console.log(`${result.title} (${result.distance})`);
86
+ });
87
+ ```
88
+
89
+ ## Embedding Providers
90
+
91
+ ### Local (Xenova/Transformers.js)
92
+
93
+ **Free, no API key required**. Runs `all-MiniLM-L6-v2` in Node.js using ONNX.
94
+
95
+ ```typescript
96
+ embeddingOptions: {
97
+ provider: 'local',
98
+ dimensions: 768 // 384 native, padded to 768
99
+ }
100
+ ```
101
+
102
+ **Pros:**
103
+ - ✅ No API costs
104
+ - ✅ No rate limits
105
+ - ✅ Works offline
106
+ - ✅ Privacy-friendly
107
+
108
+ **Cons:**
109
+ - ⚠️ First run downloads model (~50MB)
110
+ - ⚠️ Slower than API-based options
111
+ - ⚠️ Lower quality than large models
112
+
113
+ ### Google Gemini
114
+
115
+ **Free tier: 1,500 requests/day**. Uses `text-embedding-004` model.
116
+
117
+ ```typescript
118
+ embeddingOptions: {
119
+ provider: 'gemini',
120
+ apiKey: process.env.GEMINI_API_KEY,
121
+ dimensions: 768 // native
122
+ }
123
+ ```
124
+
125
+ **Pros:**
126
+ - ✅ Generous free tier
127
+ - ✅ High quality embeddings
128
+ - ✅ Fast
129
+
130
+ **Cons:**
131
+ - ⚠️ Requires API key
132
+ - ⚠️ Rate limited
133
+
134
+ ### OpenAI
135
+
136
+ **Paid only**. Uses `text-embedding-3-small` or `text-embedding-3-large`.
137
+
138
+ ```typescript
139
+ embeddingOptions: {
140
+ provider: 'openai',
141
+ apiKey: process.env.OPENAI_API_KEY,
142
+ dimensions: 1536 // or 3072 for large
143
+ }
144
+ ```
145
+
146
+ **Pros:**
147
+ - ✅ Highest quality
148
+ - ✅ Very fast
149
+ - ✅ Configurable dimensions
150
+
151
+ **Cons:**
152
+ - ⚠️ Costs money ($0.02 per 1M tokens)
153
+ - ⚠️ Requires API key
154
+
155
+ ## API Reference
156
+
157
+ ### Indexing
158
+
159
+ #### `indexContent(options)`
160
+
161
+ Index markdown files from a directory.
162
+
163
+ ```typescript
164
+ interface IndexerOptions {
165
+ client: Client; // libSQL client
166
+ contentPath: string; // Path to content directory
167
+ embeddingOptions?: EmbeddingOptions;
168
+ fileExtensions?: string[]; // Default: ['.md', '.markdown']
169
+ exclude?: string[]; // Default: ['node_modules', '.git']
170
+ tableName?: string; // Default: 'articles'
171
+ onProgress?: (current, total, file) => void;
172
+ }
173
+ ```
174
+
175
+ #### `createTable(client, tableName?, dimensions?)`
176
+
177
+ Create the articles table with vector index.
178
+
179
+ ### Searching
180
+
181
+ #### `search(options)`
182
+
183
+ Perform semantic search.
184
+
185
+ ```typescript
186
+ interface SearchOptions {
187
+ client: Client;
188
+ query: string;
189
+ limit?: number; // Default: 10
190
+ tableName?: string; // Default: 'articles'
191
+ embeddingOptions?: EmbeddingOptions;
192
+ }
193
+ ```
194
+
195
+ Returns `SearchResult[]`:
196
+
197
+ ```typescript
198
+ interface SearchResult {
199
+ id: number;
200
+ slug: string;
201
+ title: string;
202
+ content: string;
203
+ folder: string;
204
+ tags: string[];
205
+ distance: number; // Lower is better
206
+ created_at: string;
207
+ }
208
+ ```
209
+
210
+ #### `getAllArticles(client, tableName?)`
211
+
212
+ Get all articles (useful for building static pages).
213
+
214
+ #### `getArticleBySlug(client, slug, tableName?)`
215
+
216
+ Get a single article by slug.
217
+
218
+ #### `getArticlesByFolder(client, folder, tableName?)`
219
+
220
+ Get all articles in a folder.
221
+
222
+ #### `getFolders(client, tableName?)`
223
+
224
+ Get all unique folders.
225
+
226
+ ### Embeddings
227
+
228
+ #### `generateEmbedding(text, options?)`
229
+
230
+ Generate embeddings for arbitrary text.
231
+
232
+ ```typescript
233
+ interface EmbeddingOptions {
234
+ provider?: 'local' | 'gemini' | 'openai';
235
+ apiKey?: string;
236
+ dimensions?: number;
237
+ maxLength?: number; // Default: 8000
238
+ }
239
+ ```
240
+
241
+ #### `prepareTextForEmbedding(fields)`
242
+
243
+ Combine multiple fields into embedding text.
244
+
245
+ ```typescript
246
+ const text = prepareTextForEmbedding({
247
+ title: 'My Article',
248
+ description: 'A description',
249
+ content: '# Content here',
250
+ tags: ['astro', 'turso']
251
+ });
252
+ ```
253
+
254
+ ## Framework Integration
255
+
256
+ ### Astro
257
+
258
+ **Search API Endpoint** (`src/pages/api/search.json.ts`):
259
+
260
+ ```typescript
261
+ import type { APIRoute } from 'astro';
262
+ import { createClient } from '@libsql/client';
263
+ import { search } from 'libsql-search';
264
+
265
+ export const prerender = false;
266
+
267
+ const client = createClient({
268
+ url: import.meta.env.TURSO_DB_URL,
269
+ authToken: import.meta.env.TURSO_AUTH_TOKEN
270
+ });
271
+
272
+ export const POST: APIRoute = async ({ request }) => {
273
+ const { query, limit = 10 } = await request.json();
274
+
275
+ const results = await search({
276
+ client,
277
+ query,
278
+ limit,
279
+ embeddingOptions: { provider: 'local' }
280
+ });
281
+
282
+ return new Response(JSON.stringify({ results }), {
283
+ headers: { 'Content-Type': 'application/json' }
284
+ });
285
+ };
286
+ ```
287
+
288
+ **Static Page Generation** (`src/pages/[...slug].astro`):
289
+
290
+ ```astro
291
+ ---
292
+ import { createClient } from '@libsql/client';
293
+ import { getAllArticles, getArticleBySlug } from 'libsql-search';
294
+
295
+ export const prerender = true;
296
+
297
+ const client = createClient({
298
+ url: import.meta.env.TURSO_DB_URL,
299
+ authToken: import.meta.env.TURSO_AUTH_TOKEN
300
+ });
301
+
302
+ export async function getStaticPaths() {
303
+ const articles = await getAllArticles(client);
304
+ return articles.map(article => ({
305
+ params: { slug: article.slug }
306
+ }));
307
+ }
308
+
309
+ const { slug } = Astro.params;
310
+ const article = await getArticleBySlug(client, slug);
311
+ ---
312
+
313
+ <article>
314
+ <h1>{article.title}</h1>
315
+ <div set:html={article.content} />
316
+ </article>
317
+ ```
318
+
319
+ ### Next.js
320
+
321
+ **API Route** (`app/api/search/route.ts`):
322
+
323
+ ```typescript
324
+ import { createClient } from '@libsql/client';
325
+ import { search } from 'libsql-search';
326
+ import { NextRequest } from 'next/server';
327
+
328
+ const client = createClient({
329
+ url: process.env.TURSO_DB_URL!,
330
+ authToken: process.env.TURSO_AUTH_TOKEN!
331
+ });
332
+
333
+ export async function POST(request: NextRequest) {
334
+ const { query, limit = 10 } = await request.json();
335
+
336
+ const results = await search({
337
+ client,
338
+ query,
339
+ limit,
340
+ embeddingOptions: { provider: 'local' }
341
+ });
342
+
343
+ return Response.json({ results });
344
+ }
345
+ ```
346
+
347
+ **Static Generation** (`app/[slug]/page.tsx`):
348
+
349
+ ```typescript
350
+ import { createClient } from '@libsql/client';
351
+ import { getAllArticles, getArticleBySlug } from 'libsql-search';
352
+
353
+ const client = createClient({
354
+ url: process.env.TURSO_DB_URL!,
355
+ authToken: process.env.TURSO_AUTH_TOKEN!
356
+ });
357
+
358
+ export async function generateStaticParams() {
359
+ const articles = await getAllArticles(client);
360
+ return articles.map(article => ({
361
+ slug: article.slug
362
+ }));
363
+ }
364
+
365
+ export default async function Page({ params }: { params: { slug: string } }) {
366
+ const article = await getArticleBySlug(client, params.slug);
367
+
368
+ return (
369
+ <article>
370
+ <h1>{article.title}</h1>
371
+ <div dangerouslySetInnerHTML={{ __html: article.content }} />
372
+ </article>
373
+ );
374
+ }
375
+ ```
376
+
377
+ ## Best Practices
378
+
379
+ ### Embedding Dimensions
380
+
381
+ - Use **768 dimensions** for best compatibility
382
+ - Local model outputs 384, automatically padded to 768
383
+ - Gemini outputs 768 natively
384
+ - OpenAI supports custom dimensions
385
+
386
+ ### Index Updates
387
+
388
+ Create a script to re-index content:
389
+
390
+ ```json
391
+ {
392
+ "scripts": {
393
+ "index": "node scripts/index.js",
394
+ "build": "npm run index && astro build"
395
+ }
396
+ }
397
+ ```
398
+
399
+ ### Search Quality
400
+
401
+ Improve search results:
402
+
403
+ 1. **Include relevant fields** in embedding text (title, description, tags)
404
+ 2. **Truncate long content** to avoid noise
405
+ 3. **Use the same provider** for indexing and search
406
+ 4. **Experiment with distance thresholds** (lower is better)
407
+
408
+ ### Performance
409
+
410
+ - **Cache the embedding model** (done automatically)
411
+ - **Use edge databases** (Turso) for low latency
412
+ - **Implement search debouncing** in the UI
413
+ - **Limit result count** to 5-10 for best UX
414
+
415
+ ## Examples
416
+
417
+ See the `/examples` directory for complete implementations:
418
+
419
+ - [Astro Documentation Site](./examples/astro-docs)
420
+ - [Next.js Blog](./examples/nextjs-blog)
421
+ - [CLI Indexer](./examples/cli-indexer)
422
+
423
+ ## CLI Usage
424
+
425
+ For a standalone indexing script:
426
+
427
+ ```javascript
428
+ // scripts/index.js
429
+ import { createClient } from '@libsql/client';
430
+ import { createTable, indexContent } from 'libsql-search';
431
+
432
+ const client = createClient({
433
+ url: process.env.TURSO_DB_URL,
434
+ authToken: process.env.TURSO_AUTH_TOKEN
435
+ });
436
+
437
+ await createTable(client);
438
+
439
+ const result = await indexContent({
440
+ client,
441
+ contentPath: './content',
442
+ embeddingOptions: {
443
+ provider: process.env.EMBEDDING_PROVIDER || 'local'
444
+ },
445
+ onProgress: (current, total, file) => {
446
+ console.log(`[${current}/${total}] ${file}`);
447
+ }
448
+ });
449
+
450
+ console.log(`✅ Indexed ${result.success} documents`);
451
+ ```
452
+
453
+ Run with:
454
+ ```bash
455
+ node --env-file=.env scripts/index.js
456
+ ```
457
+
458
+ ## License
459
+
460
+ MIT
461
+
462
+ ## Contributing
463
+
464
+ Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/llbbl/libsql-search).
465
+
466
+ ## Related Projects
467
+
468
+ - [Turso](https://turso.tech) - Edge SQLite database
469
+ - [libSQL](https://github.com/tursodatabase/libsql) - Open source SQLite fork
470
+ - [Astro](https://astro.build) - Static site framework
471
+ - [Transformers.js](https://huggingface.co/docs/transformers.js) - ML models in JavaScript
472
+
473
+ ## Support
474
+
475
+ - [Documentation](https://github.com/llbbl/libsql-search)
476
+ - [Issues](https://github.com/llbbl/libsql-search/issues)
477
+ - [Discussions](https://github.com/llbbl/libsql-search/discussions)
@@ -0,0 +1,134 @@
1
+ import { Client } from '@libsql/client';
2
+
3
+ /**
4
+ * Multi-provider embedding generation
5
+ * Supports local (Xenova), Gemini, and OpenAI
6
+ */
7
+ type EmbeddingProvider = 'local' | 'gemini' | 'openai';
8
+ interface EmbeddingOptions {
9
+ provider?: EmbeddingProvider;
10
+ apiKey?: string;
11
+ dimensions?: number;
12
+ maxLength?: number;
13
+ }
14
+ /**
15
+ * Generate embeddings using the specified provider
16
+ */
17
+ declare function generateEmbedding(text: string, options?: EmbeddingOptions): Promise<number[]>;
18
+ /**
19
+ * Pad or truncate embedding to target dimensions
20
+ */
21
+ declare function padEmbedding(embedding: number[], targetDimensions: number): number[];
22
+ /**
23
+ * Prepare text for embedding by combining multiple fields
24
+ */
25
+ declare function prepareTextForEmbedding(fields: {
26
+ title?: string;
27
+ description?: string;
28
+ content?: string;
29
+ tags?: string[];
30
+ [key: string]: any;
31
+ }): string;
32
+
33
+ /**
34
+ * Content indexer for markdown and other formats
35
+ */
36
+
37
+ interface IndexerOptions {
38
+ client: Client;
39
+ contentPath: string;
40
+ embeddingOptions?: EmbeddingOptions;
41
+ fileExtensions?: string[];
42
+ exclude?: string[];
43
+ tableName?: string;
44
+ onProgress?: (current: number, total: number, file: string) => void;
45
+ }
46
+ interface IndexedDocument {
47
+ slug: string;
48
+ title: string;
49
+ content: string;
50
+ folder: string;
51
+ tags: string[];
52
+ embedding: number[];
53
+ metadata?: Record<string, any>;
54
+ }
55
+ /**
56
+ * Index markdown content from a directory
57
+ */
58
+ declare function indexContent(options: IndexerOptions): Promise<{
59
+ success: number;
60
+ failed: number;
61
+ total: number;
62
+ }>;
63
+ /**
64
+ * Create the articles table if it doesn't exist
65
+ */
66
+ declare function createTable(client: Client, tableName?: string, dimensions?: number): Promise<void>;
67
+
68
+ /**
69
+ * Vector search functionality
70
+ */
71
+
72
+ interface SearchOptions {
73
+ client: Client;
74
+ query: string;
75
+ limit?: number;
76
+ tableName?: string;
77
+ embeddingOptions?: EmbeddingOptions;
78
+ }
79
+ interface SearchResult {
80
+ id: number;
81
+ slug: string;
82
+ title: string;
83
+ content: string;
84
+ folder: string;
85
+ tags: string[];
86
+ distance: number;
87
+ created_at: string;
88
+ }
89
+ /**
90
+ * Perform semantic search using vector similarity
91
+ */
92
+ declare function search(options: SearchOptions): Promise<SearchResult[]>;
93
+ /**
94
+ * Get all articles (for building static pages, navigation, etc.)
95
+ */
96
+ declare function getAllArticles(client: Client, tableName?: string): Promise<Array<{
97
+ id: number;
98
+ slug: string;
99
+ title: string;
100
+ folder: string;
101
+ tags: string[];
102
+ created_at: string;
103
+ updated_at: string;
104
+ }>>;
105
+ /**
106
+ * Get a single article by slug
107
+ */
108
+ declare function getArticleBySlug(client: Client, slug: string, tableName?: string): Promise<{
109
+ id: number;
110
+ slug: string;
111
+ title: string;
112
+ content: string;
113
+ folder: string;
114
+ tags: string[];
115
+ created_at: string;
116
+ updated_at: string;
117
+ } | null>;
118
+ /**
119
+ * Get articles by folder
120
+ */
121
+ declare function getArticlesByFolder(client: Client, folder: string, tableName?: string): Promise<Array<{
122
+ id: number;
123
+ slug: string;
124
+ title: string;
125
+ folder: string;
126
+ tags: string[];
127
+ }>>;
128
+ /**
129
+ * Get all unique folders
130
+ */
131
+ declare function getFolders(client: Client, tableName?: string): Promise<string[]>;
132
+
133
+ export { createTable, generateEmbedding, getAllArticles, getArticleBySlug, getArticlesByFolder, getFolders, indexContent, padEmbedding, prepareTextForEmbedding, search };
134
+ export type { EmbeddingOptions, EmbeddingProvider, IndexedDocument, IndexerOptions, SearchOptions, SearchResult };