←Projects

vectra

2026

A Python library for vector search that I built from scratch, with support for exact search, approximate nearest-neighbour search, and vector quantization.

PythonNumPyHNSWVector QuantizationGitHub

I wanted to understand how vector databases handle search under the hood, so I built Vectra without relying on existing approximate nearest-neighbour libraries like FAISS. You give it embedding vectors, and it can search them either exactly or approximately using an HNSW graph implemented in NumPy.

The library supports cosine similarity, Euclidean distance, and dot product, all using float32 vectors.

The main API is a single VectorIndex class. It supports adding batches of vectors, searching, deleting entries, rebuilding the index, and filtering results using metadata. Queries can also be run asynchronously or in batches. I added a CLI for building indexes, inspecting them, and running benchmarks.

HNSW

The main part of the project is the approximate search implementation. I implemented the Hierarchical Navigable Small World (HNSW) algorithm directly in NumPy.

The index uses randomly assigned levels to build a multi-layer graph. Searches start at the upper layers, where the graph helps narrow down the search area, before moving to a more detailed search at the bottom layer.

I also implemented the neighbour selection heuristic used when building the graph, along with the main HNSW parameters: M, ef_construction, and ef_search.

One thing I wanted to support was adjusting the search quality without having to rebuild the index. You can pass a different ef_search value for an individual query, trading search speed for recall depending on what you need.

I also wrote a benchmark script to measure recall, query latency, and memory usage using clustered datasets.

How a query crosses the graph

The query enters at the top of the graph, where links are long and few.

  • vector
  • hop
  • 1result
Each vector sits on every layer up to a random height, so the top stays sparse and the base stays dense. Nodes further left are nearer this query. A real graph links each node to 16 neighbours; this draws only the path the search takes.

Storage and quantization

I wanted indexes to be usable beyond what could comfortably fit in memory, so I built a custom binary storage format called .vdb.

The file is split into sections, with a header and a table describing where each section is stored. These include the configuration, IDs, raw vectors, deletion bitmask, metadata, quantization codes, and serialized HNSW graph.

I also implemented memory-mapped loading through VectorIndex.mmap(). This maps the vector section directly from disk, allowing a large flat index to be searched without loading the entire vector data into memory first.

What a search reads

The file is a header, then a table of named sections.

index.vdb
  • headerVTRA
  • configjson
  • idsutf-8
  • vectorsfloat32
  • alivebitmask
  • metadatajson
  • codesoptional
  • hnswgraph
Indexes persist as a sectioned .vdb file: magic header, then config, ids, vectors, an alive bitmask, metadata, optional codes, and the HNSW graph. mmap keeps the vector bytes on disk until a search touches them.

I added two quantization methods to reduce how much space the vectors take up.

Scalar quantization maps each dimension to a uint8 value, reducing vector storage by roughly 4x. Product quantization takes a different approach: it splits each vector into smaller subspaces and learns a separate k-means codebook for each one. Vectors can then be represented using compact codes instead of storing their original values.

Both quantization methods, including the clustering used to build the codebooks, are implemented directly in the library.

Scalar quantization

Each dimension starts as a 32-bit float.

0.14
0.86
0.41
0.93
0.28
0.67
stored width32-bit
A scalar quantizer maps every dimension onto a uint8 bin. Distances are then computed on the codes, so a large flat index can stay in a quarter of the memory.
Product quantization

The vector is cut into subspaces that are quantized on their own.

subspace a
  • 0
  • 1
  • 2
  • 3
subspace b
  • 0
  • 1
  • 2
  • 3
stored21
A product quantizer learns a small codebook for each subspace. What gets stored is the list of chosen codebook indexes. Four centroids are drawn here. A real codebook is larger.