NumPy Vector Store
A fast, lightweight, zero-setup in-memory vector store powered by NumPy.
- Tiny local vector search for projects that do not need a vector database
- Fast exact vector search using vectorized NumPy operations
- Simple typed API returning
VectorHit(index, value, metadata) - Composable filtering by passing prefiltered row indexes with
within_rows - Portable persistence as trusted local
.npzfiles withvectors+metadata - No framework opinions: bring your own embeddings, chunking, async, and metadata model
Why?
This library is purpose-built for small to medium-scale vector search tasks and offers a simple alternative to heavyweight vector databases when you do not need network services, indexing infrastructure, ingestion pipelines, or domain-specific metadata filtering.
When/Where?
Below are benchmark results for cosine similarity search to help you assess its suitability for your use case.
| Embedding Type | Dimensions | ~5ms | ~25ms | ~100ms | ~500ms |
|---|---|---|---|---|---|
| Sentence Transformers | 384 | 1K vectors 1.5MB |
10K vectors 15MB |
100K vectors 147MB |
500K vectors 732MB |
| OpenAI Small | 1536 | 500 vectors 3MB |
5K vectors 29MB |
25K vectors 147MB |
100K vectors 586MB |
| OpenAI Large | 3072 | 200 vectors 2MB |
2.5K vectors 29MB |
5K vectors 59MB |
25K vectors 293MB |
Benchmarks performed on Apple M2 hardware.
Installation
uv add numpy-vector-store
Quick Start
import numpy as np
from numpy_vector_store import VectorStore
store = VectorStore[dict[str, str]](dimensions=3)
store.add(
vectors=np.array([
[1.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.0, 0.0, 1.0],
]),
metadata=[
{"title": "x-axis"},
{"title": "y-axis"},
{"title": "z-axis"},
],
)
hits = store.cosine_search(
query=np.array([0.9, 0.1, 0.0]),
top_k=2,
)
for hit in hits:
print(f"{hit.metadata['title']}: {hit.value:.3f}")
metadata is an outer sequence with one opaque payload for each vector row.
Each payload can be a dict, dataclass, tuple, list, string, integer row ID, or
another Python object that fits your application. Tuple and list payloads remain
single row values rather than being interpreted as additional array dimensions.
Normalization
VectorStore defaults to normalize=True, which scales each stored vector to
length 1. Normalization preserves vector direction while discarding magnitude:
[3.0, 4.0] -> [0.6, 0.8]
This is the default because it makes cosine similarity fast and direction-only,
which is the common case for semantic embeddings. Use normalize=False when
vector length matters, such as when magnitude encodes strength, confidence,
counts, scale, or raw geometry.
Zero vectors are rejected when normalize=True because they cannot be scaled to
unit length. Raw stores accept zero vectors for dot-product and Euclidean
search. Because cosine similarity is undefined for zero vectors,
cosine_search raises an error when its selected rows include one; use
within_rows to exclude zero rows when needed.
Numerical inputs
Stored vectors use float32 to keep the store compact. Vectors and queries must
remain finite when converted to float32, and search thresholds must also be
finite. Invalid values are rejected before they can affect stored state or
ranking.
Norms and raw metric values use float64 accumulation where float32
intermediate calculations could overflow or underflow. This allows finite
float32 vectors across the representable magnitude range to be normalized and
compared reliably.
| Method | normalize=True default |
normalize=False |
|---|---|---|
cosine_search |
True cosine similarity over stored unit vectors; fastest/default path for embeddings | True cosine similarity over raw vectors; computes vector norms during search |
dot_search |
Dot product of unit vectors, effectively equivalent to cosine similarity | True dot product over original vectors; use when magnitude should affect ranking |
euclidean_search |
Distance between normalized directions; useful only when direction-normalized distance is intended | True Euclidean distance over original vectors; use for geometric/feature-space nearest neighbors |
get |
Returns normalized vectors | Returns original vectors |
save |
Saves normalized vectors | Saves raw vectors |
load |
Loads and normalizes vectors | Loads vectors exactly as stored |
Search Methods
Use cosine_search for semantic embeddings and direction-only similarity:
hits = store.cosine_search(query, top_k=10, min_value=0.75)
Use dot_search with normalize=False when larger-magnitude vectors should
rank higher:
store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
store.add(vectors, metadata)
hits = store.dot_search(query, top_k=10, min_value=0.0)
Use euclidean_search with normalize=False for raw coordinate or feature-space
nearest-neighbor search:
store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
store.add(vectors, metadata)
hits = store.euclidean_search(query, top_k=10, max_value=1.5)
Prefiltering
The store does not implement a metadata query language. To filter by metadata,
produce row indexes first, then pass them with within_rows.
rows = [
i
for i, metadata in enumerate(store.metadata)
if metadata["title"].startswith("x")
]
hits = store.cosine_search(query, top_k=10, within_rows=rows)
Searches without within_rows compute directly against the stored vector matrix
and do not make a full copy of it. A filtered search gathers the selected rows
into a temporary matrix, so its additional memory use scales with the number of
selected rows and the vector dimensions. Omit within_rows when every row
should be searched; passing every row explicitly would create an unnecessary
full-size temporary matrix.
For structured NumPy metadata, use NumPy to produce the row indexes:
metadata_table = np.array(
[
("intro", "A", 2024),
("setup", "A", 2023),
("guide", "B", 2024),
],
dtype=[("title", "U20"), ("product", "U10"), ("year", "i4")],
)
store = VectorStore[int](dimensions=3)
store.add(vectors, metadata=np.arange(len(metadata_table)))
mask = (metadata_table["product"] == "A") & (metadata_table["year"] >= 2024)
rows = np.flatnonzero(mask)
hits = store.cosine_search(query, within_rows=rows)
for hit in hits:
row = metadata_table[hit.metadata]
print(row["title"], hit.value)
Persistence
Pass a file_path and call save() / load() explicitly:
store = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
store.add(embeddings, metadata)
store.save()
loaded = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
loaded.load()
The .npz suffix may be omitted. An extensionless path such as "vectors" is
resolved to "vectors.npz" for both saving and loading.
If you save with normalize=False, load with normalize=False too:
store = VectorStore[dict[str, str]](
dimensions=1536,
file_path="raw-vectors.npz",
normalize=False,
)
store.add(raw_vectors, metadata)
store.save()
loaded = VectorStore[dict[str, str]](
dimensions=1536,
file_path="raw-vectors.npz",
normalize=False,
)
loaded.load()
Context manager usage auto-saves on exit:
with VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz") as store:
store.add(embeddings, metadata)
Persistence uses a minimal NumPy .npz contract with vectors and metadata
arrays. The .npz file does not encode the normalize setting; choose the same
setting when loading that you used when saving. Loading validates shape,
dimensions, row counts, and zero-norm vectors. A load() call made before the
file exists can be retried after the file is created. Repeated calls after a
successful load do nothing unless clear() resets the in-memory store. Opaque
metadata values remain individual row payloads across save/load round trips.
Persistence uses allow_pickle=True for flexible Python metadata payloads, so
only load files generated by your own application or another trusted local
process. Loading untrusted .npz files is not a supported security model.
Compatibility
This project is still pre-1.0, so occasional breaking changes are expected while the API stabilizes. Changes are documented in the changelog and GitHub release notes. Deprecated APIs will keep warning for at least one point release before removal.
Supported Python versions are listed in the package classifiers and exercised in CI. The project generally retains stable CPython versions until their upstream end-of-life, adds new versions after its dependencies and CI support them, and drops versions only in minor releases.
See the changelog for release history and the project roadmap for the planned path to stable API and persistence contracts.
Contributing
git clone https://github.com/tvanreenen/numpy-vector-store.git
cd numpy-vector-store
uv sync --frozen --group dev
Before submitting a pull request:
- Run
uv run ruff check - Run
uv run ruff format --check - Run
uv run mypy src/ - Run
uv run pytest
License
MIT License - see LICENSE file for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file numpy_vector_store-0.3.2.tar.gz.
File metadata
- Download URL: numpy_vector_store-0.3.2.tar.gz
- Upload date:
- Size: 56.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d7022f2a93c2045fe29e87eb62f03ee0830e140f4b0c25f954a95edaff73e29a
|
|
| MD5 |
45f6e814f40422751a8f26111c4f087b
|
|
| BLAKE2b-256 |
128d4f9b6a8b92928819378c796146c6a05dc3fffb565c46b93ad02278bd0890
|
Provenance
The following attestation bundles were made for numpy_vector_store-0.3.2.tar.gz:
Publisher:
publish-pypi.yml on tvanreenen/numpy-vector-store
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
numpy_vector_store-0.3.2.tar.gz -
Subject digest:
d7022f2a93c2045fe29e87eb62f03ee0830e140f4b0c25f954a95edaff73e29a - Sigstore transparency entry: 2260968949
- Sigstore integration time:
-
Permalink:
tvanreenen/numpy-vector-store@d2c481c355b930b95f417514132898b96cf15e6f -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/tvanreenen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@d2c481c355b930b95f417514132898b96cf15e6f -
Trigger Event:
release
-
Statement type:
File details
Details for the file numpy_vector_store-0.3.2-py3-none-any.whl.
File metadata
- Download URL: numpy_vector_store-0.3.2-py3-none-any.whl
- Upload date:
- Size: 10.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8f3d552f67c68bebb7f48f94c53cfd26d6777087b94240eac7f50c582aeacc23
|
|
| MD5 |
4855238b5b9fd6d3bebca33dd9ef16eb
|
|
| BLAKE2b-256 |
221609151c6a6969fdb3b3c7101bd00e532459e3f27c6b4b9089455999b808a3
|
Provenance
The following attestation bundles were made for numpy_vector_store-0.3.2-py3-none-any.whl:
Publisher:
publish-pypi.yml on tvanreenen/numpy-vector-store
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
numpy_vector_store-0.3.2-py3-none-any.whl -
Subject digest:
8f3d552f67c68bebb7f48f94c53cfd26d6777087b94240eac7f50c582aeacc23 - Sigstore transparency entry: 2260969164
- Sigstore integration time:
-
Permalink:
tvanreenen/numpy-vector-store@d2c481c355b930b95f417514132898b96cf15e6f -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/tvanreenen
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@d2c481c355b930b95f417514132898b96cf15e6f -
Trigger Event:
release
-
Statement type: