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 +7 -0
- vicinity/backends/__init__.py +17 -0
- vicinity/backends/base.py +99 -0
- vicinity/backends/basic.py +151 -0
- vicinity/backends/hnsw.py +100 -0
- vicinity/datatypes.py +20 -0
- vicinity/py.typed +0 -0
- vicinity/utils.py +51 -0
- vicinity/version.py +2 -0
- vicinity/vicinity.py +228 -0
- vicinity-0.1.0.dist-info/LICENSE +21 -0
- vicinity-0.1.0.dist-info/METADATA +200 -0
- vicinity-0.1.0.dist-info/RECORD +15 -0
- vicinity-0.1.0.dist-info/WHEEL +5 -0
- vicinity-0.1.0.dist-info/top_level.txt +1 -0
vicinity/__init__.py
ADDED
|
@@ -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
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 @@
|
|
|
1
|
+
vicinity
|