Skip to content

Performance and practical limits

Exact search scores every selected row. Budget for vector dimensions, metadata, temporary arrays, and your query rate. Validate representative workloads on your hardware before deciding whether an in-process collection fits.

Memory and workload shape

The active vector matrix alone needs rows × dimensions × 4 bytes. This excludes private spare capacity, metadata objects, temporary arrays, and Python overhead. Filtered searches allocate a gathered vector matrix, and raw Euclidean search uses a float64 difference matrix. Concurrent searches multiply temporary memory.

Small top-k queries use partial selection before sorting the selected hits; reducing top_k does not remove the full candidate metric calculation. Repeated small additions reuse geometric capacity. Passing a batch still reduces Python and validation overhead when inputs are already available together.

Published reference measurements

The following section is shared with the standalone package README. Its commit, machine, and dependency versions describe the measurement, not a new benchmark run of every later release.

VectorStore performs exact search over every selected row. The following measurements are reference points, not latency guarantees:

Each cell shows the median time per cosine query followed by the stored vector matrix size:

Rows 384 dimensions 1,536 dimensions 3,072 dimensions
1,000 0.032 ms · 1.5 MB 0.049 ms · 6.1 MB 0.088 ms · 12.3 MB
10,000 0.286 ms · 15.4 MB 1.065 ms · 61.4 MB 2.122 ms · 122.9 MB
100,000 2.983 ms · 153.6 MB 9.877 ms · 614.4 MB 20.509 ms · 1.23 GB

These benchmarks intentionally stop at 100,000 rows. NumPy Vector Store is designed for small-to-medium, in-process exact search; 100,000 rows is an upper reference, not a promised limit or a target for continued scaling. The practical boundary depends on vector dimensions, metadata, available memory, and latency requirements. Workloads that routinely reach millions of vectors generally need an indexed or service-backed system.

These are unfiltered top_k=10 searches on a normalized store. Each row divides the median duration of seven measured 20-query trials by 20, after two discarded warmup trials. The vector matrix size excludes metadata, temporary search arrays, and Python process overhead.

The measurements were taken from commit 4b23810 on a 24 GB Apple M4 Mac mini with macOS 26.6.1, CPython 3.13.5, NumPy 2.3.3, and Accelerate BLAS. Hardware, operating system activity, Python and NumPy versions, BLAS implementation, and thread settings can all change the result.

The repository includes benchmark commands that emit the inputs, environment, raw samples, and median as JSON:

uv run python benchmarks/benchmark.py search \
  --rows 10000 --dimensions 384 --queries 20 --top-k 10 \
  --warmup 2 --repetitions 7 > /tmp/nvs-search.json

uv run python benchmarks/benchmark.py ingest \
  --rows 10000 --dimensions 384 --batch-size 1 \
  --warmup 2 --repetitions 7 > /tmp/nvs-ingest.json

For the same 10,000-by-384 prepared input, repeated single-row ingestion had a median of 78.6 ms, or about 127,000 rows per second. Supplying 1,000 rows per add() call had a median of 6.63 ms, or about 1.51 million rows per second. Both measurements include construction of a fresh normalized store and every add() call, but exclude input generation.

See the benchmark guide for the timed regions, complete options, output fields, and interpretation notes. The project keeps structural complexity checks in CI, but does not fail shared runners on wall-clock timing.