numpy-vector-store 0.1.0__py3-none-any.whl

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,13 @@
1
+ """
2
+ NumPy Vector Store - A simple vector store implementation using NumPy.
3
+
4
+ This package provides a lightweight vector store implementation for storing
5
+ and searching vector embeddings using NumPy arrays.
6
+ """
7
+
8
+ __version__ = "0.1.0"
9
+ __author__ = "Tim VanReenen"
10
+
11
+ from .vector_store import VectorStore
12
+
13
+ __all__ = ["VectorStore"]
@@ -0,0 +1,253 @@
1
+ from pathlib import Path
2
+ from typing import Any, Optional
3
+
4
+ import numpy as np
5
+
6
+
7
+ class VectorStore:
8
+ """
9
+ A simple vector store implementation using NumPy.
10
+
11
+ This class provides basic functionality for storing and searching
12
+ vector embeddings using cosine similarity.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ dimensions: int,
18
+ file_path: Optional[str] = None,
19
+ metadata_schema: Optional[dict] = None,
20
+ ):
21
+ """
22
+ Initialize the vector store.
23
+
24
+ Args:
25
+ dimensions: The number of dimensions for vectors to be stored.
26
+ file_path: Optional path to save/load vectors from.
27
+ metadata_schema: Optional schema for structured metadata operations.
28
+ """
29
+ self.dimensions = dimensions
30
+ self.file_path = Path(file_path) if file_path else None
31
+ # Initialize as empty 2D array with correct dimensions
32
+ self.vectors: np.ndarray = np.array([]).reshape(0, dimensions)
33
+ self._loaded = False
34
+
35
+ if metadata_schema:
36
+ # Use structured NumPy array for performance
37
+ self.metadata = np.array([], dtype=self._create_dtype(metadata_schema))
38
+ self._use_structured = True
39
+ self._metadata_schema: Optional[dict] = metadata_schema
40
+ else:
41
+ # Use object array for flexibility (still NumPy)
42
+ self.metadata = np.array([], dtype=object)
43
+ self._use_structured = False
44
+ self._metadata_schema = None
45
+
46
+ def _create_dtype(self, schema: dict) -> list:
47
+ """Convert schema dict to NumPy dtype."""
48
+ dtype = []
49
+ for field, field_type in schema.items():
50
+ if isinstance(field_type, str):
51
+ dtype.append((field, field_type))
52
+ else:
53
+ dtype.append((field, "O")) # Object type for complex data
54
+ return dtype
55
+
56
+ def add_vectors(self, vectors_2d: np.ndarray, metadata_array: np.ndarray) -> None:
57
+ """
58
+ Add vectors and metadata directly as NumPy arrays (most efficient).
59
+
60
+ Args:
61
+ vectors_2d: 2D NumPy array of shape (n_vectors, dimensions)
62
+ metadata_array: 1D NumPy array of metadata objects
63
+ """
64
+ if vectors_2d.ndim != 2:
65
+ raise ValueError("vectors_2d must be a 2D NumPy array")
66
+
67
+ if metadata_array.ndim != 1:
68
+ raise ValueError("metadata_array must be a 1D NumPy array")
69
+
70
+ if vectors_2d.shape[1] != self.dimensions:
71
+ raise ValueError(
72
+ f"Vector dimensions {vectors_2d.shape[1]} doesn't match store dimensions {self.dimensions}"
73
+ )
74
+
75
+ if len(vectors_2d) != len(metadata_array):
76
+ raise ValueError("Number of vectors must match number of metadata items")
77
+
78
+ # Normalize vectors in batch
79
+ norms = np.linalg.norm(vectors_2d, axis=1, keepdims=True)
80
+ if np.any(norms == 0):
81
+ raise ValueError("Cannot add zero-norm vectors")
82
+ normalized_vectors = vectors_2d / norms
83
+
84
+ # Add vectors directly
85
+ if len(self.vectors) == 0:
86
+ self.vectors = normalized_vectors
87
+ else:
88
+ self.vectors = np.vstack([self.vectors, normalized_vectors])
89
+
90
+ # Add metadata directly
91
+ self.metadata = np.append(self.metadata, metadata_array)
92
+
93
+ def load(self) -> None:
94
+ """
95
+ Load vectors from file if file_path is specified.
96
+ """
97
+ if self._loaded or not self.file_path:
98
+ return
99
+
100
+ if self.file_path.exists():
101
+ data = np.load(self.file_path, allow_pickle=True)
102
+ loaded_vectors = data["vectors"]
103
+ loaded_metadata = data["metadata"]
104
+ data.close()
105
+
106
+ if loaded_vectors.ndim != 2:
107
+ raise ValueError("Loaded vectors must be a 2D array")
108
+
109
+ if loaded_vectors.shape[1] != self.dimensions:
110
+ raise ValueError(
111
+ f"Loaded vector dimension {loaded_vectors.shape[1]} doesn't match store dimensions {self.dimensions}"
112
+ )
113
+
114
+ if len(loaded_vectors) != len(loaded_metadata):
115
+ raise ValueError("Loaded vectors and metadata length mismatch")
116
+
117
+ if self._use_structured and self._metadata_schema is not None:
118
+ expected_fields = tuple(self._metadata_schema.keys())
119
+ loaded_fields = loaded_metadata.dtype.names
120
+ if loaded_fields != expected_fields:
121
+ raise ValueError(
122
+ f"Loaded metadata schema {loaded_fields} doesn't match expected schema {expected_fields}"
123
+ )
124
+ loaded_metadata = loaded_metadata.astype(
125
+ np.dtype(self._create_dtype(self._metadata_schema)), copy=False
126
+ )
127
+
128
+ self.vectors = loaded_vectors
129
+ self.metadata = loaded_metadata
130
+
131
+ self._loaded = True
132
+
133
+ def save(self) -> None:
134
+ """
135
+ Save vectors to file if file_path is specified.
136
+ """
137
+ if not self.file_path:
138
+ return
139
+
140
+ vectors_array: np.ndarray = self.vectors.astype(np.float32)
141
+ # Preserve metadata dtype so structured arrays remain structured
142
+ # after save/load round-trips.
143
+ metadata_array = np.array(self.metadata, copy=True)
144
+ np.savez_compressed(self.file_path, vectors=vectors_array, metadata=metadata_array)
145
+
146
+ def search(
147
+ self, query_vector: np.ndarray, top_k: int = 10, score_cutoff: float = 0.0
148
+ ) -> list[tuple[int, float, dict]]:
149
+ """
150
+ Search for the most similar vectors.
151
+
152
+ Args:
153
+ query_vector: The query vector to search with.
154
+ top_k: Number of top results to return.
155
+ score_cutoff: Minimum similarity score to include in results.
156
+
157
+ Returns:
158
+ List of tuples containing (index, similarity_score, metadata).
159
+ """
160
+ if len(query_vector) != self.dimensions:
161
+ raise ValueError(
162
+ f"Query vector dimension {len(query_vector)} doesn't match store dimensions {self.dimensions}"
163
+ )
164
+
165
+ if top_k <= 0:
166
+ raise ValueError("top_k must be greater than 0")
167
+
168
+ if len(self.vectors) == 0:
169
+ return []
170
+
171
+ # Use efficient cosine similarity computation
172
+ similarities = self._cosine_similarity_numpy(query_vector, self.vectors)
173
+
174
+ # Filter by similarity cutoff first
175
+ valid_indices = np.where(similarities >= score_cutoff)[0]
176
+ if len(valid_indices) == 0:
177
+ return []
178
+
179
+ # Get top_k results from valid indices
180
+ valid_similarities = similarities[valid_indices]
181
+ top_valid_indices = np.argsort(valid_similarities)[::-1][:top_k]
182
+ top_indices = valid_indices[top_valid_indices]
183
+
184
+ results = []
185
+ for idx in top_indices:
186
+ results.append((int(idx), float(similarities[idx]), self.metadata[idx]))
187
+
188
+ return results
189
+
190
+ def _cosine_similarity_numpy(
191
+ self, query: np.ndarray, vectors: np.ndarray
192
+ ) -> np.ndarray:
193
+ """
194
+ Efficient cosine similarity computation using NumPy.
195
+
196
+ Args:
197
+ query: Query vector.
198
+ vectors: Array of vectors to compare against.
199
+
200
+ Returns:
201
+ Array of similarity scores.
202
+ """
203
+ query_magnitude = np.linalg.norm(query)
204
+ if query_magnitude == 0:
205
+ raise ValueError("Cannot search with zero-norm query vector")
206
+ query_norm = query / query_magnitude
207
+ vector_norms = np.linalg.norm(vectors, axis=1, keepdims=True)
208
+ if np.any(vector_norms == 0):
209
+ raise ValueError("Store contains zero-norm vectors")
210
+ vectors_norm = vectors / vector_norms
211
+ return np.dot(vectors_norm, query_norm) # type: ignore[no-any-return]
212
+
213
+ def get(self, index: int) -> Optional[tuple[np.ndarray, dict]]:
214
+ """
215
+ Get vector and metadata by index.
216
+
217
+ Args:
218
+ index: The index of the vector to retrieve.
219
+
220
+ Returns:
221
+ Tuple of (vector, metadata) or None if index is out of bounds.
222
+ """
223
+ if 0 <= index < len(self.vectors):
224
+ if self._use_structured and self._metadata_schema is not None:
225
+ # Convert structured array row to dict
226
+ metadata_row = self.metadata[index]
227
+ metadata_dict = {
228
+ field: metadata_row[field] for field in self._metadata_schema.keys()
229
+ }
230
+ return (self.vectors[index], metadata_dict)
231
+ else:
232
+ # Object array - return the dict directly
233
+ return (self.vectors[index], self.metadata[index])
234
+ return None
235
+
236
+ def clear(self) -> None:
237
+ """Clear all vectors and metadata from the store."""
238
+ self.vectors = np.array([]).reshape(0, self.dimensions)
239
+ if self._use_structured and self._metadata_schema is not None:
240
+ self.metadata = np.array(
241
+ [], dtype=self._create_dtype(self._metadata_schema)
242
+ )
243
+ else:
244
+ self.metadata = np.array([], dtype=object)
245
+
246
+ def __enter__(self) -> "VectorStore":
247
+ """Enter the context manager."""
248
+ return self
249
+
250
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
251
+ """Exit the context manager, auto-save if file_path is specified."""
252
+ if self.file_path:
253
+ self.save()
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: numpy-vector-store
3
+ Version: 0.1.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.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: numpy>=1.20.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # NumPy Vector Store
25
+
26
+ A fast, lightweight, and zero-setup in-memory vector store powered by NumPy.
27
+
28
+ - **High-performance** index-free O(n) cosine similarity searches
29
+ - **Vectorized** metadata queries with NumPy operations
30
+ - **Easy persistence** to and from compressed NumPy binary files (.npz)
31
+ - **Zero dependency** on external services or data stores
32
+
33
+ ## Why?
34
+
35
+ This library is purpose-built for small to medium-scale vector search tasks and offers a simple, lightweight alternative to heavyweight solutions like Pinecone, Qdrant, Weaviate, Postgres + pgvector, or Azure AI Search—no complex setup or infrastructure required.
36
+
37
+ > Sometimes you don't need a sledgehammer to crack a nut.
38
+
39
+ ## When/Where?
40
+
41
+ Below are benchmark results for the module's search method to help you assess its suitability for your use case.
42
+
43
+ | Embedding Type | Dimensions | ~5ms | ~25ms | ~100ms | ~500ms |
44
+ |----------------|------------|------|-------|--------|--------|
45
+ | **Sentence Transformers** | 384 | 1K vectors<br/>1.5MB | 10K vectors<br/>15MB | 100K vectors<br/>147MB | 500K vectors<br/>732MB |
46
+ | **OpenAI Small** | 1536 | 500 vectors<br/>3MB | 5K vectors<br/>29MB | 25K vectors<br/>147MB | 100K vectors<br/>586MB |
47
+ | **OpenAI Large** | 3072 | 200 vectors<br/>2MB | 2.5K vectors<br/>29MB | 5K vectors<br/>59MB | 25K vectors<br/>293MB |
48
+
49
+ *Benchmarks performed on Apple M2 hardware*
50
+
51
+ ## Installation
52
+
53
+ **⚠️ Pending submission to PyPI**
54
+
55
+ ```bash
56
+ uv add numpy-vector-store
57
+ ```
58
+
59
+ ## Quick Start
60
+
61
+ ```python
62
+ import numpy as np
63
+ from numpy_vector_store import VectorStore
64
+
65
+ # Load your vector store
66
+ store = VectorStore(dimensions=1536, file_path='vectors.npz')
67
+ store.load()
68
+
69
+ # Embed your search query
70
+ query = np.array([0.2, 0.3, 0.4, ...])
71
+
72
+ # Search using cosine similarity
73
+ results = store.search(query, top_k=3)
74
+
75
+ # Compare the results
76
+ for index, similarity, meta in results:
77
+ print(f"{meta['title']}: {similarity:.3f}")
78
+ ```
79
+
80
+ ## Usage Examples
81
+
82
+ ### Adding Vectors
83
+
84
+ #### Adding Vectors in Batch
85
+
86
+ The `add_vectors` method takes a **2D NumPy array** where each row is a vector, and a **1D NumPy array** of metadata objects.
87
+
88
+ ```python
89
+ # 2D np.array of vectors
90
+ embeddings = np.array([
91
+ [0.1, 0.2, 0.3, ...], # Text embedding 1
92
+ [0.4, 0.5, 0.6, ...], # Text embedding 2
93
+ [0.7, 0.8, 0.9, ...] # Text embedding 3
94
+ ])
95
+
96
+ # 1D np.array of metadata objects
97
+ metadata = np.array([
98
+ {"title": "AI Overview", "word_count": 12},
99
+ {"title": "Python Guide", "word_count": 10},
100
+ {"title": "Vector DBs", "word_count": 8}
101
+ ])
102
+
103
+ # Vectors and metadata added using efficient vectorized NumPy operations
104
+ store.add_vectors(embeddings, metadata)
105
+ ```
106
+
107
+ #### Adding Vectors Individually
108
+
109
+ Batch operations are generally preferable and more efficient. But individual vectors can be added with the same method.
110
+
111
+ ```python
112
+ store.add_vectors(
113
+ np.array([new_embedding]), # or np.atleast_2d(new_embedding)
114
+ np.array([{"title": "Neural Networks Paper", "word_count": 15}])
115
+ )
116
+ ```
117
+
118
+ ### Save the Vector Store
119
+
120
+ Save to a gzip compressed NumPy binary file directly
121
+
122
+ ```python
123
+ store.save()
124
+ ```
125
+
126
+ Or use context manager for automatic persistence
127
+
128
+ ```python
129
+ with VectorStore(dimensions=3, file_path="vectors.npz") as store:
130
+ store.add_vectors(vectors_2d, metadata_array)
131
+
132
+ # Automatically saves when exiting the context
133
+ ```
134
+
135
+ ### Working with Metadata
136
+
137
+ Work with flexible, unstructured metadata using standard Python operations. No schema required - perfect for getting started quickly:
138
+
139
+ ```python
140
+ # Access individual metadata
141
+ first_metadata = store.metadata[0]
142
+ print(f"First entry: {first_metadata}")
143
+
144
+ # Iterate through all metadata
145
+ for i, metadata in enumerate(store.metadata):
146
+ print(f"Entry {i}: {metadata}")
147
+ ```
148
+
149
+ ### Advanced: Structured Metadata for Performance
150
+
151
+ If you define a homogeneous NumPy schema upfront, you get **significant performance improvements** for metadata operations. Instead of Python loops and dictionary lookups, you get **vectorized NumPy operations** that are orders of magnitude faster.
152
+
153
+ > **Learn more**: [NumPy Structured Arrays Documentation](https://numpy.org/doc/stable/user/basics.rec.html)
154
+
155
+ ```python
156
+ # Define schema for performance
157
+ store = VectorStore(
158
+ dimensions=512,
159
+ metadata_schema={'title': 'U200', 'year': 'i4', 'citations': 'i4'}
160
+ )
161
+ store.add_vectors(
162
+ np.array([vector1, vector2]),
163
+ np.array([
164
+ {"title": "Paper 1", "year": 2023, "citations": 100},
165
+ {"title": "Paper 2", "year": 2022, "citations": 50}
166
+ ])
167
+ )
168
+
169
+ # Perform vectorized NumPy operations on metadata
170
+ recent_mask = store.metadata['year'] == 2023
171
+ recent_vectors = np.array(store.vectors)[recent_mask]
172
+
173
+ # Sort by citations
174
+ sorted_indices = np.argsort(store.metadata['citations'])[::-1]
175
+ top_vectors = np.array(store.vectors)[sorted_indices]
176
+
177
+ # Complex filtering
178
+ high_impact = store.metadata['citations'] > 100
179
+ recent = store.metadata['year'] > 2020
180
+ combined_mask = high_impact & recent
181
+ filtered_vectors = np.array(store.vectors)[combined_mask]
182
+ ```
183
+
184
+ ## Contributing
185
+
186
+ ### Setup Development Environment
187
+
188
+ ```bash
189
+ git clone https://github.com/tvanreenen/numpy-vector-store.git
190
+ cd numpy-vector-store
191
+ uv sync --frozen --group dev
192
+ ```
193
+
194
+ ### Before Submitting a Pull Request
195
+
196
+ Please ensure:
197
+
198
+ 1. **Code Quality**: Run `uv run ruff check` - should show no issues
199
+ 2. **Formatting**: Run `uv run ruff format` - should show "files left unchanged"
200
+ 3. **Type Checking**: Run `uv run mypy src/` - should show no errors
201
+ 4. **Tests**: Run `uv run pytest` - all tests should pass
202
+
203
+ ## License
204
+
205
+ MIT License - see [LICENSE](LICENSE) file for details.
@@ -0,0 +1,6 @@
1
+ numpy_vector_store/__init__.py,sha256=0_LXVUazbViKSm_aaZvRX7kEIti9YoIATGV31ycifKo,326
2
+ numpy_vector_store/vector_store.py,sha256=-FFqRg0-tG6xJdz8eg0aaUdIqq5WB9LUf3FTLfdTQRI,9399
3
+ numpy_vector_store-0.1.0.dist-info/METADATA,sha256=kK2_AnERaUVSWtEY3SCT9nWDpVt1NPNZLqg8l1Nc424,6478
4
+ numpy_vector_store-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
5
+ numpy_vector_store-0.1.0.dist-info/licenses/LICENSE,sha256=SUVvvB3m6p_RRGYL6l9PPdlUfg39dxYNqeF0b9-z_bg,1070
6
+ numpy_vector_store-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tim VanReenen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.