, _____
( \ _______ ______ _____\ \ _____ _____
) \ \ | | | / / | ||\ \ / |_
((\ ) | / / /|/ / /___/| \\ \ / \
|`-;-._ |\ \ \ |/| |__ |___|/ \\ \ | /\ \
|`-. ) \ \ \ | | | \ \| | ______ | | | \
| \ ( \| \| | | __/ __ | |/ \| \/ \
| \ ) |\ /| |\ \ / \ / ||\ /\ \
| |/ | \_______/ | | \____\/ | /_____/\_____/|| \_____\ \_____\
| `| \ | | / | | |____/| | | | ||| | | | |
| | \|_____|/ \|____| | | |______|/|____|/ \|_____|\|_____|
| | .
| | .
| | .
|_____| .
_(_______)_ .
| | .
|___________| .
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.
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 12Same 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.
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.
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 ./memoryRegister 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.
consolidateproposes; 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.
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.
| 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) |
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.99Building 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.
- Crash safety is a
kill -9test in CI: every acknowledged insert survives. Persistence format and recovery cases are indocs/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.
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).
MIT OR Apache-2.0.