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/README.md +28 -48
- package/dist/index.cjs +184 -31
- package/dist/index.d.ts +56 -8
- package/dist/index.esm.js +184 -32
- package/docs/API.md +182 -112
- package/docs/INDEXING.md +88 -26
- package/docs/INTEGRATIONS.md +220 -40
- package/docs/MIGRATIONS.md +176 -0
- package/docs/PROVIDERS.md +86 -245
- package/docs/README.md +7 -9
- package/docs/RELEASING.md +11 -3
- package/docs/TESTING.md +138 -0
- package/package.json +1 -1
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
|
|
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
|
-
`
|
|
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
|
-
-
|
|
107
|
-
-
|
|
108
|
-
|
|
109
|
-
-
|
|
110
|
-
|
|
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
|
|
237
|
+
All retrieval helpers validate `tableName` before executing SQL.
|
|
175
238
|
|
|
176
239
|
## Embedding Helpers
|
|
177
240
|
|
|
178
|
-
### `
|
|
241
|
+
### `EmbeddingOptions`
|
|
179
242
|
|
|
180
|
-
|
|
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
|
-
|
|
266
|
+
Important option rules:
|
|
183
267
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
277
|
+
Dimension rules:
|
|
188
278
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
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
|
-
|
|
300
|
+
### `generateEmbeddings(texts, options?)`
|
|
205
301
|
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
Gemini dimensions must be an integer from 128 through 3072.
|
|
308
|
+
### `createEmbeddingProvider(options?)`
|
|
218
309
|
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
321
|
+
console.log(provider.metadata);
|
|
322
|
+
```
|
|
227
323
|
|
|
228
|
-
Provider
|
|
324
|
+
Provider clients return a rich `EmbeddingBatchResult`; the compatibility helpers `generateEmbedding()` and `generateEmbeddings()` return only vectors.
|
|
229
325
|
|
|
230
|
-
|
|
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
|
-
|
|
234
|
-
mode: EmbeddingBatchMode;
|
|
235
|
-
maxSize?: number;
|
|
236
|
-
}
|
|
237
|
-
```
|
|
328
|
+
### `getEmbeddingProviderMetadata(options?)`
|
|
238
329
|
|
|
239
|
-
|
|
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
|
-
|
|
332
|
+
Metadata shape:
|
|
245
333
|
|
|
246
334
|
```ts
|
|
247
|
-
interface
|
|
248
|
-
|
|
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
|
-
|
|
345
|
+
batch: {
|
|
346
|
+
mode: "native" | "sequential";
|
|
347
|
+
maxSize?: number;
|
|
348
|
+
};
|
|
259
349
|
}
|
|
260
350
|
```
|
|
261
351
|
|
|
262
|
-
|
|
352
|
+
Batch interpretation:
|
|
263
353
|
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
`
|
|
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
|
|
271
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
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
|
-
|
|
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()`
|
|
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: "
|
|
24
|
+
tableName: "articles_local_384",
|
|
26
25
|
embeddingOptions: {
|
|
27
26
|
provider: "local",
|
|
28
27
|
},
|
|
29
28
|
});
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
leave the index partially repopulated.
|
|
31
|
+
The rebuild runs in two phases:
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
-
-
|
|
82
|
-
|
|
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
|