libsql-search 0.7.0 → 0.7.1

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
@@ -5,27 +5,13 @@
5
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
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
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
+ `libsql-search` adds semantic search to Markdown-backed sites with a small TypeScript API. It indexes frontmatter and content from files on disk, stores vectors in libSQL/Turso, and lets you query by meaning instead of exact keywords.
11
9
 
12
10
  Use it when you want:
13
11
 
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 hosted embeddings behind the same indexing/search API
17
- - direct control over table names, dimensions, content shape, and deployment
18
-
19
- ## What It Supports
20
-
21
- - Markdown indexing from local directories with frontmatter via `gray-matter`
22
- - libSQL/Turso storage and vector search
23
- - Embedding providers: local Hugging Face `Xenova/all-MiniLM-L6-v2`,
24
- Cloudflare Workers AI `@cf/baai/bge-m3`, Mistral `mistral-embed`, Google Gemini
25
- `gemini-embedding-2`, and OpenAI `text-embedding-3-small` /
26
- `text-embedding-3-large`; self-hosted OpenAI-compatible endpoints are also
27
- available for TEI and similar trusted deployments
28
- - npm distribution plus JSR publishing
12
+ - one indexing/search API across local and hosted embedding providers
13
+ - direct control over vector dimensions, table names, and deployment shape
14
+ - a lightweight library instead of a hosted search product
29
15
 
30
16
  ## Install
31
17
 
@@ -44,18 +30,10 @@ deno add jsr:@logan/libsql-search npm:@libsql/client
44
30
  ```
45
31
 
46
32
  For npm usage, the package requires Node `>=22.12.0`.
47
- Node examples in this README import from `libsql-search` and `@libsql/client`.
48
- In Deno, after `deno add`, import from `@logan/libsql-search` and
49
- `@libsql/client`.
50
33
 
51
34
  ## Quick Start
52
35
 
53
- The shortest working flow is:
54
-
55
- 1. create a libSQL client
56
- 2. create the search table
57
- 3. index a Markdown directory
58
- 4. query it with the same embedding provider and dimensions
36
+ This example uses the default local provider. Local embeddings run in-process after the initial model download and cache warmup; they are not automatically air-gapped.
59
37
 
60
38
  ```ts
61
39
  import { createClient } from "@libsql/client";
@@ -66,11 +44,12 @@ const client = createClient({
66
44
  authToken: "your-auth-token",
67
45
  });
68
46
 
69
- await createTable(client);
47
+ await createTable(client, "articles_local_384", 384);
70
48
 
71
49
  await indexContent({
72
50
  client,
73
51
  contentPath: "./content",
52
+ tableName: "articles_local_384",
74
53
  embeddingOptions: {
75
54
  provider: "local",
76
55
  },
@@ -79,6 +58,7 @@ await indexContent({
79
58
  const results = await search({
80
59
  client,
81
60
  query: "how do I deploy my docs site",
61
+ tableName: "articles_local_384",
82
62
  limit: 5,
83
63
  embeddingOptions: {
84
64
  provider: "local",
@@ -95,33 +75,31 @@ console.log(results.map((result) => ({
95
75
  Important behavior:
96
76
 
97
77
  - 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.
100
- - `local` is the default offline provider and uses 384 dimensions. Cloudflare is
101
- the recommended hosted option. Cloudflare and Mistral use 1024 dimensions.
102
- Gemini defaults to 3072 dimensions and supports 128-3072.
103
-
104
- ## Core API
105
-
106
- - `createTable(client, tableName?, dimensions?)`
107
- - `indexContent(options)`
108
- - `search(options)`
109
- - `getAllArticles(client, tableName?)`
110
- - `getArticleBySlug(client, slug, tableName?)`
111
- - `getArticlesByFolder(client, folder, tableName?)`
112
- - `getFolders(client, tableName?)`
113
- - `generateEmbedding(text, options?)`
114
- - `prepareTextForEmbedding(fields)`
78
+ - Keep table width, provider, and dimensions aligned across create/index/query.
79
+ - `indexContent()` clears existing rows before rebuilding and is not transactional.
80
+ - Hosted providers send indexed and queried text to external services and may incur provider charges.
81
+
82
+ ## Providers
83
+
84
+ Built-in providers:
85
+
86
+ - `local` with `Xenova/all-MiniLM-L6-v2` at 384 dimensions
87
+ - `cloudflare` with `@cf/baai/bge-m3` at 1024 dimensions
88
+ - `mistral` with `mistral-embed` at 1024 dimensions
89
+ - `gemini` with `gemini-embedding-2` at 128-3072 dimensions, default 3072
90
+ - `openai` with `text-embedding-3-small` or `text-embedding-3-large`, default 768
91
+ - `openai-compatible` for trusted OpenAI-compatible endpoints such as TEI
115
92
 
116
93
  ## Docs
117
94
 
118
- - [Docs index](./docs/README.md)
119
- - [Provider guide](./docs/PROVIDERS.md)
95
+ - [Documentation index](./docs/README.md)
96
+ - [Provider selection and configuration](./docs/PROVIDERS.md)
120
97
  - [API reference](./docs/API.md)
121
98
  - [Integration examples](./docs/INTEGRATIONS.md)
99
+ - [Migration and reindexing guide](./docs/MIGRATIONS.md)
100
+ - [Testing guidance](./docs/TESTING.md)
122
101
  - [Indexing and operations](./docs/INDEXING.md)
123
102
  - [Troubleshooting](./docs/TROUBLESHOOTING.md)
124
- - [Release workflow](./docs/RELEASING.md)
125
103
 
126
104
  ## License
127
105
 
package/docs/API.md CHANGED
@@ -37,9 +37,16 @@ It also exports these types:
37
37
  - `SearchOptions`
38
38
  - `SearchResult`
39
39
 
40
+ ## Navigation
41
+
42
+ - [Provider matrix and credential rules](./PROVIDERS.md)
43
+ - [Migration and reindexing guide](./MIGRATIONS.md)
44
+ - [Indexing and operational behavior](./INDEXING.md)
45
+ - [Testing guidance](./TESTING.md)
46
+
40
47
  ## `createTable(client, tableName?, dimensions?)`
41
48
 
42
- Creates the table and supporting indexes used by search.
49
+ Creates the search table and supporting indexes.
43
50
 
44
51
  ```ts
45
52
  await createTable(client);
@@ -50,10 +57,7 @@ Defaults:
50
57
  - `tableName`: `"articles"`
51
58
  - `dimensions`: `384`
52
59
 
53
- `tableName` must be an ASCII SQLite identifier matching
54
- `[A-Za-z_][A-Za-z0-9_]*`. Valid identifiers are quoted internally, so reserved
55
- words such as `"select"` are safe to use. `dimensions` must be a positive
56
- integer.
60
+ `tableName` must be an ASCII SQLite identifier matching `[A-Za-z_][A-Za-z0-9_]*`. Valid identifiers are quoted internally, so reserved words such as `"select"` are safe to use. `dimensions` must be a positive integer.
57
61
 
58
62
  The created schema includes:
59
63
 
@@ -63,10 +67,12 @@ The created schema includes:
63
67
  - `content`
64
68
  - `folder`
65
69
  - `tags`
66
- - `embedding`
70
+ - `embedding F32_BLOB(dimensions)`
67
71
  - `created_at`
68
72
  - `updated_at`
69
73
 
74
+ `createTable()` uses `CREATE TABLE IF NOT EXISTS`, so it does not resize an existing vector column. See [Migration and reindexing guide](./MIGRATIONS.md) before changing widths or providers.
75
+
70
76
  ## `indexContent(options)`
71
77
 
72
78
  Indexes Markdown files from a directory on disk.
@@ -89,8 +95,6 @@ Defaults:
89
95
  - `exclude`: ["node_modules", ".git", "dist", "build"]
90
96
  - `tableName`: `"articles"`
91
97
 
92
- `tableName` follows the same identifier policy as `createTable()`.
93
-
94
98
  Return shape:
95
99
 
96
100
  ```ts
@@ -104,10 +108,9 @@ Return shape:
104
108
  Behavior notes:
105
109
 
106
110
  - `indexContent()` deletes existing rows in the target table before rebuilding
107
- - frontmatter `title`, `description`, and `tags` are folded into the embedding
108
- text
109
- - embeddings default to `intent: "document"` unless `embeddingOptions.intent`
110
- is set explicitly
111
+ - rebuilds are not transactional
112
+ - frontmatter `title`, `description`, and `tags` are folded into the embedding text
113
+ - embeddings default to `intent: "document"` unless `embeddingOptions.intent` is set explicitly
111
114
  - if a file has no frontmatter title, the filename becomes the title
112
115
 
113
116
  ## `search(options)`
@@ -129,9 +132,7 @@ Defaults:
129
132
  - `limit`: `10`
130
133
  - `tableName`: `"articles"`
131
134
 
132
- `limit` must be an integer from `1` through `100`; invalid values are rejected
133
- before query embedding generation. `tableName` follows the same identifier
134
- policy as `createTable()`.
135
+ `limit` must be an integer from `1` through `100`; invalid values are rejected before query embedding generation.
135
136
 
136
137
  Result shape:
137
138
 
@@ -150,8 +151,7 @@ interface SearchResult {
150
151
 
151
152
  Lower `distance` values are better matches.
152
153
 
153
- Search embeddings default to `intent: "query"` unless
154
- `embeddingOptions.intent` is set explicitly.
154
+ Search embeddings default to `intent: "query"` unless `embeddingOptions.intent` is set explicitly.
155
155
 
156
156
  ## Article Retrieval Helpers
157
157
 
@@ -171,82 +171,106 @@ Returns articles in a specific folder.
171
171
 
172
172
  Returns distinct folder names from the index.
173
173
 
174
- All article retrieval helpers validate `tableName` before executing SQL.
174
+ All retrieval helpers validate `tableName` before executing SQL.
175
175
 
176
176
  ## Embedding Helpers
177
177
 
178
- ### `generateEmbedding(text, options?)`
178
+ ### `EmbeddingOptions`
179
+
180
+ ```ts
181
+ interface EmbeddingOptions {
182
+ provider?:
183
+ | "local"
184
+ | "cloudflare"
185
+ | "mistral"
186
+ | "gemini"
187
+ | "openai"
188
+ | "openai-compatible";
189
+ apiKey?: string;
190
+ accountId?: string;
191
+ apiToken?: string;
192
+ baseUrl?: string;
193
+ model?: string;
194
+ batchSize?: number;
195
+ dimensions?: number;
196
+ maxLength?: number;
197
+ intent?: "document" | "query";
198
+ timeoutMs?: number;
199
+ signal?: AbortSignal;
200
+ }
201
+ ```
179
202
 
180
- Generates an embedding for arbitrary text using the selected provider.
203
+ Important option rules:
181
204
 
182
- ### `generateEmbeddings(texts, options?)`
205
+ - `provider` defaults to `local`
206
+ - `maxLength` defaults to `8000`
207
+ - `timeoutMs` defaults to `30000`
208
+ - `model` is only used by `openai-compatible`
209
+ - `baseUrl`, `model`, and `dimensions` are required for `openai-compatible`
210
+ - `batchSize` only applies to `openai-compatible` and defaults to `32`
211
+ - `openai-compatible` never reads `OPENAI_API_KEY`
212
+ - only the Gemini adapter currently changes payload formatting by `intent`
183
213
 
184
- Generates an ordered batch of embeddings. Empty batches return `[]` without
185
- creating a hosted provider client or making a network request.
214
+ Dimension rules:
186
215
 
187
- ### `createEmbeddingProvider(options?)`
216
+ - local: fixed `384`
217
+ - Cloudflare: fixed `1024`
218
+ - Mistral: fixed `1024`
219
+ - Gemini: default `3072`, allowed integer range `128-3072`
220
+ - OpenAI: default `768`; `text-embedding-3-small` through `1536`, `text-embedding-3-large` above `1536`
221
+ - OpenAI-compatible: required positive integer, no default
188
222
 
189
- Creates a provider client with immutable metadata and an `embed(texts, options?)`
190
- method. Provider clients return a rich `EmbeddingBatchResult`; the compatibility
191
- helpers `generateEmbedding()` and `generateEmbeddings()` continue returning only
192
- vectors.
223
+ See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.
224
+
225
+ ### `generateEmbedding(text, options?)`
226
+
227
+ Generates one embedding vector.
193
228
 
194
229
  ```ts
195
- const provider = createEmbeddingProvider({
230
+ const embedding = await generateEmbedding("deploy docs", {
196
231
  provider: "openai",
197
232
  apiKey: process.env.OPENAI_API_KEY,
198
233
  dimensions: 1536,
199
234
  });
200
-
201
- console.log(provider.metadata);
202
235
  ```
203
236
 
204
- Provider metadata includes:
237
+ ### `generateEmbeddings(texts, options?)`
205
238
 
206
- - `name`
207
- - `model`
208
- - `dimensions`
209
- - `batch.mode`
210
- - `batch.maxSize`, when the provider has a hard maximum
239
+ Generates an ordered batch of embeddings.
211
240
 
212
- Hosted provider clients are scoped to their options. The library does not reuse
213
- a Cloudflare, Mistral, Gemini, or OpenAI client created with different
214
- credentials or configuration.
241
+ - empty batches return `[]` without loading the local model or making a hosted call
242
+ - OpenAI batches above `2048` inputs are rejected before network work
243
+ - `openai-compatible` batches are chunked sequentially according to `batchSize`
215
244
 
216
- Gemini uses `gemini-embedding-2`. Its default is 3072 dimensions, and explicit
217
- Gemini dimensions must be an integer from 128 through 3072.
245
+ ### `createEmbeddingProvider(options?)`
218
246
 
219
- Local embeddings use `Xenova/all-MiniLM-L6-v2` through
220
- `@huggingface/transformers` and are fixed at the model's native 384 dimensions.
221
- Passing any other local dimension is rejected before the runtime is loaded.
247
+ Creates a provider client with immutable metadata and an `embed(texts, options?)` method.
222
248
 
223
- ### `getEmbeddingProviderMetadata(options?)`
249
+ ```ts
250
+ const provider = createEmbeddingProvider({
251
+ provider: "openai-compatible",
252
+ baseUrl: "https://tei.example.internal/v1",
253
+ model: "bge-large-en-v1.5",
254
+ dimensions: 1024,
255
+ batchSize: 32,
256
+ });
224
257
 
225
- Returns the same metadata exposed by `createEmbeddingProvider(options).metadata`
226
- without resolving hosted-provider credentials.
258
+ console.log(provider.metadata);
259
+ ```
227
260
 
228
- Provider batch metadata uses:
261
+ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers `generateEmbedding()` and `generateEmbeddings()` return only vectors.
229
262
 
230
- ```ts
231
- type EmbeddingBatchMode = "native" | "sequential";
263
+ Hosted provider clients are scoped to their current options. The library does not reuse a Cloudflare, Mistral, Gemini, or OpenAI client across different credential sets or configurations.
232
264
 
233
- interface EmbeddingBatchBehavior {
234
- mode: EmbeddingBatchMode;
235
- maxSize?: number;
236
- }
237
- ```
265
+ ### `getEmbeddingProviderMetadata(options?)`
238
266
 
239
- `"native"` means the upstream provider accepts the batch in one request.
240
- `"sequential"` means the library accepts an input batch but processes items one
241
- at a time. If `maxSize` is present, the library enforces it before provider or
242
- network work.
267
+ Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.
243
268
 
244
- Provider clients return:
269
+ Metadata shape:
245
270
 
246
271
  ```ts
247
- interface EmbeddingBatchResult {
248
- embeddings: number[][];
249
- provider:
272
+ interface EmbeddingProviderMetadata {
273
+ name:
250
274
  | "local"
251
275
  | "cloudflare"
252
276
  | "mistral"
@@ -255,69 +279,52 @@ interface EmbeddingBatchResult {
255
279
  | "openai-compatible";
256
280
  model: string;
257
281
  dimensions: number;
258
- intent: "document" | "query";
282
+ batch: {
283
+ mode: "native" | "sequential";
284
+ maxSize?: number;
285
+ };
259
286
  }
260
287
  ```
261
288
 
262
- ### `validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider)`
289
+ Batch interpretation:
263
290
 
264
- Validates provider results before they are written to the database. It checks
265
- cardinality, dimensions, finite numeric values, and indexed batch ordering.
291
+ - `"native"` means the provider accepts a batch request upstream
292
+ - `"sequential"` means the library accepts a batch but processes items one-by-one
293
+ - `batch.maxSize` is a hard client-side limit when present
266
294
 
267
- `EmbeddingOptions` supports:
295
+ `openai-compatible` metadata reports `batch.mode: "native"` because the remote endpoint is expected to accept batch inputs, even though the library may split large arrays into sequential outbound chunks at `batchSize`.
296
+
297
+ ### `EmbeddingBatchResult`
268
298
 
269
299
  ```ts
270
- interface EmbeddingOptions {
271
- provider?:
300
+ interface EmbeddingBatchResult {
301
+ embeddings: number[][];
302
+ provider:
272
303
  | "local"
273
304
  | "cloudflare"
274
305
  | "mistral"
275
306
  | "gemini"
276
307
  | "openai"
277
308
  | "openai-compatible";
278
- apiKey?: string;
279
- accountId?: string;
280
- apiToken?: string;
281
- baseUrl?: string;
282
- model?: string;
283
- batchSize?: number;
284
- dimensions?: number;
285
- maxLength?: number;
286
- intent?: "document" | "query";
287
- timeoutMs?: number;
288
- signal?: AbortSignal;
309
+ model: string;
310
+ dimensions: number;
311
+ intent: "document" | "query";
289
312
  }
290
313
  ```
291
314
 
292
- `apiKey` is used by Mistral, Gemini, and OpenAI. It falls back to
293
- `MISTRAL_API_KEY`, `GEMINI_API_KEY`, or `OPENAI_API_KEY` for those providers.
294
- Cloudflare uses `accountId` and `apiToken`, which fall back to
295
- `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN`.
315
+ ### `validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider)`
296
316
 
297
- For `provider: "openai-compatible"`, callers must pass `baseUrl`, `model`, and
298
- `dimensions`. `apiKey` is optional and is used only when explicitly provided;
299
- the provider never reads `OPENAI_API_KEY`. `batchSize` controls the maximum
300
- items per outbound request and defaults to `32`. The endpoint is treated as
301
- trusted server-side configuration and should not be derived from untrusted
302
- request input.
317
+ Validates provider responses before they reach the database:
303
318
 
304
- ### `padEmbedding(embedding, targetDimensions)`
319
+ - result count must match the requested input count
320
+ - vectors must match the effective dimensions
321
+ - values must be finite numbers
322
+ - indexed provider responses are reordered and checked for contiguous indices
305
323
 
306
- Pads or truncates an embedding array to the requested length.
324
+ ### `padEmbedding(embedding, targetDimensions)`
307
325
 
308
- This helper remains exported for callers that used it directly. The local
309
- provider does not use it; local vectors are validated at native 384 dimensions.
326
+ Pads or truncates a vector to the target width. This is exported for compatibility and migration workflows, but the current local provider uses its native `384` dimensions rather than padding by default.
310
327
 
311
328
  ### `prepareTextForEmbedding(fields)`
312
329
 
313
- Combines title, description, tags, and content into the text sent to the
314
- embedding model.
315
-
316
- ```ts
317
- const text = prepareTextForEmbedding({
318
- title: "My Article",
319
- description: "How semantic search works",
320
- tags: ["search", "turso"],
321
- content: "# Content",
322
- });
323
- ```
330
+ Builds the text that is embedded from title, description, content, and tags.
package/docs/INDEXING.md CHANGED
@@ -2,8 +2,7 @@
2
2
 
3
3
  ## Content Shape
4
4
 
5
- `indexContent()` walks a directory tree, reads Markdown files, parses
6
- frontmatter with `gray-matter`, and stores:
5
+ `indexContent()` walks a directory tree, reads Markdown files, parses frontmatter with `gray-matter`, and stores:
7
6
 
8
7
  - `slug`
9
8
  - `title`
@@ -22,41 +21,32 @@ The slug is derived from the file path relative to `contentPath`.
22
21
  await indexContent({
23
22
  client,
24
23
  contentPath: "./content",
25
- tableName: "articles",
24
+ tableName: "articles_local_384",
26
25
  embeddingOptions: {
27
26
  provider: "local",
28
27
  },
29
28
  });
30
29
  ```
31
30
 
32
- That keeps the implementation simple, but it also means a failed rebuild can
33
- leave the index partially repopulated.
31
+ That keeps the implementation simple, but it also means:
34
32
 
35
- Changing an embedding provider or dimension count requires a full re-embed.
36
- For existing local indexes created with the older padded-local behavior, create
37
- or recreate a 384-dimensional table before rebuilding. Those older local
38
- 768-dimensional tables stored 384 model values followed by zero padding;
39
- `indexContent()` clears rows but does not change the table's `F32_BLOB` width.
33
+ - failed rebuilds can leave the table partially repopulated
34
+ - provider or dimension changes should use a parallel table migration
35
+ - `createTable()` does not resize an existing vector column
40
36
 
41
- For Gemini specifically, indexes created with the retired `text-embedding-004`
42
- model must be rebuilt for `gemini-embedding-2` even when staying at 768
43
- dimensions, because the model and query/document formatting both changed. If
44
- you adopt Gemini's 3072-dimensional default, recreate the vector table or build
45
- into a separate table first; clearing rows with `indexContent()` does not change
46
- the table's `F32_BLOB` width.
37
+ If provider, dimensions, model, endpoint, or embedding-space assumptions change, fully reindex into a new table. See the canonical [Migration and reindexing guide](./MIGRATIONS.md).
47
38
 
48
39
  ## Quality Guidelines
49
40
 
50
41
  - include descriptive frontmatter titles
51
42
  - add meaningful `tags` when they help retrieval
52
- - use the same embedding provider and dimensions at index and query time
43
+ - use the same provider and dimensions at index and query time
53
44
  - keep `maxLength` intentional if your content is very large
54
45
  - start with a smaller search `limit` and tune from real query behavior
55
46
 
56
47
  ## Build Integration
57
48
 
58
- Many projects wire indexing into a dedicated script and call it before their
59
- site build:
49
+ Many projects wire indexing into a dedicated script and call it before their site build:
60
50
 
61
51
  ```json
62
52
  {
@@ -69,14 +59,11 @@ site build:
69
59
 
70
60
  ## Table Names
71
61
 
72
- `tableName` must be an ASCII SQLite identifier matching
73
- `[A-Za-z_][A-Za-z0-9_]*`. Valid names are quoted internally for table and index
74
- SQL, so reserved words such as `"select"` work safely. Invalid names fail before
75
- database calls or embedding generation.
62
+ `tableName` must be an ASCII SQLite identifier matching `[A-Za-z_][A-Za-z0-9_]*`. Valid names are quoted internally for table and index SQL, so reserved words such as `"select"` work safely. Invalid names fail before database calls or embedding generation.
76
63
 
77
64
  ## Runtime Notes
78
65
 
79
66
  - local embeddings may download and cache a model on the first run
80
67
  - Node users need `@libsql/client` installed alongside the package
81
- - the repository validates both the npm package build and `deno check`, but the
82
- indexing flow itself still depends on filesystem access
68
+ - hosted providers send indexed or queried text to external services
69
+ - the repository validates package build and `deno check`, but indexing still depends on filesystem access