@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.
- package/bin/okdb.js +1 -1
- package/docs/data-model.md +73 -7
- package/docs/http-api.md +57 -0
- package/docs/transactions.md +58 -0
- package/okdb.js +1 -1
- package/package.json +1 -1
- package/types/features/fts.d.ts +86 -15
package/docs/data-model.md
CHANGED
|
@@ -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
|
|
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/
|
|
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
|
|
167
|
-
| ---------- |
|
|
168
|
-
| `strict` | Full fsync on every write
|
|
169
|
-
| `balanced` | `overlappingSync`
|
|
170
|
-
| `fast` | `noSync`
|
|
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
|
package/docs/transactions.md
CHANGED
|
@@ -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.
|