Skip to content
olartgaboPublic

About

From-scratch vector search engine in Rust: HNSW + PQ + SIMD, designed as native agent memory over MCP

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

VELA

     ,                              _____                                      
    ( \              _______    ______   _____\    \  _____                _____         
     ) \             \      |  |      | /    / |    ||\    \             /      |_       
    ((\ )             |     /  /     /|/    /  /___/| \\    \           /         \      
   |`-;-._            |\    \  \    |/|    |__ |___|/  \\    \         |     /\    \     
   |`-.   )           \ \    \ |    | |       \         \|    | ______ |    |  |    \    
   |   \ (             \|     \|    | |     __/ __       |    |/      \|     \/      \   
   |    \ )             |\         /| |\    \  /  \      /            ||\      /\     \  
   |    |/              | \_______/ | | \____\/    |    /_____/\_____/|| \_____\ \_____\ 
   |    `|               \ |     | /  | |    |____/|   |      | |    ||| |     | |     | 
   |     |                \|_____|/    \|____|   | |   |______|/|____|/ \|_____|\|_____| 
   |     |                                                                           .   
   |     |                                                                           .   
   |     |                                                                           .   
   |_____|                                                                           .   
 _(_______)_                                                                           .   
|           |                                                                           .   
|___________|                                                                            .   

A from-scratch vector search engine in Rust — HNSW from the paper, SIMD kernels, crash-safe persistence — designed as native agent memory over MCP.

VELA is the layer other projects import: a Hierarchical Navigable Small World index built from Malkov & Yashunin (2016), Product Quantization from Jégou et al. (2011), hand-written AVX2 and NEON distance kernels, a snapshot + write-ahead-log store with kill -9 recovery, and an MCP server whose tool surface is designed for how agents actually use memory. Everything in the hot path — graph, kernels, PQ, k-means, storage format — is written from scratch; the third-party dependencies are the ONNX runtime for embeddings and JSON/CLI plumbing.

Status: the engine, benchmarks, MCP interface, ingestion/clients, and the Raspberry Pi deployment are done and tested. The demo video is the remaining piece. Desktop numbers are x86-64; Pi numbers are measured on a Pi 5 and labelled as such — nothing here is a QEMU timing.

Benchmarks

SIFT1M (1M × 128-d), same machine same day, pinned versions (faiss-cpu 1.14.3, hnswlib 0.8.0), M=16, ef_construction=200, ef=100, k=10, single-threaded search, recall@10 vs. the shipped ground truth. Build shown single-threaded (t1) and 12-thread (t12); search single-threaded throughout.

Library build t1 (s) build t12 (s) QPS recall@10
hnswlib 0.8.0 514.9 156.5 3332 0.9828
VELA 656.9 170.2 2577 0.9852
FAISS HNSW 1.14.3 1075.6 208.7 1619 0.9854

Reading it honestly:

  • VELA beats FAISS HNSW on every axis — 1.6× faster build (t1), 1.6× QPS, at equal recall.
  • VELA trails hnswlib — ~28% slower build (t1), ~23% lower QPS at equal recall. That gap is the honest loss; it decomposed to memory latency in the neighbor scan, and a prefetch pass (on by default, results bit-identical) has since closed much of it — measured +24–28% QPS in-process, pending a clean-machine re-run before it's quoted as a headline.

The perf profile behind these (distance is ~80% of build time; ~59% LLC-miss rate inside the distance function) is why the 11× hot-cache AVX2 kernel collapses to 1.9× end-to-end at 1M: the memory wall, not arithmetic, bounds the build. That's the write-up's centerpiece, and it's stated plainly rather than hidden.

Reproduce (Linux, dataset auto-downloads to ~/.cache/vela/datasets):

cargo run --release -p vela-bench -- recall --dataset sift1m --index hnsw --ef 100 --threads 12

On a Raspberry Pi 5

Same SIFT1M, same parameters, single-threaded, NEON kernels — measured on the hardware, not emulated. Cold start at 51 °C, and vcgencmd get_throttled reported the same flags before and after the run, so nothing throttled inside the measured window.

build t1 (s) QPS recall@10
desktop (x86-64, AVX2) 656.9 2577 0.9852
Pi 5 (aarch64, NEON) 810.4 2264.9 0.9852

Recall is identical to the digit on both machines. That is the point of the bit-for-bit kernel agreement, and it is a measurement rather than a claim: a million-vector graph built with NEON and one built with AVX2 return the same neighbors.

The prefetch pass that buys +24-28% QPS on the desktop is worth +7.2% here (2112 → 2264 QPS, five alternating rounds on a loaded graph). Different memory system, different payoff; the desktop figure does not transfer.

Product Quantization

Two-stage PQ (ADC scan → exact re-rank), codebooks trained with hand-rolled k-means++. SIFT1M, compression measured as scan bytes per vector:

Config bytes/vec recall@10 (ADC only) recall@10 (re-rank 1000) QPS
full precision 512 — 0.9852 ~2600
PQ8×8 8 0.312 0.9856 75
PQ16×8 16 0.545 0.9993 36

Re-rank recovers full-precision recall from an 8-byte code; the honest framing is that full vectors stay resident for the re-rank stage, so PQ buys scan bandwidth, not resident footprint.

Agent memory over MCP

The MCP server is the primary interface, not a REST wrapper. It embeds text server-side (bundled all-MiniLM-L6-v2 via ONNX), so a client sends raw text:

cargo run --release -p vela-mcp -- fetch-model          # ~90 MB, once
cargo run --release -p vela-mcp -- serve --store ./memory

Register it with Claude Code and it works with zero glue:

claude mcp add vela -- /path/to/target/release/vela-mcp serve --store /path/to/memory
Tool What it does
remember(content, metadata?) Embeds and stores; returns the id.
recall(query, k?, filters?) Semantic search, optionally filtered by a MongoDB-flavored metadata predicate applied during graph traversal.
forget(id) Soft-delete (tombstone) with an audit entry.
consolidate(similarity, dry_run?) Find near-duplicate memories and merge each group into its newest member. Dry-run by default.
compact(dry_run?) Rebuild the index without tombstoned memories, reclaiming space. Crash-safe; renumbers ids.

Resources memory://stats, memory://graph/health, and memory://audit let an agent introspect its own memory. Transports are stdio (local) and streamable HTTP with per-client read-only / read-write scopes; a dry-run consolidate or compact is a read, applying one is a write, and that split is enforced at the authorization boundary.

Two design findings worth the click, both from measurement rather than opinion:

  • Consolidation is human-in-the-loop by necessity. "the wifi password is hunter2" and "…hunter3" embed closer (0.975) than two genuine paraphrases of the same fact (0.836) — so no similarity threshold separates a duplicate from a contradiction. consolidate proposes; a reader disposes.
  • CLIP's cross-modal cosine is uncalibrated. A correct text→image top hit scores ~0.28, not the ~0.8 a good paraphrase scores in the text store. Only ordering is meaningful; a threshold carried from the text store would reject every correct answer.

The homelab demo

An ingestion pipeline (vela-ingest for documents, vela-photo for CLIP image embeddings) syncs a file tree into a running server as an MCP client, not a second writer — incremental, content-hash-deduped, crash-resumable. Clients: Claude via config, and a Telegram bot (vela-telegram). "dog photos from 2023" becomes a CLIP text embedding plus a {taken_year: 2023} filter — the filtered search earning its keep. Full runbook: docs/clients.md; the Pi deployment itself is in docs/pi-deployment.md.

From scratch vs. reused

Component Origin
HNSW graph — build, search, both neighbor-selection heuristics from scratch (paper)
Distance kernels — scalar, AVX2, NEON (bit-identical on SIFT) from scratch
Product Quantization + k-means++ codebook training from scratch
PRNG (xoshiro256**), heaps, dataset loaders, recall harness from scratch
Snapshot + WAL format, crash recovery, compaction from scratch
MCP protocol layer — JSON-RPC over stdio and streamable HTTP from scratch (no SDK)
BERT WordPiece + CLIP byte-level BPE tokenizers from scratch (validated vs. HuggingFace)
SHA-256, CRC32, EXIF reader, CLIP image preprocessing from scratch (validated vs. references)
Transformer inference (ort / ONNX Runtime) reused (embeddings only, never the index)
JPEG/PNG decoding (image), TLS HTTP for Telegram (ureq) reused (demo layer)
serde, clap, anyhow/thiserror, criterion reused (not hot path)

Quickstart

Linux (x86-64 or aarch64):

# validate the exact-search baseline on the small SIFT10K set (~5 MB)
cargo run --release -p vela-bench -- download --dataset siftsmall
cargo run --release -p vela-bench -- recall --dataset siftsmall --index bruteforce --k 100   # 1.0000
cargo run --release -p vela-bench -- recall --dataset siftsmall --index hnsw --ef 100         # ≥0.99

Building under WSL2 from a Windows-mounted path? Point the target dir at native ext4 for sane build times: export CARGO_TARGET_DIR=$HOME/.cache/vela-target.

Design notes

  • Crash safety is a kill -9 test in CI: every acknowledged insert survives. Persistence format and recovery cases are in docs/persistence.md.
  • Filtered search gates admission to the result set, never routing — so the graph stays navigable — with a visit budget that reports truncation instead of silently walking the whole graph under a selective predicate.
  • NEON, AVX2, and scalar agree bit-for-bit on SIFT (integer components stay exactly representable in f32), so Pi recall must equal desktop recall — any difference would be a bug, not a rounding story. Measured: 0.9852 on both.

Out of scope (by design)

Distributed/multi-node index, other index types (IVF/DiskANN), GPU kernels, full graph-repair deletion (tombstones + compaction instead), a query language beyond JSON filter predicates, and training embedding models (inference only).

License

MIT OR Apache-2.0.

About

From-scratch vector search engine in Rust: HNSW + PQ + SIMD, designed as native agent memory over MCP

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages