Repository navigation
Releases
v0.7.0
Summary
Version 0.7.0 closes the remaining public contract gaps found during the 1.0 readiness audit. It rejects two forms of lossy NumPy coercion, makes a bound archive path stable for the lifetime of its store, and publishes the API and compatibility policy intended for 1.x. It also adds release checks that enforce package and artifact versions before publication.
Valid, documented 0.6 usage continues to work. This release does not add a new feature family, remove documented API, change search ranking, add a runtime dependency, or change archive format version 1.
Public API and 1.x compatibility policy
The supported package-level imports are now explicit:
from numpy_vector_store import VectorHit, VectorStore, __version__- The new public API and compatibility policy names the supported
VectorStoremethods and properties,VectorHitfields, return types, ownership behavior, exception classes, and persistence guarantees. - The policy distinguishes supported top-level imports from private helpers, submodule layout, subclass hooks, exact error wording, allocation details, and internal algorithms.
- The proposed 1.x rules cover semantic versioning, deprecation and removal, Python and NumPy support, and archive compatibility before those become long-term commitments in 1.0.
VectorHitremains an immutable record whose equality and hashability follow its values. Opaque metadata retains its own comparison and hashing behavior, so a hit containing a dictionary remains unhashable.
The runtime API remains centered on VectorStore, VectorHit, three explicit metric methods, typed opaque metadata, and the create/open/save/reload persistence lifecycle.
Lossless numeric inputs
- Python row-selector sequences containing a Python or NumPy Boolean are rejected before NumPy can coerce the Boolean to row zero or one. This includes mixed values such as
[0, True]. - Native NumPy integer selectors retain vectorized validation and the existing shape, uniqueness, and bounds rules.
- Complex vector batches passed to
add()and complex queries passed to any metric are rejected before conversion tofloat32can discard imaginary components. Complex dtypes are rejected even when their current values have a zero imaginary part. - Complex-query validation runs before empty-store and empty-filter shortcuts, so the accepted query contract does not depend on current store state.
Calls that deliberately used Boolean row indexes or complex vectors now raise TypeError. Real-valued integer and floating-point inputs keep their existing behavior.
Stable archive bindings
- A relative path is anchored to the current working directory when
save(path)orVectorStore.open(path)binds it. file_pathreturns the anchored absolute path, and latersave()andreload()calls remain pointed at the same archive if the process changes working directory.- Extensionless
.npzhandling and lexical path behavior remain unchanged. Anchoring does not resolve..components or dereference symlinks. - Save As remains transactional: a new binding becomes visible only after the new archive is written successfully.
Format version 1 is unchanged, and no archive conversion is required. Code that compares file_path with a relative path should compare against its anchored absolute form. Code that changed directories to redirect a later pathless save should pass the new destination explicitly to save(path).
Packaging and release safeguards
- Distribution metadata now uses the SPDX
MITlicense expression and includesLICENSEin both the wheel and source distribution. - The existing
py.typedmarker is declared throughTyping :: Typed, and the package is classified as Beta for the 0.7 adoption cycle. - Publication verifies that source
__version__, the GitHub release tag, wheel metadata, and source-distribution metadata agree. It also requires exactly one wheel and one source distribution so stale build output cannot be uploaded accidentally. - Remote GitHub Actions are pinned to immutable commits with readable release comments beside each pin.
- The full suite now runs on Windows with Python 3.13 in addition to Python 3.11 through 3.14 on Linux and the Python 3.11 minimum NumPy check.
- Archive regression coverage now includes unexpected fields, nonpositive dimensions, malformed array shapes, and platform-specific filesystem behavior.
The release verifier and benchmark tools remain outside the runtime wheel and add no package dependency.
Compatibility and upgrade guidance
- Python 3.11 through 3.14
- NumPy 1.23.2 or newer
- Archive format version 1
Most applications can upgrade from 0.6 without changes. Review code that used Boolean row selectors, supplied complex vectors or queries, compared file_path with a relative path, or relied on changing the process working directory to redirect a bound store. Persistence remains a trusted-file feature because opaque metadata uses pickle-backed NumPy object arrays.
See the repository changelog for the complete release record and the migration guide for applications upgrading from the older 0.3 persistence API.
Full comparison: v0.6.0...v0.7.0
v0.6.0
Summary
Version 0.6.0 makes the existing API's input and failure contracts predictable before 1.0. It closes cases where the same invalid call could behave differently on an empty and populated store, where Python booleans could act as row indexes or counts, and where loosely coerced configuration could change meaning after persistence. It also turns the project's performance claims and format-version-1 compatibility promise into reproducible regression evidence.
Valid, documented 0.5 usage continues to work unchanged. This release does not add a new feature family, archive format, runtime dependency, or public exception hierarchy.
Predictable scalar inputs
dimensions,top_k, andget(index)accept Python integers, NumPy integer scalars, and other integer-index values, then convert them to a Pythonint.- Python and NumPy booleans are rejected where integer semantics are required.
top_k=Trueno longer means one, andget(True)no longer invokes NumPy boolean indexing. normalizeaccepts Python and NumPy booleans and retains a canonical Pythonboolin memory and persistence.min_valueandmax_valueaccept finite Python and NumPy integer or floating-point scalars. Booleans, strings, complex values, arrays, and non-finite thresholds are rejected before search.
An inappropriate scalar type now raises TypeError, while a supported type with an invalid value raises ValueError. Row selector bounds use IndexError. Exact error wording remains explanatory rather than a compatibility guarantee.
Search checks independent of store state
within_rowsis validated before an empty result can be returned. It must be a one-dimensional sequence of unique integer row indexes, without booleans, and every index must exist in the current store.- Duplicate filtered indexes are rejected instead of returning the same stored row more than once.
- Zero-query rules now apply before empty-store shortcuts. Cosine search always requires a non-zero query. Dot-product and Euclidean searches require one when the store normalizes vectors, while raw stores continue accepting zero queries for those two metrics.
- Native NumPy integer selectors retain vectorized validation, and Python sequence validation no longer changes process-global warning filters.
These changes affect preconditions, not ranking. Valid 0.5 searches retain the same metric calculations, thresholds, deterministic tie handling, filtered-row semantics, and VectorHit results.
Persistence inputs and compatibility
open()andsave(path)accept strings and string-returning path-like objects. Explicitly empty paths raiseValueError, while inappropriate path types, including bytes paths, raiseTypeError.save()with no argument remains distinct fromsave(""): omission means save to the current binding; an empty string is invalid.- Extension resolution is unchanged. A path without
.npzresolves to the same archive for saving, opening, and reloading. - Persistence failures keep their owning exception types. Filesystem failures use the relevant
OSErrorsubclass, malformed schemas useValueError, and NumPy, pickle, or application metadata-loading exceptions are not hidden inside a package wrapper.
Archive format version 1 is unchanged. The compatibility suite now opens a fixture created by the published 0.4.0 package on Python 3.11 with NumPy 1.23.2 and verifies its configuration, vectors, and metadata. This protects forward reading of the self-describing format; it does not restore support for unversioned two-array archives or promise that an older package can read arbitrary future formats.
No archive conversion is required when upgrading from 0.5. Persistence remains a trusted-file feature because opaque metadata uses NumPy's pickle-backed object arrays.
Reproducible performance evidence
- New repository commands benchmark public exact search and ingestion and emit workload details, seeded input digests, complete timing samples, medians, Git state, Python and NumPy versions, timer information, thread-related environment variables, and NumPy build configuration as JSON.
- Structural tests protect geometric capacity reuse, partial top-k selection, and unfiltered access to the stored vector matrix without a preliminary full-matrix copy.
- Shared CI intentionally avoids wall-clock limits, which would make results depend on runner load rather than code structure.
The README reports an aligned cosine-search grid at 1,000, 10,000, and 100,000 rows for 384, 1,536, and 3,072 dimensions. Measurements on a 24 GB Apple M4 Mac mini range from about 0.032 ms per query at 1,000 rows × 384 dimensions to 20.509 ms at 100,000 rows × 3,072 dimensions. The largest matrix alone uses about 1.23 GB.
These are reference measurements, not latency guarantees. NumPy Vector Store is intended for small-to-medium, in-process exact search; 100,000 rows is an upper reference rather than a target for indefinite scaling. Workloads that routinely reach millions of vectors generally need an indexed or service-backed system.
Compatibility and upgrade guidance
- Python 3.11 through 3.14
- NumPy 1.23.2 or newer
- Archive format version 1
Applications using documented 0.5 input types should not need code changes. Review call sites that pass booleans where integers are expected, fractional result counts, string or boolean thresholds, duplicate or malformed within_rows values, zero queries that were only attempted against empty stores, or explicitly empty persistence paths. Those accidentally accepted or state-dependent cases now fail consistently at the public boundary.
See the repository changelog for the complete release record and the persistence migration guide for applications upgrading from the older 0.3 persistence API.
Full comparison: v0.5.0...v0.6.0
v0.5.0
Summary
Version 0.5.0 makes VectorStore state harder to corrupt accidentally, removes the repeated full-store copies that previously made small incremental additions increasingly expensive, and gives tied search results a stable order. It also completes the persistence transition announced in 0.4, leaving one explicit create/open/save/reload lifecycle.
Safer state ownership
- Make
dimensions,normalize, andfile_pathread-only properties so store configuration cannot drift away from the vectors it describes. - Return zero-copy, non-writeable views from
vectorsandmetadata. Request a new view afteradd(),clear(), orreload(); an older view may still refer to the previous buffer. - Return an independent
float32vector copy fromget(), preventing changes to a retrieved vector from affecting later searches. - Continue preserving opaque metadata payloads by reference. The metadata array protects row alignment, but mutable dictionaries, lists, and other payload objects remain owned by the application.
These boundaries protect ordinary use of the public API. They are not intended to make NumPy storage tamper-proof against deliberate access through private attributes, .base, ctypes, or similar escape hatches.
Better incremental ingestion
Repeated calls to add() now reuse private contiguous capacity instead of concatenating and copying every earlier row on each call. When capacity must grow, vector and metadata storage grow together and active rows are copied once. Spare capacity remains invisible to len(), inspection, search, retrieval, filtered search, and saved archives.
The public add(vectors, metadata) API, validation, normalization, metadata behavior, and insertion order are unchanged. clear() returns the store to empty arrays and releases the capacity retained by the store itself.
Deterministic tied results
Exact search ties now resolve by ascending original store row index. This rule applies when selecting rows at the top_k boundary as well as when ordering the final result, and filtered searches use original indexes regardless of the order supplied in within_rows.
Cosine and dot-product searches still rank larger values first; Euclidean search still ranks smaller distances first. Only exactly equal computed values use the row-index tie break.
Persistence migration completed
The compatibility window provided by 0.4 is now closed:
- Constructor
file_path=is removed. Create an in-memory store and callsave(path)to write and bind it. - Instance
load()is removed. UseVectorStore.open(path)to construct a store orreload()to refresh a bound store. - Context-manager persistence is removed. Call
save()explicitly where the application intends to persist state. - The reader for old, unversioned archives containing only
vectorsandmetadatais removed.
Archive format version 1 is unchanged. Code already using VectorStore.open(), save(path), save(), and reload() needs no persistence migration. Unversioned archives must be converted with version 0.4 using their original dimensions and normalization mode, or recreated from source data, before upgrading. The persistence migration guide contains side-by-side replacements and conversion instructions.
Concurrency and trust boundaries
Concurrent searches, retrievals, and inspection are supported only while the store and shared metadata payloads remain unchanged. Applications must synchronize every access when any thread may add, clear, reload, save, or mutate shared metadata. The package does not provide internal locking, file locking, snapshots for concurrent writes, or multi-writer archive coordination.
Archives still contain NumPy pickle-backed object arrays for opaque metadata. Only open files produced by your application or another trusted source.
Compatibility
- Python 3.11 through 3.14
- NumPy 1.23.2 or newer
- Archive format version 1
See the repository changelog for the complete release record.
Full comparison: v0.4.0...v0.5.0
v0.4.0
Summary
Version 0.4.0 makes persistence explicit, self-describing, and safer to update. Earlier archives stored vectors and metadata but omitted the settings needed to interpret those vectors correctly. This release records that configuration in every new archive and clearly separates creating, opening, saving, and reloading a store.
Explicit persistence lifecycle
- Add
VectorStore.open(path)to construct a store from a versioned archive and restore its dimensions and normalization mode. - Let
save(path)perform the first save or a Save As operation and bind that destination. Latersave()calls update the bound archive. - Add
reload()as a deliberate refresh from disk. Failed reads or validation leave current in-memory state unchanged. - Keep creating a new in-memory store separate from opening one on disk so file access and persistence boundaries remain visible in application code.
Versioned, self-describing archives
- Write archive format version 1 with
format_version,dimensions,normalize,vectors, andmetadatafields. - Validate the complete archive before changing live store state, including configuration, array dtypes and shapes, row counts, finite values, and normalized-store zero-vector rules.
- Reject unsupported versions and malformed archives clearly rather than guessing omitted configuration or partially applying valid fields.
- Continue preserving each metadata item as one opaque row payload.
Safer archive replacement
- Write each save to a uniquely named temporary archive in the destination directory, close it, and replace the destination with
os.replace. - Preserve the previous complete archive when writing or replacement fails and clean up temporary files after failures.
- Bind a new Save As destination only after its archive is written successfully.
This provides atomic visibility: readers see the previous complete archive or the new complete archive instead of a partially written file. It does not provide file locking, multi-writer coordination, or a universal power-loss durability guarantee.
Migration window
- Constructor
file_path=, instanceload(), and direct context-manager persistence remain available in 0.4 withFutureWarning. They will be removed in 0.5. - The deprecated context manager now saves only after a successful block. It does not save or suppress the exception when the block raises.
- Older archives containing only
vectorsandmetadataremain readable through the configuration-aware legacy API. Loading one warns, and its next save rewrites it as format version 1. VectorStore.open()intentionally rejects an unversioned archive because that file cannot report its original dimensions or normalization semantics.
Applications should migrate an old archive once with 0.4 using its original configuration or recreate it from source data. The legacy reader is removed in 0.5 rather than maintained indefinitely. See the persistence migration guide for side-by-side API replacements and conversion instructions.
Runtime compatibility
- Support Python 3.11 through 3.14. Python 3.10 remains supported by the 0.3 release series but is not supported by 0.4.
- Raise the minimum NumPy version from 1.21.3 to 1.23.2, the earliest release that supports Python 3.11.
- Test every supported Python version and the minimum Python/NumPy combination in GitHub Actions.
Upgrade notes
- Search, insertion, retrieval, clearing, normalization, and metadata behavior are unchanged from 0.3.2.
- Code using
VectorStore.open(),save(path),save(), andreload()is on the persistence API intended for 0.5. - Transitional entry points continue to work in 0.4 but warn so the required 0.5 migration is visible during testing.
- Metadata still uses NumPy's pickle-backed object-array loading. Only open archives produced by your application or another trusted source.
- Mutable public state, repeated-add performance, deterministic tie ordering, and a formal thread-safety contract remain planned for later releases.
See the repository changelog for the complete release record.
Full comparison: v0.3.2...v0.4.0
v0.3.2
Summary
Version 0.3.2 is a reliability and performance patch that makes existing vector storage, search, metadata, and persistence behavior safer and more predictable. It does not change public method signatures or the existing vectors and metadata .npz archive format.
Numerical reliability
- Reject vectors, queries, and search thresholds that contain non-finite values or cannot remain finite when represented as
float32. - Calculate norms and raw metric intermediates with
float64wherefloat32could overflow or underflow. - Allow zero vectors in raw stores for dot-product and Euclidean search.
- Raise a clear error when raw cosine search includes a zero vector, where cosine similarity is undefined.
- Avoid duplicate full-size
float64buffers during raw Euclidean distance calculations.
Persistence
- Resolve paths without an
.npzsuffix consistently for both saving and loading. - Allow
load()to be retried after a missing persistence file is created. - Let
clear()reset load state so a later explicitload()can restore saved rows. - Keep repeated
load()calls idempotent after a successful load.
Metadata
- Preserve tuple, list, dataclass, scalar, and dictionary values as individual opaque metadata rows.
- Keep payload boundaries intact through insertion, search results, saving, and loading.
- Continue rejecting explicitly multidimensional NumPy metadata arrays rather than silently flattening ambiguous input.
Search memory use
- Search the stored vector matrix directly when
within_rowsis omitted instead of copying every stored row first. - Preserve original store indexes and metadata during filtered searches.
- Document the temporary matrix allocation used when
within_rowsselects a subset.
Compatibility and validation
- Validate Python 3.10 through 3.14 in GitHub Actions.
- Add a dedicated Python 3.10 check for the minimum supported NumPy version.
- Raise the minimum NumPy requirement from 1.20 to 1.21.3.
- Require linting, formatting, type checking, and the full test matrix before PyPI publication.
Upgrade notes
- Existing trusted
.npzarchives remain readable. - Environments using NumPy 1.20 must upgrade to NumPy 1.21.3 or newer.
- Inputs that previously produced
nan,inf, or unreliable rankings now raiseValueError.
See the repository changelog for the complete release record.
Full comparison: v0.3.1...v0.3.2
v0.3.1
Summary
This patch release publishes numpy-vector-store as a PEP 561 typed package.
Highlights
- Added the
py.typedmarker to the distributed package. - Installed consumers can now use the package's inline annotations with mypy and other type checkers.
- Imports such as
from numpy_vector_store import VectorStoreno longer require# type: ignore[import-untyped].
Runtime APIs and behavior are unchanged.
v0.3.0
Summary
This release broadens numpy-vector-store from cosine-only search to a small exact vector-search API with explicit normalization semantics.
Highlights
- Added
normalize: bool = TruetoVectorStore, keeping normalized cosine search as the default behavior. - Added
dot_search(...)for dot-product ranking. - Added
euclidean_search(...)for nearest-neighbor distance ranking. - Added raw-vector mode with
normalize=Falsefor workloads where magnitude or original geometry matters. - Documented normalization behavior, method-selection guidance, and the behavior matrix for normalized vs raw storage.
- Kept
.npzpersistence minimal with onlyvectorsandmetadata; callers should load with the intendednormalizesetting.
Breaking changes
- Removed deprecated
add_vectors(...). - Removed deprecated legacy
search(...)tuple API.
Use add(...) and the explicit metric methods: cosine_search(...), dot_search(...), or euclidean_search(...).
v0.2.0
This release introduces the new stable pre-1.0 API direction for numpy-vector-store: a small, typed, exact cosine vector store built on NumPy, with explicit composable filtering and no framework-specific metadata query layer.
Highlights
- Added
VectorStore[TMetadata]for typed metadata payloads. - Added
VectorHit(index, value, metadata)as the typed search result object. - Added
VectorStore.add(vectors, metadata)as the preferred insertion API. - Added
VectorStore.cosine_search(...)as the preferred explicit cosine similarity search API. - Added
within_rowsfor efficient prefiltered search using row indexes from Python, NumPy, pandas, SQL, or any external filtering system. - Added
__len__support. - Simplified persistence around trusted local
.npzfiles containing onlyvectorsandmetadata.
Compatibility
The older 0.1 methods are still available but now deprecated:
add_vectors(...)still works and delegates toadd(...).search(...)still works and returns the same tuple shape as before.- Both methods now emit
DeprecationWarning.
metadata_schema has been removed. The supported model is now: store metadata as an opaque row payload, perform filtering externally, and pass matching row indexes to within_rows.
Breaking Changes
- Python support now requires Python
>=3.10. metadata_schemais no longer supported.- The documented search result shape has moved from tuples to
VectorHit. - The preferred search method is now
cosine_search, withmin_valueinstead of the legacyscore_cutoff.
Why This Release Matters
0.2.0 defines the project’s intended API shape before broader adoption: explicit cosine search, typed metadata, simple local persistence, and composable filtering without taking on a metadata query language or embedding framework responsibilities.
v0.1.0
Full Changelog: https://github.com/tvanreenen/numpy-vector-store/commits/v0.1.0