numpy-vector-store 0.3.2__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/.github/workflows/checks.yml +5 -6
  2. numpy_vector_store-0.5.0/CHANGELOG.md +291 -0
  3. numpy_vector_store-0.5.0/MIGRATION.md +289 -0
  4. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/PKG-INFO +163 -42
  5. numpy_vector_store-0.5.0/README.md +395 -0
  6. numpy_vector_store-0.5.0/ROADMAP.md +423 -0
  7. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/pyproject.toml +4 -5
  8. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/src/numpy_vector_store/__init__.py +1 -1
  9. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/src/numpy_vector_store/vector_store.py +333 -104
  10. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/tests/test_vector_store.py +699 -183
  11. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/uv.lock +4 -110
  12. numpy_vector_store-0.3.2/CHANGELOG.md +0 -73
  13. numpy_vector_store-0.3.2/README.md +0 -273
  14. numpy_vector_store-0.3.2/ROADMAP.md +0 -140
  15. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/.github/FUNDING.yml +0 -0
  16. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  17. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/.github/workflows/publish-testpypi.yml +0 -0
  18. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/.gitignore +0 -0
  19. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/LICENSE +0 -0
  20. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/justfile +0 -0
  21. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/src/numpy_vector_store/py.typed +0 -0
  22. {numpy_vector_store-0.3.2 → numpy_vector_store-0.5.0}/tests/__init__.py +0 -0
@@ -55,7 +55,6 @@ jobs:
55
55
  fail-fast: false
56
56
  matrix:
57
57
  python-version:
58
- - "3.10"
59
58
  - "3.11"
60
59
  - "3.12"
61
60
  - "3.13"
@@ -84,7 +83,7 @@ jobs:
84
83
  run: uv run --locked pytest
85
84
 
86
85
  minimum-numpy:
87
- name: Test minimum NumPy on Python 3.10
86
+ name: Test minimum NumPy on Python 3.11
88
87
  runs-on: ubuntu-latest
89
88
 
90
89
  steps:
@@ -94,13 +93,13 @@ jobs:
94
93
  - name: Set up Python
95
94
  uses: actions/setup-python@v6
96
95
  with:
97
- python-version: "3.10"
96
+ python-version: "3.11"
98
97
 
99
98
  - name: Set up uv
100
99
  uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
101
100
  with:
102
101
  version: "0.11.32"
103
- python-version: "3.10"
102
+ python-version: "3.11"
104
103
  enable-cache: true
105
104
 
106
105
  - name: Run tests with the minimum NumPy version
@@ -108,8 +107,8 @@ jobs:
108
107
  uv run
109
108
  --isolated
110
109
  --no-project
111
- --python 3.10
110
+ --python 3.11
112
111
  --with-editable .
113
- --with numpy==1.21.3
112
+ --with numpy==1.23.2
114
113
  --with pytest==8.4.2
115
114
  python -m pytest
@@ -0,0 +1,291 @@
1
+ # Changelog
2
+
3
+ This changelog records user-visible changes to NumPy Vector Store. Earlier
4
+ release notes remain available on the
5
+ [GitHub releases page](https://github.com/tvanreenen/numpy-vector-store/releases).
6
+
7
+ ## 0.5.0 - 2026-08-21
8
+
9
+ This release gives `VectorStore` clear ownership of its configuration and row
10
+ storage, makes repeated additions scale without recopying the complete store on
11
+ every call, and defines deterministic ordering for equal search values. It also
12
+ finishes the persistence transition announced in 0.4: the explicit
13
+ create/open/save/reload lifecycle is now the only persistence API.
14
+
15
+ ### API at a glance
16
+
17
+ The core workflow remains small:
18
+
19
+ ```python
20
+ store = VectorStore(dimensions=1536, normalize=True)
21
+ store.add(vectors, metadata)
22
+
23
+ hits = store.cosine_search(query, top_k=10)
24
+ row = store.get(0)
25
+
26
+ store.save("vectors.npz")
27
+ store.save()
28
+
29
+ loaded = VectorStore.open("vectors.npz")
30
+ loaded.reload()
31
+ ```
32
+
33
+ `VectorStore` and `VectorHit` remain the only public classes. Version 0.5 does
34
+ not add a document wrapper, builder, snapshot object, metadata query language,
35
+ or another persistence abstraction.
36
+
37
+ ### Store-owned configuration and rows
38
+
39
+ - Make `dimensions`, `normalize`, and `file_path` read-only properties. The
40
+ constructor owns configuration, while `open(path)` and a successful
41
+ `save(path)` own archive binding changes.
42
+ - Return zero-copy, non-writeable active-row views from `vectors` and
43
+ `metadata`. Direct item assignment and ordinary attempts to enable writes are
44
+ rejected.
45
+ - Keep inspection views moment-in-time. Code holding a view across `add()`,
46
+ `clear()`, or `reload()` must request a new one to inspect current rows.
47
+ - Return an independent `float32` vector copy from `get(index)`, so changing a
48
+ retrieved vector cannot change normalized storage or a later search.
49
+ - Preserve opaque metadata payloads by reference. The read-only metadata view
50
+ protects row alignment, not the contents of a caller-owned dict, list,
51
+ dataclass, or other application object.
52
+
53
+ The views prevent accidental mutation through the supported API; they are not
54
+ tamper-proof snapshots. Deliberately reaching backing storage through `.base`,
55
+ private attributes, `ctypes`, or similar escape hatches remains unsupported.
56
+ Call `.copy()` when code needs an independently mutable array.
57
+
58
+ ### Amortized repeated additions
59
+
60
+ - Replace whole-store concatenation on every noninitial `add()` with private
61
+ contiguous vector and metadata capacity plus an active row count.
62
+ - Reuse spare rows when a new batch fits. When it does not, grow vector and
63
+ metadata storage together and copy active rows once.
64
+ - Keep spare capacity out of `len()`, inspection, search, `within_rows`,
65
+ retrieval, and saved archives.
66
+ - Make `clear()` return the store to empty arrays and drop the store's retained
67
+ capacity. A caller-held older NumPy view may still keep its previous buffer
68
+ alive until that view is released.
69
+
70
+ The `add(vectors, metadata)` signature, insertion order, validation,
71
+ normalization, and opaque metadata behavior do not change. Passing a batch is
72
+ still useful when the application already has one, but repeated small
73
+ additions no longer move every earlier row on each call.
74
+
75
+ ### Deterministic search ties
76
+
77
+ - Continue ordering cosine and dot-product results from larger values to
78
+ smaller values and Euclidean results from smaller distances to larger ones.
79
+ - Break exact computed-value ties by ascending original store row index.
80
+ - Apply the row-index tie break when choosing which rows cross the `top_k`
81
+ boundary, not only when ordering an already selected subset.
82
+ - Use original store indexes for filtered searches, so shuffling the same
83
+ `within_rows` values does not change tied results.
84
+ - Preserve partial top-k selection rather than replacing it with a full-store
85
+ sort.
86
+
87
+ Only exactly equal computed values use the row-index tie break. Close but
88
+ unequal values remain ordered by their metric value.
89
+
90
+ ### Final persistence lifecycle
91
+
92
+ The 0.4 compatibility window is now closed:
93
+
94
+ - Remove constructor `file_path=`. Create an in-memory store, then call
95
+ `save(path)` to write and bind it.
96
+ - Remove instance `load()`. Use `VectorStore.open(path)` to construct a store
97
+ from an archive and `reload()` to refresh a bound store.
98
+ - Remove context-manager persistence. Call `save()` explicitly where the
99
+ application intends to persist state.
100
+ - Remove the reader for unversioned archives containing only `vectors` and
101
+ `metadata`.
102
+
103
+ Archive format version 1 is unchanged. Applications that already use the 0.4
104
+ `open()`, `save(path)`, `save()`, and `reload()` lifecycle need no persistence
105
+ changes. An older unversioned archive must be converted with 0.4 using its
106
+ original dimensions and normalization mode, or recreated from source data,
107
+ before upgrading. See the [persistence migration guide](MIGRATION.md) for the
108
+ side-by-side replacements and conversion procedure.
109
+
110
+ ### Thread safety and persistence boundaries
111
+
112
+ - Support concurrent search, `get()`, and inspection on one instance only
113
+ while its state and shared metadata payloads remain unchanged.
114
+ - Require application-level synchronization for every access when any thread
115
+ may call `add()`, `clear()`, `reload()`, or `save()`, or mutate shared
116
+ metadata.
117
+ - Keep atomic archive replacement as a destination-visibility guarantee, not a
118
+ store snapshot, file lock, or multi-writer coordination system.
119
+
120
+ Separate store instances writing the same path can still replace one another;
121
+ applications with multiple writers must serialize them. Metadata persistence
122
+ continues to use NumPy's pickle-backed object arrays, so archives remain trusted
123
+ input and must not be opened from untrusted or unverifiable sources.
124
+
125
+ ### Runtime compatibility and upgrade notes
126
+
127
+ - Continue supporting Python 3.11 through 3.14 and NumPy 1.23.2 or newer.
128
+ - Continue exercising every supported Python version in CI, with a dedicated
129
+ minimum-NumPy job on Python 3.11.
130
+ - Keep archive format version 1 readable and writable without a file migration.
131
+ - Expect `AttributeError` from code that assigns public configuration or row
132
+ arrays, and different ordering from code that relied on incidental NumPy
133
+ partition order for exact ties.
134
+ - Expect a migration before upgrading code that still uses constructor
135
+ `file_path=`, instance `load()`, context-manager persistence, or an
136
+ unversioned two-array archive.
137
+
138
+ ## 0.4.0 - 2026-08-09
139
+
140
+ This release makes persistence explicit, self-describing, and safer to update.
141
+ The earlier archive format stored vectors and metadata but omitted the settings
142
+ needed to interpret those vectors correctly. Version 0.4 records that
143
+ configuration in every new archive and introduces a lifecycle that clearly
144
+ separates creating, opening, saving, and reloading a store.
145
+
146
+ ### Explicit persistence lifecycle
147
+
148
+ - Add `VectorStore.open(path)` to construct a store from a versioned archive.
149
+ The archive supplies its own dimensions and normalization mode, so callers no
150
+ longer need to repeat configuration that may be wrong.
151
+ - Let `save(path)` perform the first save or a Save As operation and bind that
152
+ destination. Later `save()` calls update the bound archive.
153
+ - Add `reload()` as a deliberate refresh from disk. It always attempts to read
154
+ the bound archive and leaves current in-memory state unchanged if reading or
155
+ validation fails.
156
+ - Keep creating a new in-memory store separate from opening one on disk. This
157
+ makes file access and persistence boundaries visible in application code.
158
+
159
+ ### Versioned, self-describing archives
160
+
161
+ - Write archive format version 1 with `format_version`, `dimensions`,
162
+ `normalize`, `vectors`, and `metadata` fields.
163
+ - Validate the complete archive before changing live store state, including
164
+ field names, scalar configuration, array dtypes and shapes, row counts,
165
+ finite vector values, and normalized-store zero-vector rules.
166
+ - Reject unsupported format versions and malformed archives clearly rather
167
+ than inferring missing configuration or partially applying valid fields.
168
+ - Continue preserving each metadata item as one opaque row payload.
169
+
170
+ ### Safer archive replacement
171
+
172
+ - Write each save to a uniquely named temporary archive in the destination
173
+ directory, close it, and then replace the destination with `os.replace`.
174
+ - Preserve the previous complete archive when writing or replacement fails and
175
+ clean up temporary files after failures.
176
+ - Bind a new Save As destination only after its archive has been written
177
+ successfully.
178
+
179
+ This provides an atomic visibility boundary: a reader opening the destination
180
+ sees the previous complete archive or the new complete archive instead of a
181
+ partially written file. It does not provide file locking, multi-writer
182
+ coordination, or a universal power-loss durability guarantee.
183
+
184
+ ### Short migration window
185
+
186
+ - Keep constructor `file_path=`, instance `load()`, and direct context-manager
187
+ persistence for version 0.4 with `FutureWarning`. They will be removed in
188
+ 0.5.
189
+ - Make the deprecated context manager save only after a successful block. If
190
+ the block raises, it does not save or suppress the exception.
191
+ - Keep a configuration-aware reader for older archives containing only
192
+ `vectors` and `metadata`. Loading one warns, and its next save rewrites it as
193
+ format version 1.
194
+ - Intentionally make `open()` reject an unversioned archive because that file
195
+ cannot report its original dimensions or normalization semantics.
196
+ - Add a dedicated [persistence migration guide](MIGRATION.md) with side-by-side
197
+ API replacements and one-time legacy archive conversion instructions.
198
+
199
+ The legacy API and unversioned archive reader are removed in 0.5. Applications
200
+ should migrate an old archive once with 0.4 or recreate it from source data;
201
+ indefinite compatibility with the incomplete two-array format is not planned.
202
+
203
+ ### Runtime compatibility
204
+
205
+ - Support Python 3.11 through 3.14. Python 3.10 remains supported by the 0.3
206
+ release series but is not supported by 0.4.
207
+ - Raise the minimum NumPy version from 1.21.3 to 1.23.2, the earliest release
208
+ that supports Python 3.11.
209
+ - Exercise every supported Python version in CI and test NumPy 1.23.2 in a
210
+ dedicated minimum-dependency job.
211
+
212
+ ### Upgrade notes and boundaries
213
+
214
+ - Search, insertion, retrieval, clearing, normalization, and metadata behavior
215
+ are unchanged from 0.3.2.
216
+ - Code using `VectorStore.open()`, `save(path)`, `save()`, and `reload()` is on
217
+ the persistence API intended for 0.5.
218
+ - Code using a transitional entry point continues to work in 0.4 but emits a
219
+ warning so the required 0.5 migration is visible during testing.
220
+ - Metadata still uses NumPy's pickle-backed object-array loading. Only open
221
+ archives produced by your application or another trusted source.
222
+ - Mutable public state, repeated-add performance, deterministic tie ordering,
223
+ and a formal thread-safety contract remain planned for later releases.
224
+
225
+ ## 0.3.2 - 2026-07-27
226
+
227
+ This reliability and performance patch makes existing vector storage, search,
228
+ metadata, and persistence behavior safer and more predictable. It does not
229
+ intentionally break valid existing usage or change the `.npz` archive format.
230
+
231
+ ### Numerical reliability
232
+
233
+ - Reject vectors, queries, and search thresholds that contain non-finite values
234
+ or cannot remain finite when represented as `float32`. Invalid input now
235
+ fails before it can corrupt stored state or ranking.
236
+ - Calculate norms and raw metric intermediates with `float64` where `float32`
237
+ could overflow or underflow. Large and very small finite vectors can now be
238
+ normalized and compared reliably.
239
+ - Allow zero vectors in stores created with `normalize=False`, where they are
240
+ valid for dot-product and Euclidean search.
241
+ - Raise a clear error if a raw cosine search includes a zero vector, because
242
+ cosine similarity is undefined for that row.
243
+ - Avoid duplicate full-size `float64` buffers when calculating raw Euclidean
244
+ distance.
245
+
246
+ ### Persistence
247
+
248
+ - Resolve a path without an `.npz` suffix to the same archive for both saving
249
+ and loading. For example, `file_path="vectors"` consistently uses
250
+ `vectors.npz`.
251
+ - Allow `load()` to be retried when the persistence file did not exist during
252
+ an earlier attempt.
253
+ - Reset load state in `clear()` so a subsequent explicit `load()` can restore
254
+ the saved rows.
255
+ - Keep repeated `load()` calls idempotent after a successful load.
256
+
257
+ ### Metadata
258
+
259
+ - Preserve each item in the outer metadata sequence as one opaque row payload.
260
+ Tuples and lists are no longer mistaken for extra NumPy array dimensions.
261
+ - Support dictionary, dataclass, tuple, list, string, integer, and other scalar
262
+ payloads consistently through insertion, search results, saving, and loading.
263
+ - Continue rejecting explicitly multidimensional NumPy metadata arrays rather
264
+ than silently flattening ambiguous input.
265
+
266
+ ### Search memory use
267
+
268
+ - Search the stored vector matrix directly when `within_rows` is omitted,
269
+ avoiding an unnecessary full-matrix copy on every unfiltered query.
270
+ - Preserve original store indexes and metadata when `within_rows` selects a
271
+ filtered subset.
272
+ - Document that filtered searches allocate a temporary matrix proportional to
273
+ the selected row count and vector dimensions.
274
+
275
+ ### Compatibility and validation
276
+
277
+ - Test Python 3.10 through 3.14 in GitHub Actions.
278
+ - Test the minimum supported NumPy version in a dedicated Python 3.10 job.
279
+ - Raise the minimum NumPy requirement from 1.20 to 1.21.3 so it is compatible
280
+ with the oldest supported Python version.
281
+ - Require linting, formatting, type checking, and the full Python test matrix
282
+ before publishing to PyPI.
283
+
284
+ ### Upgrade notes
285
+
286
+ - No public method signatures or persisted field names changed.
287
+ - Existing trusted `.npz` archives with `vectors` and `metadata` remain
288
+ readable.
289
+ - Environments using NumPy 1.20 must upgrade to NumPy 1.21.3 or newer.
290
+ - Inputs that previously produced `nan`, `inf`, or unreliable rankings now
291
+ raise `ValueError` instead.
@@ -0,0 +1,289 @@
1
+ # Persistence migration guide
2
+
3
+ Version 0.5 completes the persistence lifecycle introduced in 0.4. Creating a
4
+ store, opening an archive, saving, and refreshing from disk now have separate,
5
+ explicit operations. Applications that adopted the recommended 0.4 API need no
6
+ further persistence changes.
7
+
8
+ The 0.4 compatibility paths are no longer present: constructor `file_path=`,
9
+ instance `load()`, context-manager persistence, and the unversioned archive
10
+ reader have been removed.
11
+
12
+ ## API at a glance
13
+
14
+ ```python
15
+ store = VectorStore(dimensions=1536, normalize=True)
16
+ store.add(vectors, metadata)
17
+ store.save("vectors.npz")
18
+
19
+ store.save() # Update the bound archive.
20
+
21
+ loaded = VectorStore.open("vectors.npz")
22
+ loaded.reload() # Deliberately discard memory and reread the archive.
23
+ ```
24
+
25
+ The persistence changes from 0.4 to 0.5 are:
26
+
27
+ | Area | 0.4 | 0.5 |
28
+ |---|---|---|
29
+ | Create a store | `VectorStore(dimensions, normalize=...)` | Unchanged |
30
+ | Constructor `file_path=` | Works with `FutureWarning` | Removed |
31
+ | Open an archive | `VectorStore.open(path)` | Unchanged |
32
+ | Save and bind | `save(path)` | Unchanged |
33
+ | Save again | `save()` | Unchanged |
34
+ | Refresh from disk | `reload()` | Unchanged |
35
+ | Instance `load()` | Works with `FutureWarning` | Removed |
36
+ | Context manager | Works with `FutureWarning` | Removed |
37
+ | Unversioned archive | Temporary migration reader | Reader removed |
38
+ | Format version 1 archive | Supported | Supported unchanged |
39
+ | `dimensions`, `normalize`, `file_path` | Writable attributes | Read-only properties |
40
+ | `vectors`, `metadata` | Writable owning arrays | Read-only inspection views |
41
+ | Vector returned by `get()` | View into live storage | Independent `float32` copy |
42
+ | Repeated `add()` calls | Recopy all existing rows | Reuse private spare capacity |
43
+ | Equal search values | Unspecified order | Lower original store row index first |
44
+ | Thread safety | Not formally defined | Concurrent reads only while state is unchanged |
45
+
46
+ ## Store-owned state
47
+
48
+ Most code that reads configuration or uses NumPy operations for inspection and
49
+ prefiltering remains unchanged. The familiar property names are still present:
50
+
51
+ ```python
52
+ store.dimensions
53
+ store.normalize
54
+ store.file_path
55
+ store.vectors
56
+ store.metadata
57
+ ```
58
+
59
+ The difference is ownership. Configuration cannot be assigned directly, and
60
+ the vector and metadata views reject normal row mutation. Their views describe
61
+ the rows present when the property was requested, so code should request a
62
+ fresh view after `add()`, `clear()`, or `reload()`.
63
+
64
+ The views are a supported inspection boundary, not tamper-proof snapshots.
65
+ Deliberately mutating their backing storage through `.base`, private attributes,
66
+ `ctypes`, or similar escape hatches is unsupported and can corrupt the store.
67
+ Call `.copy()` when code needs an independently mutable full-array snapshot.
68
+ For metadata, this copies the outer row array while preserving the opaque
69
+ payload objects by reference.
70
+
71
+ Row retrieval deliberately treats vectors and metadata differently:
72
+
73
+ ```python
74
+ vector, payload = store.get(0)
75
+
76
+ vector[0] = 10.0 # Independent copy; the store is unchanged.
77
+ payload["reviewed"] = True # Shared application metadata object.
78
+ ```
79
+
80
+ Vectors have one uniform NumPy representation, so copying a single row gives
81
+ the caller clear ownership at bounded cost. Metadata can be any Python object,
82
+ so the store preserves payload identity instead of imposing a potentially
83
+ expensive or invalid deep-copy policy. Applications that need immutable
84
+ metadata can use frozen application objects or copy payloads themselves.
85
+
86
+ ## Repeated additions
87
+
88
+ The `add(vectors, metadata)` signature and validation rules do not change. In
89
+ 0.4, every noninitial call concatenated the new rows with the complete store.
90
+ In 0.5, the store reserves private row capacity and grows it geometrically when
91
+ needed. Repeated small additions therefore copy existing rows only when the
92
+ current allocation is full.
93
+
94
+ This does not add a public capacity setting or change row indexes. `len(store)`,
95
+ inspection, search, retrieval, and persistence continue to use only active
96
+ rows. `clear()` discards reserved storage as well as active rows, so it does not
97
+ retain metadata payloads through unused capacity. A caller-held inspection view
98
+ can still keep its previous NumPy buffer and payload references alive until the
99
+ view itself is released.
100
+
101
+ ## Search result ordering
102
+
103
+ Version 0.5 makes exact metric ties deterministic. Cosine and dot-product
104
+ searches still rank larger values first, and Euclidean search still ranks
105
+ smaller distances first. When computed values are equal, the lower original
106
+ store row index comes first.
107
+
108
+ This tie break also decides which rows are returned when equal values cross
109
+ the `top_k` boundary. A shuffled `within_rows` input does not change the result:
110
+ the original store indexes, rather than positions in the filtered input, break
111
+ the tie. No call-site change is required, but code that depended on an
112
+ incidental NumPy partition order should update its expectations.
113
+
114
+ ## Thread safety
115
+
116
+ Version 0.5 defines the existing synchronization boundary without adding
117
+ internal locks. Search, `get()`, and inspection may run concurrently on one
118
+ instance only while store state and shared metadata payloads remain unchanged.
119
+
120
+ Applications must externally synchronize all access to an instance whenever
121
+ any thread may call `add()`, `clear()`, `reload()`, or `save()`, or mutate a
122
+ metadata payload shared with the store. Saves must not overlap in-memory
123
+ mutation because archive replacement protects the destination path, not the
124
+ consistency of vectors and metadata read from a changing instance.
125
+
126
+ Atomic replacement also does not coordinate separate store instances writing
127
+ the same path. Applications with multiple writers must serialize them; without
128
+ that coordination, each complete save may replace another and the last
129
+ successful replacement wins.
130
+
131
+ ## Creating and saving a new store
132
+
133
+ Previously, the destination was supplied while constructing the store:
134
+
135
+ ```python
136
+ store = VectorStore(dimensions=1536, file_path="vectors.npz")
137
+ store.add(vectors, metadata)
138
+ store.save()
139
+ ```
140
+
141
+ Create the in-memory store first, then bind its destination with the first
142
+ save:
143
+
144
+ ```python
145
+ store = VectorStore(dimensions=1536)
146
+ store.add(vectors, metadata)
147
+ store.save("vectors.npz")
148
+ ```
149
+
150
+ Later `save()` calls update the bound archive. Supplying another path performs
151
+ a Save As operation and binds the new destination after the write succeeds.
152
+ Calling `save()` before a store is bound raises `ValueError` rather than
153
+ silently leaving the data unsaved.
154
+
155
+ ## Opening an existing archive
156
+
157
+ The old API required callers to repeat configuration that should belong to the
158
+ archive:
159
+
160
+ ```python
161
+ store = VectorStore(
162
+ dimensions=1536,
163
+ file_path="vectors.npz",
164
+ normalize=True,
165
+ )
166
+ store.load()
167
+ ```
168
+
169
+ Open a version 1 archive directly:
170
+
171
+ ```python
172
+ store = VectorStore.open("vectors.npz")
173
+ ```
174
+
175
+ `open()` restores `dimensions` and `normalize` from the archive, validates its
176
+ contents, loads its rows, and binds its path. Applications no longer need to
177
+ keep archive configuration separately or risk loading the same vectors with
178
+ different semantics.
179
+
180
+ The generic parameter still describes application metadata. It can be kept
181
+ when useful:
182
+
183
+ ```python
184
+ from dataclasses import dataclass
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class ChunkMetadata:
189
+ source: str
190
+ chunk_index: int
191
+
192
+
193
+ store = VectorStore[ChunkMetadata].open("vectors.npz")
194
+ ```
195
+
196
+ `ChunkMetadata` is an example application type, not a class provided by this
197
+ library.
198
+
199
+ ## Refreshing from disk
200
+
201
+ Use `reload()` when another process may have changed the bound archive and the
202
+ current in-memory changes should be discarded:
203
+
204
+ ```python
205
+ store = VectorStore.open("vectors.npz")
206
+
207
+ # Later, after the file may have changed:
208
+ store.reload()
209
+ ```
210
+
211
+ `reload()` always attempts to read. It raises if the store is unbound, the file
212
+ is missing, or the archive is invalid. A failed reload leaves the current
213
+ in-memory vectors and metadata unchanged.
214
+
215
+ ## Replacing context-manager persistence
216
+
217
+ The earlier context manager saved automatically on exit:
218
+
219
+ ```python
220
+ with VectorStore(dimensions=1536, file_path="vectors.npz") as store:
221
+ store.add(vectors, metadata)
222
+ ```
223
+
224
+ Use an explicit save after the work succeeds:
225
+
226
+ ```python
227
+ store = VectorStore(dimensions=1536)
228
+ store.add(vectors, metadata)
229
+ store.save("vectors.npz")
230
+ ```
231
+
232
+ Normal Python control flow already prevents the final line from running if
233
+ `add()` raises. The persistence boundary is visible, and readers do not need to
234
+ remember an implicit exit side effect.
235
+
236
+ There is no replacement autosave context manager. An explicit `save()` keeps
237
+ the persistence boundary visible and lets the application decide whether work
238
+ completed successfully enough to persist.
239
+
240
+ ## Migrating an archive created before 0.4
241
+
242
+ Older archives contain only `vectors` and `metadata`. They do not record their
243
+ dimensions or whether vectors use normalized or raw semantics, so `open()`
244
+ cannot construct a correct store from them.
245
+
246
+ Before upgrading to 0.5, use the 0.4 compatibility API once with the archive's
247
+ original configuration:
248
+
249
+ ```python
250
+ legacy = VectorStore(
251
+ dimensions=1536,
252
+ file_path="legacy-vectors.npz",
253
+ normalize=True,
254
+ )
255
+ legacy.load()
256
+ legacy.save()
257
+ ```
258
+
259
+ This code emits transition warnings in 0.4 by design. The final `save()`
260
+ rewrites the archive as format version 1 with `format_version`, `dimensions`,
261
+ `normalize`, `vectors`, and `metadata`. It can then be opened by 0.5:
262
+
263
+ ```python
264
+ store = VectorStore.open("legacy-vectors.npz")
265
+ ```
266
+
267
+ Applications that can recreate archives from source vectors and metadata may
268
+ do that instead. NumPy Vector Store 0.5 cannot perform this conversion because
269
+ the unversioned file does not contain enough information to reconstruct its
270
+ configuration safely.
271
+
272
+ ## Removal schedule
273
+
274
+ | Transitional behavior | 0.4 | 0.5 |
275
+ |---|---|---|
276
+ | Constructor `file_path=` | Works with `FutureWarning` | Removed |
277
+ | Instance `load()` | Works with `FutureWarning` | Removed |
278
+ | Direct context-manager persistence | Saves only on successful exit and warns | Removed |
279
+ | Unversioned two-array archives | Load with known configuration and warn | Reader removed |
280
+ | `open()`, `save(path)`, `save()`, and `reload()` | Preferred | Supported |
281
+
282
+ ## Persistence boundaries that do not change
283
+
284
+ Metadata is stored in a pickle-backed NumPy object array. Archives remain
285
+ trusted input and must not be opened from untrusted or unverifiable sources.
286
+
287
+ Saves use same-directory temporary files and atomic replacement, but the
288
+ library does not add file locking, coordinate concurrent writers, or promise
289
+ power-loss durability across every operating system and filesystem.