@harperfast/hnsw 0.1.0 → 0.2.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/Cargo.lock +1 -0
- package/DESIGN.md +95 -9
- package/README.md +16 -6
- package/build.mjs +7 -6
- package/index.d.ts +42 -1
- package/index.js +39 -18
- package/package.json +8 -3
- package/src/bin/bench.rs +1 -1
- package/src/format.rs +241 -19
- package/src/graph.rs +263 -69
- package/src/insert.rs +141 -40
- package/src/invalidate.rs +336 -0
- package/src/lib.rs +3 -1
- package/src/napi.rs +92 -4
- package/src/search.rs +413 -87
- package/prebuilds/darwin-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-arm64/hnsw-plane.node +0 -0
- package/prebuilds/linux-x64/hnsw-plane.node +0 -0
- package/prebuilds/win32-x64/hnsw-plane.node +0 -0
package/Cargo.lock
CHANGED
package/DESIGN.md
CHANGED
|
@@ -96,18 +96,24 @@ One file per index (per slice, once C2 lands): `<index-path>.hnsw`.
|
|
|
96
96
|
| freelist_head | u64 atomic | CAS push/pop; ABA-guarded with a 32-bit tag |
|
|
97
97
|
| txn_watermark | u64 | last durably indexed transaction; advanced by msync cadence |
|
|
98
98
|
| clean_shutdown flag | u8 | torn-state detection on open |
|
|
99
|
+
| invalidated latch | u8 | one-way (v7): watermark reads 0 on every handle, open refuses |
|
|
100
|
+
| write_epoch | u64 atomic | bumped by every node write; re-arms the read-side repair probe |
|
|
99
101
|
|
|
100
102
|
**Main region — layer-0 slots**, addressed `4096 + id × slot_size`:
|
|
101
103
|
|
|
102
|
-
| Field | Size (768-d int8, cap 64)
|
|
103
|
-
| ------------------------------- |
|
|
104
|
-
| seq (seqlock) | 4 B
|
|
105
|
-
| flags (valid/deleted) + level | 2 B
|
|
106
|
-
| scale (f32) + invMag (f32) | 8 B
|
|
107
|
-
| degree | 2 B
|
|
108
|
-
| vector (int8 × 768) | 768 B
|
|
109
|
-
| neighbor ids (u32 × layer0_cap) | 256 B
|
|
110
|
-
| **total, padded** | **1,040 B → 1 KB-aligned 1,088 B**
|
|
104
|
+
| Field | Size (768-d int8, cap 64) |
|
|
105
|
+
| ------------------------------- | ----------------------------------- |
|
|
106
|
+
| seq (seqlock) | 4 B |
|
|
107
|
+
| flags (valid/deleted) + level | 2 B |
|
|
108
|
+
| scale (f32) + invMag (f32) | 8 B |
|
|
109
|
+
| degree | 2 B |
|
|
110
|
+
| vector (int8 × 768) | 768 B (padded to a 4-byte boundary) |
|
|
111
|
+
| neighbor ids (u32 × layer0_cap) | 256 B |
|
|
112
|
+
| **total, padded** | **1,040 B → 1 KB-aligned 1,088 B** |
|
|
113
|
+
|
|
114
|
+
The vector's trailing pad keeps the neighbor array 4-aligned for every `dims`, so the search
|
|
115
|
+
hot path reads each neighbor id as one aligned volatile `u32`. Upper-layer id lists are padded
|
|
116
|
+
the same way (`degree u16 + pad u16 + ids`).
|
|
111
117
|
|
|
112
118
|
At 100M nodes: ~109 GB (int8). A binary-code v2 slot (96 B codes + ids) is ~384 B → ~38 GB.
|
|
113
119
|
For comparison, today's encoding averages 1,425 B/node _plus_ RocksDB overhead — so v1 is
|
|
@@ -167,6 +173,24 @@ bounded-lag durability with deterministic catch-up. For an approximate index who
|
|
|
167
173
|
truth (records + pk→nodeId) remains fully transactional, bounded lag is the right trade — it
|
|
168
174
|
buys the entire performance model.
|
|
169
175
|
|
|
176
|
+
**Invalidation (a plane the host cannot delete).** Disabling a plane deletes its file; when the
|
|
177
|
+
unlink fails (Windows sharing violation while another process maps it) the file must not be
|
|
178
|
+
adopted later at its nonzero watermark, or it silently serves searches missing every mutation
|
|
179
|
+
made while mirroring was off. `invalidate_plane(path)` / `invalidate_file(&handle)` leave two
|
|
180
|
+
markers, always attempting both: in band — `PlaneFile::invalidate` sets a one-way header latch,
|
|
181
|
+
zeroes the watermark, and msyncs the header page alone (a whole-mapping flush cannot run inline
|
|
182
|
+
on a multi-GB plane, and lowering the watermark is the safe direction) — then a `<path>.stale`
|
|
183
|
+
sidecar, created with create-new semantics (a planted symlink is never followed) and fsync'd
|
|
184
|
+
together with its directory entry (the directory fsync is skipped on Windows, where `std` has no
|
|
185
|
+
directory handle and `FlushFileBuffers` on the marker covers its creation). The package enforces
|
|
186
|
+
both markers: `open` refuses a file carrying either, `create` refuses a path with a leftover
|
|
187
|
+
sidecar, and `watermark()` reads 0 on every handle while the latch is set — so a flush already
|
|
188
|
+
in flight on another handle, which still stamps the word, cannot revive the plane. In band
|
|
189
|
+
first: the sidecar is what a process that cannot map the file checks, the latch is what covers a
|
|
190
|
+
plane whose sidecar a crash lost. A temporary handle opened for the in-band mark is unmapped
|
|
191
|
+
and closed before the sidecar step — its own mapping would keep the file undeletable — and the
|
|
192
|
+
call fails only when neither marker is durable, leaving the file exactly as found.
|
|
193
|
+
|
|
170
194
|
**Backup/copy-db/reseed:** the file is node-local derived state. Backup either includes it
|
|
171
195
|
(consistent-enough after an msync barrier) or marks the index rebuild-on-restore. Replica
|
|
172
196
|
reseed = rebuild from records (C5 bulk construction makes this fast; until then, the existing
|
|
@@ -186,6 +210,60 @@ search(sliceHandles, queryVector: Float32Array, k, ef, filter?): Promise<{ids, d
|
|
|
186
210
|
- Auto-ef / auto-efC read the node count from the header high-water minus freelist length —
|
|
187
211
|
same semantics as today, minus the #2182 inflation (freed ids return to the pool).
|
|
188
212
|
|
|
213
|
+
**Upper-layer descent is a beam, not hill climbing** (`beam_descend`, `DESCENT_EF = 16`). The
|
|
214
|
+
textbook width-1 descent halts at the first upper-layer node no neighbor improves on. On a
|
|
215
|
+
clustered corpus that local minimum can sit in the wrong basin, and layer-0 adjacency is
|
|
216
|
+
intra-basin, so the layer-0 beam has no uphill edge with which to leave — the query's true
|
|
217
|
+
nearest neighbor is then unreachable at *any* ef, and raising ef only expands the wrong basin.
|
|
218
|
+
Measured on the `tests/concurrent.rs` corpus (8 000 nodes, 64-d, self-query every node at
|
|
219
|
+
ef 256, insertion order fixed by seed): width 1 loses 125 nodes over 200 builds, width 4 loses
|
|
220
|
+
20 over 200, width 8 loses 8 over 700, width 16 loses 0 over 700. Cost at 50 000 × 768-d:
|
|
221
|
+
visits/query +17 % to +27 %, p50 +0.06 ms flat (0.15 → 0.21 ms at ef 16, 0.21 → 0.28 at ef 64,
|
|
222
|
+
0.46 → 0.47 at ef 512 — the descent is a fixed cost, so it hurts most where ef is small), build
|
|
223
|
+
throughput -20 %. recall@10 improves below ef 128 (0.844 → 0.903 at ef 16, 0.983 → 1.000 at
|
|
224
|
+
ef 64) and is unchanged above.
|
|
225
|
+
|
|
226
|
+
`beam_descend` is shared by the read and write paths deliberately: insert must route through
|
|
227
|
+
the same graph its queries will, or nodes get their neighbors chosen from a basin searches
|
|
228
|
+
never reach. The width is a compile-time constant rather than a parameter because it is a
|
|
229
|
+
correctness floor, not a recall/latency dial — `ef` is the dial.
|
|
230
|
+
|
|
231
|
+
Three facts worth keeping when working on this.
|
|
232
|
+
|
|
233
|
+
The trap is a property of graph *shape*, not of concurrency: it reproduces single-threaded from
|
|
234
|
+
a fixed insertion permutation, and concurrency only shuffles that permutation. It also needs the
|
|
235
|
+
full corpus — no seed reproduces it at 32 dims, or at 2 000 / 4 000 nodes, so a shrunken repro
|
|
236
|
+
is not evidence of a fix. `descent_width_sweep` in `tests/concurrent.rs` (ignored by default) is
|
|
237
|
+
the harness behind the table above.
|
|
238
|
+
|
|
239
|
+
Read and write descent widths must match. Measured over 200 builds per cell: width 1 both sides
|
|
240
|
+
loses 125 nodes, read-only widening loses 37, **write-only widening loses 245 — worse than
|
|
241
|
+
either**, and both sides widened loses 0. `insert` seeds each level's `search_layer` from the
|
|
242
|
+
descent's landing point, so a graph wired under one routing policy and queried under another is
|
|
243
|
+
less navigable than one where they agree. This is also the upgrade story: an existing plane file
|
|
244
|
+
read by a new binary is the read-only row, improved but not repaired until its nodes are
|
|
245
|
+
re-inserted.
|
|
246
|
+
|
|
247
|
+
`HNSW_SWEEP_READ_EF` sets the sweep's query-side width; the build side is whatever `DESCENT_EF`
|
|
248
|
+
is compiled as, so the four cells are two runs per value of the constant:
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
HNSW_SWEEP_SEEDS=200 HNSW_SWEEP_READ_EF=1 cargo test --release --test concurrent \
|
|
252
|
+
descent_width_sweep -- --ignored --nocapture
|
|
253
|
+
HNSW_SWEEP_SEEDS=200 HNSW_SWEEP_READ_EF=16 cargo test --release --test concurrent \
|
|
254
|
+
descent_width_sweep -- --ignored --nocapture
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Do not add a per-level visit cap to the descent without re-measuring. The obvious ceiling,
|
|
258
|
+
`DESCENT_EF * UPPER_CAP` = 1024, is already exceeded by ordinary queries: the worst of 3 000
|
|
259
|
+
random queries visits 788 nodes at level 1 on a 50 000-node graph and 1 044 on a 500 000-node
|
|
260
|
+
one. A cap that binds silently degrades recall, which is the defect this exists to fix. What
|
|
261
|
+
bounds the pathological case instead is `search_layer`'s strict `d < worst`: with every distance
|
|
262
|
+
tied — a zero query ties them all at exactly 1.0 — a full result set never admits another
|
|
263
|
+
candidate, so the descent drains after `ef` expansions per level (measured 608 visits at 50 000
|
|
264
|
+
nodes, 990 at 500 000). `a_tied_distance_descent_stops_at_its_visit_cap` fails if that `<` is
|
|
265
|
+
ever relaxed.
|
|
266
|
+
|
|
189
267
|
**Filtering** (predicate-aware / ACORN, `filteredSearch = true` today):
|
|
190
268
|
|
|
191
269
|
1. **Bitset fast path.** RBAC allow-lists and companion-condition candidate sets are computed
|
|
@@ -272,6 +350,14 @@ Decided (Kris, 2026-08-31):
|
|
|
272
350
|
|
|
273
351
|
Open:
|
|
274
352
|
|
|
353
|
+
- **Atomic slot payloads.** Fields a concurrent reader acts on (flags, level, degree, scale,
|
|
354
|
+
invMag, neighbor and upper ids) are read through aligned `read_volatile`, which forbids the
|
|
355
|
+
reload/split/sink across the seqlock's validating fence that `lto = true, codegen-units = 1`
|
|
356
|
+
otherwise licenses. That is not the same as being race-free under Rust's memory model: only
|
|
357
|
+
making those fields `AtomicU8`/`AtomicU16`/`AtomicU32` in the slot layout would be, and that
|
|
358
|
+
is a format change deferred past phase 1. The stored vector stays an ordinary load on
|
|
359
|
+
purpose — `cosine_int8_raw` must keep autovectorizing, and a torn vector only perturbs a
|
|
360
|
+
distance the generation check discards.
|
|
275
361
|
- **msync cadence default** — bounded-lag durability window vs write amplification; needs a
|
|
276
362
|
workload measurement, not a guess.
|
|
277
363
|
- **f32 (quantization:"none") slot variant** — 3,072 B vectors → 3.4 KB slots; supported by the
|
package/README.md
CHANGED
|
@@ -35,15 +35,18 @@ the host's authoritative record store. Approximate indexes don't need per-commit
|
|
|
35
35
|
they need cheap, bounded catch-up. See [DESIGN.md](DESIGN.md) for the format, the
|
|
36
36
|
concurrency model, measured baselines, and the reasoning behind every trade.
|
|
37
37
|
|
|
38
|
-
## Install
|
|
38
|
+
## Install
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
41
|
npm install @harperfast/hnsw
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
(`
|
|
46
|
-
|
|
44
|
+
Prebuilt bindings ship as platform-specific `optionalDependencies`
|
|
45
|
+
(`@harperfast/hnsw-<platform>-<arch>[-glibc]`) for linux-x64, linux-arm64, darwin-arm64, and
|
|
46
|
+
win32-x64. Platforms without a published binding (musl, darwin-x64, win32-arm64) build from
|
|
47
|
+
source on install when a [Rust toolchain](https://rustup.rs) is present, and throw a clear
|
|
48
|
+
error otherwise. Linux x86_64 is the performance target (AVX2 + kernel-lock crash recovery);
|
|
49
|
+
macOS and Windows are functional (no lock takeover — bounded degradation instead).
|
|
47
50
|
|
|
48
51
|
## Usage
|
|
49
52
|
|
|
@@ -65,6 +68,13 @@ const predicated = await plane.searchWithPredicate(queryVector, 10, 512, (ids) =
|
|
|
65
68
|
);
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
A plane is derived state; when the host must stop maintaining one and cannot delete the file
|
|
72
|
+
(Windows sharing violations while another process maps it), `invalidatePlane(path)` — or
|
|
73
|
+
`plane.invalidateFile()` through a handle the host already holds — durably marks it
|
|
74
|
+
unadoptable: a one-way in-band latch (watermark reads 0, `Plane.open` refuses) plus a fsync'd
|
|
75
|
+
`<path>.stale` sidecar (`stalePathFor(path)`, which `open` also refuses). It throws only when
|
|
76
|
+
neither marker lands. Hosts delete both files and rebuild.
|
|
77
|
+
|
|
68
78
|
Full API in [index.d.ts](index.d.ts).
|
|
69
79
|
|
|
70
80
|
## Benchmarks
|
|
@@ -78,8 +88,8 @@ equal recall.
|
|
|
78
88
|
|
|
79
89
|
## Status
|
|
80
90
|
|
|
81
|
-
Extracted from the Harper vector-index engine; the format (
|
|
82
|
-
change with a
|
|
91
|
+
Extracted from the Harper vector-index engine; the format (v7) and API are young and may
|
|
92
|
+
change with a version bump + reindex (an older format version fails to open; rebuild). Roadmap: prebuilds, binary-quantized slot format
|
|
83
93
|
(~4× smaller traversal plane), Matryoshka dimension truncation, mremap growth, index
|
|
84
94
|
slicing with native top-k merge.
|
|
85
95
|
|
package/build.mjs
CHANGED
|
@@ -10,12 +10,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
const crateRoot = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const ifNeeded = process.argv.includes('--if-needed');
|
|
12
12
|
const artifact = join(crateRoot, 'hnsw-plane.node');
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// a prebuild satisfies --if-needed only if it actually LOADS on this system (a newer-glibc
|
|
16
|
-
// artifact exists but throws at require; fall through to a local build in that case)
|
|
13
|
+
// a platform optionalDependency or a loadable local artifact satisfies --if-needed (a
|
|
14
|
+
// binding that exists but fails to link falls through to a local build)
|
|
17
15
|
function loads(path) {
|
|
18
|
-
if (!existsSync(path)) return false;
|
|
19
16
|
try {
|
|
20
17
|
createRequire(import.meta.url)(path);
|
|
21
18
|
return true;
|
|
@@ -23,7 +20,11 @@ function loads(path) {
|
|
|
23
20
|
return false;
|
|
24
21
|
}
|
|
25
22
|
}
|
|
26
|
-
|
|
23
|
+
function platformPackageLoads() {
|
|
24
|
+
const libc = process.platform === 'linux' ? '-glibc' : '';
|
|
25
|
+
return loads(`@harperfast/hnsw-${process.platform}-${process.arch}${libc}`);
|
|
26
|
+
}
|
|
27
|
+
if (ifNeeded && (platformPackageLoads() || (existsSync(artifact) && loads(artifact)))) {
|
|
27
28
|
process.exit(0);
|
|
28
29
|
}
|
|
29
30
|
try {
|
package/index.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export interface SearchHit {
|
|
|
12
12
|
export declare class Plane {
|
|
13
13
|
/** Create a new plane file. `maxNodes` is a sparse reservation — pages materialize on write. */
|
|
14
14
|
static create(path: string, dims: number, layer0Cap: number, maxNodes: number): Plane;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Open an existing plane file. Throws on a format-version mismatch and on an invalidated
|
|
17
|
+
* plane (header latch or `.stale` sidecar): delete the file and its sidecar, rebuild.
|
|
18
|
+
*/
|
|
16
19
|
static open(path: string): Plane;
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -101,4 +104,42 @@ export declare class Plane {
|
|
|
101
104
|
flush(watermark?: number): void;
|
|
102
105
|
/** flush() on the libuv thread pool — a whole-map msync can stall its calling thread. */
|
|
103
106
|
flushAsync(watermark?: number): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* In-band half of invalidateFile() only — no sidecar, so a process that cannot map the
|
|
109
|
+
* file sees nothing; prefer invalidateFile(). Sets the one-way header latch, zeroes the
|
|
110
|
+
* watermark, msyncs the header page (a 4 KB barrier, not a whole-mapping flush). From then
|
|
111
|
+
* on every handle reads watermark 0, whatever a racing flush stamps, and open() throws.
|
|
112
|
+
*/
|
|
113
|
+
invalidate(): void;
|
|
114
|
+
/**
|
|
115
|
+
* invalidatePlane() through this handle: the in-band mark via this mapping (no second open,
|
|
116
|
+
* no second registry slot — on Windows this mapping is why the unlink failed) and the
|
|
117
|
+
* `.stale` sidecar next to the path it opened. The path must not have been replaced since.
|
|
118
|
+
*/
|
|
119
|
+
invalidateFile(): InvalidationOutcome;
|
|
120
|
+
/** Whether the plane was invalidated, by any handle, since this one opened. */
|
|
121
|
+
invalidated(): boolean;
|
|
104
122
|
}
|
|
123
|
+
|
|
124
|
+
export interface InvalidationOutcome {
|
|
125
|
+
/** The watermark was zeroed and its header page msync'd. */
|
|
126
|
+
inBand: boolean;
|
|
127
|
+
/** `<path>.stale` exists and is fsync'd (on POSIX, so is its directory entry). */
|
|
128
|
+
sidecar: boolean;
|
|
129
|
+
inBandError?: string;
|
|
130
|
+
sidecarError?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Make a plane file that could not be deleted unadoptable, durably, through a temporary
|
|
135
|
+
* handle that is unmapped and closed before this returns. Both markers are always attempted:
|
|
136
|
+
* the in-band latch and the fsync'd `.stale` sidecar; open() refuses a file carrying either.
|
|
137
|
+
* Throws only when neither marker became durable; nothing is deleted or renamed, and an
|
|
138
|
+
* in-band mark whose msync failed may still have landed in the shared mapping (the safe
|
|
139
|
+
* direction: it reads as incomplete). Idempotent. Synchronous (three small fsyncs on a cold path).
|
|
140
|
+
*/
|
|
141
|
+
export declare function invalidatePlane(path: string): InvalidationOutcome;
|
|
142
|
+
/** invalidatePlane() on the libuv thread pool. */
|
|
143
|
+
export declare function invalidatePlaneAsync(path: string): Promise<InvalidationOutcome>;
|
|
144
|
+
/** The sidecar convention: `<path>.stale`. */
|
|
145
|
+
export declare function stalePathFor(path: string): string;
|
package/index.js
CHANGED
|
@@ -1,30 +1,51 @@
|
|
|
1
|
-
// Loads the native module
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Loads the native module, mirroring @harperfast/rocksdb-js's model: a platform-specific
|
|
2
|
+
// optionalDependency package when one exists for this platform (linux bindings are split by
|
|
3
|
+
// libc), else a locally built artifact (`npm run build`, requires a Rust toolchain).
|
|
4
4
|
'use strict';
|
|
5
5
|
const { existsSync } = require('node:fs');
|
|
6
6
|
const { join } = require('node:path');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!
|
|
8
|
+
function libcSuffix() {
|
|
9
|
+
if (process.platform !== 'linux') return '';
|
|
10
|
+
let isMusl = false;
|
|
11
|
+
try {
|
|
12
|
+
const { glibcVersionRuntime } = process.report?.getReport?.()?.header ?? {};
|
|
13
|
+
isMusl = !glibcVersionRuntime;
|
|
14
|
+
} catch {
|
|
15
|
+
// fall through to ldd probing
|
|
16
|
+
}
|
|
17
|
+
if (!isMusl) return '-glibc';
|
|
18
18
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} catch
|
|
22
|
-
|
|
19
|
+
const { execSync } = require('node:child_process');
|
|
20
|
+
isMusl = execSync('ldd --version', { encoding: 'utf8', stdio: 'pipe' }).includes('musl');
|
|
21
|
+
} catch {
|
|
22
|
+
// ldd may not exist; keep the report-based verdict
|
|
23
|
+
}
|
|
24
|
+
return isMusl ? '-musl' : '-glibc';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const failures = [];
|
|
28
|
+
let native0;
|
|
29
|
+
// 1. platform package (published binding)
|
|
30
|
+
try {
|
|
31
|
+
native0 = require(`@harperfast/hnsw-${process.platform}-${process.arch}${libcSuffix()}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
failures.push(`platform package: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
// 2. local build (dev checkouts, source-build installs)
|
|
36
|
+
if (!native0) {
|
|
37
|
+
const local = join(__dirname, 'hnsw-plane.node');
|
|
38
|
+
if (existsSync(local)) {
|
|
39
|
+
try {
|
|
40
|
+
native0 = require(local);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
failures.push(`${local}: ${error.message}`);
|
|
43
|
+
}
|
|
23
44
|
}
|
|
24
45
|
}
|
|
25
46
|
if (!native0) {
|
|
26
47
|
throw new Error(
|
|
27
|
-
`@harperfast/hnsw could not load a native
|
|
48
|
+
`@harperfast/hnsw could not load a native binding for ${process.platform}-${process.arch}. ` +
|
|
28
49
|
'Build one with `npm run build` in ' +
|
|
29
50
|
__dirname +
|
|
30
51
|
' (requires a Rust toolchain: https://rustup.rs).' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harperfast/hnsw",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Persistent, incrementally-maintained, concurrently-searchable native HNSW for Node.js: a memory-mapped fixed-slot graph file with off-event-loop search, seqlock concurrency, int8 asymmetric distance, and bitset/predicate filtering.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"index.js",
|
|
14
14
|
"index.d.ts",
|
|
15
|
-
"prebuilds",
|
|
16
15
|
"hnsw-plane.node",
|
|
17
16
|
"src",
|
|
18
17
|
"build.rs",
|
|
@@ -40,5 +39,11 @@
|
|
|
40
39
|
"mmap",
|
|
41
40
|
"napi",
|
|
42
41
|
"rust"
|
|
43
|
-
]
|
|
42
|
+
],
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"@harperfast/hnsw-darwin-arm64": "0.2.1",
|
|
45
|
+
"@harperfast/hnsw-linux-arm64-glibc": "0.2.1",
|
|
46
|
+
"@harperfast/hnsw-linux-x64-glibc": "0.2.1",
|
|
47
|
+
"@harperfast/hnsw-win32-x64": "0.2.1"
|
|
48
|
+
}
|
|
44
49
|
}
|
package/src/bin/bench.rs
CHANGED
|
@@ -122,7 +122,7 @@ fn main() {
|
|
|
122
122
|
let build_start = Instant::now();
|
|
123
123
|
for i in 0..n {
|
|
124
124
|
let v = corpus.row(&mut rng);
|
|
125
|
-
insert(&graph, &v, ¶ms, &mut scratch);
|
|
125
|
+
insert(&graph, &v, ¶ms, &mut scratch).expect("build insert");
|
|
126
126
|
if (i + 1) % 50_000 == 0 {
|
|
127
127
|
let rate = (i + 1) as f64 / build_start.elapsed().as_secs_f64();
|
|
128
128
|
println!(" built {} ({:.0} inserts/s)", i + 1, rate);
|