numpy-vector-store 0.1.0__tar.gz → 0.3.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.
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: numpy-vector-store
3
+ Version: 0.3.0
4
+ Summary: A fast, lightweight, and zero-setup in-memory vector store powered by NumPy
5
+ Project-URL: Homepage, https://github.com/tvanreenen/numpy-vector-store
6
+ Project-URL: Repository, https://github.com/tvanreenen/numpy-vector-store
7
+ Project-URL: Issues, https://github.com/tvanreenen/numpy-vector-store/issues
8
+ Author: Tim VanReenen
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: embeddings,numpy,search,store,vector
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: numpy>=1.20.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # NumPy Vector Store
26
+
27
+ A fast, lightweight, zero-setup in-memory vector store powered by NumPy.
28
+
29
+ - **Tiny local vector search** for projects that do not need a vector database
30
+ - **Fast exact vector search** using vectorized NumPy operations
31
+ - **Simple typed API** returning `VectorHit(index, value, metadata)`
32
+ - **Composable filtering** by passing prefiltered row indexes with `within_rows`
33
+ - **Portable persistence** as trusted local `.npz` files with `vectors` + `metadata`
34
+ - **No framework opinions**: bring your own embeddings, chunking, async, and metadata model
35
+
36
+ ## Why?
37
+
38
+ This library is purpose-built for small to medium-scale vector search tasks and
39
+ offers a simple alternative to heavyweight vector databases when you do not need
40
+ network services, indexing infrastructure, ingestion pipelines, or domain-specific
41
+ metadata filtering.
42
+
43
+ ## When/Where?
44
+
45
+ Below are benchmark results for cosine similarity search to help you assess its
46
+ suitability for your use case.
47
+
48
+ | Embedding Type | Dimensions | ~5ms | ~25ms | ~100ms | ~500ms |
49
+ |----------------|------------|------|--------|---------|---------|
50
+ | **Sentence Transformers** | 384 | 1K vectors<br/>1.5MB | 10K vectors<br/>15MB | 100K vectors<br/>147MB | 500K vectors<br/>732MB |
51
+ | **OpenAI Small** | 1536 | 500 vectors<br/>3MB | 5K vectors<br/>29MB | 25K vectors<br/>147MB | 100K vectors<br/>586MB |
52
+ | **OpenAI Large** | 3072 | 200 vectors<br/>2MB | 2.5K vectors<br/>29MB | 5K vectors<br/>59MB | 25K vectors<br/>293MB |
53
+
54
+ *Benchmarks performed on Apple M2 hardware.*
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ uv add numpy-vector-store
60
+ ```
61
+
62
+ ## Quick Start
63
+
64
+ ```python
65
+ import numpy as np
66
+ from numpy_vector_store import VectorStore
67
+
68
+ store = VectorStore[dict[str, str]](dimensions=3)
69
+
70
+ store.add(
71
+ vectors=np.array([
72
+ [1.0, 0.0, 0.0],
73
+ [0.0, 1.0, 0.0],
74
+ [0.0, 0.0, 1.0],
75
+ ]),
76
+ metadata=[
77
+ {"title": "x-axis"},
78
+ {"title": "y-axis"},
79
+ {"title": "z-axis"},
80
+ ],
81
+ )
82
+
83
+ hits = store.cosine_search(
84
+ query=np.array([0.9, 0.1, 0.0]),
85
+ top_k=2,
86
+ )
87
+
88
+ for hit in hits:
89
+ print(f"{hit.metadata['title']}: {hit.value:.3f}")
90
+ ```
91
+
92
+ `metadata` is an opaque row payload returned with hits. It can be a dict,
93
+ dataclass, string, integer row ID, or any other Python object that fits your
94
+ application.
95
+
96
+ ## Normalization
97
+
98
+ `VectorStore` defaults to `normalize=True`, which scales each stored vector to
99
+ length `1`. Normalization preserves vector direction while discarding magnitude:
100
+
101
+ ```python
102
+ [3.0, 4.0] -> [0.6, 0.8]
103
+ ```
104
+
105
+ This is the default because it makes cosine similarity fast and direction-only,
106
+ which is the common case for semantic embeddings. Use `normalize=False` when
107
+ vector length matters, such as when magnitude encodes strength, confidence,
108
+ counts, scale, or raw geometry.
109
+
110
+ Zero vectors are rejected in both modes because cosine similarity is undefined
111
+ for zero-norm vectors.
112
+
113
+ | Method | `normalize=True` default | `normalize=False` |
114
+ |---|---|---|
115
+ | `cosine_search` | True cosine similarity over stored unit vectors; fastest/default path for embeddings | True cosine similarity over raw vectors; computes vector norms during search |
116
+ | `dot_search` | Dot product of unit vectors, effectively equivalent to cosine similarity | True dot product over original vectors; use when magnitude should affect ranking |
117
+ | `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 |
118
+ | `get` | Returns normalized vectors | Returns original vectors |
119
+ | `save` | Saves normalized vectors | Saves raw vectors |
120
+ | `load` | Loads and normalizes vectors | Loads vectors exactly as stored |
121
+
122
+ ## Search Methods
123
+
124
+ Use `cosine_search` for semantic embeddings and direction-only similarity:
125
+
126
+ ```python
127
+ hits = store.cosine_search(query, top_k=10, min_value=0.75)
128
+ ```
129
+
130
+ Use `dot_search` with `normalize=False` when larger-magnitude vectors should
131
+ rank higher:
132
+
133
+ ```python
134
+ store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
135
+ store.add(vectors, metadata)
136
+ hits = store.dot_search(query, top_k=10, min_value=0.0)
137
+ ```
138
+
139
+ Use `euclidean_search` with `normalize=False` for raw coordinate or feature-space
140
+ nearest-neighbor search:
141
+
142
+ ```python
143
+ store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
144
+ store.add(vectors, metadata)
145
+ hits = store.euclidean_search(query, top_k=10, max_value=1.5)
146
+ ```
147
+
148
+ ## Prefiltering
149
+
150
+ The store does not implement a metadata query language. To filter by metadata,
151
+ produce row indexes first, then pass them with `within_rows`.
152
+
153
+ ```python
154
+ rows = [
155
+ i
156
+ for i, metadata in enumerate(store.metadata)
157
+ if metadata["title"].startswith("x")
158
+ ]
159
+
160
+ hits = store.cosine_search(query, top_k=10, within_rows=rows)
161
+ ```
162
+
163
+ For structured NumPy metadata, use NumPy to produce the row indexes:
164
+
165
+ ```python
166
+ metadata_table = np.array(
167
+ [
168
+ ("intro", "A", 2024),
169
+ ("setup", "A", 2023),
170
+ ("guide", "B", 2024),
171
+ ],
172
+ dtype=[("title", "U20"), ("product", "U10"), ("year", "i4")],
173
+ )
174
+
175
+ store = VectorStore[int](dimensions=3)
176
+ store.add(vectors, metadata=np.arange(len(metadata_table)))
177
+
178
+ mask = (metadata_table["product"] == "A") & (metadata_table["year"] >= 2024)
179
+ rows = np.flatnonzero(mask)
180
+
181
+ hits = store.cosine_search(query, within_rows=rows)
182
+
183
+ for hit in hits:
184
+ row = metadata_table[hit.metadata]
185
+ print(row["title"], hit.value)
186
+ ```
187
+
188
+ ## Persistence
189
+
190
+ Pass a `file_path` and call `save()` / `load()` explicitly:
191
+
192
+ ```python
193
+ store = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
194
+ store.add(embeddings, metadata)
195
+ store.save()
196
+
197
+ loaded = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
198
+ loaded.load()
199
+ ```
200
+
201
+ If you save with `normalize=False`, load with `normalize=False` too:
202
+
203
+ ```python
204
+ store = VectorStore[dict[str, str]](
205
+ dimensions=1536,
206
+ file_path="raw-vectors.npz",
207
+ normalize=False,
208
+ )
209
+ store.add(raw_vectors, metadata)
210
+ store.save()
211
+
212
+ loaded = VectorStore[dict[str, str]](
213
+ dimensions=1536,
214
+ file_path="raw-vectors.npz",
215
+ normalize=False,
216
+ )
217
+ loaded.load()
218
+ ```
219
+
220
+ Context manager usage auto-saves on exit:
221
+
222
+ ```python
223
+ with VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz") as store:
224
+ store.add(embeddings, metadata)
225
+ ```
226
+
227
+ Persistence uses a minimal NumPy `.npz` contract with `vectors` and `metadata`
228
+ arrays. The `.npz` file does not encode the `normalize` setting; choose the same
229
+ setting when loading that you used when saving. Loading validates shape,
230
+ dimensions, row counts, and zero-norm vectors. It also uses `allow_pickle=True`
231
+ for flexible Python metadata payloads, so only load files generated by your own
232
+ application or another trusted local process. Loading untrusted `.npz` files is
233
+ not a supported security model.
234
+
235
+ ## Compatibility
236
+
237
+ This project is still pre-1.0, so occasional breaking changes are expected while
238
+ the API stabilizes. Breaking changes are documented in GitHub release notes.
239
+ Deprecated APIs will keep warning for at least one point release before removal.
240
+
241
+ ## Contributing
242
+
243
+ ```bash
244
+ git clone https://github.com/tvanreenen/numpy-vector-store.git
245
+ cd numpy-vector-store
246
+ uv sync --frozen --group dev
247
+ ```
248
+
249
+ Before submitting a pull request:
250
+
251
+ 1. Run `uv run ruff check`
252
+ 2. Run `uv run ruff format --check`
253
+ 3. Run `uv run mypy src/`
254
+ 4. Run `uv run pytest`
255
+
256
+ ## License
257
+
258
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -0,0 +1,234 @@
1
+ # NumPy Vector Store
2
+
3
+ A fast, lightweight, zero-setup in-memory vector store powered by NumPy.
4
+
5
+ - **Tiny local vector search** for projects that do not need a vector database
6
+ - **Fast exact vector search** using vectorized NumPy operations
7
+ - **Simple typed API** returning `VectorHit(index, value, metadata)`
8
+ - **Composable filtering** by passing prefiltered row indexes with `within_rows`
9
+ - **Portable persistence** as trusted local `.npz` files with `vectors` + `metadata`
10
+ - **No framework opinions**: bring your own embeddings, chunking, async, and metadata model
11
+
12
+ ## Why?
13
+
14
+ This library is purpose-built for small to medium-scale vector search tasks and
15
+ offers a simple alternative to heavyweight vector databases when you do not need
16
+ network services, indexing infrastructure, ingestion pipelines, or domain-specific
17
+ metadata filtering.
18
+
19
+ ## When/Where?
20
+
21
+ Below are benchmark results for cosine similarity search to help you assess its
22
+ suitability for your use case.
23
+
24
+ | Embedding Type | Dimensions | ~5ms | ~25ms | ~100ms | ~500ms |
25
+ |----------------|------------|------|--------|---------|---------|
26
+ | **Sentence Transformers** | 384 | 1K vectors<br/>1.5MB | 10K vectors<br/>15MB | 100K vectors<br/>147MB | 500K vectors<br/>732MB |
27
+ | **OpenAI Small** | 1536 | 500 vectors<br/>3MB | 5K vectors<br/>29MB | 25K vectors<br/>147MB | 100K vectors<br/>586MB |
28
+ | **OpenAI Large** | 3072 | 200 vectors<br/>2MB | 2.5K vectors<br/>29MB | 5K vectors<br/>59MB | 25K vectors<br/>293MB |
29
+
30
+ *Benchmarks performed on Apple M2 hardware.*
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ uv add numpy-vector-store
36
+ ```
37
+
38
+ ## Quick Start
39
+
40
+ ```python
41
+ import numpy as np
42
+ from numpy_vector_store import VectorStore
43
+
44
+ store = VectorStore[dict[str, str]](dimensions=3)
45
+
46
+ store.add(
47
+ vectors=np.array([
48
+ [1.0, 0.0, 0.0],
49
+ [0.0, 1.0, 0.0],
50
+ [0.0, 0.0, 1.0],
51
+ ]),
52
+ metadata=[
53
+ {"title": "x-axis"},
54
+ {"title": "y-axis"},
55
+ {"title": "z-axis"},
56
+ ],
57
+ )
58
+
59
+ hits = store.cosine_search(
60
+ query=np.array([0.9, 0.1, 0.0]),
61
+ top_k=2,
62
+ )
63
+
64
+ for hit in hits:
65
+ print(f"{hit.metadata['title']}: {hit.value:.3f}")
66
+ ```
67
+
68
+ `metadata` is an opaque row payload returned with hits. It can be a dict,
69
+ dataclass, string, integer row ID, or any other Python object that fits your
70
+ application.
71
+
72
+ ## Normalization
73
+
74
+ `VectorStore` defaults to `normalize=True`, which scales each stored vector to
75
+ length `1`. Normalization preserves vector direction while discarding magnitude:
76
+
77
+ ```python
78
+ [3.0, 4.0] -> [0.6, 0.8]
79
+ ```
80
+
81
+ This is the default because it makes cosine similarity fast and direction-only,
82
+ which is the common case for semantic embeddings. Use `normalize=False` when
83
+ vector length matters, such as when magnitude encodes strength, confidence,
84
+ counts, scale, or raw geometry.
85
+
86
+ Zero vectors are rejected in both modes because cosine similarity is undefined
87
+ for zero-norm vectors.
88
+
89
+ | Method | `normalize=True` default | `normalize=False` |
90
+ |---|---|---|
91
+ | `cosine_search` | True cosine similarity over stored unit vectors; fastest/default path for embeddings | True cosine similarity over raw vectors; computes vector norms during search |
92
+ | `dot_search` | Dot product of unit vectors, effectively equivalent to cosine similarity | True dot product over original vectors; use when magnitude should affect ranking |
93
+ | `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 |
94
+ | `get` | Returns normalized vectors | Returns original vectors |
95
+ | `save` | Saves normalized vectors | Saves raw vectors |
96
+ | `load` | Loads and normalizes vectors | Loads vectors exactly as stored |
97
+
98
+ ## Search Methods
99
+
100
+ Use `cosine_search` for semantic embeddings and direction-only similarity:
101
+
102
+ ```python
103
+ hits = store.cosine_search(query, top_k=10, min_value=0.75)
104
+ ```
105
+
106
+ Use `dot_search` with `normalize=False` when larger-magnitude vectors should
107
+ rank higher:
108
+
109
+ ```python
110
+ store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
111
+ store.add(vectors, metadata)
112
+ hits = store.dot_search(query, top_k=10, min_value=0.0)
113
+ ```
114
+
115
+ Use `euclidean_search` with `normalize=False` for raw coordinate or feature-space
116
+ nearest-neighbor search:
117
+
118
+ ```python
119
+ store = VectorStore[dict[str, str]](dimensions=3, normalize=False)
120
+ store.add(vectors, metadata)
121
+ hits = store.euclidean_search(query, top_k=10, max_value=1.5)
122
+ ```
123
+
124
+ ## Prefiltering
125
+
126
+ The store does not implement a metadata query language. To filter by metadata,
127
+ produce row indexes first, then pass them with `within_rows`.
128
+
129
+ ```python
130
+ rows = [
131
+ i
132
+ for i, metadata in enumerate(store.metadata)
133
+ if metadata["title"].startswith("x")
134
+ ]
135
+
136
+ hits = store.cosine_search(query, top_k=10, within_rows=rows)
137
+ ```
138
+
139
+ For structured NumPy metadata, use NumPy to produce the row indexes:
140
+
141
+ ```python
142
+ metadata_table = np.array(
143
+ [
144
+ ("intro", "A", 2024),
145
+ ("setup", "A", 2023),
146
+ ("guide", "B", 2024),
147
+ ],
148
+ dtype=[("title", "U20"), ("product", "U10"), ("year", "i4")],
149
+ )
150
+
151
+ store = VectorStore[int](dimensions=3)
152
+ store.add(vectors, metadata=np.arange(len(metadata_table)))
153
+
154
+ mask = (metadata_table["product"] == "A") & (metadata_table["year"] >= 2024)
155
+ rows = np.flatnonzero(mask)
156
+
157
+ hits = store.cosine_search(query, within_rows=rows)
158
+
159
+ for hit in hits:
160
+ row = metadata_table[hit.metadata]
161
+ print(row["title"], hit.value)
162
+ ```
163
+
164
+ ## Persistence
165
+
166
+ Pass a `file_path` and call `save()` / `load()` explicitly:
167
+
168
+ ```python
169
+ store = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
170
+ store.add(embeddings, metadata)
171
+ store.save()
172
+
173
+ loaded = VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz")
174
+ loaded.load()
175
+ ```
176
+
177
+ If you save with `normalize=False`, load with `normalize=False` too:
178
+
179
+ ```python
180
+ store = VectorStore[dict[str, str]](
181
+ dimensions=1536,
182
+ file_path="raw-vectors.npz",
183
+ normalize=False,
184
+ )
185
+ store.add(raw_vectors, metadata)
186
+ store.save()
187
+
188
+ loaded = VectorStore[dict[str, str]](
189
+ dimensions=1536,
190
+ file_path="raw-vectors.npz",
191
+ normalize=False,
192
+ )
193
+ loaded.load()
194
+ ```
195
+
196
+ Context manager usage auto-saves on exit:
197
+
198
+ ```python
199
+ with VectorStore[dict[str, str]](dimensions=1536, file_path="vectors.npz") as store:
200
+ store.add(embeddings, metadata)
201
+ ```
202
+
203
+ Persistence uses a minimal NumPy `.npz` contract with `vectors` and `metadata`
204
+ arrays. The `.npz` file does not encode the `normalize` setting; choose the same
205
+ setting when loading that you used when saving. Loading validates shape,
206
+ dimensions, row counts, and zero-norm vectors. It also uses `allow_pickle=True`
207
+ for flexible Python metadata payloads, so only load files generated by your own
208
+ application or another trusted local process. Loading untrusted `.npz` files is
209
+ not a supported security model.
210
+
211
+ ## Compatibility
212
+
213
+ This project is still pre-1.0, so occasional breaking changes are expected while
214
+ the API stabilizes. Breaking changes are documented in GitHub release notes.
215
+ Deprecated APIs will keep warning for at least one point release before removal.
216
+
217
+ ## Contributing
218
+
219
+ ```bash
220
+ git clone https://github.com/tvanreenen/numpy-vector-store.git
221
+ cd numpy-vector-store
222
+ uv sync --frozen --group dev
223
+ ```
224
+
225
+ Before submitting a pull request:
226
+
227
+ 1. Run `uv run ruff check`
228
+ 2. Run `uv run ruff format --check`
229
+ 3. Run `uv run mypy src/`
230
+ 4. Run `uv run pytest`
231
+
232
+ ## License
233
+
234
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -7,7 +7,7 @@ name = "numpy-vector-store"
7
7
  dynamic = ["version"]
8
8
  description = "A fast, lightweight, and zero-setup in-memory vector store powered by NumPy"
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  license = {text = "MIT"}
12
12
  authors = [
13
13
  {name = "Tim VanReenen"},
@@ -18,10 +18,11 @@ classifiers = [
18
18
  "Intended Audience :: Developers",
19
19
  "License :: OSI Approved :: MIT License",
20
20
  "Programming Language :: Python :: 3",
21
- "Programming Language :: Python :: 3.9",
22
21
  "Programming Language :: Python :: 3.10",
23
22
  "Programming Language :: Python :: 3.11",
24
23
  "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
25
26
  ]
26
27
  dependencies = [
27
28
  "numpy>=1.20.0",
@@ -41,7 +42,7 @@ path = "src/numpy_vector_store/__init__.py"
41
42
 
42
43
  [tool.ruff]
43
44
  line-length = 88
44
- target-version = "py39"
45
+ target-version = "py310"
45
46
 
46
47
  [tool.ruff.lint]
47
48
  select = [
@@ -62,7 +63,7 @@ ignore = [
62
63
  known-first-party = ["numpy_vector_store"]
63
64
 
64
65
  [tool.mypy]
65
- python_version = "3.9"
66
+ python_version = "3.10"
66
67
  warn_return_any = true
67
68
  warn_unused_configs = true
68
69
  disallow_untyped_defs = true
@@ -5,9 +5,9 @@ This package provides a lightweight vector store implementation for storing
5
5
  and searching vector embeddings using NumPy arrays.
6
6
  """
7
7
 
8
- __version__ = "0.1.0"
8
+ __version__ = "0.3.0"
9
9
  __author__ = "Tim VanReenen"
10
10
 
11
- from .vector_store import VectorStore
11
+ from .vector_store import VectorHit, VectorStore
12
12
 
13
- __all__ = ["VectorStore"]
13
+ __all__ = ["VectorHit", "VectorStore"]