03/Search + storage/2025–26
VecStream
Vector search with HNSW
A compact vector-search engine centered on a from-scratch HNSW implementation. It does not wrap FAISS or hnswlib.
- SEARCH START
- GREEDY RESULT
- QUERY
- TOP 10
Fig. 03.1 / A real graph built by VecStream, layer by layer
01/LAYER 3
Every query starts its descent here, at the graph’s single entry point.
220 VECTORS / M=4 / ef_search 40
Recall,
measured.
VecStream is an engineering study of graph construction, recall and latency tradeoffs, numerical behavior and crash-safe local persistence. It is deliberately small enough that the important algorithm fits in one module.
An exact cosine search is kept as a separate recall oracle, so every approximate result can be checked against ground truth.
01/Five decisions
- 01
Layered graph
Node levels are drawn as floor(−ln u / ln M), so each layer is exponentially smaller than the one below. A seed makes levels and topology reproducible.
- 02
Diversity heuristic
Picking the nearest M neighbors tends to choose a tight cluster of nearly collinear points. A candidate is accepted only if it is closer to the query than to every already selected neighbor, which keeps links in different directions.
- 03
Normalize once
Vectors are normalized at insertion, so cosine distance becomes 1 − dot(a, b). Profiling found the original distance function recomputing both norms on every edge evaluation.
- 04
A graph that validates itself
validate() checks the entry point, level membership, reciprocal edges, degree limits (2M at level 0, M above) and vector norms.
- 05
Crash-safe checkpoints
Each mutation writes an immutable generation, fsynced before an atomically replaced pointer makes it visible. On open, VecStream falls back once to the previous complete generation or raises an explicit error. Corruption never becomes an empty database.
02/Evidence
- 0.99recall@10 at ef_search 40
- 1.1 msp50 query latency at that recall
- 1.0kLines of Python in the engine
- 44Tests, including property tests on randomized graph mutations
Committed smoke measurement, not a large-scale claim: 1,000 Gaussian vectors, 64 dimensions, 50 queries, M=16, ef_construction=100, seed 42, Python 3.14.3 and NumPy 2.5.3 on Apple arm64. Latency is single-query wall time. Results are hardware and dataset specific.
| ef_search | recall@10 | p50 ms | p99 ms | QPS |
|---|---|---|---|---|
| 10 | 0.768 | 0.522 | 0.666 | 1,902 |
| 20 | 0.908 | 0.746 | 0.857 | 1,327 |
| 40 | 0.990 | 1.106 | 1.189 | 903 |
| 80 | 0.996 | 1.475 | 1.652 | 674 |
| 160 | 1.000 | 1.777 | 1.861 | 562 |
What was measured, and what wasn’t
Greater search effort recovers more exact neighbors at the cost of throughput: recall climbs from 0.768 to 1.000 as ef_search rises from 10 to 160, while queries per second fall from 1,902 to 562.
Five insertion orders at ef_search 20 gave recall@10 from 0.904 to 0.922, which shows topology sensitivity on one small dataset and is not a significance claim. Deleting 20% of vectors did not monotonically reduce recall, but rebuilding the remaining vectors raised it from 0.916 to 0.954, so an unrepaired graph and a fresh one are measurably different.
The normalize-once change is documented without a speedup number: the original benchmark did not measure recall at comparable settings, so none is claimed.
Tests include Hypothesis property tests on randomized graph mutations, explicit graph validation, checkpoint recovery and corruption cases, and rejection of empty, wrong-dimension, non-numeric, NaN and infinite inputs.
03/Limits
An educational, research-quality local engine, not a production database.
The graph is in memory, and mutation and search concurrency are not synchronized.
Metadata filtering is post-filtered, and physical deletion can reduce graph quality.
Python graph traversal is the main scaling boundary. No replication, transactions or multi-process writers.
SourceView on GitHub