libsql-search 0.7.0 → 0.8.0

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/docs/API.md CHANGED
@@ -18,6 +18,7 @@
18
18
  - `validateEmbeddingBatch`
19
19
  - `padEmbedding`
20
20
  - `prepareTextForEmbedding`
21
+ - `IndexingError`
21
22
 
22
23
  It also exports these types:
23
24
 
@@ -34,12 +35,24 @@ It also exports these types:
34
35
  - `EmbeddingOptions`
35
36
  - `IndexerOptions`
36
37
  - `IndexedDocument`
38
+ - `IndexResult`
39
+ - `IndexFailure`
40
+ - `IndexFailurePolicy`
41
+ - `IndexFailureStage`
42
+ - `IndexingErrorPhase`
37
43
  - `SearchOptions`
38
44
  - `SearchResult`
39
45
 
46
+ ## Navigation
47
+
48
+ - [Provider matrix and credential rules](./PROVIDERS.md)
49
+ - [Migration and reindexing guide](./MIGRATIONS.md)
50
+ - [Indexing and operational behavior](./INDEXING.md)
51
+ - [Testing guidance](./TESTING.md)
52
+
40
53
  ## `createTable(client, tableName?, dimensions?)`
41
54
 
42
- Creates the table and supporting indexes used by search.
55
+ Creates the search table and supporting indexes.
43
56
 
44
57
  ```ts
45
58
  await createTable(client);
@@ -50,10 +63,7 @@ Defaults:
50
63
  - `tableName`: `"articles"`
51
64
  - `dimensions`: `384`
52
65
 
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.
66
+ `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
67
 
58
68
  The created schema includes:
59
69
 
@@ -63,10 +73,12 @@ The created schema includes:
63
73
  - `content`
64
74
  - `folder`
65
75
  - `tags`
66
- - `embedding`
76
+ - `embedding F32_BLOB(dimensions)`
67
77
  - `created_at`
68
78
  - `updated_at`
69
79
 
80
+ `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.
81
+
70
82
  ## `indexContent(options)`
71
83
 
72
84
  Indexes Markdown files from a directory on disk.
@@ -80,6 +92,8 @@ interface IndexerOptions {
80
92
  exclude?: string[];
81
93
  tableName?: string;
82
94
  onProgress?: (current: number, total: number, file: string) => void;
95
+ failurePolicy?: "abort" | "skip";
96
+ allowEmptyIndex?: boolean;
83
97
  }
84
98
  ```
85
99
 
@@ -88,28 +102,80 @@ Defaults:
88
102
  - `fileExtensions`: [".md", ".markdown"]
89
103
  - `exclude`: ["node_modules", ".git", "dist", "build"]
90
104
  - `tableName`: `"articles"`
91
-
92
- `tableName` follows the same identifier policy as `createTable()`.
105
+ - `failurePolicy`: `"abort"`
106
+ - `allowEmptyIndex`: `false`
93
107
 
94
108
  Return shape:
95
109
 
96
110
  ```ts
97
- {
98
- success: number;
99
- failed: number;
100
- total: number;
111
+ interface IndexResult {
112
+ success: number; // documents written
113
+ failed: number; // files that could not be indexed
114
+ total: number; // files discovered on disk
115
+ replaced: boolean; // whether table contents were replaced by this call
116
+ partial: boolean; // replaced, but some files were skipped
117
+ failures: IndexFailure[];
118
+ }
119
+
120
+ interface IndexFailure {
121
+ file: string; // path relative to contentPath
122
+ stage: "read" | "parse" | "embed";
123
+ error: Error;
101
124
  }
102
125
  ```
103
126
 
104
127
  Behavior notes:
105
128
 
106
- - `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
129
+ - every file is read, parsed, and embedded in memory before any database state changes
130
+ - the target table is then replaced in a single write transaction, so a failed rebuild leaves the previous index exactly as it was
131
+ - that costs peak memory proportional to the whole corpus, and against remote clients the replacement travels as a single un-chunked batch request; see [Costs of the two-phase rebuild](./INDEXING.md#costs-of-the-two-phase-rebuild) before rebuilding a very large corpus in place
132
+ - files are discovered and indexed in sorted path order
133
+ - frontmatter `title` must be a scalar; a structured title such as a YAML list fails the file at the `parse` stage
134
+ - two files that reduce to the same slug (`foo.md` and `foo.markdown`) collide: the first in sorted path order keeps the slug and the later file is reported as a `parse` failure
135
+ - `failurePolicy: "abort"` throws `IndexingError` on the first file that fails
136
+ - `failurePolicy: "skip"` drops the failing file, records it in `failures`, and rebuilds from the survivors, returning `partial: true`
137
+ - under `"skip"`, if every discovered file fails, the rebuild throws instead of replacing a valid index with an empty one
138
+ - an empty source directory throws unless `allowEmptyIndex: true`, which intentionally empties the index
139
+ - `onProgress` is called once per file during the build phase
140
+ - frontmatter `title`, `description`, and `tags` are folded into the embedding text
141
+ - embeddings default to `intent: "document"` unless `embeddingOptions.intent` is set explicitly
111
142
  - if a file has no frontmatter title, the filename becomes the title
112
143
 
144
+ ### `IndexingError`
145
+
146
+ Thrown when a rebuild cannot complete. The previously indexed rows are always left unchanged.
147
+
148
+ ```ts
149
+ class IndexingError extends Error {
150
+ readonly phase: "build" | "replace";
151
+ readonly failures: IndexFailure[];
152
+ }
153
+ ```
154
+
155
+ - `phase: "build"` means the failure happened before any database work: a file failed, every file failed, the source directory was empty, or it could not be scanned
156
+ - `phase: "replace"` means the replacement transaction failed and was rolled back
157
+ - `cause` carries the underlying error
158
+ - on a `phase: "replace"` error, `failures` lists files skipped during the build phase. They are not the cause of the rollback, which is carried by `cause`
159
+
160
+ ```ts
161
+ import { indexContent, IndexingError } from "libsql-search";
162
+
163
+ try {
164
+ await indexContent({ client, contentPath: "./content" });
165
+ } catch (error) {
166
+ if (error instanceof IndexingError) {
167
+ console.error(error.phase, error.failures);
168
+ }
169
+
170
+ throw error;
171
+ }
172
+ ```
173
+
174
+ Breaking changes in this behavior:
175
+
176
+ - partial failures previously counted into `failed` and still replaced the table; they now throw. Pass `failurePolicy: "skip"` for the previous lenient behavior.
177
+ - an empty source directory previously returned zeros and left stale rows in place; it now throws. Pass `allowEmptyIndex: true` to intentionally empty the index.
178
+
113
179
  ## `search(options)`
114
180
 
115
181
  Generates a query embedding and performs vector similarity search.
@@ -129,9 +195,7 @@ Defaults:
129
195
  - `limit`: `10`
130
196
  - `tableName`: `"articles"`
131
197
 
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()`.
198
+ `limit` must be an integer from `1` through `100`; invalid values are rejected before query embedding generation.
135
199
 
136
200
  Result shape:
137
201
 
@@ -150,8 +214,7 @@ interface SearchResult {
150
214
 
151
215
  Lower `distance` values are better matches.
152
216
 
153
- Search embeddings default to `intent: "query"` unless
154
- `embeddingOptions.intent` is set explicitly.
217
+ Search embeddings default to `intent: "query"` unless `embeddingOptions.intent` is set explicitly.
155
218
 
156
219
  ## Article Retrieval Helpers
157
220
 
@@ -171,82 +234,106 @@ Returns articles in a specific folder.
171
234
 
172
235
  Returns distinct folder names from the index.
173
236
 
174
- All article retrieval helpers validate `tableName` before executing SQL.
237
+ All retrieval helpers validate `tableName` before executing SQL.
175
238
 
176
239
  ## Embedding Helpers
177
240
 
178
- ### `generateEmbedding(text, options?)`
241
+ ### `EmbeddingOptions`
179
242
 
180
- Generates an embedding for arbitrary text using the selected provider.
243
+ ```ts
244
+ interface EmbeddingOptions {
245
+ provider?:
246
+ | "local"
247
+ | "cloudflare"
248
+ | "mistral"
249
+ | "gemini"
250
+ | "openai"
251
+ | "openai-compatible";
252
+ apiKey?: string;
253
+ accountId?: string;
254
+ apiToken?: string;
255
+ baseUrl?: string;
256
+ model?: string;
257
+ batchSize?: number;
258
+ dimensions?: number;
259
+ maxLength?: number;
260
+ intent?: "document" | "query";
261
+ timeoutMs?: number;
262
+ signal?: AbortSignal;
263
+ }
264
+ ```
181
265
 
182
- ### `generateEmbeddings(texts, options?)`
266
+ Important option rules:
183
267
 
184
- Generates an ordered batch of embeddings. Empty batches return `[]` without
185
- creating a hosted provider client or making a network request.
268
+ - `provider` defaults to `local`
269
+ - `maxLength` defaults to `8000`
270
+ - `timeoutMs` defaults to `30000`
271
+ - `model` is only used by `openai-compatible`
272
+ - `baseUrl`, `model`, and `dimensions` are required for `openai-compatible`
273
+ - `batchSize` only applies to `openai-compatible` and defaults to `32`
274
+ - `openai-compatible` never reads `OPENAI_API_KEY`
275
+ - only the Gemini adapter currently changes payload formatting by `intent`
186
276
 
187
- ### `createEmbeddingProvider(options?)`
277
+ Dimension rules:
188
278
 
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.
279
+ - local: fixed `384`
280
+ - Cloudflare: fixed `1024`
281
+ - Mistral: fixed `1024`
282
+ - Gemini: default `3072`, allowed integer range `128-3072`
283
+ - OpenAI: default `768`; `text-embedding-3-small` through `1536`, `text-embedding-3-large` above `1536`
284
+ - OpenAI-compatible: required positive integer, no default
285
+
286
+ See [Provider matrix and credential rules](./PROVIDERS.md) for the canonical provider table.
287
+
288
+ ### `generateEmbedding(text, options?)`
289
+
290
+ Generates one embedding vector.
193
291
 
194
292
  ```ts
195
- const provider = createEmbeddingProvider({
293
+ const embedding = await generateEmbedding("deploy docs", {
196
294
  provider: "openai",
197
295
  apiKey: process.env.OPENAI_API_KEY,
198
296
  dimensions: 1536,
199
297
  });
200
-
201
- console.log(provider.metadata);
202
298
  ```
203
299
 
204
- Provider metadata includes:
300
+ ### `generateEmbeddings(texts, options?)`
205
301
 
206
- - `name`
207
- - `model`
208
- - `dimensions`
209
- - `batch.mode`
210
- - `batch.maxSize`, when the provider has a hard maximum
302
+ Generates an ordered batch of embeddings.
211
303
 
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.
304
+ - empty batches return `[]` without loading the local model or making a hosted call
305
+ - OpenAI batches above `2048` inputs are rejected before network work
306
+ - `openai-compatible` batches are chunked sequentially according to `batchSize`
215
307
 
216
- Gemini uses `gemini-embedding-2`. Its default is 3072 dimensions, and explicit
217
- Gemini dimensions must be an integer from 128 through 3072.
308
+ ### `createEmbeddingProvider(options?)`
218
309
 
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.
310
+ Creates a provider client with immutable metadata and an `embed(texts, options?)` method.
222
311
 
223
- ### `getEmbeddingProviderMetadata(options?)`
312
+ ```ts
313
+ const provider = createEmbeddingProvider({
314
+ provider: "openai-compatible",
315
+ baseUrl: "https://tei.example.internal/v1",
316
+ model: "bge-large-en-v1.5",
317
+ dimensions: 1024,
318
+ batchSize: 32,
319
+ });
224
320
 
225
- Returns the same metadata exposed by `createEmbeddingProvider(options).metadata`
226
- without resolving hosted-provider credentials.
321
+ console.log(provider.metadata);
322
+ ```
227
323
 
228
- Provider batch metadata uses:
324
+ Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers `generateEmbedding()` and `generateEmbeddings()` return only vectors.
229
325
 
230
- ```ts
231
- type EmbeddingBatchMode = "native" | "sequential";
326
+ 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
327
 
233
- interface EmbeddingBatchBehavior {
234
- mode: EmbeddingBatchMode;
235
- maxSize?: number;
236
- }
237
- ```
328
+ ### `getEmbeddingProviderMetadata(options?)`
238
329
 
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.
330
+ Returns the same metadata exposed by `createEmbeddingProvider(options).metadata` without resolving hosted-provider credentials.
243
331
 
244
- Provider clients return:
332
+ Metadata shape:
245
333
 
246
334
  ```ts
247
- interface EmbeddingBatchResult {
248
- embeddings: number[][];
249
- provider:
335
+ interface EmbeddingProviderMetadata {
336
+ name:
250
337
  | "local"
251
338
  | "cloudflare"
252
339
  | "mistral"
@@ -255,69 +342,52 @@ interface EmbeddingBatchResult {
255
342
  | "openai-compatible";
256
343
  model: string;
257
344
  dimensions: number;
258
- intent: "document" | "query";
345
+ batch: {
346
+ mode: "native" | "sequential";
347
+ maxSize?: number;
348
+ };
259
349
  }
260
350
  ```
261
351
 
262
- ### `validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider)`
352
+ Batch interpretation:
263
353
 
264
- Validates provider results before they are written to the database. It checks
265
- cardinality, dimensions, finite numeric values, and indexed batch ordering.
354
+ - `"native"` means the provider accepts a batch request upstream
355
+ - `"sequential"` means the library accepts a batch but processes items one-by-one
356
+ - `batch.maxSize` is a hard client-side limit when present
266
357
 
267
- `EmbeddingOptions` supports:
358
+ `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`.
359
+
360
+ ### `EmbeddingBatchResult`
268
361
 
269
362
  ```ts
270
- interface EmbeddingOptions {
271
- provider?:
363
+ interface EmbeddingBatchResult {
364
+ embeddings: number[][];
365
+ provider:
272
366
  | "local"
273
367
  | "cloudflare"
274
368
  | "mistral"
275
369
  | "gemini"
276
370
  | "openai"
277
371
  | "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;
372
+ model: string;
373
+ dimensions: number;
374
+ intent: "document" | "query";
289
375
  }
290
376
  ```
291
377
 
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`.
378
+ ### `validateEmbeddingBatch(items, expectedCount, expectedDimensions, provider)`
379
+
380
+ Validates provider responses before they reach the database:
296
381
 
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.
382
+ - result count must match the requested input count
383
+ - vectors must match the effective dimensions
384
+ - values must be finite numbers
385
+ - indexed provider responses are reordered and checked for contiguous indices
303
386
 
304
387
  ### `padEmbedding(embedding, targetDimensions)`
305
388
 
306
- Pads or truncates an embedding array to the requested length.
307
-
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.
389
+ 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
390
 
311
391
  ### `prepareTextForEmbedding(fields)`
312
392
 
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
- ```
393
+ 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`
@@ -16,47 +15,113 @@ The slug is derived from the file path relative to `contentPath`.
16
15
 
17
16
  ## Rebuild Behavior
18
17
 
19
- `indexContent()` clears the target table before rebuilding:
18
+ `indexContent()` replaces the whole target table:
20
19
 
21
20
  ```ts
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
+ The rebuild runs in two phases:
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
+ 1. build: every file is read, parsed, and embedded in memory, touching no database state
34
+ 2. replace: the delete and all inserts run in a single write transaction
40
35
 
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.
36
+ That means:
37
+
38
+ - a failed rebuild leaves the previously indexed rows exactly as they were
39
+ - provider or dimension changes should still use a parallel table migration
40
+ - `createTable()` does not resize an existing vector column
41
+
42
+ Files are discovered and indexed in sorted path order, so a rebuild is deterministic.
43
+
44
+ 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).
45
+
46
+ ### Costs Of The Two-Phase Rebuild
47
+
48
+ Atomicity is not free, and both costs scale with corpus size:
49
+
50
+ - **Peak memory holds the whole corpus.** The build phase keeps every document in memory: content, frontmatter, and one embedding array per document. The replace phase then builds insert statements including a JSON copy of each embedding, roughly 5-8 KB per document at 384 dimensions and considerably more at 3072. Documents are released as their statements are built, but peak usage is still proportional to the entire corpus rather than to one file.
51
+ - **Remote clients send one request.** Against Turso or any remote client, the delete and every insert travel as a single batch. There is no chunking fallback, because splitting the batch would give up the atomicity this design exists to provide. A corpus large enough to exceed a remote request-size limit fails as an opaque `phase: "replace"` error.
52
+
53
+ For very large corpora, index into a parallel table and switch reads over once it validates, rather than rebuilding a live table in place. See the [Migration and reindexing guide](./MIGRATIONS.md).
54
+
55
+ ## Content Requirements
56
+
57
+ Two authoring mistakes fail a file at the `parse` stage rather than corrupting the rebuild:
58
+
59
+ - **Frontmatter `title` must be a scalar.** Strings, numbers, booleans, and dates are accepted; dates are stored as ISO strings. A structured title such as a YAML list fails the file. A missing or empty title still falls back to the filename.
60
+ - **Slugs must be unique.** The slug comes from the path with the extension removed, so `foo.md` and `foo.markdown` collide. Files are processed in sorted path order and the first file to claim a slug keeps it, so `foo.markdown` wins and `foo.md` is reported as the failure.
61
+
62
+ Both are governed by `failurePolicy` like any other build failure, so they abort by default and are skippable.
63
+
64
+ ## Failure Handling
65
+
66
+ `indexContent()` throws `IndexingError` instead of reporting a partially applied rebuild. The error carries `phase` (`"build"` or `"replace"`), a `failures` array, and the underlying error as `cause`.
67
+
68
+ ```ts
69
+ import { indexContent, IndexingError } from "libsql-search";
70
+
71
+ try {
72
+ await indexContent({ client, contentPath: "./content" });
73
+ } catch (error) {
74
+ if (error instanceof IndexingError) {
75
+ for (const failure of error.failures) {
76
+ console.error(`${failure.file} failed during ${failure.stage}`);
77
+ }
78
+ }
79
+
80
+ throw error;
81
+ }
82
+ ```
83
+
84
+ By default one bad file aborts the whole rebuild. To index everything that can be indexed, opt into `failurePolicy: "skip"`:
85
+
86
+ ```ts
87
+ const result = await indexContent({
88
+ client,
89
+ contentPath: "./content",
90
+ failurePolicy: "skip",
91
+ });
92
+
93
+ if (result.partial) {
94
+ console.warn(`Indexed ${result.success} of ${result.total} files`);
95
+ }
96
+ ```
97
+
98
+ Skipped rebuilds still replace the table, so treat `partial: true` as a build warning rather than a clean rebuild. If every discovered file fails, the rebuild throws rather than trading a valid index for an empty one.
99
+
100
+ ## Empty Source Directories
101
+
102
+ An empty source directory throws by default, because silently leaving stale rows in place serves search traffic from content that no longer exists. Emptying an index has to be intentional:
103
+
104
+ ```ts
105
+ await indexContent({
106
+ client,
107
+ contentPath: "./content",
108
+ allowEmptyIndex: true,
109
+ });
110
+ ```
111
+
112
+ Both behaviors changed in a breaking way: partial failures used to be counted and reported, and an empty directory used to return zeros without clearing the table.
47
113
 
48
114
  ## Quality Guidelines
49
115
 
50
116
  - include descriptive frontmatter titles
51
117
  - add meaningful `tags` when they help retrieval
52
- - use the same embedding provider and dimensions at index and query time
118
+ - use the same provider and dimensions at index and query time
53
119
  - keep `maxLength` intentional if your content is very large
54
120
  - start with a smaller search `limit` and tune from real query behavior
55
121
 
56
122
  ## Build Integration
57
123
 
58
- Many projects wire indexing into a dedicated script and call it before their
59
- site build:
124
+ Many projects wire indexing into a dedicated script and call it before their site build:
60
125
 
61
126
  ```json
62
127
  {
@@ -69,14 +134,11 @@ site build:
69
134
 
70
135
  ## Table Names
71
136
 
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.
137
+ `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
138
 
77
139
  ## Runtime Notes
78
140
 
79
141
  - local embeddings may download and cache a model on the first run
80
142
  - 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
143
+ - hosted providers send indexed or queried text to external services
144
+ - the repository validates package build and `deno check`, but indexing still depends on filesystem access