lucivy 4.0.2 → 4.1.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 +21 -3
- package/index.d.ts +39 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
# lucivy 4.0
|
|
1
|
+
# lucivy 4.1.0
|
|
2
2
|
|
|
3
3
|
**One index answers every question, and every answer is checked.** The default index answers exact substrings, matches across separators, typos across token boundaries, regular expressions and two-character needles — with BM25 and the exact bytes of every match — and nothing to configure per question; the ground-truth harness compares every answer to a scan of the files. From Node.js. Runs in your process, in your transaction (bring your own storage), and the same engine runs in the browser. Powered by Rust via napi-rs, MIT.
|
|
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 4.1
|
|
8
|
+
|
|
9
|
+
- **`Index.create(path, fields, { positions: false })` — an index half the size.** The postings keep each token's documents and frequencies instead of its positions, and no position sidecar is written: the whole Linux kernel (101 141 files, 941 MB of text) 5 289 → 2 598 MB, ×2.8 its text instead of ×5.6; 10 000 files −37 %. Nothing is rebuilt when the index opens, and indexing is a little faster (109 → 101 s). Every match is found as candidate documents and verified on the stored text with the ground truth's own definitions: same documents, same spans, same scores, the ground-truth panel 10/10. Some queries pay for it (whole kernel, file cache warm): literal substrings 11-19 → 27-56 ms (the documents found are re-read), a one-edit fuzzy 50 → 200 ms; others get faster — a regex 237 → 22 ms, a two-character needle 630-700 → 330 ms. Every text field must be stored (the default); excludes `derivedInRam`; fixed at creation, also as the `BlobIndex` option `positions`. An index created this way cannot be searched by 4.0.x (it opens, and every search fails: `sfxpost: invalid V2 format`)
|
|
10
|
+
- **The options of `Index.create()` as one object** — `{ shards, sharedDictionary, derivedInRam, dictionaryWait, positions }` (`IndexOptions`, typed) in place of the arguments after `fields`; the positional form still works, giving both is refused
|
|
11
|
+
- **Fix: one occurrence, one span.** In relaxed mode a needle that ends a word cut into chunks — `lock` in `superblock` — came back twice, and counted twice in the score (542 duplicated spans for `lock` over 10 000 kernel files, since the v3 engine, 4.0.2 included); the ground-truth harness now counts duplicates too
|
|
12
|
+
|
|
7
13
|
### What's new in 4.0.0
|
|
8
14
|
|
|
9
15
|
- **The index is 3.7× smaller** — the whole Linux kernel: 18 057 MB in 3.0.8, 4 938 MB in 4.0, 3 344 MB with `derivedInRam`; same answers, same spans, checked against the files ([the comparison with Elasticsearch and tantivy](https://github.com/L-Defraiteur/lucivy/blob/main/docs/compare-engines-2026-09-05.md))
|
|
@@ -20,7 +26,7 @@ Same 93 983 Linux kernel files, 857 MB of text. Each engine is configured at its
|
|
|
20
26
|
|---|---|---|---|---|
|
|
21
27
|
| `spin_lock`, separators relaxed (also `spin lock`, `spin-lock`, `spinlock`) | 9 552 | **9 552**, 23 ms | 6 577 — not with this analyzer: its trigrams carry the underscore | 6 601 — relaxed is the only mode it has: the separator never enters its index |
|
|
22
28
|
| `spinlokc`, two edits, across the token boundary | 10 034 | **10 034**, 148 ms | 3 549 — fuzziness compares whole terms | 6 557 — same |
|
|
23
|
-
| `spin_lock_[a-z]+`, a regex | 5 510 | **5 510**, 219 ms | 5
|
|
29
|
+
| `spin_lock_[a-z]+`, a regex, case folded | 5 510 | **5 510**, 219 ms | 5 510 on the wildcard field, 1 ms warm — as `[a-zA-Z]+`: Lucene's `case_insensitive` folds a pattern's literals, not its character classes | 0 — terms are already cut |
|
|
24
30
|
| `de`, two characters | 93 009 | **93 009**, 7.7 M spans, 561 ms | 0, silently | 0, silently |
|
|
25
31
|
| `retur -ENOMEM`, a fuzzy phrase | 14 449 | **14 449**, 30 ms | 14 446 (`span_near`), 24 ms — it does this well | — |
|
|
26
32
|
| **where it matched**: `mutex_lock`, 5 145 documents | 20 797 spans | **all 20 797, 15 ms** | `highlight` on the top 200: 179 ms | verifying 5 145 stored texts: 96 ms |
|
|
@@ -43,7 +49,7 @@ Still there from 2.x: SFX-only engine, distributed search (`exportStats` / `merg
|
|
|
43
49
|
## Install
|
|
44
50
|
|
|
45
51
|
```bash
|
|
46
|
-
npm install lucivy # 4.0
|
|
52
|
+
npm install lucivy # 4.1.0
|
|
47
53
|
```
|
|
48
54
|
|
|
49
55
|
## Quick start
|
|
@@ -92,6 +98,18 @@ const compact = Index.create('/tmp/compact', [...], 1, true);
|
|
|
92
98
|
// never a query. (BlobIndex.create: `{ derivedInRam: true }`.)
|
|
93
99
|
const lean = Index.create('/tmp/lean', [...], 1, true, true);
|
|
94
100
|
|
|
101
|
+
// Half the size (4.1): the postings keep documents and frequencies, not
|
|
102
|
+
// positions, and no position sidecar is written. Every match is verified on
|
|
103
|
+
// the stored text: same documents, spans and scores; literal queries pay a
|
|
104
|
+
// re-read of the documents found (tens of milliseconds on the whole
|
|
105
|
+
// kernel), regexes get faster. Text fields must be stored (the default);
|
|
106
|
+
// excludes derivedInRam. Fixed at creation. (BlobIndex: `{ positions: false }`.)
|
|
107
|
+
const half = Index.create('/tmp/half', [...], { positions: false });
|
|
108
|
+
|
|
109
|
+
// Every option can be given this way, in place of the positional arguments:
|
|
110
|
+
// { shards, sharedDictionary, derivedInRam, dictionaryWait, positions }
|
|
111
|
+
const wide = Index.create('/tmp/wide', [...], { shards: 4, positions: false });
|
|
112
|
+
|
|
95
113
|
// Open existing
|
|
96
114
|
const index2 = Index.open('/tmp/my_index');
|
|
97
115
|
```
|
package/index.d.ts
CHANGED
|
@@ -39,6 +39,28 @@ export interface SearchOptions {
|
|
|
39
39
|
* @returns JSON string of merged `ExportableStats` ready for `searchWithGlobalStats()`.
|
|
40
40
|
*/
|
|
41
41
|
export declare function mergeStats(statsList: Array<string>): string
|
|
42
|
+
/**
|
|
43
|
+
* The options of `Index.create()` as one object, in place of its arguments
|
|
44
|
+
* after `fields`: `Index.create(path, fields, { positions: false })`.
|
|
45
|
+
*/
|
|
46
|
+
export interface IndexOptions {
|
|
47
|
+
/** Number of shards (default 1). */
|
|
48
|
+
shards?: number
|
|
49
|
+
/** One dictionary per shard instead of one per segment (default `true`). */
|
|
50
|
+
sharedDictionary?: boolean
|
|
51
|
+
/** The derived sidecars rebuilt in RAM at open instead of written. */
|
|
52
|
+
derivedInRam?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* Shared dictionary: a search waits for the background merge of the
|
|
55
|
+
* last commit's texts (default `true`).
|
|
56
|
+
*/
|
|
57
|
+
dictionaryWait?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* `false`: documents and frequencies instead of positions, about half
|
|
60
|
+
* the index; every match verified on the stored text.
|
|
61
|
+
*/
|
|
62
|
+
positions?: boolean
|
|
63
|
+
}
|
|
42
64
|
/**
|
|
43
65
|
* The object a JavaScript program hands to `BlobIndex.create()` /
|
|
44
66
|
* `BlobIndex.open()`. Methods are called with the object as `this`. Each may
|
|
@@ -98,6 +120,11 @@ export interface BlobIndexOptions {
|
|
|
98
120
|
* merge of the last commit's texts (default `true`) — see `Index.create()`.
|
|
99
121
|
*/
|
|
100
122
|
dictionaryWait?: boolean
|
|
123
|
+
/**
|
|
124
|
+
* `create()` only: `false` keeps documents and frequencies instead of
|
|
125
|
+
* positions, every match verified on the stored text — see `Index.create()`.
|
|
126
|
+
*/
|
|
127
|
+
positions?: boolean
|
|
101
128
|
}
|
|
102
129
|
export declare class Index {
|
|
103
130
|
/**
|
|
@@ -107,6 +134,8 @@ export declare class Index {
|
|
|
107
134
|
* @param fields - Field definitions: `[{name: "body", type: "text", stored: true}]`.
|
|
108
135
|
* Types: `"text"` (full-text), `"u64"`, `"i64"`, `"f64"`, `"bool"`, `"date"`.
|
|
109
136
|
* @param shards - Number of shards (default 1). More shards = faster search on large datasets.
|
|
137
|
+
* Or an options object in place of this argument and all that follow:
|
|
138
|
+
* `Index.create(path, fields, { shards: 4, positions: false })`.
|
|
110
139
|
* @param sharedDictionary - Store each distinct token text once per shard
|
|
111
140
|
* instead of once per segment: the index is about 20 % smaller on disk
|
|
112
141
|
* and in RAM, queries are slightly slower at cold cache (roughly x1.2
|
|
@@ -125,8 +154,16 @@ export declare class Index {
|
|
|
125
154
|
* task does it); a search waits for that merge, so that its cost never
|
|
126
155
|
* depends on when it runs. `false` searches at once over the
|
|
127
156
|
* not-yet-merged parts. On by default; fixed at creation.
|
|
157
|
+
* @param positions - `false` keeps each token's documents and frequencies
|
|
158
|
+
* instead of its positions, and writes no position sidecar (`.posmap`,
|
|
159
|
+
* `.word_pos_map`, `.sibling_v3`): 37 % smaller on 10 000 kernel files.
|
|
160
|
+
* Every match is then verified on the stored text — same documents,
|
|
161
|
+
* spans and scores; literal queries pay a re-read of the documents
|
|
162
|
+
* found (tens of milliseconds on the whole kernel), regexes get
|
|
163
|
+
* faster. Every text field must be stored (the default); excludes
|
|
164
|
+
* `derivedInRam`. On by default; fixed at creation.
|
|
128
165
|
*/
|
|
129
|
-
static create(path: string, fields: Array<FieldDef>, shards?: number | undefined | null, sharedDictionary?: boolean | undefined | null, derivedInRam?: boolean | undefined | null, dictionaryWait?: boolean | undefined | null): Index
|
|
166
|
+
static create(path: string, fields: Array<FieldDef>, shards?: number | IndexOptions | undefined | null, sharedDictionary?: boolean | undefined | null, derivedInRam?: boolean | undefined | null, dictionaryWait?: boolean | undefined | null, positions?: boolean | undefined | null): Index
|
|
130
167
|
/**
|
|
131
168
|
* Open an existing index at the given path.
|
|
132
169
|
*
|
|
@@ -437,7 +474,7 @@ export declare class BlobIndex {
|
|
|
437
474
|
* @param store - Object implementing the store protocol (`load`, `save`, `delete`, `exists`, `list`, optional `blobLen` / `loadRange`).
|
|
438
475
|
* @param indexName - Name of the index inside the store.
|
|
439
476
|
* @param fields - Field definitions, as for `Index.create()`.
|
|
440
|
-
* @param options - `{cacheDir?, lazy?, shards?, sharedDictionary?, derivedInRam?, dictionaryWait?}`.
|
|
477
|
+
* @param options - `{cacheDir?, lazy?, shards?, sharedDictionary?, derivedInRam?, dictionaryWait?, positions?}`.
|
|
441
478
|
*/
|
|
442
479
|
static create(store: BlobStoreCallbacks, indexName: string, fields: Array<FieldDef>, options?: BlobIndexOptions | undefined | null): Promise<BlobIndex>
|
|
443
480
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lucivy",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"build:debug": "napi build"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"lucivy-linux-x64-gnu": "4.0
|
|
43
|
-
"lucivy-linux-arm64-gnu": "4.0
|
|
44
|
-
"lucivy-darwin-x64": "4.0
|
|
45
|
-
"lucivy-darwin-arm64": "4.0
|
|
46
|
-
"lucivy-windows-x64": "4.0
|
|
42
|
+
"lucivy-linux-x64-gnu": "4.1.0",
|
|
43
|
+
"lucivy-linux-arm64-gnu": "4.1.0",
|
|
44
|
+
"lucivy-darwin-x64": "4.1.0",
|
|
45
|
+
"lucivy-darwin-arm64": "4.1.0",
|
|
46
|
+
"lucivy-windows-x64": "4.1.0"
|
|
47
47
|
}
|
|
48
48
|
}
|