lucivy 2.0.2 → 3.0.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 +224 -8
- package/index.d.ts +198 -3
- package/index.js +2 -2
- package/lucivy.node +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
# lucivy
|
|
1
|
+
# lucivy 3.0.1
|
|
2
2
|
|
|
3
3
|
Fast BM25 full-text search for Node.js — with substring matching, fuzzy search, regex, and highlights. Powered by Rust via napi-rs.
|
|
4
4
|
|
|
5
5
|
[**Try the live playground**](https://l-defraiteur.github.io/lucivy/) — runs entirely in your browser via WASM.
|
|
6
6
|
|
|
7
|
-
### What's new in
|
|
7
|
+
### What's new in 3.0.0
|
|
8
8
|
|
|
9
|
-
- **SFX
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
9
|
+
- **SFX v3 segment format** — per-field suffix FST files, the default for every new index; v2 indexes still open
|
|
10
|
+
- **`parse` query type** — boolean syntax (`AND` / `OR` / `NOT`, quotes, `+` / `-`, parentheses) on top of substring matching
|
|
11
|
+
- **`queryWarnings()`** — what the engine will really search, and where it falls back to a scan, before running the query
|
|
12
|
+
- **Snapshots served in place** — `Index.openSnapshot()` reads a LUCE blob without extracting it
|
|
13
|
+
- **Maintenance** — `compact()`, `waitMergesQuiet()`, `indexBytes()`, `dropIndex()`
|
|
14
|
+
- **Bring your own storage** — `BlobIndex` keeps an index in the transactional store of your choice through a plain object of callbacks; asynchronous API
|
|
15
|
+
|
|
16
|
+
Still there from 2.x: SFX-only engine, distributed search (`exportStats` / `mergeStats` / `searchWithGlobalStats`), incremental LUCIDS delta sync, BM25 scores identical across 1 or N shards, bindings for Python, Node.js, C++, WASM and Rust.
|
|
14
17
|
|
|
15
18
|
## Install
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
|
-
npm install lucivy
|
|
21
|
+
npm install lucivy@3.0.0
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
## Quick start
|
|
@@ -100,6 +103,11 @@ index.search({ type: 'contains', field: 'body', value: 'program' });
|
|
|
100
103
|
// Fuzzy substring (Levenshtein distance)
|
|
101
104
|
index.search({ type: 'contains', field: 'body', value: 'mutx', distance: 1 });
|
|
102
105
|
|
|
106
|
+
// Fuzzy with Jaro-Winkler instead of Levenshtein: candidates come from the
|
|
107
|
+
// trigram pigeonhole at `distance` (default 2), Jaro-Winkler decides, and
|
|
108
|
+
// hits are tiered by similarity (a typo at the end ranks above one at the start)
|
|
109
|
+
index.search({ type: 'fuzzy', field: 'body', value: 'kmalloc', fuzzy_metric: 'jaro_winkler', min_similarity: 0.9 });
|
|
110
|
+
|
|
103
111
|
// Regex substring — cross-token regex matching
|
|
104
112
|
index.search({ type: 'contains', field: 'body', value: 'lock.*mutex', regex: true });
|
|
105
113
|
|
|
@@ -141,6 +149,45 @@ index.search({
|
|
|
141
149
|
});
|
|
142
150
|
```
|
|
143
151
|
|
|
152
|
+
#### parse — boolean syntax in one string
|
|
153
|
+
|
|
154
|
+
A plain value (no operators) is an OR of substring `contains` queries, one per word
|
|
155
|
+
and per field. With boolean syntax the string is compiled into a `boolean` query of
|
|
156
|
+
`contains` clauses: `AND` / `OR` / `NOT`, `"quoted phrases"`, `+required` / `-excluded`,
|
|
157
|
+
and parentheses. Precedence is `NOT` > `AND` > `OR`; words side by side are OR'd.
|
|
158
|
+
Highlights work in both cases. `fields` takes several fields at once.
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// Plain value: OR of contains per word x field
|
|
162
|
+
index.search({ type: 'parse', field: 'body', value: 'kmalloc spin_lock' });
|
|
163
|
+
|
|
164
|
+
// Boolean syntax
|
|
165
|
+
index.search({ type: 'parse', field: 'body', value: 'kmalloc AND NOT vfree' });
|
|
166
|
+
index.search({ type: 'parse', field: 'body', value: '"spin_lock" -vfree' });
|
|
167
|
+
index.search({ type: 'parse', fields: ['title', 'body'], value: '(mutex OR spinlock) AND init' });
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### queryWarnings — know what will run before running it
|
|
171
|
+
|
|
172
|
+
Returns plain-text warnings for a query, without executing it: separators ignored
|
|
173
|
+
in relaxed mode, a fuzzy distance too loose for the query length, a regex without a
|
|
174
|
+
usable literal (full scan), segments written by the legacy indexer. Empty array when
|
|
175
|
+
nothing applies. For `parse`, the warnings say which of the two modes the value
|
|
176
|
+
selected.
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
index.queryWarnings({ type: 'contains', field: 'body', value: 'kmalloc' });
|
|
180
|
+
// []
|
|
181
|
+
index.queryWarnings({ type: 'contains', field: 'body', value: '__init' });
|
|
182
|
+
// ['separators are ignored (strict_separators=false): "__init" is searched as "init"']
|
|
183
|
+
index.queryWarnings({ type: 'regex', field: 'body', value: '[0-9]{8}' });
|
|
184
|
+
// ['"[0-9]{8}" requires no literal the index can look up: every document is scanned whole (full scan, ...)']
|
|
185
|
+
index.queryWarnings({ type: 'fuzzy', field: 'body', value: 'init' });
|
|
186
|
+
// ['distance 1 on "init" (4 chars) rewrites a quarter of the query or more: unrelated short words will match (...)']
|
|
187
|
+
index.queryWarnings({ type: 'parse', field: 'body', value: 'kmalloc spin' });
|
|
188
|
+
// ['parse without boolean operators: "kmalloc spin" runs as OR of substring contains, one per word']
|
|
189
|
+
```
|
|
190
|
+
|
|
144
191
|
#### Filtering
|
|
145
192
|
|
|
146
193
|
Filter on non-text fields (combined with AND):
|
|
@@ -180,6 +227,35 @@ const restored = Index.importSnapshotFrom('./backup.luce', './restored_index');
|
|
|
180
227
|
|
|
181
228
|
// Import from Buffer
|
|
182
229
|
const restored2 = Index.importSnapshot(buf, './restored_index');
|
|
230
|
+
|
|
231
|
+
// Serve a snapshot in place — nothing extracted, nothing written to disk.
|
|
232
|
+
// Memory cost = the blob's length. Read-only: add / delete / commit / compact
|
|
233
|
+
// and the snapshot or delta exports throw. Use importSnapshot() for a writable copy.
|
|
234
|
+
const served = Index.openSnapshot(buf);
|
|
235
|
+
const served2 = Index.openSnapshotFrom('./backup.luce');
|
|
236
|
+
served.search('programming'); // same results as the source index
|
|
237
|
+
served.path; // '' — a served snapshot has no directory
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Maintenance
|
|
241
|
+
|
|
242
|
+
```javascript
|
|
243
|
+
// Once after a bulk load: merge every shard's segments into segments of at
|
|
244
|
+
// most maxDocs documents (default 10000), then commit. Returns the number of
|
|
245
|
+
// merge rounds that reduced a shard's segment count.
|
|
246
|
+
const merges = index.compact(); // or index.compact(50000)
|
|
247
|
+
|
|
248
|
+
// Block until no background merge is running or about to start.
|
|
249
|
+
// Returns the number of rounds that still saw activity (0 = already quiet).
|
|
250
|
+
index.waitMergesQuiet();
|
|
251
|
+
|
|
252
|
+
// On-disk bytes of every searchable segment, all shards. Call waitMergesQuiet()
|
|
253
|
+
// first for a stable figure.
|
|
254
|
+
const bytes = index.indexBytes();
|
|
255
|
+
|
|
256
|
+
// Delete the whole index: close, then remove its files. The instance is
|
|
257
|
+
// consumed — every later call on it throws.
|
|
258
|
+
index.dropIndex();
|
|
183
259
|
```
|
|
184
260
|
|
|
185
261
|
### Delta sync (incremental)
|
|
@@ -229,9 +305,149 @@ index.numShards // number of shards (getter)
|
|
|
229
305
|
index.path // index directory path (getter)
|
|
230
306
|
index.schema // array of {name, type} objects (getter)
|
|
231
307
|
index.shardVersions // per-shard version info for delta sync (getter)
|
|
308
|
+
index.indexBytes() // on-disk size of all searchable segments
|
|
232
309
|
index.close() // flush + release writer lock
|
|
310
|
+
index.dropIndex() // close + delete the index files (instance unusable afterwards)
|
|
233
311
|
```
|
|
234
312
|
|
|
313
|
+
## Bring your own storage (ACID)
|
|
314
|
+
|
|
315
|
+
`Index` stores its files on the filesystem. `BlobIndex` stores them wherever
|
|
316
|
+
you say: every file of the index becomes a blob that your JavaScript object
|
|
317
|
+
loads and saves — a transactional database, an object store, a `Map`. The
|
|
318
|
+
store is the source of truth; a local directory only caches the blobs for
|
|
319
|
+
mmap reads and can be thrown away. `meta.json` is written last at each commit,
|
|
320
|
+
so a store with transactions gives you an index that is either at the previous
|
|
321
|
+
commit or at the new one, never in between.
|
|
322
|
+
|
|
323
|
+
### The store object
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
const store = {
|
|
327
|
+
load(indexName, fileName) // → Buffer | Uint8Array | null (null = does not exist)
|
|
328
|
+
save(indexName, fileName, data) // data: Buffer; create or overwrite
|
|
329
|
+
delete(indexName, fileName) // a missing blob is not an error
|
|
330
|
+
exists(indexName, fileName) // → boolean
|
|
331
|
+
list(indexName) // → string[]: every fileName under indexName
|
|
332
|
+
// optional, for lazy loading:
|
|
333
|
+
blobLen(indexName, fileName) // → number | null (null = unknown)
|
|
334
|
+
loadRange(indexName, fileName, offset, length) // → Buffer | Uint8Array | null (null = unsupported)
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Each method may return its value directly or a Promise of it. A thrown error
|
|
339
|
+
or a rejection is reported as the rejection of the `BlobIndex` call that
|
|
340
|
+
needed it. Methods are called with the store as `this`.
|
|
341
|
+
|
|
342
|
+
`indexName` is a namespace, not just the name you gave: segment files live
|
|
343
|
+
under `"Lucivy_<name>/shard_<i>"`, the root files (`_shard_config.json`,
|
|
344
|
+
`_shard_stats.bin`) under the bare `"<name>"`. Keep blobs keyed by the pair
|
|
345
|
+
`(indexName, fileName)` and `list()` cheap. Opening an index takes the writer
|
|
346
|
+
lock through the store (`.lucivy-writer.lock` is saved, then deleted) and
|
|
347
|
+
`close()` always commits, so `save` and `delete` must work even in a process
|
|
348
|
+
that only searches.
|
|
349
|
+
|
|
350
|
+
### Why `BlobIndex` is asynchronous
|
|
351
|
+
|
|
352
|
+
The engine calls the store from its own threads — segment writers, merges,
|
|
353
|
+
lazy loads — while a JavaScript callback can only run on the JavaScript
|
|
354
|
+
thread. If that thread were blocked inside a synchronous call such as
|
|
355
|
+
`index.commit()`, the callbacks it waits for could never run. So every
|
|
356
|
+
`BlobIndex` method runs its work on the Node.js thread pool and returns a
|
|
357
|
+
Promise; while you `await` it, the event loop is free and the store
|
|
358
|
+
callbacks are dispatched through it (one `ThreadsafeFunction` per method;
|
|
359
|
+
the engine thread blocks on a channel until the callback answered).
|
|
360
|
+
|
|
361
|
+
The corollary: **the JavaScript thread must stay free while a `BlobIndex`
|
|
362
|
+
call is pending.** Never `await` a `BlobIndex` call from inside a store
|
|
363
|
+
callback, and do not block the event loop with synchronous work (a
|
|
364
|
+
`readFileSync` inside `load` is fine; a `while` loop waiting for something
|
|
365
|
+
is not). Store callbacks that return Promises are awaited normally.
|
|
366
|
+
|
|
367
|
+
### Example: a Map
|
|
368
|
+
|
|
369
|
+
```javascript
|
|
370
|
+
const { BlobIndex } = require('lucivy');
|
|
371
|
+
|
|
372
|
+
const blobs = new Map();
|
|
373
|
+
const key = (indexName, fileName) => `${indexName}|${fileName}`;
|
|
374
|
+
const store = {
|
|
375
|
+
load: (i, f) => blobs.get(key(i, f)) ?? null,
|
|
376
|
+
save: (i, f, data) => { blobs.set(key(i, f), Buffer.from(data)); },
|
|
377
|
+
delete: (i, f) => { blobs.delete(key(i, f)); },
|
|
378
|
+
exists: (i, f) => blobs.has(key(i, f)),
|
|
379
|
+
list: (i) => [...blobs.keys()].filter(k => k.startsWith(i + '|')).map(k => k.slice(i.length + 1)),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
const index = await BlobIndex.create(store, 'articles', [
|
|
383
|
+
{ name: 'body', type: 'text', stored: true },
|
|
384
|
+
], { shards: 2 });
|
|
385
|
+
await index.addMany([{ docId: 1, body: 'kmalloc' }, { docId: 2, body: 'spin_lock_init' }]);
|
|
386
|
+
await index.commit();
|
|
387
|
+
const hits = await index.search('kmalloc', { highlights: true });
|
|
388
|
+
await index.close();
|
|
389
|
+
|
|
390
|
+
// Later, elsewhere — same blobs, same answers:
|
|
391
|
+
const again = await BlobIndex.open(store, 'articles');
|
|
392
|
+
await again.search({ type: 'contains', field: 'body', value: 'lock' });
|
|
393
|
+
await again.close();
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
With a database, `save` is an upsert, `load` a select, and `commit()` can be
|
|
397
|
+
wrapped in a transaction by the store itself.
|
|
398
|
+
|
|
399
|
+
### API
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
BlobIndex.create(store, indexName, fields, options?) // → Promise<BlobIndex>
|
|
403
|
+
BlobIndex.open(store, indexName, options?) // → Promise<BlobIndex>
|
|
404
|
+
// options: { cacheDir?: string, lazy?: boolean, shards?: number (create only) }
|
|
405
|
+
|
|
406
|
+
await index.add(docId, fields); await index.addMany(docs);
|
|
407
|
+
await index.delete(docId); await index.update(docId, fields);
|
|
408
|
+
await index.commit(); await index.search(query, options);
|
|
409
|
+
await index.queryWarnings(query); await index.numDocs();
|
|
410
|
+
await index.compact(maxDocs?); await index.waitMergesQuiet();
|
|
411
|
+
await index.indexBytes(); await index.close();
|
|
412
|
+
await index.dropIndex();
|
|
413
|
+
index.indexName; index.numShards; index.schema // synchronous getters
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Queries, options and results are exactly those of `Index`. `dropIndex()`
|
|
417
|
+
closes the index, then deletes every blob it owns: it lists and deletes the
|
|
418
|
+
`Lucivy_<name>/shard_<i>` namespaces and the `<name>` namespace through
|
|
419
|
+
`store.list()` / `store.delete()`; other indexes in the same store are
|
|
420
|
+
untouched. As with `Index`, the instance is consumed — every later call
|
|
421
|
+
rejects.
|
|
422
|
+
|
|
423
|
+
### Cache directory and lazy loading
|
|
424
|
+
|
|
425
|
+
`cacheDir` (default: `lucivy_blob_cache` under the OS temp dir) receives a
|
|
426
|
+
`<pid>/<namespace>_<n>/` directory per shard, filled from the store and
|
|
427
|
+
removed when the index is dropped from memory. It is disposable; it must be
|
|
428
|
+
on a local filesystem that supports mmap.
|
|
429
|
+
|
|
430
|
+
By default every blob is pulled at `open()`. With `lazy: true` — worth it
|
|
431
|
+
only when the store implements `blobLen` and `loadRange` — `open()` reads
|
|
432
|
+
the metadata files and probes segment footers with small `loadRange` calls;
|
|
433
|
+
each other file is loaded whole the first time a query needs more than a few
|
|
434
|
+
kilobytes of it. Searches give the same answers either way; the first query
|
|
435
|
+
pays for what it touches.
|
|
436
|
+
|
|
437
|
+
### Close before exit
|
|
438
|
+
|
|
439
|
+
`close()` waits for background merges, commits, releases the lock and
|
|
440
|
+
guarantees that nothing touches the store afterwards — call it before tearing
|
|
441
|
+
down a database connection and before the process exits. A store that stays
|
|
442
|
+
idle does not keep the process alive; if an index is garbage-collected
|
|
443
|
+
without `close()`, a flush that would have needed the JavaScript thread is
|
|
444
|
+
refused rather than deadlocked, and only the last uncommitted changes are
|
|
445
|
+
lost.
|
|
446
|
+
|
|
447
|
+
A store error — thrown or rejected, from a segment write in the background
|
|
448
|
+
or from `meta.json` at the commit point — comes back as the rejection of the
|
|
449
|
+
`BlobIndex` call that needed it, with the store's own message.
|
|
450
|
+
|
|
235
451
|
## License
|
|
236
452
|
|
|
237
453
|
MIT
|
package/index.d.ts
CHANGED
|
@@ -30,11 +30,60 @@ export interface SearchOptions {
|
|
|
30
30
|
/**
|
|
31
31
|
* Merge BM25 stats from multiple nodes into global stats (for distributed search).
|
|
32
32
|
*
|
|
33
|
+
* Each node calls `index.exportStats(queryJson)` which returns a JSON string.
|
|
34
|
+
* The coordinator collects all JSON strings and merges them with this function.
|
|
35
|
+
* The merged result is then passed back to each node via
|
|
36
|
+
* `index.searchWithGlobalStats(queryJson, mergedJson)`.
|
|
37
|
+
*
|
|
33
38
|
* @param statsList - Array of JSON strings, one per node (from `exportStats()`).
|
|
34
39
|
* @returns JSON string of merged `ExportableStats` ready for `searchWithGlobalStats()`.
|
|
35
40
|
*/
|
|
36
41
|
export declare function mergeStats(statsList: Array<string>): string
|
|
37
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The object a JavaScript program hands to `BlobIndex.create()` /
|
|
44
|
+
* `BlobIndex.open()`. Methods are called with the object as `this`. Each may
|
|
45
|
+
* return its value directly or a Promise of it.
|
|
46
|
+
*
|
|
47
|
+
* Keys: `indexName` is `"Lucivy_<name>/shard_<i>"` for segment files and the
|
|
48
|
+
* bare `<name>` for the root files (`_shard_config.json`, `_shard_stats.bin`);
|
|
49
|
+
* `fileName` is the file within that namespace.
|
|
50
|
+
*/
|
|
51
|
+
export interface BlobStoreCallbacks {
|
|
52
|
+
/** Bytes of a blob, or `null` when it does not exist. */
|
|
53
|
+
load: (indexName: string, fileName: string) => Buffer | Uint8Array | null | Promise<Buffer | Uint8Array | null>
|
|
54
|
+
/** Create or overwrite a blob. */
|
|
55
|
+
save: (indexName: string, fileName: string, data: Buffer) => void | Promise<void>
|
|
56
|
+
/** Remove a blob; a missing blob is not an error. */
|
|
57
|
+
delete: (indexName: string, fileName: string) => void | Promise<void>
|
|
58
|
+
exists: (indexName: string, fileName: string) => boolean | Promise<boolean>
|
|
59
|
+
/** Every file name stored under `indexName`. */
|
|
60
|
+
list: (indexName: string) => string[] | Promise<string[]>
|
|
61
|
+
/**
|
|
62
|
+
* Optional, for `lazy: true`: size of a blob without loading it
|
|
63
|
+
* (`null` = unknown, the file is then loaded whole on first open).
|
|
64
|
+
*/
|
|
65
|
+
blobLen?: (indexName: string, fileName: string) => number | null | Promise<number | null>
|
|
66
|
+
/**
|
|
67
|
+
* Optional, for `lazy: true`: `length` bytes of a blob from `offset`
|
|
68
|
+
* (`null` = unsupported, the file is then loaded whole).
|
|
69
|
+
*/
|
|
70
|
+
loadRange?: (indexName: string, fileName: string, offset: number, length: number) => Buffer | Uint8Array | null | Promise<Buffer | Uint8Array | null>
|
|
71
|
+
}
|
|
72
|
+
/** Options of `BlobIndex.create()` / `BlobIndex.open()`. */
|
|
73
|
+
export interface BlobIndexOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Local directory for the mmap cache of the blobs (default: `lucivy_blob_cache`
|
|
76
|
+
* under the OS temp dir). Disposable: the store is the source of truth.
|
|
77
|
+
*/
|
|
78
|
+
cacheDir?: string
|
|
79
|
+
/**
|
|
80
|
+
* Pull blobs on first use instead of all at open. Needs `blobLen` and
|
|
81
|
+
* `loadRange` on the store to be worth it.
|
|
82
|
+
*/
|
|
83
|
+
lazy?: boolean
|
|
84
|
+
/** `create()` only: number of shards (default 1). */
|
|
85
|
+
shards?: number
|
|
86
|
+
}
|
|
38
87
|
export declare class Index {
|
|
39
88
|
/**
|
|
40
89
|
* Create a new index at the given path.
|
|
@@ -100,6 +149,9 @@ export declare class Index {
|
|
|
100
149
|
*
|
|
101
150
|
* After `close()`, the index data remains on disk and can be re-opened
|
|
102
151
|
* with `Index.open()`. No further mutations are allowed on this instance.
|
|
152
|
+
*
|
|
153
|
+
* On a snapshot served with `openSnapshot()` there is nothing to flush
|
|
154
|
+
* and no lock to release: `close()` is a no-op.
|
|
103
155
|
*/
|
|
104
156
|
close(): void
|
|
105
157
|
/**
|
|
@@ -124,7 +176,7 @@ export declare class Index {
|
|
|
124
176
|
* - `{type: "more_like_this", field: "body", value: "sample text", min_doc_frequency: 1}` — similarity
|
|
125
177
|
*
|
|
126
178
|
* **Filtering:**
|
|
127
|
-
* - `
|
|
179
|
+
* - `allowedIds` in options: pre-filter by _node_id (fast, bitmap-based)
|
|
128
180
|
* - `filters` key in query: filter on non-text fields (AND'd with search):
|
|
129
181
|
* ```json
|
|
130
182
|
* {type: "contains", field: "body", value: "lock",
|
|
@@ -137,8 +189,16 @@ export declare class Index {
|
|
|
137
189
|
* Ops: `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `in`, `not_in`, `between`, `starts_with`, `contains`.
|
|
138
190
|
* Composite: `must`, `should`, `must_not` with nested `clauses`.
|
|
139
191
|
*
|
|
140
|
-
*
|
|
192
|
+
* Honest warnings for a query, without running it.
|
|
193
|
+
*
|
|
194
|
+
* Plain-text warnings describing what the engine will actually search
|
|
195
|
+
* and where it falls back to brute force: separators ignored in relaxed
|
|
196
|
+
* mode, fuzzy distance too loose for the query length, regex without a
|
|
197
|
+
* usable literal (full scan), segments written by the legacy indexer.
|
|
198
|
+
* Empty array when nothing applies.
|
|
141
199
|
*/
|
|
200
|
+
queryWarnings(query: any): Array<string>
|
|
201
|
+
/** @param options - `{limit?: number, highlights?: boolean, fields?: boolean, allowedIds?: number[]}` */
|
|
142
202
|
search(query: any, options?: SearchOptions | undefined | null): Array<SearchResult>
|
|
143
203
|
/**
|
|
144
204
|
* Number of documents in the index (getter, access as `index.numDocs`).
|
|
@@ -196,6 +256,69 @@ export declare class Index {
|
|
|
196
256
|
* @returns A new `Index` instance ready for search.
|
|
197
257
|
*/
|
|
198
258
|
static importSnapshotFrom(path: string, destPath?: string | undefined | null): Index
|
|
259
|
+
/**
|
|
260
|
+
* Serve a LUCE snapshot (Buffer) directly, without extracting it.
|
|
261
|
+
*
|
|
262
|
+
* The blob itself is the index: readers get slices of it, nothing is
|
|
263
|
+
* written to disk and the memory cost is the blob's own length. The
|
|
264
|
+
* result is **read-only** — `add()`, `delete()`, `commit()`, `compact()`
|
|
265
|
+
* and the delta/snapshot exports fail with a clear error. To get a
|
|
266
|
+
* writable index back, use `Index.importSnapshot()` instead.
|
|
267
|
+
*
|
|
268
|
+
* @param data - Raw LUCE snapshot bytes (Buffer), as produced by `exportSnapshot()`.
|
|
269
|
+
* @returns A read-only `Index` ready for search. Its `path` is `""`.
|
|
270
|
+
*/
|
|
271
|
+
static openSnapshot(data: Buffer): Index
|
|
272
|
+
/**
|
|
273
|
+
* Serve a LUCE snapshot file (.luce) directly, without extracting it.
|
|
274
|
+
*
|
|
275
|
+
* Convenience wrapper that reads the file then calls `openSnapshot()`.
|
|
276
|
+
* Same read-only semantics.
|
|
277
|
+
*
|
|
278
|
+
* @param path - Path to the `.luce` snapshot file.
|
|
279
|
+
* @returns A read-only `Index` ready for search.
|
|
280
|
+
*/
|
|
281
|
+
static openSnapshotFrom(path: string): Index
|
|
282
|
+
/**
|
|
283
|
+
* Merge every shard's segments into segments of at most `maxDocs`
|
|
284
|
+
* documents, then commit.
|
|
285
|
+
*
|
|
286
|
+
* Bulk loading leaves many small segments behind; one `compact()` after
|
|
287
|
+
* the load makes searches faster and the index smaller on disk. Not
|
|
288
|
+
* something to call on every commit.
|
|
289
|
+
*
|
|
290
|
+
* @param maxDocs - Upper bound on documents per merged segment (default 10000).
|
|
291
|
+
* @returns Number of merge rounds that actually reduced a shard's segment count.
|
|
292
|
+
*/
|
|
293
|
+
compact(maxDocs?: number | undefined | null): number
|
|
294
|
+
/**
|
|
295
|
+
* Block until no background merge is running or about to start.
|
|
296
|
+
*
|
|
297
|
+
* Segment merges run in the background after commits. Call this before
|
|
298
|
+
* anything that needs a stable set of files or the full address space —
|
|
299
|
+
* measuring `indexBytes()`, copying the directory, exporting a snapshot
|
|
300
|
+
* under memory pressure.
|
|
301
|
+
*
|
|
302
|
+
* @returns Number of rounds that still saw merge activity (0 = already quiet).
|
|
303
|
+
*/
|
|
304
|
+
waitMergesQuiet(): number
|
|
305
|
+
/**
|
|
306
|
+
* On-disk bytes of every searchable segment of every shard.
|
|
307
|
+
*
|
|
308
|
+
* Sums the segment files; the number moves while merges run, so call
|
|
309
|
+
* `waitMergesQuiet()` first for a stable figure.
|
|
310
|
+
*
|
|
311
|
+
* @returns Total size in bytes (a `number`; exact below 2^53).
|
|
312
|
+
*/
|
|
313
|
+
indexBytes(): number
|
|
314
|
+
/**
|
|
315
|
+
* Delete the whole index: commit and release everything (like `close()`),
|
|
316
|
+
* then remove the index files from disk.
|
|
317
|
+
*
|
|
318
|
+
* This consumes the underlying handle. After `dropIndex()` every other
|
|
319
|
+
* method on this instance throws; create or open a new `Index` instead.
|
|
320
|
+
*/
|
|
321
|
+
dropIndex(): void
|
|
199
322
|
/**
|
|
200
323
|
* Schema as a list of field definitions (getter, access as `index.schema`).
|
|
201
324
|
*
|
|
@@ -259,3 +382,75 @@ export declare class Index {
|
|
|
259
382
|
*/
|
|
260
383
|
searchWithGlobalStats(queryJson: string, globalStatsJson: string, limit?: number | undefined | null, highlights?: boolean | undefined | null): Array<SearchResult>
|
|
261
384
|
}
|
|
385
|
+
/**
|
|
386
|
+
* An index whose files live in a storage you provide — a transactional
|
|
387
|
+
* database, an object store, a Map — through the `BlobStoreCallbacks`
|
|
388
|
+
* object. Every method returns a Promise: the work runs off the JS thread,
|
|
389
|
+
* which stays free to serve the store callbacks.
|
|
390
|
+
*/
|
|
391
|
+
export declare class BlobIndex {
|
|
392
|
+
/**
|
|
393
|
+
* Create a new index in the given store.
|
|
394
|
+
*
|
|
395
|
+
* @param store - Object implementing the store protocol (`load`, `save`, `delete`, `exists`, `list`, optional `blobLen` / `loadRange`).
|
|
396
|
+
* @param indexName - Name of the index inside the store.
|
|
397
|
+
* @param fields - Field definitions, as for `Index.create()`.
|
|
398
|
+
* @param options - `{cacheDir?, lazy?, shards?}`.
|
|
399
|
+
*/
|
|
400
|
+
static create(store: BlobStoreCallbacks, indexName: string, fields: Array<FieldDef>, options?: BlobIndexOptions | undefined | null): Promise<BlobIndex>
|
|
401
|
+
/**
|
|
402
|
+
* Open an index that already exists in the store.
|
|
403
|
+
*
|
|
404
|
+
* @param store - Same protocol as for `create()`.
|
|
405
|
+
* @param indexName - Name given at creation.
|
|
406
|
+
* @param options - `{cacheDir?, lazy?}`.
|
|
407
|
+
*/
|
|
408
|
+
static open(store: BlobStoreCallbacks, indexName: string, options?: BlobIndexOptions | undefined | null): Promise<BlobIndex>
|
|
409
|
+
/** Add a document. Same arguments as `Index.add()`. */
|
|
410
|
+
add(docId: number, fields: Record<string, any>): Promise<void>
|
|
411
|
+
/** Add multiple documents. Same arguments as `Index.addMany()`. */
|
|
412
|
+
addMany(docs: Array<Record<string, any>>): Promise<void>
|
|
413
|
+
/** Delete a document by its `_node_id` (staged until `commit()`). */
|
|
414
|
+
delete(docId: number): Promise<void>
|
|
415
|
+
/** Update a document (delete old + re-add with new fields). */
|
|
416
|
+
update(docId: number, fields: Record<string, any>): Promise<void>
|
|
417
|
+
/**
|
|
418
|
+
* Commit pending changes to the store: segment files are saved through
|
|
419
|
+
* `store.save()`, `meta.json` last (the commit point).
|
|
420
|
+
*/
|
|
421
|
+
commit(): Promise<void>
|
|
422
|
+
/** Search. Same arguments and results as `Index.search()`. */
|
|
423
|
+
search(query: any, options?: SearchOptions | undefined | null): Promise<Array<SearchResult>>
|
|
424
|
+
/** Honest warnings for a query, without running it. See `Index.queryWarnings()`. */
|
|
425
|
+
queryWarnings(query: any): Promise<Array<string>>
|
|
426
|
+
/** Number of documents across all shards. */
|
|
427
|
+
numDocs(): Promise<number>
|
|
428
|
+
/**
|
|
429
|
+
* Flush pending writes, wait for merges, release the writer lock.
|
|
430
|
+
* After `close()` the store is not touched again: it is safe to tear
|
|
431
|
+
* down whatever backs it. Always call it before the process exits.
|
|
432
|
+
*/
|
|
433
|
+
close(): Promise<void>
|
|
434
|
+
/**
|
|
435
|
+
* Merge every shard's segments into segments of at most `maxDocs`
|
|
436
|
+
* documents, then commit. See `Index.compact()`.
|
|
437
|
+
*/
|
|
438
|
+
compact(maxDocs?: number | undefined | null): Promise<number>
|
|
439
|
+
/** Block until no background merge is running or about to start. */
|
|
440
|
+
waitMergesQuiet(): Promise<number>
|
|
441
|
+
/** Bytes of every searchable segment of every shard, as cached locally. */
|
|
442
|
+
indexBytes(): Promise<number>
|
|
443
|
+
/**
|
|
444
|
+
* Delete the whole index: `close()`, then every blob the store holds
|
|
445
|
+
* for it — the `Lucivy_<name>/shard_<i>` namespaces and the root
|
|
446
|
+
* `<name>` namespace, each listed with `store.list()` and removed with
|
|
447
|
+
* `store.delete()`. Consumes the handle: every later call throws.
|
|
448
|
+
*/
|
|
449
|
+
dropIndex(): Promise<void>
|
|
450
|
+
/** Name of the index inside the store (getter). */
|
|
451
|
+
get indexName(): string
|
|
452
|
+
/** Number of shards (getter). */
|
|
453
|
+
get numShards(): number
|
|
454
|
+
/** Schema as a list of field definitions (getter). */
|
|
455
|
+
get schema(): Array<FieldDef>
|
|
456
|
+
}
|
package/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
const { Index, mergeStats } = require('./lucivy.node');
|
|
1
|
+
const { Index, BlobIndex, mergeStats } = require('./lucivy.node');
|
|
2
2
|
|
|
3
|
-
module.exports = { Index, mergeStats };
|
|
3
|
+
module.exports = { Index, BlobIndex, mergeStats };
|
package/lucivy.node
CHANGED
|
Binary file
|