numpy-vector-store 0.4.0__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 (21) hide show
  1. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/CHANGELOG.md +131 -0
  2. numpy_vector_store-0.5.0/MIGRATION.md +289 -0
  3. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/PKG-INFO +101 -32
  4. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/README.md +99 -30
  5. numpy_vector_store-0.5.0/ROADMAP.md +423 -0
  6. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/__init__.py +1 -1
  7. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/vector_store.py +194 -182
  8. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/tests/test_vector_store.py +318 -330
  9. numpy_vector_store-0.4.0/MIGRATION.md +0 -169
  10. numpy_vector_store-0.4.0/ROADMAP.md +0 -225
  11. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/FUNDING.yml +0 -0
  12. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/checks.yml +0 -0
  13. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  14. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/publish-testpypi.yml +0 -0
  15. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.gitignore +0 -0
  16. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/LICENSE +0 -0
  17. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/justfile +0 -0
  18. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/pyproject.toml +0 -0
  19. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/py.typed +0 -0
  20. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/tests/__init__.py +0 -0
  21. {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/uv.lock +0 -0
@@ -4,6 +4,137 @@ This changelog records user-visible changes to NumPy Vector Store. Earlier
4
4
  release notes remain available on the
5
5
  [GitHub releases page](https://github.com/tvanreenen/numpy-vector-store/releases).
6
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
+
7
138
  ## 0.4.0 - 2026-08-09
8
139
 
9
140
  This release makes persistence explicit, self-describing, and safer to update.
@@ -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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: numpy-vector-store
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: A fast, lightweight, and zero-setup in-memory vector store powered by NumPy
5
5
  Project-URL: Homepage, https://github.com/tvanreenen/numpy-vector-store
6
6
  Project-URL: Repository, https://github.com/tvanreenen/numpy-vector-store
@@ -93,6 +93,62 @@ Each payload can be a dict, dataclass, tuple, list, string, integer row ID, or
93
93
  another Python object that fits your application. Tuple and list payloads remain
94
94
  single row values rather than being interpreted as additional array dimensions.
95
95
 
96
+ ## State ownership
97
+
98
+ The store owns its configuration and row structure. `dimensions`, `normalize`,
99
+ and `file_path` are readable properties, but callers cannot assign them
100
+ directly. Use the constructor for configuration, `open(path)` to open an
101
+ archive, and `save(path)` to establish or change a binding.
102
+
103
+ `store.vectors` and `store.metadata` are zero-copy, non-writeable NumPy views
104
+ for inspection and metadata prefiltering. Direct item assignment and ordinary
105
+ attempts to enable writes are rejected. Request a fresh view after `add()`,
106
+ `clear()`, or `reload()` when current rows are required.
107
+
108
+ These views prevent accidental mutation through the supported API; they are not
109
+ tamper-proof snapshots. NumPy exposes shared buffers, and Python private state
110
+ can be reached deliberately. Mutating backing storage through `.base`, private
111
+ attributes, `ctypes`, or similar escape hatches is unsupported and can corrupt
112
+ store invariants. Use `.copy()` when code needs an independently mutable
113
+ full-array snapshot:
114
+
115
+ ```python
116
+ vectors = store.vectors.copy()
117
+ metadata_rows = store.metadata.copy()
118
+ ```
119
+
120
+ Copying `metadata` isolates the outer row array but still shares the opaque
121
+ payload objects stored inside it.
122
+
123
+ `get(index)` has a narrower ownership boundary:
124
+
125
+ ```python
126
+ vector, payload = store.get(0)
127
+
128
+ vector[0] = 10.0 # Independent copy; the store is unchanged.
129
+ payload["reviewed"] = True # Shared application metadata object.
130
+ ```
131
+
132
+ The returned vector is an independent `float32` copy. The metadata payload is
133
+ the same opaque object supplied to `add()` or restored from the archive; the
134
+ store protects the metadata row structure but does not deep-copy arbitrary
135
+ dicts, lists, dataclasses, or application objects. Applications that require
136
+ immutable payloads can use frozen objects or copy them at their own boundary.
137
+
138
+ ## Repeated additions
139
+
140
+ `add()` accepts one row or a batch, and preserves insertion order in either
141
+ case. The store keeps private spare capacity so repeated small additions do not
142
+ copy every existing row on every call. When that capacity is full, the vector
143
+ and metadata arrays grow together and the active rows are copied once.
144
+
145
+ Spare capacity is internal. `len(store)`, inspection views, search, `get()`, and
146
+ saved archives contain only rows that were added. `clear()` releases both the
147
+ active rows and any reserved capacity held by the store. As with any NumPy
148
+ view, an older inspection view keeps its previous buffer alive until that view
149
+ is released. Adding a batch is still preferable when the application already
150
+ has one because it also reduces per-call validation and Python overhead.
151
+
96
152
  ## Normalization
97
153
 
98
154
  `VectorStore` defaults to `normalize=True`, which scales each stored vector to
@@ -132,7 +188,7 @@ compared reliably.
132
188
  | `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 |
133
189
  | `get` | Returns normalized vectors | Returns original vectors |
134
190
  | `save` | Saves normalized vectors | Saves raw vectors |
135
- | `load` | Loads and normalizes vectors | Loads vectors exactly as stored |
191
+ | `open` and `reload` | Restore normalized storage semantics | Restore raw vectors exactly as stored |
136
192
 
137
193
  ## Search Methods
138
194
 
@@ -160,6 +216,18 @@ store.add(vectors, metadata)
160
216
  hits = store.euclidean_search(query, top_k=10, max_value=1.5)
161
217
  ```
162
218
 
219
+ ### Result ordering
220
+
221
+ Cosine and dot-product results are ordered from larger values to smaller
222
+ values. Euclidean results are ordered from smaller distances to larger ones.
223
+ When two computed values are exactly equal, the row with the lower original
224
+ store index comes first.
225
+
226
+ The same rule determines which tied rows cross the `top_k` boundary. It also
227
+ applies to `within_rows`: the original store index breaks a tie, regardless of
228
+ the order in which filtered row indexes were supplied. Values that are close
229
+ but not exactly equal remain ordered by their computed metric value.
230
+
163
231
  ## Prefiltering
164
232
 
165
233
  The store does not implement a metadata query language. To filter by metadata,
@@ -207,6 +275,24 @@ for hit in hits:
207
275
  print(row["title"], hit.value)
208
276
  ```
209
277
 
278
+ ## Thread safety
279
+
280
+ `VectorStore` does not use internal locks. Multiple threads may call search,
281
+ `get()`, or the inspection properties on the same instance while its rows,
282
+ configuration, binding, and metadata payloads remain unchanged.
283
+
284
+ If any thread may call `add()`, `clear()`, `reload()`, or `save()`, every access
285
+ to that store must use the same application-level lock or another external
286
+ synchronization mechanism. The same rule applies when application code mutates
287
+ a metadata payload shared with the store. In particular, do not overlap a save
288
+ with an in-memory mutation: an atomic file replacement cannot turn two separate
289
+ array reads into a consistent store snapshot.
290
+
291
+ Separate store instances writing the same path also need external writer
292
+ coordination. Atomic replacement prevents readers from seeing a partially
293
+ written archive, but concurrent writers can replace one another and the last
294
+ successful replacement wins.
295
+
210
296
  ## Persistence
211
297
 
212
298
  Create a new store normally, then supply its destination on the first save:
@@ -235,10 +321,9 @@ explicitly want to discard current in-memory changes:
235
321
  store.reload()
236
322
  ```
237
323
 
238
- Unlike the transitional `load()` method, `reload()` always rereads the bound
239
- archive and raises if the store is unbound, the file is missing, or the archive
240
- is invalid. A failed reload leaves the current in-memory vectors and metadata
241
- unchanged.
324
+ `reload()` always rereads the bound archive and raises if the store is unbound,
325
+ the file is missing, or the archive is invalid. A failed reload leaves the
326
+ current in-memory vectors and metadata unchanged.
242
327
 
243
328
  The `.npz` suffix may be omitted. An extensionless path such as `"vectors"` is
244
329
  resolved to `"vectors.npz"` for saving, opening, and reloading.
@@ -279,30 +364,14 @@ writers can still replace one another, and the library does not promise that a
279
364
  successful save has reached durable hardware storage across every operating
280
365
  system or power failure.
281
366
 
282
- Older archives containing only `vectors` and `metadata` remain readable in 0.4
283
- through the configuration-aware legacy API:
284
-
285
- ```python
286
- legacy = VectorStore[dict[str, str]](
287
- dimensions=1536,
288
- file_path="legacy-vectors.npz",
289
- normalize=True,
290
- )
291
- legacy.load()
292
- legacy.save()
293
- ```
294
-
295
- Loading a legacy archive emits a `FutureWarning`, and saving rewrites it as
296
- format version 1. `open()` intentionally rejects unversioned archives because
297
- they do not contain enough configuration to construct a store safely. The
298
- legacy reader will be removed in 0.5; migrate an archive once with 0.4 or
299
- recreate it from source data.
367
+ Version 0.5 reads only self-describing format version 1 archives. Unversioned
368
+ archives containing only `vectors` and `metadata` cannot be opened because they
369
+ do not record dimensions or normalization semantics. Recreate those archives
370
+ from source data, or convert them with NumPy Vector Store 0.4 before upgrading.
300
371
 
301
- Constructor `file_path=`, instance `load()`, and direct context-manager
302
- persistence remain available with `FutureWarning` during the 0.4 transition.
303
- The explicit lifecycle shown above is the preferred API and will be the only
304
- persistence API in 0.5. See the [persistence migration guide](MIGRATION.md) for
305
- side-by-side replacements and the one-time legacy archive conversion.
372
+ Constructor `file_path=`, instance `load()`, and context-manager persistence
373
+ were removed in 0.5. See the [persistence migration guide](MIGRATION.md) for the
374
+ direct replacements and the pre-upgrade procedure for an unversioned archive.
306
375
 
307
376
  Metadata persistence uses `allow_pickle=True` for flexible Python payloads, so
308
377
  only load files generated by your own application or another trusted local
@@ -315,10 +384,10 @@ the API stabilizes. Changes are documented in the [changelog](CHANGELOG.md) and
315
384
  GitHub release notes. Deprecated APIs will keep warning for at least one point
316
385
  release before removal.
317
386
 
318
- Version 0.4 supports Python 3.11 through 3.14 and NumPy 1.23.2 or newer. These
387
+ Version 0.5 supports Python 3.11 through 3.14 and NumPy 1.23.2 or newer. These
319
388
  versions are listed in the package metadata and exercised in CI, including a
320
389
  dedicated check against the minimum NumPy version. Python 3.10 remains supported
321
- by the 0.3 release series but is not supported by 0.4.
390
+ by the 0.3 release series but is not supported by 0.4 or 0.5.
322
391
 
323
392
  The project generally retains stable CPython versions until their upstream
324
393
  end-of-life, adds new versions after its dependencies and CI support them, and