@kedem/okdb 1.5.3 → 1.6.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.
@@ -8,7 +8,7 @@ A **type** is a named collection of records — similar to a table in SQL or a c
8
8
  await okdb.registerType('articles');
9
9
  ```
10
10
 
11
- Each type gets its own set of LMDB sub-databases: one for data, one for each secondary index, and one for FTS index metadata. All sub-databases live inside the same LMDB environment (directory) and participate in the same transactions.
11
+ Each type gets its own set of LMDB sub-databases: one for data and one for each secondary index. All sub-databases live inside the same LMDB environment (directory) and participate in the same transactions. FTS data is stored in a separate shared LMDB environment pair (see [Storage layout](#storage-layout)).
12
12
 
13
13
  ### Type operations
14
14
 
@@ -118,10 +118,14 @@ mydb/
118
118
  ~system/ ← node identity + env registry
119
119
  default/ ← your types and records
120
120
  ~<env>:emb:<type>/← per-type vector store
121
- ~fts/ ← FTS indexes (one sub-dir per index)
121
+ ~fts/
122
+ <envName>/ ← shared FTS inverted index (all types + indexes in this env)
123
+ <envName>_docs/ ← shared FTS forward index (docKey → tokens, compressed)
122
124
  blobs/ ← file attachment blobs (SHA-256 content-addressable)
123
125
  ```
124
126
 
127
+ All FTS indexes across all types within the same OKDB env share a single pair of LMDB environments. Each index is differentiated by an auto-increment `ftsId` stored as a compound key prefix inside those shared environments.
128
+
125
129
  Inside each LMDB environment, sub-databases are named by convention:
126
130
 
127
131
  ```
@@ -141,6 +145,49 @@ per type T:
141
145
 
142
146
  ---
143
147
 
148
+ ## Full-text search
149
+
150
+ FTS indexes are **async by default**. When a document is written, the change is recorded in the change log. A background processor picks up changes and updates the FTS index. This means:
151
+
152
+ - Writes return immediately — no FTS I/O on the write path.
153
+ - There is a short lag between a write and when the document becomes searchable.
154
+ - Use `fts.flush(type)` to wait for the processor to catch up before querying. This is useful in tests and after bulk imports.
155
+ - Use `fts.ready(type)` to wait for the initial index build to complete after registering a new index.
156
+
157
+ ```javascript
158
+ // Register an FTS index
159
+ await okdb.fts.register('articles', 'main', { fields: ['title', 'body'] });
160
+
161
+ // Wait for the initial build
162
+ await okdb.fts.ready('articles');
163
+
164
+ // Write documents
165
+ await okdb.put('articles', 'a1', { title: 'Hello world', body: 'Some content' });
166
+
167
+ // Flush processor before querying (ensures doc is indexed)
168
+ await okdb.fts.flush('articles');
169
+
170
+ // Search
171
+ const keys = okdb.fts.search('articles', 'main', 'hello');
172
+ ```
173
+
174
+ ### `ready(type)`
175
+
176
+ `fts.ready(type)` resolves when all FTS indexes registered on `type` have completed their initial build. The `name` parameter is accepted for backward compatibility but is ignored — readiness is per-type, not per-index.
177
+
178
+ ### `flush(type)`
179
+
180
+ `fts.flush(type)` awaits the background processor catching up to the current write position. Returns immediately if no processor is running or if already caught up. Use this instead of `setTimeout` workarounds in tests or after bulk imports.
181
+
182
+ ### list() response
183
+
184
+ `fts.list(type)` returns an entry per registered index. Each entry includes `processorState` and `lag` — per-type fields reflecting the single processor that drives all indexes on that type:
185
+
186
+ - `processorState`: `'building' | 'online' | 'error' | 'waiting' | null` — `null` if no processor is running (no writes have occurred yet).
187
+ - `lag`: number of write operations the processor has not yet indexed. `0` means fully caught up. `null` if no processor is running.
188
+
189
+ ---
190
+
144
191
  ## Field schema
145
192
 
146
193
  OKDB observes field types as you write data and maintains a coarse schema (`string | number | boolean | null | array | object`) per field. You can inspect it:
@@ -163,12 +210,31 @@ Schema is **observed-only** — no validation on write. If you need strict valid
163
210
 
164
211
  Set at construction time via the `durability` option:
165
212
 
166
- | Mode | Behaviour | Use when |
167
- | ---------- | -------------------------------------------------------- | ------------------------------ |
168
- | `strict` | Full fsync on every write | Mission-critical, money |
169
- | `balanced` | `overlappingSync` background sync, still safe on crash | Default; most use cases |
170
- | `fast` | `noSync` OS controls flush timing | Dev, caches, re-generable data |
213
+ | Mode | Underlying LMDB flag | Behaviour | Use when |
214
+ | ---------- | -------------------- | ------------------------------------ | --------------------------------- |
215
+ | `strict` | _(none)_ | Full fsync on every write | Mission-critical, money |
216
+ | `balanced` | `overlappingSync` | Background sync, still safe on crash | Default; most use cases |
217
+ | `fast` | `noSync` | OS controls flush timing | Dev, caches, re-generable data |
218
+ | `custom` | _(see lmdb below)_ | No preset — raw `lmdb` options apply | Append-only, replicated followers |
171
219
 
172
220
  ```javascript
173
221
  const okdb = new OKDB('./db', { durability: 'fast' });
174
222
  ```
223
+
224
+ ### Raw LMDB passthrough
225
+
226
+ For workloads like replication followers or append-only archives where you need precise control, pass raw LMDB flags via the `lmdb` option. Use `durability: 'custom'` to disable preset interference:
227
+
228
+ ```javascript
229
+ const okdb = new OKDB('./db', {
230
+ durability: 'custom',
231
+ lmdb: {
232
+ noSync: true, // skip fsync — fastest, survives only clean shutdown
233
+ noMetaSync: true, // skip metadata page fsync — partial durability improvement
234
+ },
235
+ });
236
+ ```
237
+
238
+ `noSync: true` is equivalent to `durability: 'fast'` but lets you combine it with other raw flags. `noMetaSync: true` without `noSync` is the middle ground: data pages are synced, metadata pages are not — faster than `strict`, safer than `noSync`.
239
+
240
+ OKDB enforces guardrails: specifying `noSync` or `noMetaSync` together with `durability: 'strict'` is a hard error. Use `durability: 'custom'` when passing these flags explicitly.
package/docs/http-api.md CHANGED
@@ -349,6 +349,8 @@ POST /api/:env/type/:type/query → query records
349
349
  POST /api/:env/transaction → atomic batch writes
350
350
  ```
351
351
 
352
+ **Atomic bulk writes:** `POST /api/:env/transaction` accepts an array of `put`, `update`, `patch`, and `remove` operations and commits them all in one LMDB transaction — the right primitive for bulk ingest. See [Transactions](./transactions.md#http-api--bulk-writes) for the full body format and examples.
353
+
352
354
  **Hybrid search:** The `options` body field now accepts `fts` and `vector` sub-objects alongside `index`. See the querying guide for full documentation. When `fts` or `vector` is provided, the route automatically awaits the async result.
353
355
 
354
356
  ```bash
@@ -456,6 +458,61 @@ GET /api/:env/type/:type/index/:idx/count?start=<json>&end=<json> → count in
456
458
 
457
459
  ---
458
460
 
461
+ ## Full-text search endpoints
462
+
463
+ ```http
464
+ GET /api/:env/type/:type/fts → list FTS indexes
465
+ POST /api/:env/type/:type/fts/:name → register a new FTS index
466
+ DELETE /api/:env/type/:type/fts/:name → drop an FTS index
467
+ POST /api/:env/type/:type/fts/:name/reset → rebuild an FTS index from scratch
468
+ POST /api/:env/type/:type/fts/:name/search → search an FTS index
469
+ POST /api/:env/type/:type/fts/flush → flush FTS processor (await catch-up)
470
+ ```
471
+
472
+ ### GET /api/:env/type/:type/fts — list response
473
+
474
+ Each entry in the returned array includes:
475
+
476
+ | Field | Type | Description |
477
+ | ---------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
478
+ | `name` | `string` | Index name |
479
+ | `status` | `'creating' \| 'resetting' \| 'ready' \| 'waiting' \| null` | Per-index lifecycle status |
480
+ | `config` | `object \| null` | Index configuration as registered |
481
+ | `created` | `number \| null` | HLC clock at registration time |
482
+ | `updated` | `number \| null` | HLC clock at last config update |
483
+ | `error` | `object \| null` | Last per-index error, if any |
484
+ | `processorState` | `'building' \| 'online' \| 'error' \| 'waiting' \| null` | State of the background FTS processor for this type. `null` if no processor is running. |
485
+ | `lag` | `number \| null` | Number of writes the processor has not yet indexed. `0` = fully caught up. `null` if no processor. |
486
+
487
+ `processorState` and `lag` are per-type fields — they reflect the single background processor that drives all FTS indexes on a type and are identical on every entry of the same type.
488
+
489
+ ### POST /api/:env/type/:type/fts/flush — flush processor
490
+
491
+ Waits for the background FTS processor to catch up to the current write position for this type. Responds `204 No Content` when fully caught up. This is a no-op if no processor is running or if the processor is already caught up.
492
+
493
+ - **Permission:** `schema:read`
494
+ - **Idempotent:** yes
495
+ - **Long-running:** yes (blocks until processor catches up)
496
+
497
+ Use this after bulk writes when immediate search consistency is required:
498
+
499
+ ```bash
500
+ # Bulk-import docs, then flush before querying
501
+ curl -X POST http://localhost:8080/api/default/transaction \
502
+ -H "Authorization: Bearer token" \
503
+ -d '[{ "action": "put", "type": "articles", "key": "a1", "value": { "body": "hello" } }]'
504
+
505
+ curl -X POST http://localhost:8080/api/default/type/articles/fts/flush \
506
+ -H "Authorization: Bearer token"
507
+
508
+ # Now search is guaranteed up-to-date
509
+ curl -X POST http://localhost:8080/api/default/type/articles/fts/main/search \
510
+ -H "Authorization: Bearer token" \
511
+ -d '{ "query": "hello" }'
512
+ ```
513
+
514
+ ---
515
+
459
516
  ## System info
460
517
 
461
518
  ```http
@@ -112,6 +112,48 @@ try {
112
112
 
113
113
  ---
114
114
 
115
+ ## HTTP API — bulk writes
116
+
117
+ The `/transaction` endpoint is the right tool for bulk ingest over HTTP. It accepts an array of operations and commits them all in a single LMDB transaction.
118
+
119
+ ```bash
120
+ curl -X POST http://localhost:8080/api/default/transaction \
121
+ -H "Authorization: Bearer my-token" \
122
+ -H "Content-Type: application/json" \
123
+ -d '{
124
+ "operations": [
125
+ { "action": "put", "type": "orders", "key": "o1", "value": { "item": "widget", "qty": 3 } },
126
+ { "action": "put", "type": "orders", "key": "o2", "value": { "item": "gadget", "qty": 1 } },
127
+ { "action": "put", "type": "orders", "key": "o3", "value": { "item": "doohickey", "qty": 5 } }
128
+ ]
129
+ }'
130
+ ```
131
+
132
+ Each operation is `{ action, type, key, … }`:
133
+
134
+ | `action` | Extra fields | Notes |
135
+ | -------- | ------------------------------- | --------------------------------------- |
136
+ | `put` | `value` (required), `ifVersion` | Upsert — create or overwrite |
137
+ | `update` | `value` (required), `ifVersion` | Throws if key absent |
138
+ | `patch` | `patch` (required), `ifVersion` | Same patch format as `PATCH /item/:key` |
139
+ | `remove` | `ifVersion` | Throws if key absent |
140
+
141
+ The body can also be sent as a bare array (omitting the `operations` wrapper):
142
+
143
+ ```bash
144
+ curl -X POST http://localhost:8080/api/default/transaction \
145
+ -H "Authorization: Bearer my-token" \
146
+ -H "Content-Type: application/json" \
147
+ -d '[
148
+ { "action": "put", "type": "prices", "key": "sku-001", "value": { "price": 9.99 } },
149
+ { "action": "put", "type": "prices", "key": "sku-002", "value": { "price": 19.99 } }
150
+ ]'
151
+ ```
152
+
153
+ Responds `204 No Content` on success. If any operation fails validation, the entire transaction is aborted and nothing is written.
154
+
155
+ ---
156
+
115
157
  ## Timestamp and origin
116
158
 
117
159
  Every write accepts a `timestamp` and `origin` option used by the sync system:
@@ -138,3 +180,19 @@ Everything that happens inside a single `db.transaction()` call is atomic:
138
180
  No partial state is ever visible. If the process crashes mid-write, LMDB rolls back the transaction on the next open.
139
181
 
140
182
  Post-commit events (`item:create`, `system:clock_change`, etc.) fire **after** the LMDB transaction has been durably committed — never speculatively.
183
+
184
+ ---
185
+
186
+ ## FTS and read-your-writes consistency
187
+
188
+ FTS writes are **always async** — the FTS index is updated by a background processor after each write commits. Secondary indexes (field indexes, geo indexes) are updated synchronously inside the same LMDB transaction as the write, so they are immediately consistent.
189
+
190
+ For applications that require read-your-writes consistency after a write, use `fts.flush(type)`:
191
+
192
+ ```javascript
193
+ await okdb.put('articles', 'a1', { title: 'Hello world' });
194
+ await okdb.fts.flush('articles'); // wait for processor to index the write
195
+ const results = okdb.fts.search('articles', 'main', 'hello'); // guaranteed to include a1
196
+ ```
197
+
198
+ `fts.flush()` is a no-op if the processor is already caught up. It is safe to call unconditionally.