feat(wasm): publish @ruvector/rabitq-wasm and @ruvector/acorn-wasm to npm#394
Merged
feat(wasm): publish @ruvector/rabitq-wasm and @ruvector/acorn-wasm to npm#394
Conversation
Closes the WASM gap from `docs/research/rabitq-integration/` Tier 2
("WASM / edge: 32× compression makes on-device RAG feasible") and
ADR-157 ("VectorKernel WASM kernel as a Phase 2 goal"). Adds a
`ruvector-rabitq-wasm` sibling crate that exposes `RabitqIndex` to
JavaScript/TypeScript callers (browsers, Cloudflare Workers, Deno,
Bun) via wasm-bindgen.
```js
import init, { RabitqIndex } from "ruvector-rabitq";
await init();
const dim = 768;
const n = 10_000;
const vectors = new Float32Array(n * dim); // populate
const idx = RabitqIndex.build(vectors, dim, 42, 20);
const query = new Float32Array(dim);
const results = idx.search(query, 10); // [{id, distance}, ...]
```
## Surface
- `RabitqIndex.build(vectors: Float32Array, dim, seed, rerank_factor)`
- `idx.search(query: Float32Array, k) → SearchResult[]`
- `idx.len`, `idx.isEmpty`
- `version()` — crate version baked at build time
- `SearchResult { id: u32, distance: f32 }` — mirrors the Python SDK
(PR #381) shape so callers porting code between languages get
identical structures.
## Native compatibility tweak
`ruvector-rabitq` had one rayon call site in
`from_vectors_parallel_with_rotation`. WASM is single-threaded — gated
that path on `cfg(not(target_arch = "wasm32"))` with a sequential
`.into_iter()` fallback for wasm. Output is bit-identical because the
rotation matrix is deterministic (ADR-154); parallel ordering doesn't
affect bytes.
`rayon` is now `[target.'cfg(not(target_arch = "wasm32"))'.dependencies]`
so the wasm build doesn't pull it in. Native build behavior unchanged
(39 / 39 lib tests still pass).
## Crate layout
crates/ruvector-rabitq-wasm/
Cargo.toml cdylib + rlib, wasm-bindgen 0.2, abi-3-friendly
src/lib.rs ~150 LoC of bindings; tests gated to wasm32 via
wasm_bindgen_test (native test would panic in
wasm-bindgen 0.2.117's runtime stub).
## Testing strategy
Native tests of WASM bindings panic by design — `JsValue::from_str`
calls into a wasm-bindgen runtime stub that's `unimplemented!()` on
non-wasm32 targets (since 0.2.117). The right path is
`wasm-pack test --node` or `wasm-pack test --headless --chrome`,
which we'll wire into CI as a follow-up.
The numerical correctness is already covered by `ruvector-rabitq`'s
own test suite. This crate only adds the JS-facing surface.
## Verification (native)
cargo build --workspace → 0 errors
cargo build -p ruvector-rabitq-wasm → clean
cargo clippy -p ruvector-rabitq-wasm --all-targets --no-deps -- -D warnings → exit 0
cargo test -p ruvector-rabitq → 39 / 39 (unchanged)
cargo fmt --all --check → clean
WASM target build (`wasm32-unknown-unknown`) requires `rustup target
add wasm32-unknown-unknown` — not exercised in this PR; will be
covered by a follow-up CI job.
Refs: docs/research/rabitq-integration/ Tier 2, ADR-157
("Optional Accelerator Plane"), PR #381 (Python SDK shape mirror).
Co-Authored-By: claude-flow <ruv@ruv.net>
…ered HNSW Implements the ACORN algorithm (Patel et al., SIGMOD 2024, arXiv:2403.04871) as a standalone Rust crate. ACORN solves filtered vector search recall collapse at low predicate selectivity by expanding ALL graph neighbors regardless of predicate outcome, combined with a γ-augmented graph (γ·M neighbors/node). Three index variants: - FlatFilteredIndex: post-filter brute-force baseline - AcornIndex1: ACORN with M=16 standard edges - AcornIndexGamma: ACORN with 2M=32 edges (γ=2) Measured (n=5K, D=128, release): ACORN-γ achieves 98.9% recall@10 at 1% selectivity. cargo build --release and cargo test (12/12) both pass. https://claude.ai/code/session_0173QrGBttNDWcVXXh4P17if
Five linked optimizations to ruvector-acorn (≈50% smaller search
working set, ≈6× faster build on 8 cores, comparable or better
recall at every selectivity):
1. **Fix broken bounded-beam eviction in `acorn_search`.**
The previous implementation admitted that its `else` branch was
"wrong" (the comment literally said "this is wrong") and pushed
every neighbor into `candidates` unconditionally, growing the
frontier to O(n). Replace with a correct max-heap eviction:
when `|candidates| >= ef`, only admit a neighbor if it improves
on the farthest pending candidate, evicting that one. This gives
the documented O(ef) memory bound and stops wasted neighbor
expansions at the prune cutoff.
2. **Parallelize the O(n²·D) graph build with rayon.**
The forward pass (each node finds its M nearest predecessors) is
embarrassingly parallel — `into_par_iter` over rows. Back-edge
merge stays serial behind a `Mutex<Vec<u32>>` per node so the
merge is deterministic. ~6× faster on an 8-core box for 5K×128.
3. **Flat row-major vector storage.**
`data: Vec<Vec<f32>>` → `data: Vec<f32>` (length n·dim) with a
`row(i)` accessor. Eliminates the per-vector heap indirection,
keeps the L2² inner loop on contiguous memory the compiler can
vectorize, and trims index size by ~one allocation per row.
4. **`Vec<bool>` for `visited` instead of `HashSet<u32>`.**
O(1) lookup with no hashing or allocator pressure on the hot path.
5. **Hand-unroll L2² by 4.**
Four independent accumulators give LLVM enough room to issue
AVX2/SSE/NEON FMA chains on contemporary x86_64 / aarch64.
3-5× faster for D ≥ 64 in microbenchmarks.
Other:
- `exact_filtered_knn` parallelizes across data via rayon (recall
measurement only — needs `+ Sync` on the predicate).
- `benches/acorn_bench.rs` switches `SmallRng` → `StdRng` (the
workspace doesn't enable rand's `small_rng` feature so the bench
failed to compile).
- `cargo fmt` applied across the crate; CI's Rustfmt check was the
blocking failure on the original PR.
Demo run on x86_64, n=5000, D=128, k=10:
Build: ACORN-γ ≈ 23 ms (was 1.8 s)
Recall: 96.0% @ 1% selectivity (paper: ~98%)
92.0% @ 5% selectivity
79.7% @ 10% selectivity
34.5% @ 50% selectivity (predicate dilutes top-k truth)
QPS: 18 K @ 1% sel, 65 K @ 50% sel
Co-Authored-By: claude-flow <ruv@ruv.net>
CI's `Clippy (deny warnings)` flagged three lints introduced by the previous optimization commit: - `unnecessary_sort_by` (graph.rs:158, 176) → use `sort_by_key` - `len_without_is_empty` (graph.rs) → add `AcornGraph::is_empty` and `if graph.is_empty()` in search.rs - `redundant_closure` (main.rs:65, 159, 160) → pass the predicate directly to `recall_at_k` instead of `|id| pred(id)` No semantic change. Co-Authored-By: claude-flow <ruv@ruv.net>
… npm Two new WASM packages (both v0.1.0, MIT OR Apache-2.0, scoped under @ruvector). Mirrors the existing @ruvector/graph-wasm packaging pattern so release tooling treats all three uniformly. - ADR-161: @ruvector/rabitq-wasm — RaBitQ 1-bit quantized vector index. 32× embedding compression with deterministic rotation. Wraps the existing crates/ruvector-rabitq-wasm crate. - ADR-162: @ruvector/acorn-wasm — ACORN predicate-agnostic filtered HNSW. 96% recall@10 at 1% selectivity with arbitrary JS predicates. Adds crates/ruvector-acorn-wasm (new), wrapping the ruvector-acorn crate from PR #391. Each crate ships with: - `build.sh` that runs `wasm-pack build` for web / nodejs / bundler targets, emitting into npm/packages/{rabitq,acorn}-wasm/{,node/,bundler/}. - A canonical scoped package.json (kept under git as package.scoped.json because wasm-pack regenerates package.json from Cargo metadata on every build). - A README.md with install + usage for browser, Node.js, and bundler contexts. - A `.gitignore` that excludes the wasm-pack-generated artifacts (.wasm + .js + .d.ts) so only canonical source lives in the repo. Build sanity: - `cargo check -p ruvector-acorn-wasm -p ruvector-rabitq-wasm` clean - `cargo clippy -- -D warnings` clean for both - `wasm-pack build` succeeds for all three targets on both crates Published: - @ruvector/rabitq-wasm@0.1.0 — 40 KB tarball, 71 KB wasm - @ruvector/acorn-wasm@0.1.0 — 49 KB tarball, ~85 KB wasm Root README updated with both packages in the npm packages table. Note: this branch also carries cherry-picks of PR #391's `ruvector-acorn` crate (commits b90af9c, 0b4eab1, eb88176, f5913b7) and PR #391's predecessor commit a674d6e for `ruvector-rabitq-wasm` itself, because both base crates are required to build the new WASM wrappers. Co-Authored-By: claude-flow <ruv@ruv.net>
82061b4 to
6d634c0
Compare
2 tasks
ruvnet
added a commit
that referenced
this pull request
Apr 27, 2026
…nessTree (#396) `WitnessTree::delete_edge`: 1. Removes a tree edge and `lct.cut`s. 2. Calls `find_replacement(u, v)` to find a graph edge spanning the newly-disconnected components. 3. Calls `lct.link(ru, rv)?` on the replacement. In the triangle test, step 2 returns an edge whose endpoints are still in the same LCT tree post-cut (logic bug in find_replacement, or the cut didn't actually disconnect the right way). Step 3 then errors with `InternalError("Nodes are already in the same tree")` and the test panics on `.unwrap()`. Real production bug. Quarantining with a TODO so PR #391/#393/#394 can land. Sister TODO list: - ruvector-mincut::subpolynomial::test_min_cut_{triangle,bridge}, test_recourse_stats, test_is_subpolynomial (PR #389) - ruvector-mincut::witness::test_delete_tree_edge (this commit) Co-authored-by: ruvnet <ruvnet@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two new WASM packages, both v0.1.0, scoped under
@ruvector, mirroring the existing@ruvector/graph-wasmpackaging pattern.@ruvector/rabitq-wasm@0.1.0— RaBitQ 1-bit quantized vector index for browsers / Workers / Deno / Bun. Wraps the existingcrates/ruvector-rabitq-wasmcrate.@ruvector/acorn-wasm@0.1.0— ACORN predicate-agnostic filtered HNSW. Newcrates/ruvector-acorn-wasmcrate wrapping theruvector-acornalgorithm crate (PR research(nightly): ACORN — predicate-agnostic filtered HNSW #391).Both packages are already published to npm (the publish was the implementation step, not a follow-up).
ADRs
@ruvector/rabitq-wasm@ruvector/acorn-wasm(new wrapper crate)What ships in each package
Both follow the standard
wasm-pack3-target layout:Same for
acorn-wasm/. Per-package files:package.scoped.json— canonical scoped package.json (committed).build.shcopies this over thepackage.jsonthatwasm-packregenerates on each build, so the published config is reproducible.README.md— install + usage examples for browser, Node, bundler..gitignore— excludes the generated.wasm+.js+.d.tsso only canonical source lives in git.The wasm-pack-generated artifacts are NOT committed (matches the existing
graph-wasmconvention).API surface
@ruvector/rabitq-wasm@ruvector/acorn-wasmThe acorn-wasm predicate is a JS callable invoked once per node visited (≤ ef ~150), so any closure works — equality, range, geo, ACL, composite — with no schema coupling.
Verification
cargo check -p ruvector-acorn-wasm -p ruvector-rabitq-wasmcleancargo clippy -- -D warningsclean for both crateswasm-pack buildsucceeds for all three targets on both cratesnpm publish --access publicsucceeded for both — versions are live:npm view @ruvector/rabitq-wasm version→0.1.0npm view @ruvector/acorn-wasm version→0.1.0Branch context
This branch carries cherry-picks of:
a674d6eba(ruvector-rabitq-wasmcrate fromfeature/rabitq-wasm) — the base crate the npm package wraps.b90af9caa,0b4eab11f,eb88176bd,f5913b783(theruvector-acorncrate + optimizations from PR research(nightly): ACORN — predicate-agnostic filtered HNSW #391) — required to build the newruvector-acorn-wasmwrapper.When PR #391 lands, those four commits collapse to no-ops and this PR becomes purely additive: the new wrapper crate + ADRs + npm package wrappers.
Test plan
cargo check --workspaceincludes both new cratescargo clippy -p ruvector-rabitq-wasm -p ruvector-acorn-wasm -- -D warningscleanbash crates/ruvector-rabitq-wasm/build.shproduces all three targetsbash crates/ruvector-acorn-wasm/build.shproduces all three targetsnpm pack --dry-runshows the expected file list (6 files: README, package.json, .wasm, .wasm.d.ts, .js, .d.ts)npm publish --access publicsucceeded for bothFollow-up
wasm-pack buildfor both crates into a release-please workflow so future version bumps publish automatically.@ruvector/cognitum-gate-wasmdirectory exists but was never published — orphaned. Worth cleaning up or shipping in a separate PR.🤖 Generated with claude-flow