vicinity 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.
vicinity/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Small vector store."""
2
+
3
+ from vicinity.utils import normalize
4
+ from vicinity.version import __version__
5
+ from vicinity.vicinity import Vicinity
6
+
7
+ __all__ = ["Vicinity", "normalize", "__version__"]
@@ -0,0 +1,17 @@
1
+ from vicinity.backends.base import AbstractBackend
2
+ from vicinity.backends.basic import BasicBackend
3
+ from vicinity.datatypes import Backend
4
+
5
+
6
+ def get_backend_class(backend: Backend | str) -> type[AbstractBackend]:
7
+ """Get all available backends."""
8
+ backend = Backend(backend)
9
+ if backend == Backend.BASIC:
10
+ return BasicBackend
11
+ elif backend == Backend.HNSW:
12
+ from vicinity.backends.hnsw import HNSWBackend
13
+
14
+ return HNSWBackend
15
+
16
+
17
+ __all__ = ["get_backend_class", "AbstractBackend"]
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from abc import ABC, abstractmethod
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any, TypeVar
8
+
9
+ from numpy import typing as npt
10
+
11
+ from vicinity.datatypes import Backend, QueryResult
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class BaseArgs:
16
+ dim: int | None = None
17
+
18
+ def dump(self, file: Path) -> None:
19
+ """Dump the arguments to a file."""
20
+ with open(file, "w") as f:
21
+ json.dump(asdict(self), f)
22
+
23
+ @classmethod
24
+ def load(cls: type[ArgType], file: Path) -> ArgType:
25
+ """Load the arguments from a file."""
26
+ with open(file, "r") as f:
27
+ return cls(**json.load(f))
28
+
29
+ def dict(self) -> dict[str, Any]:
30
+ """Dump the arguments to a string."""
31
+ return asdict(self)
32
+
33
+
34
+ ArgType = TypeVar("ArgType", bound=BaseArgs)
35
+
36
+
37
+ class AbstractBackend(ABC):
38
+ argument_class: type[BaseArgs]
39
+
40
+ def __init__(self, arguments: ArgType, *args: Any, **kwargs: Any) -> None:
41
+ """Initialize the backend with vectors."""
42
+ self.arguments = arguments
43
+
44
+ @classmethod
45
+ @abstractmethod
46
+ def from_vectors(cls: type[BaseType], vectors: npt.NDArray, *args: Any, **kwargs: Any) -> BaseType:
47
+ """Create a new instance from vectors."""
48
+ raise NotImplementedError()
49
+
50
+ @abstractmethod
51
+ def __len__(self) -> int:
52
+ """The number of items in the backend."""
53
+ raise NotImplementedError()
54
+
55
+ @property
56
+ @abstractmethod
57
+ def backend_type(self) -> Backend:
58
+ """The type of the backend."""
59
+ raise NotImplementedError()
60
+
61
+ @property
62
+ @abstractmethod
63
+ def dim(self) -> int:
64
+ """The size of the space."""
65
+ raise NotImplementedError()
66
+
67
+ @classmethod
68
+ @abstractmethod
69
+ def load(cls: type[BaseType], path: Path) -> BaseType:
70
+ """Load a backend from a file."""
71
+ raise NotImplementedError()
72
+
73
+ @abstractmethod
74
+ def save(self, base_path: Path) -> None:
75
+ """Save the backend to a file."""
76
+ raise NotImplementedError()
77
+
78
+ @abstractmethod
79
+ def insert(self, vectors: npt.NDArray) -> None:
80
+ """Insert vectors into the backend."""
81
+ raise NotImplementedError()
82
+
83
+ @abstractmethod
84
+ def delete(self, indices: list[int]) -> None:
85
+ """Delete vectors from the backend."""
86
+ raise NotImplementedError()
87
+
88
+ @abstractmethod
89
+ def threshold(self, vectors: npt.NDArray, threshold: float) -> list[npt.NDArray]:
90
+ """Threshold the backend."""
91
+ raise NotImplementedError()
92
+
93
+ @abstractmethod
94
+ def query(self, vectors: npt.NDArray, k: int) -> QueryResult:
95
+ """Query the backend."""
96
+ raise NotImplementedError()
97
+
98
+
99
+ BaseType = TypeVar("BaseType", bound=AbstractBackend)
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import numpy as np
8
+ from numpy import typing as npt
9
+
10
+ from vicinity.backends.base import AbstractBackend, BaseArgs
11
+ from vicinity.datatypes import Backend, Matrix, QueryResult
12
+ from vicinity.utils import normalize, normalize_or_copy
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class BasicArgs(BaseArgs): ...
17
+
18
+
19
+ class BasicBackend(AbstractBackend):
20
+ argument_class = BasicArgs
21
+
22
+ def __init__(self, vectors: npt.NDArray, arguments: BasicArgs) -> None:
23
+ """Initialize the backend using vectors."""
24
+ super().__init__(arguments)
25
+ self._vectors = vectors
26
+ self._norm_vectors: npt.NDArray | None = None
27
+
28
+ def __len__(self) -> int:
29
+ """Get the number of vectors."""
30
+ return self.vectors.shape[0]
31
+
32
+ @property
33
+ def backend_type(self) -> Backend:
34
+ """The type of the backend."""
35
+ return Backend.BASIC
36
+
37
+ @classmethod
38
+ def from_vectors(cls: type[BasicBackend], vectors: npt.NDArray, dim: int | None = None) -> BasicBackend:
39
+ """Create a new instance from vectors."""
40
+ if dim is None:
41
+ dim = vectors.shape[1]
42
+ return cls(vectors, BasicArgs(dim=dim))
43
+
44
+ @classmethod
45
+ def load(cls: type[BasicBackend], folder: Path) -> BasicBackend:
46
+ """Load the vectors from a path."""
47
+ path = folder / "vectors.npy"
48
+ arguments = BasicArgs.load(folder / "arguments.json")
49
+ with open(path, "rb") as f:
50
+ return cls(np.load(f), arguments)
51
+
52
+ def save(self, folder: Path) -> None:
53
+ """Save the vectors to a path."""
54
+ path = Path(folder) / "vectors.npy"
55
+ self.arguments.dump(folder / "arguments.json")
56
+ with open(path, "wb") as f:
57
+ np.save(f, self._vectors)
58
+
59
+ @property
60
+ def dim(self) -> int:
61
+ """The size of the space."""
62
+ return self.vectors.shape[1]
63
+
64
+ @property
65
+ def vectors(self) -> npt.NDArray:
66
+ """The vectors themselves."""
67
+ return self._vectors
68
+
69
+ @vectors.setter
70
+ def vectors(self, x: Matrix) -> None:
71
+ matrix = np.asarray(x)
72
+ if not np.ndim(matrix) == 2:
73
+ raise ValueError(f"Your array does not have 2 dimensions: {np.ndim(matrix)}")
74
+ self._vectors = matrix
75
+ # Make sure norm vectors is updated.
76
+ if self._norm_vectors is not None:
77
+ self._norm_vectors = normalize_or_copy(matrix)
78
+
79
+ @property
80
+ def norm_vectors(self) -> npt.NDArray:
81
+ """
82
+ Vectors, but normalized to unit length.
83
+
84
+ NOTE: when all vectors are unit length, this attribute _is_ vectors.
85
+ """
86
+ if self._norm_vectors is None:
87
+ self._norm_vectors = normalize_or_copy(self.vectors)
88
+ return self._norm_vectors
89
+
90
+ def threshold(
91
+ self,
92
+ vectors: npt.NDArray,
93
+ threshold: float,
94
+ ) -> list[npt.NDArray]:
95
+ """Batched cosine similarity."""
96
+ out: list[npt.NDArray] = []
97
+ for i in range(0, len(vectors), 1024):
98
+ batch = vectors[i : i + 1024]
99
+ distances = self._dist(batch, self.norm_vectors)
100
+ for _, sims in enumerate(distances):
101
+ indices = np.flatnonzero(sims <= threshold)
102
+ sorted_indices = indices[np.argsort(sims[indices])]
103
+ out.append(sorted_indices)
104
+
105
+ return out
106
+
107
+ def query(
108
+ self,
109
+ vectors: npt.NDArray,
110
+ k: int,
111
+ ) -> QueryResult:
112
+ """Batched cosine distance."""
113
+ if k < 1:
114
+ raise ValueError("num should be >= 1, is now {num}")
115
+
116
+ out: QueryResult = []
117
+
118
+ for index in range(0, len(vectors), 1024):
119
+ batch = vectors[index : index + 1024]
120
+ distances = self._dist(batch, self.norm_vectors)
121
+ if k == 1:
122
+ sorted_indices = np.argmin(distances, 1, keepdims=True)
123
+ elif k >= len(self.vectors):
124
+ # If we want more than we have, just sort everything.
125
+ sorted_indices = np.stack([np.arange(len(self.vectors))] * len(vectors))
126
+ else:
127
+ sorted_indices = np.argpartition(distances, kth=k, axis=1)
128
+ sorted_indices = sorted_indices[:, :k]
129
+ for lidx, indices in enumerate(sorted_indices):
130
+ dists_for_word = distances[lidx, indices]
131
+ word_index = np.argsort(dists_for_word)
132
+ i = indices[word_index]
133
+ d = dists_for_word[word_index]
134
+ out.append((i, d))
135
+
136
+ return out
137
+
138
+ @classmethod
139
+ def _dist(cls, x: npt.NDArray, y: npt.NDArray) -> npt.NDArray:
140
+ """Cosine distance function. This assumes y is normalized."""
141
+ sim = normalize(x).dot(y.T)
142
+
143
+ return 1 - sim
144
+
145
+ def insert(self, vectors: npt.NDArray) -> None:
146
+ """Insert vectors into the vector space."""
147
+ self._vectors = np.vstack([self._vectors, vectors])
148
+
149
+ def delete(self, indices: list[int]) -> None:
150
+ """Deletes specific indices from the vector space."""
151
+ self._vectors = np.delete(self._vectors, indices, axis=0)
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ from hnswlib import Index as HnswIndex
8
+ from numpy import typing as npt
9
+
10
+ from vicinity.backends.base import AbstractBackend, BaseArgs
11
+ from vicinity.datatypes import Backend, QueryResult
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class HNSWArgs(BaseArgs):
16
+ dim: int | None = None
17
+ space: Literal["cosine", "l2"] = "cosine"
18
+ ef_construction: int = 200
19
+ m: int = 16
20
+
21
+
22
+ class HNSWBackend(AbstractBackend):
23
+ argument_class = HNSWArgs
24
+
25
+ def __init__(
26
+ self,
27
+ index: HnswIndex,
28
+ arguments: HNSWArgs,
29
+ ) -> None:
30
+ """Initialize the backend using vectors."""
31
+ super().__init__(arguments)
32
+ self.index = index
33
+
34
+ @classmethod
35
+ def from_vectors(
36
+ cls: type[HNSWBackend],
37
+ vectors: npt.NDArray,
38
+ dim: int | None,
39
+ space: Literal["cosine", "l2"],
40
+ ef_construction: int,
41
+ m: int,
42
+ ) -> HNSWBackend:
43
+ """Create a new instance from vectors."""
44
+ if dim is None:
45
+ dim = vectors.shape[1]
46
+ index = HnswIndex(space=space, dim=dim)
47
+ index.init_index(max_elements=vectors.shape[0], ef_construction=ef_construction, M=m)
48
+ index.add_items(vectors)
49
+ arguments = HNSWArgs(dim=dim, space=space, ef_construction=ef_construction, m=m)
50
+ return HNSWBackend(index, arguments=arguments)
51
+
52
+ @property
53
+ def backend_type(self) -> Backend:
54
+ """The type of the backend."""
55
+ return Backend.HNSW
56
+
57
+ @property
58
+ def dim(self) -> int:
59
+ """Get the dimension of the space."""
60
+ return self.index.dim
61
+
62
+ def __len__(self) -> int:
63
+ """Get the number of vectors."""
64
+ return self.index.get_current_count()
65
+
66
+ @classmethod
67
+ def load(cls: type[HNSWBackend], base_path: Path) -> HNSWBackend:
68
+ """Load the vectors from a path."""
69
+ path = Path(base_path) / "index.bin"
70
+ arguments = HNSWArgs.load(base_path / "arguments.json")
71
+ index = HnswIndex(space=arguments.space, dim=arguments.dim)
72
+ index.load_index(str(path))
73
+ return cls(index, arguments=arguments)
74
+
75
+ def save(self, base_path: Path) -> None:
76
+ """Save the vectors to a path."""
77
+ path = Path(base_path) / "index.bin"
78
+ self.index.save_index(str(path))
79
+ self.arguments.dump(base_path / "arguments.json")
80
+
81
+ def query(self, vectors: npt.NDArray, k: int) -> QueryResult:
82
+ """Query the backend."""
83
+ return list(zip(*self.index.knn_query(vectors, k)))
84
+
85
+ def insert(self, vectors: npt.NDArray) -> None:
86
+ """Insert vectors into the backend."""
87
+ self.index.add_items(vectors)
88
+
89
+ def delete(self, indices: list[int]) -> None:
90
+ """Delete vectors from the backend."""
91
+ for index in indices:
92
+ self.index.mark_deleted(index)
93
+
94
+ def threshold(self, vectors: npt.NDArray, threshold: float) -> list[npt.NDArray]:
95
+ """Threshold the backend."""
96
+ out: list[npt.NDArray] = []
97
+ for x, y in self.query(vectors, 100):
98
+ out.append(x[y < threshold])
99
+
100
+ return out
vicinity/datatypes.py ADDED
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+ from pathlib import Path
3
+ from typing import Iterable, TypeAlias
4
+
5
+ import numpy as np
6
+ from numpy import typing as npt
7
+
8
+ PathLike = str | Path
9
+ Matrix: TypeAlias = npt.NDArray | list[npt.NDArray]
10
+ SimilarityItem = list[tuple[str, float]]
11
+ SimilarityResult = list[SimilarityItem]
12
+ # Tuple of (indices, distances)
13
+ SingleQueryResult = tuple[npt.NDArray, npt.NDArray]
14
+ QueryResult = list[SingleQueryResult]
15
+ Tokens = Iterable[str]
16
+
17
+
18
+ class Backend(str, Enum):
19
+ HNSW = "hnsw"
20
+ BASIC = "basic"
vicinity/py.typed ADDED
File without changes
vicinity/utils.py ADDED
@@ -0,0 +1,51 @@
1
+ import numpy as np
2
+ from numpy import typing as npt
3
+
4
+
5
+ def normalize(vectors: npt.NDArray, norms: npt.NDArray | None = None) -> npt.NDArray:
6
+ """
7
+ Normalize a matrix of row vectors to unit length.
8
+
9
+ Contains a shortcut if there are no zero vectors in the matrix.
10
+ If there are zero vectors, we do some indexing tricks to avoid
11
+ dividing by 0.
12
+
13
+ :param vectors: The vectors to normalize.
14
+ :param norms: Precomputed norms. If this is None, the norms are computed.
15
+ :return: The input vectors, normalized to unit length.
16
+ """
17
+ if np.ndim(vectors) == 1:
18
+ norm_float = np.linalg.norm(vectors)
19
+ if np.isclose(norm_float, 0):
20
+ return np.zeros_like(vectors)
21
+ return vectors / norm_float
22
+
23
+ if norms is None:
24
+ norm: npt.NDArray = np.linalg.norm(vectors, axis=1)
25
+ else:
26
+ norm = norms
27
+
28
+ if np.any(np.isclose(norm, 0.0)):
29
+ vectors = np.copy(vectors)
30
+ nonzero = norm > 0.0
31
+ result = np.zeros_like(vectors)
32
+ masked_norm = norm[nonzero]
33
+ masked_vectors = vectors[nonzero]
34
+ result[nonzero] = masked_vectors / masked_norm[:, None]
35
+
36
+ return result
37
+ else:
38
+ return vectors / norm[:, None]
39
+
40
+
41
+ def normalize_or_copy(vectors: npt.NDArray) -> npt.NDArray:
42
+ """
43
+ Return the original vectors if they are already normalized.
44
+
45
+ Otherwise, the vectors are normalized, and a new array is returned.
46
+ """
47
+ norms = np.linalg.norm(vectors, axis=-1)
48
+ all_unit_length = np.allclose(norms[norms != 0], 1)
49
+ if all_unit_length:
50
+ return vectors
51
+ return normalize(vectors, norms)
vicinity/version.py ADDED
@@ -0,0 +1,2 @@
1
+ __version_triple__ = (0, 1, 0)
2
+ __version__ = ".".join(map(str, __version_triple__))
vicinity/vicinity.py ADDED
@@ -0,0 +1,228 @@
1
+ """A small vector store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from io import open
7
+ from pathlib import Path
8
+ from typing import Any, Sequence
9
+
10
+ import numpy as np
11
+ import orjson
12
+ from numpy import typing as npt
13
+
14
+ from vicinity.backends import AbstractBackend, get_backend_class
15
+ from vicinity.datatypes import Backend, PathLike
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class Vicinity:
21
+ """
22
+ Work with vector representations of items.
23
+
24
+ Supports functions for calculating fast batched similarity
25
+ between items or composite representations of items.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ items: Sequence[str],
31
+ backend: AbstractBackend,
32
+ metadata: dict[str, Any] | None = None,
33
+ ) -> None:
34
+ """
35
+ Initialize a Vicinity instance with an array and list of items.
36
+
37
+ :param items: The items in the vector space.
38
+ A list of items. Length must be equal to the number of vectors, and
39
+ aligned with the vectors.
40
+ :param backend: The backend to use for the vector space.
41
+ :param metadata: A dictionary containing metadata about the vector space.
42
+ :raises ValueError: If the length of the items and vectors are not the same.
43
+ """
44
+ if len(items) != len(backend):
45
+ raise ValueError(
46
+ "Your vector space and list of items are not the same length: " f"{len(backend)} != {len(items)}"
47
+ )
48
+ self.items: list[str] = list(items)
49
+ self.backend: AbstractBackend = backend
50
+ self.metadata = metadata or {}
51
+
52
+ def __len__(self) -> int:
53
+ """The number of the items in the vector space."""
54
+ return len(self.items)
55
+
56
+ @classmethod
57
+ def from_vectors_and_items(
58
+ cls: type[Vicinity],
59
+ vectors: npt.NDArray,
60
+ items: Sequence[str],
61
+ backend_type: Backend = Backend.BASIC,
62
+ **kwargs: Any,
63
+ ) -> Vicinity:
64
+ """
65
+ Create a Vicinity instance from vectors and items.
66
+
67
+ :param vectors: The vectors to use.
68
+ :param items: The items to use.
69
+ :param backend_type: The type of backend to use.
70
+ :param **kwargs: Additional arguments to pass to the backend.
71
+ :return: A Vicinity instance.
72
+ """
73
+ backend_cls = get_backend_class(backend_type)
74
+ arguments = backend_cls.argument_class(**kwargs)
75
+ backend = backend_cls.from_vectors(vectors, **arguments.dict())
76
+
77
+ return cls(items, backend)
78
+
79
+ @property
80
+ def dim(self) -> int:
81
+ """The dimensionality of the vectors."""
82
+ return self.backend.dim
83
+
84
+ def query(
85
+ self,
86
+ vectors: npt.NDArray,
87
+ k: int = 10,
88
+ ) -> list[list[tuple[str, float]]]:
89
+ """
90
+ Find the nearest neighbors to some arbitrary vector.
91
+
92
+ Use this to look up the nearest neighbors to a vector that is not in the vocabulary.
93
+
94
+ :param vectors: The vectors to find the nearest neighbors to.
95
+ :param k: The number of most similar items to retrieve.
96
+ :return: For each item in the input, the num most similar items are returned in the form of
97
+ (NAME, SIMILARITY) tuples.
98
+ """
99
+ vectors = np.asarray(vectors)
100
+ if np.ndim(vectors) == 1:
101
+ vectors = vectors[None, :]
102
+
103
+ out = []
104
+ for index, distances in self.backend.query(vectors, k):
105
+ distances.clip(min=0, out=distances)
106
+ out.append([(self.items[idx], dist) for idx, dist in zip(index, distances)])
107
+
108
+ return out
109
+
110
+ def query_threshold(
111
+ self,
112
+ vectors: npt.NDArray,
113
+ threshold: float = 0.5,
114
+ ) -> list[list[str]]:
115
+ """
116
+ Find the nearest neighbors to some arbitrary vector with some threshold.
117
+
118
+ :param vectors: The vectors to find the most similar vectors to.
119
+ :param threshold: The threshold to use.
120
+
121
+ :return: For each item in the input, all items above the threshold are returned.
122
+ """
123
+ vectors = np.array(vectors)
124
+ if np.ndim(vectors) == 1:
125
+ vectors = vectors[None, :]
126
+
127
+ out = []
128
+ for indexes in self.backend.threshold(vectors, threshold):
129
+ out.append([self.items[idx] for idx in indexes])
130
+
131
+ return out
132
+
133
+ def save(
134
+ self,
135
+ folder: PathLike,
136
+ overwrite: bool = False,
137
+ ) -> None:
138
+ """
139
+ Save a Vicinity instance in a fast format.
140
+
141
+ The Vicinity fast format stores the words and vectors of a Vicinity instance
142
+ separately in a JSON and numpy format, respectively.
143
+
144
+ :param folder: The path to which to save the JSON file. The vectors are saved separately. The JSON contains a path to the numpy file.
145
+ :param overwrite: Whether to overwrite the JSON and numpy files if they already exist.
146
+ :raises ValueError: If the path is not a directory.
147
+ """
148
+ path = Path(folder)
149
+ path.mkdir(parents=True, exist_ok=overwrite)
150
+
151
+ if not path.is_dir():
152
+ raise ValueError(f"Path {path} should be a directory.")
153
+
154
+ items_dict = {"items": self.items, "metadata": self.metadata, "backend_type": self.backend.backend_type.value}
155
+
156
+ with open(path / "data.json", "wb") as file_handle:
157
+ file_handle.write(orjson.dumps(items_dict))
158
+
159
+ self.backend.save(path)
160
+
161
+ @classmethod
162
+ def load(cls, filename: PathLike) -> Vicinity:
163
+ """
164
+ Load a Vicinity instance in fast format.
165
+
166
+ As described above, the fast format stores the words and vectors of the
167
+ Vicinity instance separately and is drastically faster than loading from
168
+ .txt files.
169
+
170
+ :param filename: The filename to load.
171
+ :return: A Vicinity instance.
172
+ """
173
+ folder_path = Path(filename)
174
+
175
+ with open(folder_path / "data.json", "rb") as file_handle:
176
+ data: dict[str, Any] = orjson.loads(file_handle.read())
177
+ items: Sequence[str] = data["items"]
178
+
179
+ metadata: dict[str, Any] = data["metadata"]
180
+ backend_type = Backend(data["backend_type"])
181
+
182
+ backend_cls: type[AbstractBackend] = get_backend_class(backend_type)
183
+ backend = backend_cls.load(folder_path)
184
+
185
+ instance = cls(items, backend, metadata=metadata)
186
+
187
+ return instance
188
+
189
+ def insert(self, tokens: Sequence[str], vectors: npt.NDArray) -> None:
190
+ """
191
+ Insert new items into the vector space.
192
+
193
+ :param tokens: A list of items to insert into the vector space.
194
+ :param vectors: The vectors to insert into the vector space.
195
+ :raises ValueError: If the tokens and vectors are not the same length.
196
+ """
197
+ if len(tokens) != len(vectors):
198
+ raise ValueError(f"Your tokens and vectors are not the same length: {len(tokens)} != {len(vectors)}")
199
+
200
+ if vectors.shape[1] != self.dim:
201
+ raise ValueError("The inserted vectors must have the same dimension as the backend.")
202
+
203
+ item_set = set(self.items)
204
+ for token in tokens:
205
+ if token in item_set:
206
+ raise ValueError(f"Token {token} is already in the vector space.")
207
+ self.items.append(token)
208
+ self.backend.insert(vectors)
209
+
210
+ def delete(self, tokens: Sequence[str]) -> None:
211
+ """
212
+ Delete tokens from the vector space.
213
+
214
+ The removal of tokens is done in place. If the tokens are not in the vector space,
215
+ a ValueError is raised.
216
+
217
+ :param tokens: A list of tokens to remove from the vector space.
218
+ :raises ValueError: If any passed tokens are not in the vector space.
219
+ """
220
+ try:
221
+ curr_indices = [self.items.index(token) for token in tokens]
222
+ except KeyError as exc:
223
+ raise ValueError(f"Token {exc} was not in the vector space.") from exc
224
+
225
+ self.backend.delete(curr_indices)
226
+
227
+ for index in curr_indices:
228
+ self.items.pop(index)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 The Minish Lab
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.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.1
2
+ Name: vicinity
3
+ Version: 0.1.0
4
+ Summary: Lightweight Nearest Neighbors with Flexible Backends
5
+ Author-email: Stéphan Tulkens <stephantul@gmail.com>, Thomas van Dongen <thomas123@live.nl>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2024 The Minish Lab
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Classifier: Development Status :: 4 - Beta
29
+ Classifier: Intended Audience :: Developers
30
+ Classifier: Intended Audience :: Science/Research
31
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
32
+ Classifier: Topic :: Software Development :: Libraries
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Requires-Python: >=3.10
39
+ Description-Content-Type: text/markdown
40
+ License-File: LICENSE
41
+ Requires-Dist: numpy
42
+ Requires-Dist: orjson
43
+ Requires-Dist: tqdm
44
+ Provides-Extra: dev
45
+ Requires-Dist: black ; extra == 'dev'
46
+ Requires-Dist: ipython ; extra == 'dev'
47
+ Requires-Dist: mypy ; extra == 'dev'
48
+ Requires-Dist: pre-commit ; extra == 'dev'
49
+ Requires-Dist: pytest ; extra == 'dev'
50
+ Requires-Dist: pytest-coverage ; extra == 'dev'
51
+ Requires-Dist: ruff ; extra == 'dev'
52
+ Provides-Extra: hnsw
53
+ Requires-Dist: hnswlib ; extra == 'hnsw'
54
+
55
+ <div align="center">
56
+
57
+ # Vicinity: The Lightweight Vector Store
58
+
59
+ </div>
60
+
61
+ ## Table of contents
62
+
63
+ - [Quickstart](#quickstart)
64
+ - [Main Features](#main-features)
65
+ - [Supported Backends](#supported-backends)
66
+ - [Usage](#usage)
67
+
68
+ Vicinity is the lightest-weight vector store. Just put in some vectors, calculate query vectors, and off you go. It provides a simple and intuitive API for nearest neighbor search, with support for different backends.
69
+
70
+ ## Quickstart
71
+
72
+ Install the package with:
73
+ ```bash
74
+ pip install vicinity
75
+ ```
76
+
77
+ The following code snippet demonstrates how to use Vicinity for nearest neighbor search:
78
+ ```python
79
+ import numpy as np
80
+ from vicinity import Vicinity
81
+ from vicinity.datatypes import Backend
82
+
83
+ # Create some dummy data
84
+ items = ["triforce", "master sword", "hylian shield", "boomerang", "hookshot"]
85
+ vectors = np.random.rand(len(items), 128)
86
+
87
+ # Initialize the Vicinity instance (using the basic backend)
88
+ vicinity = Vicinity.from_vectors_and_items(vectors=vectors, items=items, backend_type=Backend.BASIC)
89
+
90
+ # Query for nearest neighbors with a top-k search
91
+ query_vector = np.random.rand(128)
92
+ results = vicinity.query([query_vector], k=3)
93
+
94
+ # Query for nearest neighbors with a threshold search
95
+ results = vicinity.query_threshold([query_vector], threshold=0.9)
96
+
97
+ # Save the vector store
98
+ vicinity.save('my_vector_store')
99
+
100
+ # Load the vector store
101
+ vicinity = Vicinity.load('my_vector_store')
102
+ ```
103
+
104
+ ## Main Features
105
+ Vicinity provides the following features:
106
+ - Lightweight: Minimal dependencies and fast performance.
107
+ - Flexible Backend Support: Use different backends for vector storage and search.
108
+ - Dynamic Updates: Insert and delete items in the vector store.
109
+ - Serialization: Save and load vector stores for persistence.
110
+ - Easy to Use: Simple and intuitive API.
111
+
112
+ ## Supported Backends
113
+ The following backends are supported:
114
+ - `BASIC`: A simple flat index for vector storage and search.
115
+ - `HNSW`: Hierarchical Navigable Small World Graph for approximate nearest neighbor search.
116
+
117
+ ## Usage
118
+
119
+ <details>
120
+ <summary> Creating a Vector Store
121
+ </summary>
122
+ <br>
123
+
124
+ You can create a Vicinity instance by providing items and their corresponding vectors:
125
+
126
+
127
+ ```python
128
+ from vicinity import Vicinity
129
+ import numpy as np
130
+
131
+ items = ["triforce", "master sword", "hylian shield", "boomerang", "hookshot"]
132
+ vectors = np.random.rand(len(items), 128)
133
+
134
+ vicinity = Vicinity.from_vectors_and_items(vectors=vectors, items=items)
135
+ ```
136
+
137
+ </details>
138
+
139
+ <details>
140
+ <summary> Querying
141
+ </summary>
142
+ <br>
143
+
144
+ Find the k nearest neighbors for a given vector:
145
+
146
+ ```python
147
+ query_vector = np.random.rand(128)
148
+ results = vicinity.query([query_vector], k=3)
149
+ ```
150
+
151
+ Find all neighbors within a given threshold:
152
+
153
+ ```python
154
+ query_vector = np.random.rand(128)
155
+ results = vicinity.query_threshold([query_vector], threshold=0.9)
156
+ ```
157
+ </details>
158
+
159
+ <details>
160
+
161
+ <summary> Inserting and Deleting Items
162
+ </summary>
163
+ <br>
164
+
165
+ Insert new items:
166
+
167
+ ```python
168
+ new_items = ["ocarina", "bow"]
169
+ new_vectors = np.random.rand(2, 128)
170
+ vicinity.insert(new_items, new_vectors)
171
+ ```
172
+
173
+ Delete items:
174
+
175
+ ```python
176
+ vicinity.delete(["hookshot"])
177
+ ```
178
+ </details>
179
+
180
+ <details>
181
+ <summary> Saving and Loading
182
+ </summary>
183
+ <br>
184
+
185
+ Save the vector store:
186
+
187
+ ```python
188
+ vicinity.save('my_vector_store')
189
+ ```
190
+
191
+ Load the vector store:
192
+
193
+ ```python
194
+ vicinity = Vicinity.load('my_vector_store')
195
+ ```
196
+ </details>
197
+
198
+ ## License
199
+
200
+ MIT
@@ -0,0 +1,15 @@
1
+ vicinity/__init__.py,sha256=66yyoNFqDf_K-O-hoFFNNPLOIym-6z7cnwOIqmpRt5E,196
2
+ vicinity/datatypes.py,sha256=Wx2SSDwOY7ZMuclk6V6zQXCCAs2jF5YpZLAQqnyyg-Q,505
3
+ vicinity/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ vicinity/utils.py,sha256=tyUpZ7R3u5SzT15AocJNQ-zN1FdcxCmxJ62-NYTTt-g,1610
5
+ vicinity/version.py,sha256=0ZST8YQ_2d0ualYw52Frv3F_O9ULTFPCjDUI8Zy4qoA,84
6
+ vicinity/vicinity.py,sha256=EbU5khCMTzJaaGowuBhwYh2zZT8aIgDJqbXbOJJDzhg,7884
7
+ vicinity/backends/__init__.py,sha256=Q48R4fedzjdAJfTLjKRb4Rtj2-NkzL6kv7plMS6qRyY,513
8
+ vicinity/backends/base.py,sha256=z8FNQKOUNBAvp5bwuZXnYbwWa0uAkntHHubu-ibfdFc,2752
9
+ vicinity/backends/basic.py,sha256=m1SaAJNKtOeDzoK3JovTfNcvMljtw3hfhVnA6O2lV8o,5109
10
+ vicinity/backends/hnsw.py,sha256=WT6xVs_Y2KLhrwcWD1Ze-76esoNsrgyY8N92IQ04WjE,3145
11
+ vicinity-0.1.0.dist-info/LICENSE,sha256=6CbsCtUBhLgsb6-YTEeH38lhp-qr9KhOAWOYT0sz_58,1071
12
+ vicinity-0.1.0.dist-info/METADATA,sha256=rrRS690_DeYn9HnYDtYXV7r6utF8BUMlg5_9TeXmrYM,5793
13
+ vicinity-0.1.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
14
+ vicinity-0.1.0.dist-info/top_level.txt,sha256=GT01ApOJe1lkgL-E_A_j0wU45CJmzQSiezfN2LOZEog,9
15
+ vicinity-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ vicinity