diff --git a/npm/packages/diskann/README.md b/npm/packages/diskann/README.md index bf514ec49..1d8049ba7 100644 --- a/npm/packages/diskann/README.md +++ b/npm/packages/diskann/README.md @@ -1,54 +1,171 @@ # @ruvector/diskann -DiskANN/Vamana approximate nearest neighbor search — built in Rust, runs on all platforms. +[![npm](https://img.shields.io/npm/v/@ruvector/diskann.svg)](https://www.npmjs.com/package/@ruvector/diskann) +[![License](https://img.shields.io/npm/l/@ruvector/diskann.svg)](https://github.com/ruvnet/ruvector/blob/main/LICENSE) +[![Node](https://img.shields.io/node/v/@ruvector/diskann.svg)](https://nodejs.org) -Implements the Vamana graph algorithm from ["DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node" (NeurIPS 2019)](https://proceedings.neurips.cc/paper/2019/hash/09853c7fb1d3f8ee67a61b6bf4a7f8e6-Abstract.html). +**DiskANN / Vamana** approximate-nearest-neighbor (ANN) search for Node.js — a Rust core compiled to native `.node` addons via [NAPI-RS](https://napi.rs/) for Linux x64/arm64, macOS x64/arm64, and Windows x64. + +DiskANN is the SSD-friendly graph index from Microsoft Research that powers billion-scale vector search on a single machine. This package implements the **Vamana** graph construction with **α-robust pruning** ([NeurIPS 2019](https://proceedings.neurips.cc/paper/2019/hash/09853c7fb1d3f8ee67a61b6bf4a7f8e6-Abstract.html)) plus optional **Product Quantization** (PQ) and **mmap** persistence so working set ≪ dataset size. + +## Why DiskANN + +| | HNSW (in-memory) | **DiskANN (this package)** | +|---|---|---| +| Scale | <1M vectors, fully resident in RAM | **1M – 1B+ vectors**, SSD-backed | +| Memory | full vectors in RAM | only graph + optional PQ codes in RAM | +| Insert | incremental | batch (build once after inserts) | +| Search | sub-ms | **~55µs** (5K · 128d · k=10, M-series) | +| Best for | real-time routing, small corpora | large-corpus RAG, retrieval, embeddings store | + +## Capabilities + +- **Vamana graph** with two-pass construction (α=1.0 then α=1.2) and α-robust pruning — the published DiskANN algorithm, not a clone of HNSW. +- **Optional Product Quantization** (M subspaces × 256 centroids, trained with k-means++ / Lloyd's) for compressed in-memory codes + fast distance tables. +- **Memory-mapped persistence** — `save()` writes a flat slab + graph + (optional) PQ codes; `load()` mmaps so the OS pages in only touched vectors. +- **Async builds and searches** that off-load to a blocking thread pool so the Node event loop stays responsive. +- **Batch insert** API for high-throughput ingestion of millions of vectors. +- **Delete** support (tombstoned then re-pruned at build). +- **Cache-friendly internals** — contiguous `FlatVectors`, generation-counter `VisitedSet` (O(1) per-query reset), flat PQ distance tables, 4-accumulator ILP for L2. +- **Optional SimSIMD acceleration** (NEON / AVX2 / AVX-512) in the Rust crate; Node bindings ship with the portable build. +- **TypeScript types** included. +- **Cross-platform prebuilds** for `linux-x64-gnu`, `linux-arm64-gnu`, `darwin-x64`, `darwin-arm64`, `win32-x64-msvc` — no toolchain or `node-gyp` required at install time. ## Install ```bash npm install @ruvector/diskann +# or +pnpm add @ruvector/diskann +# or +yarn add @ruvector/diskann ``` -## Usage +Requires Node ≥ 18. The matching platform binary (`@ruvector/diskann-`) is pulled in automatically as an optional dependency — there is no install-time compilation. + +## Quick Start ```javascript const { DiskAnn } = require('@ruvector/diskann'); +// 1. Create the index const index = new DiskAnn({ dim: 128 }); -// Insert vectors -for (let i = 0; i < 1000; i++) { +// 2. Insert vectors (string id + Float32Array) +for (let i = 0; i < 10_000; i++) { const vec = new Float32Array(128); for (let d = 0; d < 128; d++) vec[d] = Math.random(); index.insert(`vec-${i}`, vec); } -// Build Vamana graph -index.build(); +// 3. Build the Vamana graph (one-time, required before search) +await index.buildAsync(); -// Search +// 4. Search const query = new Float32Array(128).fill(0.5); -const results = index.search(query, 10); -console.log(results); // [{ id: 'vec-42', distance: 0.123 }, ...] +const results = await index.searchAsync(query, 10); +// [ { id: 'vec-42', distance: 0.123 }, ... ] -// Persist +// 5. Persist + reload index.save('./my-index'); const loaded = DiskAnn.load('./my-index'); ``` -## Performance +### With Product Quantization -| Metric | Value | -|--------|-------| -| Search latency | **55µs** (5K vectors, 128d, k=10) | -| Recall@10 | **0.998** | -| Build | ~6s for 5K vectors | +Trade a small recall hit for far smaller in-memory footprint and faster candidate scoring on millions of vectors: + +```javascript +const index = new DiskAnn({ + dim: 768, + pqSubspaces: 96, // 96 bytes per vector instead of 768 × 4 = 3072 B + pqIterations: 12, + maxDegree: 64, + buildBeam: 128, + searchBeam: 96, + alpha: 1.2, +}); +``` + +### TypeScript + +```typescript +import { DiskAnn, DiskAnnOptions, DiskAnnSearchResult } from '@ruvector/diskann'; + +const opts: DiskAnnOptions = { dim: 384, searchBeam: 96 }; +const index = new DiskAnn(opts); + +const hits: DiskAnnSearchResult[] = index.search(query, 10); +``` ## API -See full documentation at [github.com/ruvnet/ruvector](https://github.com/ruvnet/ruvector). +### `new DiskAnn(options)` + +| Option | Type | Default | Meaning | +|---|---|---|---| +| `dim` | `number` | — *(required)* | Vector dimensionality | +| `maxDegree` | `number` | `64` | Vamana graph out-degree R | +| `buildBeam` | `number` | `128` | Beam width during construction (L_build) | +| `searchBeam` | `number` | `64` | Beam width at query time (L_search) | +| `alpha` | `number` | `1.2` | α-robust pruning factor (≥ 1.0) | +| `pqSubspaces` | `number` | `0` | PQ subspaces M (0 disables PQ) | +| `pqIterations` | `number` | `10` | k-means iterations for PQ training | +| `storagePath` | `string` | — | Optional path used by the mmap layer | + +### Methods + +| Method | Description | +|---|---| +| `insert(id: string, vector: Float32Array): void` | Insert a single vector | +| `insertBatch(ids: string[], vectors: Float32Array, dim: number): void` | Insert N vectors packed as a flat `Float32Array` of length `N · dim` | +| `build(): void` | Build the Vamana graph (and train PQ if enabled) | +| `buildAsync(): Promise` | Same, off-loaded to a blocking thread pool | +| `search(query: Float32Array, k: number): DiskAnnSearchResult[]` | k-NN search | +| `searchAsync(query, k): Promise` | Async k-NN search | +| `delete(id: string): boolean` | Tombstone a vector (effective after next build) | +| `count(): number` | Number of vectors currently in the index | +| `save(dir: string): void` | Persist index files into `dir` | +| `static load(dir: string): DiskAnn` | Load and mmap an index from `dir` | + +Search results are `{ id: string, distance: number }`, where `distance` is squared-L2. + +## Benchmarks + +Reference measurements on an Apple-silicon M-series laptop, release build, single-thread search. PQ is **off** unless noted. + +| Dataset | Dim | Vectors | Build | Search (k=10) | Recall@10 | +|---|---|---|---|---|---| +| Synthetic | 64 | 2,000 | ~1.4 s | ~22 µs | **1.000** | +| Synthetic | 128 | 5,000 | ~6.2 s | **~55 µs** | **0.998** | +| Synthetic, 50 queries | 64 | 2,000 | — | — | **0.998** avg | + +Validated by the in-tree Rust test suite (17 tests across distance, PQ, Vamana, and end-to-end index) plus the Node integration test that ships with the package (`npm test`). + +## When NOT to use this + +- You have **fewer than ~10K vectors** and don't need persistence → a brute-force scan is faster and simpler. +- You need **real-time incremental inserts with immediate searchability** → use HNSW (see `@ruvector/router`). DiskANN requires a build pass. +- You're operating in a browser → this is a native Node addon; use the WASM-based packages in the ruvector family instead. + +## Algorithm notes (one paragraph) + +Insertion appends vectors to a contiguous `FlatVectors` buffer. `build()` computes the medoid (point nearest the centroid, parallel via rayon), initializes a bounded-degree random graph, then runs two passes of *greedy-search-from-medoid → α-robust-prune → bidirectional-edge-update*: pass 1 with α=1.0 (accuracy), pass 2 with α=1.2 (navigability). If `pqSubspaces > 0`, a Product Quantizer is trained with k-means++ initialization and Lloyd's iterations; per-query, a distance table is precomputed so PQ distance is a sum of M table lookups. Search is greedy beam-search from the medoid with a top-L candidate pool; with PQ enabled, top results are re-ranked with exact L2. + +For the full design — including persistence layout, optimization rationale, and trade-off analysis — see [ADR-146: DiskANN/Vamana Implementation](https://github.com/ruvnet/ruvector/blob/main/docs/adr/ADR-146-diskann-vamana-implementation.md). + +## Related packages + +- [`@ruvector/router`](https://www.npmjs.com/package/@ruvector/router) — in-memory HNSW router (sub-millisecond, small/medium corpora) +- [`ruvector`](https://www.npmjs.com/package/ruvector) — umbrella package; lazily wraps DiskANN when this addon is installed +- Rust crate: [`ruvector-diskann`](https://crates.io/crates/ruvector-diskann) + +## Links + +- Repository: +- Issues: +- DiskANN paper (NeurIPS 2019): ## License -MIT +[MIT](https://github.com/ruvnet/ruvector/blob/main/LICENSE) diff --git a/npm/packages/diskann/package.json b/npm/packages/diskann/package.json index eb7ecb217..6fe404d70 100644 --- a/npm/packages/diskann/package.json +++ b/npm/packages/diskann/package.json @@ -1,6 +1,6 @@ { "name": "@ruvector/diskann", - "version": "0.1.0", + "version": "0.1.1", "description": "DiskANN/Vamana — SSD-friendly billion-scale approximate nearest neighbor search with product quantization", "main": "index.js", "types": "index.d.ts",