libsql-search 0.1.4 → 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 CHANGED
@@ -2,482 +2,124 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/libsql-search.svg)](https://www.npmjs.com/package/libsql-search)
4
4
  [![JSR](https://jsr.io/badges/@logan/libsql-search)](https://jsr.io/@logan/libsql-search)
5
- [![npm downloads](https://img.shields.io/npm/dm/libsql-search.svg)](https://www.npmjs.com/package/libsql-search)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
5
  [![CI](https://github.com/llbbl/libsql-search/actions/workflows/ci.yml/badge.svg)](https://github.com/llbbl/libsql-search/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ `libsql-search` adds semantic search to Markdown-backed sites using libSQL/Turso.
9
+ It indexes frontmatter and content from files on disk, stores vectors in your
10
+ database, and lets you query by meaning instead of exact keywords.
8
11
 
9
- Semantic search for static sites using libSQL/Turso with multi-provider embeddings.
12
+ Use it when you want:
10
13
 
11
- 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.
14
+ - a small TypeScript library instead of a hosted search product
15
+ - one search index shared across static-site builds and app routes
16
+ - local or API-based embeddings behind the same indexing/search API
17
+ - direct control over table names, dimensions, content shape, and deployment
12
18
 
13
- ## Features
19
+ ## What It Supports
14
20
 
15
- - 🔍 **Semantic Search** - Find content by meaning, not just keywords
16
- - 🌐 **Multi-Provider Embeddings** - Choose local (Xenova), Gemini, or OpenAI
17
- - **Edge-Ready** - Works with Turso's global edge database
18
- - 📝 **Markdown Support** - Built-in gray-matter parsing
19
- - 🎯 **Type-Safe** - Full TypeScript support
20
- - 🆓 **Free Tier Friendly** - Local embeddings require no API keys
21
+ - Markdown indexing from local directories with frontmatter via `gray-matter`
22
+ - libSQL/Turso storage and vector search
23
+ - Embedding providers that exist in the code today: local
24
+ `Xenova/all-MiniLM-L6-v2`, Google Gemini `text-embedding-004`, and OpenAI
25
+ `text-embedding-3-small` and `text-embedding-3-large`
26
+ - npm distribution plus JSR publishing
21
27
 
22
- ## Installation
28
+ ## Install
29
+
30
+ `@libsql/client` is a peer dependency.
23
31
 
24
- **npm:**
25
32
  ```bash
26
- npm install libsql-search @libsql/client
33
+ pnpm add libsql-search @libsql/client
27
34
  ```
28
35
 
29
- **pnpm:**
30
36
  ```bash
31
- pnpm add libsql-search @libsql/client
37
+ npm install libsql-search @libsql/client
32
38
  ```
33
39
 
34
- **JSR:**
35
40
  ```bash
36
- deno add @logan/libsql-search
41
+ deno add jsr:@logan/libsql-search npm:@libsql/client
37
42
  ```
38
43
 
44
+ For npm usage, the package requires Node `>=22.12.0`.
45
+ Node examples in this README import from `libsql-search` and `@libsql/client`.
46
+ In Deno, after `deno add`, import from `@logan/libsql-search` and
47
+ `@libsql/client`.
48
+
39
49
  ## Quick Start
40
50
 
41
- ### 1. Set Up Your Database
51
+ The shortest working flow is:
52
+
53
+ 1. create a libSQL client
54
+ 2. create the search table
55
+ 3. index a Markdown directory
56
+ 4. query it with the same embedding provider and dimensions
42
57
 
43
- ```typescript
44
- import { createClient } from '@libsql/client';
45
- import { createTable } from 'libsql-search';
58
+ ```ts
59
+ import { createClient } from "@libsql/client";
60
+ import { createTable, indexContent, search } from "libsql-search";
46
61
 
47
62
  const client = createClient({
48
- url: 'libsql://your-db.turso.io',
49
- authToken: 'your-auth-token'
63
+ url: "libsql://your-db.turso.io",
64
+ authToken: "your-auth-token",
50
65
  });
51
66
 
52
- // Create the articles table with vector index
53
- await createTable(client, 'articles', 768);
54
- ```
55
-
56
- ### 2. Index Your Content
57
-
58
- ```typescript
59
- import { indexContent } from 'libsql-search';
67
+ await createTable(client, "articles", 768);
60
68
 
61
- const result = await indexContent({
69
+ await indexContent({
62
70
  client,
63
- contentPath: './content',
71
+ contentPath: "./content",
64
72
  embeddingOptions: {
65
- provider: 'local', // or 'gemini', 'openai'
66
- dimensions: 768
73
+ provider: "local",
74
+ dimensions: 768,
67
75
  },
68
- onProgress: (current, total, file) => {
69
- console.log(`[${current}/${total}] Indexing: ${file}`);
70
- }
71
76
  });
72
77
 
73
- console.log(`Indexed ${result.success}/${result.total} documents`);
74
- ```
75
-
76
- ### 3. Search Your Content
77
-
78
- ```typescript
79
- import { search } from 'libsql-search';
80
-
81
78
  const results = await search({
82
79
  client,
83
- query: 'how to deploy astro',
80
+ query: "how do I deploy my docs site",
84
81
  limit: 5,
85
82
  embeddingOptions: {
86
- provider: 'local'
87
- }
88
- });
89
-
90
- results.forEach(result => {
91
- console.log(`${result.title} (${result.distance})`);
92
- });
93
- ```
94
-
95
- ## Embedding Providers
96
-
97
- ### Local (Xenova/Transformers.js)
98
-
99
- **Free, no API key required**. Runs `all-MiniLM-L6-v2` in Node.js using ONNX.
100
-
101
- ```typescript
102
- embeddingOptions: {
103
- provider: 'local',
104
- dimensions: 768 // 384 native, padded to 768
105
- }
106
- ```
107
-
108
- **Pros:**
109
- - ✅ No API costs
110
- - ✅ No rate limits
111
- - ✅ Works offline
112
- - ✅ Privacy-friendly
113
-
114
- **Cons:**
115
- - ⚠️ First run downloads model (~50MB)
116
- - ⚠️ Slower than API-based options
117
- - ⚠️ Lower quality than large models
118
-
119
- ### Google Gemini
120
-
121
- **Free tier: 1,500 requests/day**. Uses `text-embedding-004` model.
122
-
123
- ```typescript
124
- embeddingOptions: {
125
- provider: 'gemini',
126
- apiKey: process.env.GEMINI_API_KEY,
127
- dimensions: 768 // native
128
- }
129
- ```
130
-
131
- **Pros:**
132
- - ✅ Generous free tier
133
- - ✅ High quality embeddings
134
- - ✅ Fast
135
-
136
- **Cons:**
137
- - ⚠️ Requires API key
138
- - ⚠️ Rate limited
139
-
140
- ### OpenAI
141
-
142
- **Paid only**. Uses `text-embedding-3-small` or `text-embedding-3-large`.
143
-
144
- ```typescript
145
- embeddingOptions: {
146
- provider: 'openai',
147
- apiKey: process.env.OPENAI_API_KEY,
148
- dimensions: 1536 // or 3072 for large
149
- }
150
- ```
151
-
152
- **Pros:**
153
- - ✅ Highest quality
154
- - ✅ Very fast
155
- - ✅ Configurable dimensions
156
-
157
- **Cons:**
158
- - ⚠️ Costs money ($0.02 per 1M tokens)
159
- - ⚠️ Requires API key
160
-
161
- ## API Reference
162
-
163
- ### Indexing
164
-
165
- #### `indexContent(options)`
166
-
167
- Index markdown files from a directory.
168
-
169
- ```typescript
170
- interface IndexerOptions {
171
- client: Client; // libSQL client
172
- contentPath: string; // Path to content directory
173
- embeddingOptions?: EmbeddingOptions;
174
- fileExtensions?: string[]; // Default: ['.md', '.markdown']
175
- exclude?: string[]; // Default: ['node_modules', '.git']
176
- tableName?: string; // Default: 'articles'
177
- onProgress?: (current, total, file) => void;
178
- }
179
- ```
180
-
181
- #### `createTable(client, tableName?, dimensions?)`
182
-
183
- Create the articles table with vector index.
184
-
185
- ### Searching
186
-
187
- #### `search(options)`
188
-
189
- Perform semantic search.
190
-
191
- ```typescript
192
- interface SearchOptions {
193
- client: Client;
194
- query: string;
195
- limit?: number; // Default: 10
196
- tableName?: string; // Default: 'articles'
197
- embeddingOptions?: EmbeddingOptions;
198
- }
199
- ```
200
-
201
- Returns `SearchResult[]`:
202
-
203
- ```typescript
204
- interface SearchResult {
205
- id: number;
206
- slug: string;
207
- title: string;
208
- content: string;
209
- folder: string;
210
- tags: string[];
211
- distance: number; // Lower is better
212
- created_at: string;
213
- }
214
- ```
215
-
216
- #### `getAllArticles(client, tableName?)`
217
-
218
- Get all articles (useful for building static pages).
219
-
220
- #### `getArticleBySlug(client, slug, tableName?)`
221
-
222
- Get a single article by slug.
223
-
224
- #### `getArticlesByFolder(client, folder, tableName?)`
225
-
226
- Get all articles in a folder.
227
-
228
- #### `getFolders(client, tableName?)`
229
-
230
- Get all unique folders.
231
-
232
- ### Embeddings
233
-
234
- #### `generateEmbedding(text, options?)`
235
-
236
- Generate embeddings for arbitrary text.
237
-
238
- ```typescript
239
- interface EmbeddingOptions {
240
- provider?: 'local' | 'gemini' | 'openai';
241
- apiKey?: string;
242
- dimensions?: number;
243
- maxLength?: number; // Default: 8000
244
- }
245
- ```
246
-
247
- #### `prepareTextForEmbedding(fields)`
248
-
249
- Combine multiple fields into embedding text.
250
-
251
- ```typescript
252
- const text = prepareTextForEmbedding({
253
- title: 'My Article',
254
- description: 'A description',
255
- content: '# Content here',
256
- tags: ['astro', 'turso']
257
- });
258
- ```
259
-
260
- ## Framework Integration
261
-
262
- ### Astro
263
-
264
- **Search API Endpoint** (`src/pages/api/search.json.ts`):
265
-
266
- ```typescript
267
- import type { APIRoute } from 'astro';
268
- import { createClient } from '@libsql/client';
269
- import { search } from 'libsql-search';
270
-
271
- export const prerender = false;
272
-
273
- const client = createClient({
274
- url: import.meta.env.TURSO_DB_URL,
275
- authToken: import.meta.env.TURSO_AUTH_TOKEN
276
- });
277
-
278
- export const POST: APIRoute = async ({ request }) => {
279
- const { query, limit = 10 } = await request.json();
280
-
281
- const results = await search({
282
- client,
283
- query,
284
- limit,
285
- embeddingOptions: { provider: 'local' }
286
- });
287
-
288
- return new Response(JSON.stringify({ results }), {
289
- headers: { 'Content-Type': 'application/json' }
290
- });
291
- };
292
- ```
293
-
294
- **Static Page Generation** (`src/pages/[...slug].astro`):
295
-
296
- ```astro
297
- ---
298
- import { createClient } from '@libsql/client';
299
- import { getAllArticles, getArticleBySlug } from 'libsql-search';
300
-
301
- export const prerender = true;
302
-
303
- const client = createClient({
304
- url: import.meta.env.TURSO_DB_URL,
305
- authToken: import.meta.env.TURSO_AUTH_TOKEN
306
- });
307
-
308
- export async function getStaticPaths() {
309
- const articles = await getAllArticles(client);
310
- return articles.map(article => ({
311
- params: { slug: article.slug }
312
- }));
313
- }
314
-
315
- const { slug } = Astro.params;
316
- const article = await getArticleBySlug(client, slug);
317
- ---
318
-
319
- <article>
320
- <h1>{article.title}</h1>
321
- <div set:html={article.content} />
322
- </article>
323
- ```
324
-
325
- ### Next.js
326
-
327
- **API Route** (`app/api/search/route.ts`):
328
-
329
- ```typescript
330
- import { createClient } from '@libsql/client';
331
- import { search } from 'libsql-search';
332
- import { NextRequest } from 'next/server';
333
-
334
- const client = createClient({
335
- url: process.env.TURSO_DB_URL!,
336
- authToken: process.env.TURSO_AUTH_TOKEN!
337
- });
338
-
339
- export async function POST(request: NextRequest) {
340
- const { query, limit = 10 } = await request.json();
341
-
342
- const results = await search({
343
- client,
344
- query,
345
- limit,
346
- embeddingOptions: { provider: 'local' }
347
- });
348
-
349
- return Response.json({ results });
350
- }
351
- ```
352
-
353
- **Static Generation** (`app/[slug]/page.tsx`):
354
-
355
- ```typescript
356
- import { createClient } from '@libsql/client';
357
- import { getAllArticles, getArticleBySlug } from 'libsql-search';
358
-
359
- const client = createClient({
360
- url: process.env.TURSO_DB_URL!,
361
- authToken: process.env.TURSO_AUTH_TOKEN!
83
+ provider: "local",
84
+ dimensions: 768,
85
+ },
362
86
  });
363
87
 
364
- export async function generateStaticParams() {
365
- const articles = await getAllArticles(client);
366
- return articles.map(article => ({
367
- slug: article.slug
368
- }));
369
- }
370
-
371
- export default async function Page({ params }: { params: { slug: string } }) {
372
- const article = await getArticleBySlug(client, params.slug);
373
-
374
- return (
375
- <article>
376
- <h1>{article.title}</h1>
377
- <div dangerouslySetInnerHTML={{ __html: article.content }} />
378
- </article>
379
- );
380
- }
381
- ```
382
-
383
- ## Best Practices
384
-
385
- ### Embedding Dimensions
386
-
387
- - Use **768 dimensions** for best compatibility
388
- - Local model outputs 384, automatically padded to 768
389
- - Gemini outputs 768 natively
390
- - OpenAI supports custom dimensions
391
-
392
- ### Index Updates
393
-
394
- Create a script to re-index content:
395
-
396
- ```json
397
- {
398
- "scripts": {
399
- "index": "node scripts/index.js",
400
- "build": "npm run index && astro build"
401
- }
402
- }
88
+ console.log(results.map((result) => ({
89
+ slug: result.slug,
90
+ title: result.title,
91
+ distance: result.distance,
92
+ })));
403
93
  ```
404
94
 
405
- ### Search Quality
406
-
407
- Improve search results:
408
-
409
- 1. **Include relevant fields** in embedding text (title, description, tags)
410
- 2. **Truncate long content** to avoid noise
411
- 3. **Use the same provider** for indexing and search
412
- 4. **Experiment with distance thresholds** (lower is better)
413
-
414
- ### Performance
415
-
416
- - **Cache the embedding model** (done automatically)
417
- - **Use edge databases** (Turso) for low latency
418
- - **Implement search debouncing** in the UI
419
- - **Limit result count** to 5-10 for best UX
420
-
421
- ## Examples
422
-
423
- See the `/examples` directory for complete implementations:
424
-
425
- - [Astro Documentation Site](./examples/astro-docs)
426
- - [Next.js Blog](./examples/nextjs-blog)
427
- - [CLI Indexer](./examples/cli-indexer)
95
+ Important behavior:
428
96
 
429
- ## CLI Usage
97
+ - Call `createTable()` before indexing or searching.
98
+ - Keep dimensions aligned across table creation, indexing, and search queries.
99
+ - `indexContent()` clears existing rows before rebuilding the index.
430
100
 
431
- For a standalone indexing script:
101
+ ## Core API
432
102
 
433
- ```javascript
434
- // scripts/index.js
435
- import { createClient } from '@libsql/client';
436
- import { createTable, indexContent } from 'libsql-search';
103
+ - `createTable(client, tableName?, dimensions?)`
104
+ - `indexContent(options)`
105
+ - `search(options)`
106
+ - `getAllArticles(client, tableName?)`
107
+ - `getArticleBySlug(client, slug, tableName?)`
108
+ - `getArticlesByFolder(client, folder, tableName?)`
109
+ - `getFolders(client, tableName?)`
110
+ - `generateEmbedding(text, options?)`
111
+ - `prepareTextForEmbedding(fields)`
437
112
 
438
- const client = createClient({
439
- url: process.env.TURSO_DB_URL,
440
- authToken: process.env.TURSO_AUTH_TOKEN
441
- });
442
-
443
- await createTable(client);
444
-
445
- const result = await indexContent({
446
- client,
447
- contentPath: './content',
448
- embeddingOptions: {
449
- provider: process.env.EMBEDDING_PROVIDER || 'local'
450
- },
451
- onProgress: (current, total, file) => {
452
- console.log(`[${current}/${total}] ${file}`);
453
- }
454
- });
455
-
456
- console.log(`✅ Indexed ${result.success} documents`);
457
- ```
113
+ ## Docs
458
114
 
459
- Run with:
460
- ```bash
461
- node --env-file=.env scripts/index.js
462
- ```
115
+ - [Docs index](./docs/README.md)
116
+ - [Provider guide](./docs/PROVIDERS.md)
117
+ - [API reference](./docs/API.md)
118
+ - [Integration examples](./docs/INTEGRATIONS.md)
119
+ - [Indexing and operations](./docs/INDEXING.md)
120
+ - [Troubleshooting](./docs/TROUBLESHOOTING.md)
121
+ - [Release workflow](./docs/RELEASING.md)
463
122
 
464
123
  ## License
465
124
 
466
125
  MIT
467
-
468
- ## Contributing
469
-
470
- Contributions welcome! Please open an issue or PR on [GitHub](https://github.com/llbbl/libsql-search).
471
-
472
- ## Related Projects
473
-
474
- - [Turso](https://turso.tech) - Edge SQLite database
475
- - [libSQL](https://github.com/tursodatabase/libsql) - Open source SQLite fork
476
- - [Astro](https://astro.build) - Static site framework
477
- - [Transformers.js](https://huggingface.co/docs/transformers.js) - ML models in JavaScript
478
-
479
- ## Support
480
-
481
- - [Documentation](https://github.com/llbbl/libsql-search)
482
- - [Issues](https://github.com/llbbl/libsql-search/issues)
483
- - [Discussions](https://github.com/llbbl/libsql-search/discussions)
package/docs/API.md ADDED
@@ -0,0 +1,167 @@
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
+ The created schema includes:
41
+
42
+ - `id` primary key
43
+ - `slug`
44
+ - `title`
45
+ - `content`
46
+ - `folder`
47
+ - `tags`
48
+ - `embedding`
49
+ - `created_at`
50
+ - `updated_at`
51
+
52
+ ## `indexContent(options)`
53
+
54
+ Indexes Markdown files from a directory on disk.
55
+
56
+ ```ts
57
+ interface IndexerOptions {
58
+ client: Client;
59
+ contentPath: string;
60
+ embeddingOptions?: EmbeddingOptions;
61
+ fileExtensions?: string[];
62
+ exclude?: string[];
63
+ tableName?: string;
64
+ onProgress?: (current: number, total: number, file: string) => void;
65
+ }
66
+ ```
67
+
68
+ Defaults:
69
+
70
+ - `fileExtensions`: [".md", ".markdown"]
71
+ - `exclude`: ["node_modules", ".git", "dist", "build"]
72
+ - `tableName`: `"articles"`
73
+
74
+ Return shape:
75
+
76
+ ```ts
77
+ {
78
+ success: number;
79
+ failed: number;
80
+ total: number;
81
+ }
82
+ ```
83
+
84
+ Behavior notes:
85
+
86
+ - `indexContent()` deletes existing rows in the target table before rebuilding
87
+ - frontmatter `title`, `description`, and `tags` are folded into the embedding
88
+ text
89
+ - if a file has no frontmatter title, the filename becomes the title
90
+
91
+ ## `search(options)`
92
+
93
+ Generates a query embedding and performs vector similarity search.
94
+
95
+ ```ts
96
+ interface SearchOptions {
97
+ client: Client;
98
+ query: string;
99
+ limit?: number;
100
+ tableName?: string;
101
+ embeddingOptions?: EmbeddingOptions;
102
+ }
103
+ ```
104
+
105
+ Defaults:
106
+
107
+ - `limit`: `10`
108
+ - `tableName`: `"articles"`
109
+
110
+ Result shape:
111
+
112
+ ```ts
113
+ interface SearchResult {
114
+ id: number;
115
+ slug: string;
116
+ title: string;
117
+ content: string;
118
+ folder: string;
119
+ tags: string[];
120
+ distance: number;
121
+ created_at: string;
122
+ }
123
+ ```
124
+
125
+ Lower `distance` values are better matches.
126
+
127
+ ## Article Retrieval Helpers
128
+
129
+ ### `getAllArticles(client, tableName?)`
130
+
131
+ Returns all indexed articles ordered by title.
132
+
133
+ ### `getArticleBySlug(client, slug, tableName?)`
134
+
135
+ Returns one article or `null`.
136
+
137
+ ### `getArticlesByFolder(client, folder, tableName?)`
138
+
139
+ Returns articles in a specific folder.
140
+
141
+ ### `getFolders(client, tableName?)`
142
+
143
+ Returns distinct folder names from the index.
144
+
145
+ ## Embedding Helpers
146
+
147
+ ### `generateEmbedding(text, options?)`
148
+
149
+ Generates an embedding for arbitrary text using the selected provider.
150
+
151
+ ### `padEmbedding(embedding, targetDimensions)`
152
+
153
+ Pads or truncates an embedding array to the requested length.
154
+
155
+ ### `prepareTextForEmbedding(fields)`
156
+
157
+ Combines title, description, tags, and content into the text sent to the
158
+ embedding model.
159
+
160
+ ```ts
161
+ const text = prepareTextForEmbedding({
162
+ title: "My Article",
163
+ description: "How semantic search works",
164
+ tags: ["search", "turso"],
165
+ content: "# Content",
166
+ });
167
+ ```
@@ -0,0 +1,68 @@
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` is interpolated into SQL. Treat it as a trusted identifier coming
61
+ from your own configuration, not from user input.
62
+
63
+ ## Runtime Notes
64
+
65
+ - local embeddings may download a model on the first run
66
+ - Node users need `@libsql/client` installed alongside the package
67
+ - the repository validates both the npm package build and `deno check`, but the
68
+ indexing flow itself still depends on filesystem access
@@ -0,0 +1,143 @@
1
+ # Integration Examples
2
+
3
+ These examples show the current exported API wired into typical server-side
4
+ routes. They are intentionally small so you can adapt them to your app.
5
+
6
+ ## Astro Search Endpoint
7
+
8
+ ```ts
9
+ import type { APIRoute } from "astro";
10
+ import { createClient } from "@libsql/client";
11
+ import { search } from "libsql-search";
12
+
13
+ export const prerender = false;
14
+
15
+ const client = createClient({
16
+ url: import.meta.env.TURSO_DB_URL,
17
+ authToken: import.meta.env.TURSO_AUTH_TOKEN,
18
+ });
19
+
20
+ export const POST: APIRoute = async ({ request }) => {
21
+ const { query, limit = 10 } = await request.json();
22
+
23
+ const results = await search({
24
+ client,
25
+ query,
26
+ limit,
27
+ embeddingOptions: {
28
+ provider: "local",
29
+ dimensions: 768,
30
+ },
31
+ });
32
+
33
+ return new Response(JSON.stringify({ results }), {
34
+ headers: { "Content-Type": "application/json" },
35
+ });
36
+ };
37
+ ```
38
+
39
+ ## Astro Static Paths
40
+
41
+ ```ts
42
+ import { createClient } from "@libsql/client";
43
+ import { getAllArticles, getArticleBySlug } from "libsql-search";
44
+
45
+ const client = createClient({
46
+ url: import.meta.env.TURSO_DB_URL,
47
+ authToken: import.meta.env.TURSO_AUTH_TOKEN,
48
+ });
49
+
50
+ export async function getStaticPaths() {
51
+ const articles = await getAllArticles(client);
52
+
53
+ return articles.map((article) => ({
54
+ params: { slug: article.slug },
55
+ }));
56
+ }
57
+
58
+ const article = await getArticleBySlug(client, "guides/getting-started");
59
+ ```
60
+
61
+ ## Next.js Route Handler
62
+
63
+ ```ts
64
+ import { createClient } from "@libsql/client";
65
+ import { search } from "libsql-search";
66
+ import { NextRequest } from "next/server";
67
+
68
+ const client = createClient({
69
+ url: process.env.TURSO_DB_URL!,
70
+ authToken: process.env.TURSO_AUTH_TOKEN!,
71
+ });
72
+
73
+ export async function POST(request: NextRequest) {
74
+ const { query, limit = 10 } = await request.json();
75
+
76
+ const results = await search({
77
+ client,
78
+ query,
79
+ limit,
80
+ embeddingOptions: {
81
+ provider: "local",
82
+ dimensions: 768,
83
+ },
84
+ });
85
+
86
+ return Response.json({ results });
87
+ }
88
+ ```
89
+
90
+ ## Next.js Static Params
91
+
92
+ ```tsx
93
+ import { createClient } from "@libsql/client";
94
+ import { getAllArticles, getArticleBySlug } from "libsql-search";
95
+
96
+ const client = createClient({
97
+ url: process.env.TURSO_DB_URL!,
98
+ authToken: process.env.TURSO_AUTH_TOKEN!,
99
+ });
100
+
101
+ export async function generateStaticParams() {
102
+ const articles = await getAllArticles(client);
103
+
104
+ return articles.map((article) => ({
105
+ slug: article.slug,
106
+ }));
107
+ }
108
+
109
+ export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
110
+ const { slug } = await params;
111
+ const article = await getArticleBySlug(client, slug);
112
+
113
+ return <article>{article?.title}</article>;
114
+ }
115
+ ```
116
+
117
+ ## Build-Time Index Script
118
+
119
+ A small script is usually enough to rebuild the index before a site build.
120
+
121
+ ```ts
122
+ import { createClient } from "@libsql/client";
123
+ import { createTable, indexContent } from "libsql-search";
124
+
125
+ const client = createClient({
126
+ url: process.env.TURSO_DB_URL!,
127
+ authToken: process.env.TURSO_AUTH_TOKEN!,
128
+ });
129
+
130
+ await createTable(client, "articles", 768);
131
+
132
+ await indexContent({
133
+ client,
134
+ contentPath: "./content",
135
+ embeddingOptions: {
136
+ provider: process.env.EMBEDDING_PROVIDER as "local" | "gemini" | "openai" | undefined,
137
+ dimensions: 768,
138
+ },
139
+ });
140
+ ```
141
+
142
+ Pair this with your framework build command so indexed content and deployed code
143
+ stay in sync.
@@ -0,0 +1,100 @@
1
+ # Embedding Providers
2
+
3
+ `libsql-search` currently supports three embedding providers:
4
+
5
+ - `local`
6
+ - `gemini`
7
+ - `openai`
8
+
9
+ Use the same provider and dimensions for both indexing and querying. A mismatch
10
+ between stored vectors and query vectors will break search quality or fail at
11
+ query time.
12
+
13
+ ## Shared Options
14
+
15
+ ```ts
16
+ interface EmbeddingOptions {
17
+ provider?: "local" | "gemini" | "openai";
18
+ apiKey?: string;
19
+ dimensions?: number;
20
+ maxLength?: number;
21
+ }
22
+ ```
23
+
24
+ - `provider` defaults to `"local"`
25
+ - `dimensions` defaults to `768`
26
+ - `maxLength` defaults to `8000`
27
+ - `apiKey` is optional in code, but required for hosted providers unless the
28
+ matching environment variable is available
29
+
30
+ ## Local
31
+
32
+ Provider value: `local`
33
+
34
+ The local provider loads `Xenova/all-MiniLM-L6-v2` through
35
+ `@xenova/transformers`.
36
+
37
+ ```ts
38
+ embeddingOptions: {
39
+ provider: "local",
40
+ dimensions: 768,
41
+ }
42
+ ```
43
+
44
+ Notes:
45
+
46
+ - the model emits 384 dimensions and `libsql-search` pads or truncates to your
47
+ requested size
48
+ - the first run downloads the model and can take longer on a fresh machine
49
+ - no API key is required
50
+
51
+ ## Gemini
52
+
53
+ Provider value: `gemini`
54
+
55
+ Gemini uses Google `text-embedding-004`.
56
+
57
+ ```ts
58
+ embeddingOptions: {
59
+ provider: "gemini",
60
+ apiKey: process.env.GEMINI_API_KEY,
61
+ }
62
+ ```
63
+
64
+ Behavior:
65
+
66
+ - if `apiKey` is omitted, the library reads `GEMINI_API_KEY`
67
+ - Gemini returns 768 dimensions natively
68
+ - the current implementation does not expose model selection
69
+
70
+ ## OpenAI
71
+
72
+ Provider value: `openai`
73
+
74
+ OpenAI uses `text-embedding-3-small` when `dimensions <= 1536` and
75
+ `text-embedding-3-large` when `dimensions > 1536`.
76
+
77
+ ```ts
78
+ embeddingOptions: {
79
+ provider: "openai",
80
+ apiKey: process.env.OPENAI_API_KEY,
81
+ dimensions: 1536,
82
+ }
83
+ ```
84
+
85
+ Behavior:
86
+
87
+ - if `apiKey` is omitted, the library reads `OPENAI_API_KEY`
88
+ - the request sends the `dimensions` value to the OpenAI embeddings API
89
+ - use the same dimension count in `createTable()`
90
+
91
+ ## Dimension Guidelines
92
+
93
+ - `768` is the easiest cross-provider target in the current implementation
94
+ - local embeddings are padded from 384 to your target size
95
+ - Gemini stays at 768
96
+ - OpenAI can be used at 1536 or 3072, or another supported OpenAI dimension
97
+ value you explicitly set
98
+
99
+ If you switch provider or dimensions for an existing table, recreate the table
100
+ or rebuild the index into a separate table so stored vectors stay consistent.
package/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # Documentation
2
+
3
+ This directory holds the longer-form reference material for `libsql-search`.
4
+ Start with the page that matches the job you are doing:
5
+
6
+ - [Provider guide](./PROVIDERS.md): local, Gemini, and OpenAI embedding options,
7
+ dimensions, and API key behavior
8
+ - [API reference](./API.md): exported functions, option shapes, and result data
9
+ - [Integration examples](./INTEGRATIONS.md): Astro and Next.js server-side usage
10
+ - [Indexing and operations](./INDEXING.md): content layout, rebuild scripts,
11
+ search quality tips, and indexing gotchas
12
+ - [Troubleshooting](./TROUBLESHOOTING.md): known install/runtime issues
13
+ - [Releasing](./RELEASING.md): maintainer release workflow
14
+
15
+ For the shortest first-use path, go back to the repository
16
+ [README](../README.md).
@@ -0,0 +1,91 @@
1
+ # Releasing
2
+
3
+ Normal releases are automatic. Merge a reviewed PR to `main`; the `Publish
4
+ Package` workflow decides the next stable SemVer version, creates any required
5
+ manifest-only release commit, creates an annotated `vX.Y.Z` tag, publishes npm
6
+ and JSR, then creates GitHub Release notes after both registries succeed.
7
+
8
+ ## Automatic release flow
9
+
10
+ 1. Every push to `main` starts `.github/workflows/publish.yml`.
11
+ 2. The workflow ignores self-generated `chore(release): ...` commits.
12
+ 3. A serialized release-writer job fetches the current `origin/main` after it
13
+ has the release slot. If `origin/main` moved beyond the triggering commit,
14
+ that older run exits and the newer run handles the accumulated changes.
15
+ 4. The release planner compares commits since the highest stable `vX.Y.Z` tag
16
+ merged into the release commit. Higher tags that are not reachable from
17
+ `main` are ignored so a stray or divergent tag cannot make the default
18
+ release line jump. Commits whose changed files are confined to `docs/**` or
19
+ `.github/**` do not count as release-eligible and do not affect the bump. If
20
+ the newest `main` commit is docs-only but an earlier untagged code or README
21
+ commit is still pending, that newest run releases the accumulated eligible
22
+ changes. Breaking changes bump major, `feat:` bumps minor, and all other
23
+ eligible commits bump patch.
24
+ 5. `package.json`, `jsr.json`, and `deno.json` are synchronized to the chosen
25
+ version. If they already match the chosen version, no release commit is
26
+ created.
27
+ 6. The workflow validates the candidate, creates an annotated tag, and atomically
28
+ pushes the release commit plus tag.
29
+ 7. npm publishes `libsql-search` through trusted publishing OIDC with the GitHub
30
+ environment named `npm`.
31
+ 8. JSR publishes `@logan/libsql-search` through a separate OIDC job.
32
+ 9. GitHub Release notes are generated only after both registry jobs complete.
33
+
34
+ The current bootstrap case is supported: if the latest tag is `v0.1.3` and the
35
+ manifests already say `0.1.4`, the first qualifying `main` release tags and
36
+ publishes `v0.1.4` without creating an empty release commit.
37
+
38
+ ## Manual overrides
39
+
40
+ Manual version and tag commands are overrides, not the default path. Use them
41
+ only when you intentionally need to publish a specific already-reviewed `main`
42
+ commit.
43
+
44
+ 1. Synchronize `package.json`, `jsr.json`, and `deno.json` to the target
45
+ `X.Y.Z` version.
46
+ 2. Merge that manifest change to `main`.
47
+ 3. Create annotated tag `vX.Y.Z` on the merged `main` commit.
48
+ 4. Push the tag.
49
+
50
+ The same `Publish Package` workflow validates manual tags before publishing. It
51
+ checks that the tag target is on `origin/main`, that the tag and manifests agree,
52
+ and that the remote tag still resolves to the checked-out commit immediately
53
+ before each registry publish.
54
+
55
+ ## Recovery dispatch
56
+
57
+ Use the guarded workflow dispatch path when an existing `vX.Y.Z` tag needs a
58
+ registry or GitHub Release recovery without creating a new version. Example:
59
+
60
+ ```bash
61
+ gh workflow run publish.yml --ref main -f release_tag=v0.1.4
62
+ ```
63
+
64
+ Dispatch validates that `release_tag` is strict `vX.Y.Z`, fetches the remote tag,
65
+ peels it to a commit, checks that commit is on `origin/main`, checks manifests
66
+ against the tag, and then publishes from the tag target SHA. Unlike a tag-push
67
+ run, dispatch does not require the tag target to equal the workflow dispatch
68
+ SHA; this allows a newer `main` workflow fix to recover an older release tag.
69
+
70
+ npm and GitHub Release recovery steps remain idempotent and skip when the
71
+ version or release already exists. JSR recovery always invokes
72
+ `pnpm dlx jsr@0.14.3 publish`; do not use `deno info jsr:...` as an existence
73
+ gate, because it can resolve successfully even when the public JSR package
74
+ version metadata is not actually published.
75
+
76
+ ## GitHub and registry settings
77
+
78
+ - npm trusted publishing must point at workflow filename `publish.yml` and use
79
+ the GitHub Actions environment named `npm`.
80
+ - The workflow does not use `NPM_TOKEN`.
81
+ - JSR publishing uses its own OIDC job and does not depend on the npm
82
+ environment.
83
+ - The repository settings must allow GitHub Actions to push the generated
84
+ `chore(release): ...` commit to `main`; otherwise the automatic release job
85
+ will validate successfully and then fail at the atomic push step.
86
+ - Configure a repository ruleset or tag protection rule for `v*` tags.
87
+ - Restrict the `npm` environment to the `main` deployment branch for automatic
88
+ releases and `v*` tags for intentional manual overrides.
89
+
90
+ GitHub Releases are an after-publish record. They are not a prerequisite for npm
91
+ or JSR publishing.
@@ -0,0 +1,64 @@
1
+ # Troubleshooting: Transitive `sharp` Install Errors
2
+
3
+ `libsql-search` does not directly depend on `sharp`. If you see an install error
4
+ mentioning `sharp`, it is coming from another dependency in your application or
5
+ toolchain.
6
+
7
+ This page exists because the error can show up in environments that also use
8
+ `libsql-search`, and it is easy to misattribute the failure to this package.
9
+
10
+ ## Typical Error
11
+
12
+ ```text
13
+ Cannot find module '../build/Release/sharp-*.node'
14
+ ```
15
+
16
+ Or:
17
+
18
+ ```text
19
+ Error: Something went wrong installing the "sharp" module
20
+ ```
21
+
22
+ ## Why It Happens
23
+
24
+ With pnpm, native packages may need explicit build-script approval. If the
25
+ relevant install script is blocked, the native binary is never downloaded or
26
+ built.
27
+
28
+ ## What To Do
29
+
30
+ First inspect which build scripts pnpm blocked:
31
+
32
+ ```bash
33
+ pnpm ignored-builds
34
+ ```
35
+
36
+ Then approve the package that is actually failing and reinstall:
37
+
38
+ ```bash
39
+ pnpm approve-builds
40
+ pnpm install
41
+ ```
42
+
43
+ In the interactive `pnpm approve-builds` prompt, select `sharp` if that is the
44
+ package reporting the native-module failure.
45
+
46
+ For a committed repository-level fix, you can also allow the package explicitly
47
+ in `pnpm-workspace.yaml` with `onlyBuiltDependencies`.
48
+
49
+ ## Relation To `libsql-search`
50
+
51
+ - local embeddings use `@xenova/transformers`
52
+ - the first local embedding run may download a model at runtime
53
+ - that runtime model download is separate from a pnpm native-module install
54
+ failure
55
+
56
+ ## Verification
57
+
58
+ After reinstalling, rerun the command that originally failed. If your app uses
59
+ `sharp` directly, verify that import in your own project context.
60
+
61
+ ## Additional Resources
62
+
63
+ - [pnpm approve-builds](https://pnpm.io/10.x/cli/approve-builds)
64
+ - [Sharp installation docs](https://sharp.pixelplumbing.com/install)
@@ -0,0 +1,12 @@
1
+ # Troubleshooting
2
+
3
+ Use the page that matches the failure mode:
4
+
5
+ - [Sharp native module issues](./TROUBLESHOOTING-SHARP.md)
6
+
7
+ Common operational checks:
8
+
9
+ - verify you called `createTable()` before indexing or searching
10
+ - verify the table dimension matches the embedding dimension in your code
11
+ - verify the same provider is used for indexing and querying
12
+ - verify hosted providers have `GEMINI_API_KEY` or `OPENAI_API_KEY` available
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libsql-search",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Semantic search for static sites using libSQL/Turso with multi-provider embeddings",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.34.5",
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist",
18
- "README.md"
18
+ "README.md",
19
+ "docs"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "tsc --noEmit && rollup -c && rollup -c rollup.dts.config.js",