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 CHANGED
@@ -1,21 +1,24 @@
1
- # lucivy v2
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 v2
7
+ ### What's new in 3.0.0
8
8
 
9
- - **SFX-only engine** — all queries route through the Suffix FST, no legacy code paths
10
- - **Distributed search** — `exportStats` / `mergeStats` / `searchWithGlobalStats`
11
- - **Incremental sync** — LUCIDS sharded delta export/apply
12
- - **Correct BM25 cross-shard** — identical scores whether 1 shard or 4
13
- - **5 bindings** — Python, Node.js, C++, WASM, Rust
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
- * - `allowed_ids` in options: pre-filter by _node_id (fast, bitmap-based)
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
- * @param options - `{limit?: number, highlights?: boolean, fields?: boolean, allowed_ids?: number[]}`
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lucivy",
3
- "version": "2.0.2",
3
+ "version": "3.0.1",
4
4
  "description": "Fast BM25 full-text search with substring matching, fuzzy search, and regex — powered by Rust",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",