Most vector databases you encounter are services: a server process, a network protocol, a deployment story. USearch from Unum-cloud takes the opposite path. It is a single-file C++11 header — include/usearch/index.hpp — that implements a multi-layer proximity graph for approximate nearest neighbor search, and it ships as a library you link or import, not a cluster you operate. Despite the small footprint, it is embedded in production systems you already know: ClickHouse and DuckDB both use it as their native vector index, and the project reports being trusted by teams at Google as well.
The repository covers an unusually wide surface for its size: C++, C99, Python, Rust, Java, JavaScript, Go, Swift, C#, Objective-C, and even Wolfram bindings all build from the same three headers. That makes it a great codebase to read if you want to understand how a real ANN engine is layered: a templated graph core, a type-punning layer that erases vector dtypes at runtime, and a stack of native bindings that each do their own FFI rather than relying on generated wrappers. In this tour we walk through that architecture with two diagrams, then get hands-on with the Python interface.
The overview below maps the whole system. On the left are the three public headers and how they include each other; in the middle, the runtime objects those headers define; on the right, the language bindings and the on-disk format that makes indexes portable between languages.
USearch overview: headers on the left define the runtime objects in the center; bindings consume the dense facade, and the serialization layer persists 64-byte-headed binary files.
Reading the overview from left to right:
index.hpp, L1 is the self-contained C++11 core. It needs no sibling includes and starts by detecting compiler, OS, and ISA macros, then defines version macros likeUSEARCH_VERSION_MAJOR, L10.index_gt, L2291 is the proximity graph itself: a template over key and slot types that owns the node tape, striped locks, and per-thread contexts. Itsadd, L3219 inserts through greedy descent, and itssearch, L3468 walks the layers down before refining candidates.index_plugins.hpp, L12 includes the core and extends it with the distance machinery:metric_kind_t, L115 enumeratescos,ip,l2sq,hamming,tanimoto,jaccard,haversineand more, whilescalar_kind_t, L140 covers every storage dtype fromf64down to single-bitb1x8and float8e5m2/e4m3.metric_punned_t, L2894 is the runtime metric dispatcher. Itsbuiltin, L2954 factory selects an ISA-specialized kernel, and itsstateless, L2984 factory wraps a raw function pointer — the hook that makes user-defined metrics possible.index_dense.hpp, L406 definesindex_dense_gt, the type-erased facade most users actually touch: it stores acasts_punned_tcaster so anf16index can acceptf32queries, and exposes typedadd/searchoverloads likeadd(f32...), L873 andsearch(f32...), L885.- The Python binding
python/lib.cpp, L1126 is a hand-written pybind11 module declared GIL-free. It exposes theIndexclass, L1251 with batch entry pointsadd_many, L1269 andsearch_many, L1280. - The Rust binding
rust/lib.rs, L668 takes a different route: acxxbridge to aNativeIndexobject, with a safe wrapper exposingsearchat L1449 andaddat L1507. - On the storage side,
index_dense_head_t, L42 is the 64-byte header that begins every serialized index, so a file written from Python can be memory-mapped from C++ or Rust.
Inside the Dense Layer
The detail diagram drills one level deeper, and it is where USearch’s design philosophy — “punning” — becomes visible. The graph core index_gt is a template: at compile time it works with whatever value and metric types you instantiate it with. But Python cannot instantiate C++ templates at runtime. So index_dense_gt erases all of that behind runtime state: a metric dispatcher, a caster table, and a pointer to the typed instantiation.
Detail view: the graph core (top), metrics and dtypes (left), the dense punning layer with its factory, insert, query, and serialization paths (center), and the Python and Rust binding internals (bottom).
Follow the search flow through the center of the diagram. A call into the dense facade lands on the per-dtype overload, which delegates to the private search_, L2242. That function casts your query buffer into the index’s native dtype using the caster, builds an index_search_config_t, L1728 carrying the per-query expansion and exact flag, and finally calls into the graph with a small closure as a predicate. That closure is the key trick: even the base graph’s search accepts an arbitrary predicate, so filtered search composes naturally instead of being bolted on. The public filtered_search, L897 overloads simply pass a user predicate through where the default path passes one that skips deleted keys.
Insertion works symmetrically. The dense add_, L2178 first tries to pop a free slot from the recycled-slots structure — deletions in USearch mark keys as free rather than rebuilding the graph — then allocates vector storage, registers the key-to-slot mapping, and calls either the graph’s add or its update path depending on whether a slot was recycled. The hierarchy itself is standard HNSW: nodes live on multiple levels, and level_of, L2838 reads how high a given slot reaches. When exactness matters more than speed, exact=true routes around the graph entirely into search_exact_, L4796, a brute-force scan.
Serialization is the third pillar. save, L1594 and load, L1601 accept paths, streams, or buffers, and view, L1353 memory-maps an existing file without loading it into RAM — the property the README credits with order-of-magnitude serving cost reductions on cloud block storage. The factory method make, L663 mirrors this: it constructs either from a metric configuration or directly from a file path with a view flag.
From Install to Your First Model
The Python package is on PyPI under the name usearch, and the whole first-run experience fits in a dozen lines:
# pip install usearch
import numpy as np
from usearch.index import Index
index = Index(ndim=384, metric="cos", dtype="bf16")
vectors = np.random.rand(10_000, 384).astype(np.float32)
index.add(keys=np.arange(10_000), vectors=vectors)
query = np.random.rand(384).astype(np.float32)
matches = index.search(query, 10)
print(matches[0].key, matches[0].distance)
Three defaults are worth knowing. The metric defaults to cosine; the dtype is chosen per hardware — the wrapper documents that nothing is stored as f32 unless you ask, because bf16 or f16 is nearly always the better trade-off on modern CPUs. And connectivity (the HNSW paper’s “M”) defaults to a compiled-in constant exposed by the module, as do the two expansion factors: expansion_add (the paper’s efConstruction) and expansion_search (ef), both changeable after construction through properties on the Index.
A realistic embedding workflow looks like this: encode your corpus with a sentence-embedding model — the same pattern used by sentence-transformers-style encoders or CLIP for multimodal data — add the vectors under your own integer keys, and keep the mapping from keys to documents in your application. Serialization then becomes one call, and the wrapper class wraps the C++ paths for save, L1101, load, and read-only view:
index.save("corpus.usearch")
# Later, on a serving box, without paying the RAM bill:
disk_index = Index(ndim=384, metric="cos", path="corpus.usearch", view=True)
For retrieval-augmented generation systems, this is the entire ANN layer: a few megabytes of compiled code, no server, and an index format you can ship alongside your model weights. Projects like LlamaIndex can sit on top of it, and if you export encoders through ONNX you can keep the whole serving stack free of framework dependencies.
Metrics, Quantization, and User-Defined Distances
USearch’s metric story is broader than the usual cosine/L2 pair. Spatial metrics (cos, ip, l2sq), binary coefficients (hamming, tanimoto, sorensen, jaccard), and even geographic haversine are built in, and the README leans into the unusual ones — molecular search over RDKit fingerprints and geo point-clouds are showcased applications.
Quantization follows a “downcast, don’t train” philosophy. Instead of learned product quantization, the same add and search calls transparently convert between f64, f32, bf16, f16, the float8 formats e5m2/e4m3, the float6 pair e3m2/e2m3, i8, u8, and single-bit b1x8, using SIMD paths when the hardware has them. You can check what your CPU gets with Index(ndim=768, metric="cos", dtype="f16").hardware_acceleration, which reports the ISA name the dispatcher selected. For quarter-precision, the README notes the constraints: i8 is only valid for cosine-like metrics and normalizes vectors to unit length before scaling, while b1x8 is only valid for binary metrics.
When none of the built-ins fit, the stateless, L2984 constructor path is exposed all the way up to Python as CompiledMetric: compile a Numba cfunc with the right signature, hand USearch its address, and the graph traverses with your distance. Because the search only requires comparable distances — not identical vector lengths — this opens genuinely odd use cases, like gzip-compression-ratio as a similarity function for fuzzy text matching.
Filtering, Clustering, and Joins
Filtered search deserves emphasis because most engines get it wrong. The naive approach retrieves top-k then filters, which falls apart when the filter is selective. USearch instead evaluates your predicate during graph traversal — every candidate is checked before it can enter the result set — and the README’s position is that predicates should compose with external containers like Bloom filters. Note the language support split though: the functionality table in the README shows filter predicates supported in C++, C, Rust, and Swift, but not yet in Python; in Python you filter after retrieval.
The same graph doubles as a clustering engine. index.cluster(min_count=..., max_count=...) produces a Clustering object with centroid popularity, per-cluster membership, and iterative sub-clustering, which the README positions against SciPy-style hierarchical clustering at much larger scale. Finally, Index.join computes stable-marriage-style one-to-one mappings between two indexes — fuzzy joins with sub-quadratic complexity — a database-flavored feature almost no other ANN library offers.
Honest Limits
Reading the source also clarifies what USearch is not, and every point below comes straight from the project’s own README and functionality table:
- It is approximate by default. HNSW trades exactness for speed; the
exact=Truebrute-force path exists, and the README reports it comparing well against FAISS’sIndexFlatL2in a Colab setting, but for small datasets a plain numpy scan may serve you fine. - Performance claims are the project’s own. The headline “10x faster HNSW than FAISS” comes from Unum’s Intel-published benchmark on Sapphire Rapids with equivalent recall — not from an independent audit. Reproduce it on your data before trusting it.
- Python lags C++ in features. No filter predicates, no joins in the other languages, no variable-length vectors outside C++, and 4B+-capacity
uint40_tslots are C++-only per the functionality table. - Quantization is lossy and one-way. Once vectors are stored downcast, the “get”-style accessors cannot recover the original data — the README explicitly tells you to keep originals elsewhere if you need them.
- No server, no replication, no consistency story. USearch is a library. Concurrency is handled inside the index (it is thread-safe for concurrent add and search), but durability, sharding, and multi-node serving are entirely your problem — that is what
Indexes, the multi-index lookup wrapper, partially addresses by viewing several shard files as one. - Key management is on you. Keys are integers; mapping them to real-world entities, handling renames and deletions, and compacting after removals are explicit API calls, not background processes.
That trade is the whole appeal, though. If your problem fits in one process and one machine’s disk, USearch gives you a serious ANN engine in a dependency-free header, with an index format you can read back from ten languages. The full tour above — graph core, punning layer, bindings, and file format — fits in roughly 3,000 lines of C++, which is exactly why it is worth reading in an afternoon. Enjoyed this post? Never miss out on future posts by following us