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.
Files changed (3) hide show
  1. package/README.md +21 -3
  2. package/index.d.ts +39 -2
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -1,9 +1,15 @@
1
- # lucivy 4.0.2
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 440 (wildcard field, 70 short), 480 ms | 0 — terms are already cut |
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.2
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.2",
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.2",
43
- "lucivy-linux-arm64-gnu": "4.0.2",
44
- "lucivy-darwin-x64": "4.0.2",
45
- "lucivy-darwin-arm64": "4.0.2",
46
- "lucivy-windows-x64": "4.0.2"
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
  }