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.
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/CHANGELOG.md +131 -0
- numpy_vector_store-0.5.0/MIGRATION.md +289 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/PKG-INFO +101 -32
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/README.md +99 -30
- numpy_vector_store-0.5.0/ROADMAP.md +423 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/__init__.py +1 -1
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/vector_store.py +194 -182
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/tests/test_vector_store.py +318 -330
- numpy_vector_store-0.4.0/MIGRATION.md +0 -169
- numpy_vector_store-0.4.0/ROADMAP.md +0 -225
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/FUNDING.yml +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/checks.yml +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.github/workflows/publish-testpypi.yml +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/.gitignore +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/LICENSE +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/justfile +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/pyproject.toml +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/src/numpy_vector_store/py.typed +0 -0
- {numpy_vector_store-0.4.0 → numpy_vector_store-0.5.0}/tests/__init__.py +0 -0
- {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.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: numpy-vector-store
|
|
3
|
-
Version: 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
|
-
| `
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
302
|
-
|
|
303
|
-
|
|
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.
|
|
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
|