usearch 2.8.7 → 2.8.8
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.
- package/CITATION.cff +1 -1
- package/CMakeLists.txt +3 -3
- package/CONTRIBUTING.md +3 -3
- package/Cargo.toml +1 -1
- package/Dockerfile +16 -0
- package/README.md +41 -38
- package/VERSION +1 -0
- package/binding.gyp +57 -8
- package/conanfile.py +1 -1
- package/include/usearch/index.hpp +1 -1
- package/include/usearch/index_plugins.hpp +27 -23
- package/package.json +1 -2
- package/setup.py +76 -15
- package/wasmer.toml +1 -1
- package/. npmignore +0 -14
- package/.gitignore +0 -95
- package/.vscode/launch.json +0 -181
- package/.vscode/settings.json +0 -180
- package/.vscode/tasks.json +0 -71
- package/Package.swift +0 -41
- package/csharp/.editorconfig +0 -361
- package/csharp/Cloud.Unum.USearch.sln +0 -34
- package/csharp/DEV_SETUP.md +0 -51
- package/csharp/Directory.Build.props +0 -29
- package/csharp/README.md +0 -74
- package/csharp/build_and_test.cmd +0 -31
- package/csharp/build_and_test.sh +0 -30
- package/csharp/nuget/nuget-package.props +0 -33
- package/csharp/pack.sh +0 -56
- package/csharp/src/Cloud.Unum.USearch.Tests/Cloud.Unum.USearch.Tests.csproj +0 -39
- package/csharp/src/Cloud.Unum.USearch.Tests/USearchIndexTests.cs +0 -1013
- package/csharp/src/Cloud.Unum.USearch.Tests/Usings.cs +0 -1
- package/docs/_static/custom.css +0 -26
- package/docs/_static/custom.js +0 -7
- package/docs/benchmarks.md +0 -245
- package/docs/benchmarks.rst +0 -5
- package/docs/c/index.rst +0 -11
- package/docs/c/reference.rst +0 -7
- package/docs/conf.dox +0 -2668
- package/docs/conf.py +0 -53
- package/docs/contributing.rst +0 -5
- package/docs/cpp/index.rst +0 -11
- package/docs/cpp/reference.rst +0 -17
- package/docs/csharp/index.rst +0 -6
- package/docs/format.md +0 -86
- package/docs/format.rst +0 -5
- package/docs/golang/index.rst +0 -6
- package/docs/index.rst +0 -33
- package/docs/java/index.rst +0 -11
- package/docs/java/reference.rst +0 -7
- package/docs/javascript/index.rst +0 -11
- package/docs/javascript/reference.rst +0 -12
- package/docs/python/index.rst +0 -11
- package/docs/python/reference.rst +0 -42
- package/docs/rust/index.rst +0 -6
- package/docs/swift/index.rst +0 -6
- package/docs/wolfram/index.rst +0 -6
- package/golang/README.md +0 -95
- package/golang/go.mod +0 -3
- package/golang/lib.go +0 -379
- package/golang/lib_test.go +0 -108
- package/java/README.md +0 -22
- package/java/cloud/unum/usearch/Index.java +0 -337
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +0 -197
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +0 -125
- package/java/test/IndexTest.java +0 -30
- package/objc/USearchObjective.mm +0 -299
- package/objc/include/USearchObjective.h +0 -154
- package/python/README.md +0 -292
- package/python/lib.cpp +0 -1216
- package/python/scripts/bench.ipynb +0 -193
- package/python/scripts/bench.py +0 -137
- package/python/scripts/bench_exact.py +0 -44
- package/python/scripts/cluster.ipynb +0 -160
- package/python/scripts/index_faiss.py +0 -100
- package/python/scripts/join.py +0 -155
- package/python/scripts/test_index.py +0 -247
- package/python/scripts/test_jit.py +0 -231
- package/python/scripts/test_sparse.py +0 -32
- package/python/scripts/test_tooling.py +0 -133
- package/python/usearch/__init__.py +0 -0
- package/python/usearch/client.py +0 -124
- package/python/usearch/eval.py +0 -470
- package/python/usearch/index.py +0 -1401
- package/python/usearch/io.py +0 -113
- package/python/usearch/numba.py +0 -121
- package/python/usearch/server.py +0 -137
- package/rust/README.md +0 -66
- package/rust/lib.cpp +0 -136
- package/rust/lib.hpp +0 -69
- package/rust/lib.rs +0 -610
- package/swift/Index+Sugar.swift +0 -153
- package/swift/README.md +0 -48
- package/swift/Test.swift +0 -37
- package/swift/USearch.swift +0 -8
- package/wasm/CMakeLists.txt +0 -7
- package/wasm/README.md +0 -13
- package/wasm/lib.cpp +0 -5
- package/wolfram/CMakeLists.txt +0 -18
- package/wolfram/README.md +0 -13
- package/wolfram/lib.cpp +0 -160
- package/wolfram/test.wls +0 -27
package/python/usearch/index.py
DELETED
|
@@ -1,1401 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
from inspect import signature
|
|
3
|
-
|
|
4
|
-
# The purpose of this file is to provide Pythonic wrapper on top
|
|
5
|
-
# the native precompiled CPython module. It improves compatibility
|
|
6
|
-
# Python tooling, linters, and static analyzers. It also embeds JIT
|
|
7
|
-
# into the primary `Index` class, connecting USearch with Numba.
|
|
8
|
-
import os
|
|
9
|
-
import math
|
|
10
|
-
from dataclasses import dataclass
|
|
11
|
-
from typing import (
|
|
12
|
-
Any,
|
|
13
|
-
Optional,
|
|
14
|
-
Union,
|
|
15
|
-
NamedTuple,
|
|
16
|
-
List,
|
|
17
|
-
Iterable,
|
|
18
|
-
Tuple,
|
|
19
|
-
Dict,
|
|
20
|
-
Callable,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
import numpy as np
|
|
24
|
-
from tqdm import tqdm
|
|
25
|
-
|
|
26
|
-
# Precompiled symbols that won't be exposed directly:
|
|
27
|
-
from usearch.compiled import (
|
|
28
|
-
Index as _CompiledIndex,
|
|
29
|
-
Indexes as _CompiledIndexes,
|
|
30
|
-
IndexStats as _CompiledIndexStats,
|
|
31
|
-
index_dense_metadata_from_path as _index_dense_metadata_from_path,
|
|
32
|
-
index_dense_metadata_from_buffer as _index_dense_metadata_from_buffer,
|
|
33
|
-
exact_search as _exact_search,
|
|
34
|
-
hardware_acceleration as _hardware_acceleration,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
# Precompiled symbols that will be exposed
|
|
38
|
-
from usearch.compiled import (
|
|
39
|
-
MetricKind,
|
|
40
|
-
ScalarKind,
|
|
41
|
-
MetricSignature,
|
|
42
|
-
DEFAULT_CONNECTIVITY,
|
|
43
|
-
DEFAULT_EXPANSION_ADD,
|
|
44
|
-
DEFAULT_EXPANSION_SEARCH,
|
|
45
|
-
USES_OPENMP,
|
|
46
|
-
USES_SIMSIMD,
|
|
47
|
-
USES_NATIVE_F16,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
MetricKindBitwise = (
|
|
51
|
-
MetricKind.Hamming,
|
|
52
|
-
MetricKind.Tanimoto,
|
|
53
|
-
MetricKind.Sorensen,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class CompiledMetric(NamedTuple):
|
|
58
|
-
pointer: int
|
|
59
|
-
kind: MetricKind
|
|
60
|
-
signature: MetricSignature
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
Key = np.uint64
|
|
64
|
-
|
|
65
|
-
KeyOrKeysLike = Union[Key, Iterable[Key], int, Iterable[int], np.ndarray, memoryview]
|
|
66
|
-
|
|
67
|
-
VectorOrVectorsLike = Union[np.ndarray, Iterable[np.ndarray], memoryview]
|
|
68
|
-
|
|
69
|
-
DTypeLike = Union[str, ScalarKind]
|
|
70
|
-
|
|
71
|
-
MetricLike = Union[str, MetricKind, CompiledMetric]
|
|
72
|
-
|
|
73
|
-
NoneType = type(None)
|
|
74
|
-
|
|
75
|
-
PathOrBuffer = Union[str, os.PathLike, bytes]
|
|
76
|
-
|
|
77
|
-
ProgressCallback = Callable[[int, int], bool]
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _match_signature(
|
|
81
|
-
func: Callable[[Any], Any], arg_types: List[type], ret_type: type
|
|
82
|
-
) -> bool:
|
|
83
|
-
assert callable(func), "Not callable"
|
|
84
|
-
sig = signature(func)
|
|
85
|
-
param_types = [param.annotation for param in sig.parameters.values()]
|
|
86
|
-
return param_types == arg_types and sig.return_annotation == ret_type
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _normalize_dtype(
|
|
90
|
-
dtype,
|
|
91
|
-
ndim: int = 0,
|
|
92
|
-
metric: MetricKind = MetricKind.Cos,
|
|
93
|
-
) -> ScalarKind:
|
|
94
|
-
if dtype is None or dtype == "":
|
|
95
|
-
if metric in MetricKindBitwise:
|
|
96
|
-
return ScalarKind.B1
|
|
97
|
-
if _hardware_acceleration(dtype=ScalarKind.F16, ndim=ndim, metric_kind=metric):
|
|
98
|
-
return ScalarKind.F16
|
|
99
|
-
return ScalarKind.F32
|
|
100
|
-
|
|
101
|
-
if isinstance(dtype, ScalarKind):
|
|
102
|
-
return dtype
|
|
103
|
-
|
|
104
|
-
if isinstance(dtype, str):
|
|
105
|
-
dtype = dtype.lower()
|
|
106
|
-
|
|
107
|
-
_normalize = {
|
|
108
|
-
"f64": ScalarKind.F64,
|
|
109
|
-
"f32": ScalarKind.F32,
|
|
110
|
-
"f16": ScalarKind.F16,
|
|
111
|
-
"i8": ScalarKind.I8,
|
|
112
|
-
"b1": ScalarKind.B1,
|
|
113
|
-
"b1x8": ScalarKind.B1,
|
|
114
|
-
"float64": ScalarKind.F64,
|
|
115
|
-
"float32": ScalarKind.F32,
|
|
116
|
-
"float16": ScalarKind.F16,
|
|
117
|
-
"int8": ScalarKind.I8,
|
|
118
|
-
np.float64: ScalarKind.F64,
|
|
119
|
-
np.float32: ScalarKind.F32,
|
|
120
|
-
np.float16: ScalarKind.F16,
|
|
121
|
-
np.int8: ScalarKind.I8,
|
|
122
|
-
np.uint8: ScalarKind.B1,
|
|
123
|
-
}
|
|
124
|
-
return _normalize[dtype]
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _to_numpy_dtype(dtype: ScalarKind):
|
|
128
|
-
_normalize = {
|
|
129
|
-
ScalarKind.F64: np.float64,
|
|
130
|
-
ScalarKind.F32: np.float32,
|
|
131
|
-
ScalarKind.F16: np.float16,
|
|
132
|
-
ScalarKind.I8: np.int8,
|
|
133
|
-
ScalarKind.B1: np.uint8,
|
|
134
|
-
}
|
|
135
|
-
if dtype in _normalize.values():
|
|
136
|
-
return dtype
|
|
137
|
-
return _normalize[dtype]
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def _normalize_metric(metric) -> MetricKind:
|
|
141
|
-
if metric is None:
|
|
142
|
-
return MetricKind.Cos
|
|
143
|
-
|
|
144
|
-
if isinstance(metric, str):
|
|
145
|
-
_normalize = {
|
|
146
|
-
"cos": MetricKind.Cos,
|
|
147
|
-
"cosine": MetricKind.Cos,
|
|
148
|
-
"ip": MetricKind.IP,
|
|
149
|
-
"dot": MetricKind.IP,
|
|
150
|
-
"inner_product": MetricKind.IP,
|
|
151
|
-
"l2sq": MetricKind.L2sq,
|
|
152
|
-
"l2_sq": MetricKind.L2sq,
|
|
153
|
-
"haversine": MetricKind.Haversine,
|
|
154
|
-
"divergence": MetricKind.Divergence,
|
|
155
|
-
"pearson": MetricKind.Pearson,
|
|
156
|
-
"hamming": MetricKind.Hamming,
|
|
157
|
-
"tanimoto": MetricKind.Tanimoto,
|
|
158
|
-
"sorensen": MetricKind.Sorensen,
|
|
159
|
-
}
|
|
160
|
-
return _normalize[metric.lower()]
|
|
161
|
-
|
|
162
|
-
return metric
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _search_in_compiled(
|
|
166
|
-
compiled_callable: Callable,
|
|
167
|
-
vectors: np.ndarray,
|
|
168
|
-
*,
|
|
169
|
-
log: Union[str, bool],
|
|
170
|
-
progress: Optional[ProgressCallback],
|
|
171
|
-
**kwargs,
|
|
172
|
-
) -> Union[Matches, BatchMatches]:
|
|
173
|
-
#
|
|
174
|
-
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
175
|
-
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
176
|
-
assert not progress or _match_signature(
|
|
177
|
-
progress, [int, int], bool
|
|
178
|
-
), "Invalid callback"
|
|
179
|
-
|
|
180
|
-
if vectors.ndim == 1:
|
|
181
|
-
vectors = vectors.reshape(1, len(vectors))
|
|
182
|
-
count_vectors = vectors.shape[0]
|
|
183
|
-
|
|
184
|
-
def distill_batch(
|
|
185
|
-
batch_matches: BatchMatches,
|
|
186
|
-
) -> Union[BatchMatches, Matches]:
|
|
187
|
-
return batch_matches[0] if count_vectors == 1 else batch_matches
|
|
188
|
-
|
|
189
|
-
# Create progress bar if needed
|
|
190
|
-
if log:
|
|
191
|
-
name = log if isinstance(log, str) else "Search"
|
|
192
|
-
progress_bar = tqdm(
|
|
193
|
-
desc=name,
|
|
194
|
-
total=count_vectors,
|
|
195
|
-
unit="vector",
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
def update_progress_bar(processed: int, total: int) -> bool:
|
|
199
|
-
progress_bar.update(processed - progress_bar.n)
|
|
200
|
-
return progress if progress else True
|
|
201
|
-
|
|
202
|
-
tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
|
|
203
|
-
progress_bar.close()
|
|
204
|
-
else:
|
|
205
|
-
tuple_ = compiled_callable(vectors, **kwargs)
|
|
206
|
-
|
|
207
|
-
return distill_batch(BatchMatches(*tuple_))
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def _add_to_compiled(
|
|
211
|
-
compiled,
|
|
212
|
-
*,
|
|
213
|
-
keys,
|
|
214
|
-
vectors,
|
|
215
|
-
copy: bool,
|
|
216
|
-
threads: int,
|
|
217
|
-
log: Union[str, bool],
|
|
218
|
-
progress: Optional[ProgressCallback],
|
|
219
|
-
) -> Union[int, np.ndarray]:
|
|
220
|
-
#
|
|
221
|
-
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
222
|
-
assert not progress or _match_signature(
|
|
223
|
-
progress, [int, int], bool
|
|
224
|
-
), "Invalid callback"
|
|
225
|
-
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
226
|
-
if vectors.ndim == 1:
|
|
227
|
-
vectors = vectors.reshape(1, len(vectors))
|
|
228
|
-
|
|
229
|
-
# Validate or generate the keys
|
|
230
|
-
count_vectors = vectors.shape[0]
|
|
231
|
-
generate_labels = keys is None
|
|
232
|
-
if generate_labels:
|
|
233
|
-
start_id = len(compiled)
|
|
234
|
-
keys = np.arange(start_id, start_id + count_vectors, dtype=Key)
|
|
235
|
-
else:
|
|
236
|
-
if not isinstance(keys, Iterable):
|
|
237
|
-
assert count_vectors == 1, "Each vector must have a key"
|
|
238
|
-
keys = [keys]
|
|
239
|
-
keys = np.array(keys).astype(Key)
|
|
240
|
-
|
|
241
|
-
assert len(keys) == count_vectors
|
|
242
|
-
|
|
243
|
-
# Create progress bar if needed
|
|
244
|
-
if log:
|
|
245
|
-
name = log if isinstance(log, str) else "Add"
|
|
246
|
-
pbar = tqdm(
|
|
247
|
-
desc=name,
|
|
248
|
-
total=count_vectors,
|
|
249
|
-
unit="vector",
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
def update_progress_bar(processed: int, total: int) -> bool:
|
|
253
|
-
pbar.update(processed - pbar.n)
|
|
254
|
-
return progress(processed, total) if progress else True
|
|
255
|
-
|
|
256
|
-
compiled.add_many(
|
|
257
|
-
keys,
|
|
258
|
-
vectors,
|
|
259
|
-
copy=copy,
|
|
260
|
-
threads=threads,
|
|
261
|
-
progress=update_progress_bar,
|
|
262
|
-
)
|
|
263
|
-
pbar.close()
|
|
264
|
-
else:
|
|
265
|
-
compiled.add_many(keys, vectors, copy=copy, threads=threads, progress=progress)
|
|
266
|
-
|
|
267
|
-
return keys
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
@dataclass
|
|
271
|
-
class Match:
|
|
272
|
-
"""This class contains information about retrieved vector."""
|
|
273
|
-
|
|
274
|
-
key: int
|
|
275
|
-
distance: float
|
|
276
|
-
|
|
277
|
-
def to_tuple(self) -> tuple:
|
|
278
|
-
return self.key, self.distance
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@dataclass
|
|
282
|
-
class Matches:
|
|
283
|
-
"""This class contains information about multiple retrieved vectors for single query,
|
|
284
|
-
i.e it is a set of `Match` instances."""
|
|
285
|
-
|
|
286
|
-
keys: np.ndarray
|
|
287
|
-
distances: np.ndarray
|
|
288
|
-
|
|
289
|
-
visited_members: int = 0
|
|
290
|
-
computed_distances: int = 0
|
|
291
|
-
|
|
292
|
-
def __len__(self) -> int:
|
|
293
|
-
return len(self.keys)
|
|
294
|
-
|
|
295
|
-
def __getitem__(self, index: int) -> Match:
|
|
296
|
-
if isinstance(index, int) and index < len(self):
|
|
297
|
-
return Match(
|
|
298
|
-
key=self.keys[index],
|
|
299
|
-
distance=self.distances[index],
|
|
300
|
-
)
|
|
301
|
-
else:
|
|
302
|
-
raise IndexError(f"`index` must be an integer under {len(self)}")
|
|
303
|
-
|
|
304
|
-
def to_list(self) -> List[tuple]:
|
|
305
|
-
"""
|
|
306
|
-
Convert matches to the list of tuples which contain matches' indices and distances to them.
|
|
307
|
-
"""
|
|
308
|
-
|
|
309
|
-
return [(int(l), float(d)) for l, d in zip(self.keys, self.distances)]
|
|
310
|
-
|
|
311
|
-
def __repr__(self) -> str:
|
|
312
|
-
return f"usearch.Matches({len(self)})"
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
@dataclass
|
|
316
|
-
class BatchMatches:
|
|
317
|
-
"""This class contains information about multiple retrieved vectors for multiple queries,
|
|
318
|
-
i.e it is a set of `Matches` instances."""
|
|
319
|
-
|
|
320
|
-
keys: np.ndarray
|
|
321
|
-
distances: np.ndarray
|
|
322
|
-
counts: np.ndarray
|
|
323
|
-
|
|
324
|
-
visited_members: int = 0
|
|
325
|
-
computed_distances: int = 0
|
|
326
|
-
|
|
327
|
-
def __len__(self) -> int:
|
|
328
|
-
return len(self.counts)
|
|
329
|
-
|
|
330
|
-
def __getitem__(self, index: int) -> Matches:
|
|
331
|
-
if isinstance(index, int) and index < len(self):
|
|
332
|
-
return Matches(
|
|
333
|
-
keys=self.keys[index, : self.counts[index]],
|
|
334
|
-
distances=self.distances[index, : self.counts[index]],
|
|
335
|
-
visited_members=self.visited_members // len(self),
|
|
336
|
-
computed_distances=self.computed_distances // len(self),
|
|
337
|
-
)
|
|
338
|
-
else:
|
|
339
|
-
raise IndexError(f"`index` must be an integer under {len(self)}")
|
|
340
|
-
|
|
341
|
-
def to_list(self) -> List[List[tuple]]:
|
|
342
|
-
"""Convert the result for each query to the list of tuples with information about its matches."""
|
|
343
|
-
list_of_matches = [self.__getitem__(row) for row in range(self.__len__())]
|
|
344
|
-
return [match.to_tuple() for matches in list_of_matches for match in matches]
|
|
345
|
-
|
|
346
|
-
def mean_recall(self, expected: np.ndarray, count: Optional[int] = None) -> float:
|
|
347
|
-
"""Measures recall [0, 1] as of `Matches` that contain the corresponding
|
|
348
|
-
`expected` entry anywhere among results."""
|
|
349
|
-
return self.count_matches(expected, count=count) / len(expected)
|
|
350
|
-
|
|
351
|
-
def count_matches(self, expected: np.ndarray, count: Optional[int] = None) -> int:
|
|
352
|
-
"""Measures recall [0, len(expected)] as of `Matches` that contain the corresponding
|
|
353
|
-
`expected` entry anywhere among results.
|
|
354
|
-
"""
|
|
355
|
-
assert len(expected) == len(self)
|
|
356
|
-
recall = 0
|
|
357
|
-
if count is None:
|
|
358
|
-
count = self.keys.shape[1]
|
|
359
|
-
|
|
360
|
-
if count == 1:
|
|
361
|
-
recall = np.sum(self.keys[:, 0] == expected)
|
|
362
|
-
else:
|
|
363
|
-
for i in range(len(self)):
|
|
364
|
-
recall += expected[i] in self.keys[i, :count]
|
|
365
|
-
return recall
|
|
366
|
-
|
|
367
|
-
def __repr__(self) -> str:
|
|
368
|
-
return f"usearch.BatchMatches({np.sum(self.counts)} across {len(self)} queries)"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
@dataclass
|
|
372
|
-
class Clustering:
|
|
373
|
-
def __init__(
|
|
374
|
-
self,
|
|
375
|
-
index: Index,
|
|
376
|
-
matches: BatchMatches,
|
|
377
|
-
queries: Optional[np.ndarray] = None,
|
|
378
|
-
) -> None:
|
|
379
|
-
if queries is None:
|
|
380
|
-
queries = index._compiled.get_keys_in_slice()
|
|
381
|
-
self.index = index
|
|
382
|
-
self.queries = queries
|
|
383
|
-
self.matches = matches
|
|
384
|
-
|
|
385
|
-
def __repr__(self) -> str:
|
|
386
|
-
return f"usearch.Clustering(for {len(self.queries)} queries)"
|
|
387
|
-
|
|
388
|
-
@property
|
|
389
|
-
def centroids_popularity(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
390
|
-
return np.unique(self.matches.keys, return_counts=True)
|
|
391
|
-
|
|
392
|
-
def members_of(self, centroid: Key) -> np.ndarray:
|
|
393
|
-
return self.queries[self.matches.keys.flatten() == centroid]
|
|
394
|
-
|
|
395
|
-
def subcluster(self, centroid: Key, **clustering_kwards) -> Clustering:
|
|
396
|
-
sub_keys = self.members_of(centroid)
|
|
397
|
-
return self.index.cluster(keys=sub_keys, **clustering_kwards)
|
|
398
|
-
|
|
399
|
-
def plot_centroids_popularity(self):
|
|
400
|
-
from matplotlib import pyplot as plt
|
|
401
|
-
|
|
402
|
-
_, sizes = self.centroids_popularity
|
|
403
|
-
plt.yscale("log")
|
|
404
|
-
plt.plot(sorted(sizes), np.arange(len(sizes)))
|
|
405
|
-
plt.show()
|
|
406
|
-
|
|
407
|
-
@property
|
|
408
|
-
def network(self):
|
|
409
|
-
import networkx as nx
|
|
410
|
-
|
|
411
|
-
keys, sizes = self.centroids_popularity
|
|
412
|
-
|
|
413
|
-
g = nx.Graph()
|
|
414
|
-
for key, size in zip(keys, sizes):
|
|
415
|
-
g.add_node(key, size=size)
|
|
416
|
-
|
|
417
|
-
for i, i_key in enumerate(keys):
|
|
418
|
-
for j_key in keys[:i]:
|
|
419
|
-
d = self.index.pairwise_distance(i_key, j_key)
|
|
420
|
-
g.add_edge(i_key, j_key, distance=d)
|
|
421
|
-
|
|
422
|
-
return g
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
class IndexedKeys:
|
|
426
|
-
"""Smart-reference for the range of keys present in a specific `Index`"""
|
|
427
|
-
|
|
428
|
-
def __init__(self, index: Index) -> None:
|
|
429
|
-
self.index = index
|
|
430
|
-
|
|
431
|
-
def __len__(self) -> int:
|
|
432
|
-
return len(self.index)
|
|
433
|
-
|
|
434
|
-
def __getitem__(
|
|
435
|
-
self,
|
|
436
|
-
offset_offsets_or_slice: Union[int, np.ndarray, slice],
|
|
437
|
-
) -> Union[Key, np.ndarray]:
|
|
438
|
-
if isinstance(offset_offsets_or_slice, slice):
|
|
439
|
-
start, stop, step = offset_offsets_or_slice.indices(len(self))
|
|
440
|
-
if step:
|
|
441
|
-
raise
|
|
442
|
-
return self.index._compiled.get_keys_in_slice(start, stop - start)
|
|
443
|
-
|
|
444
|
-
elif isinstance(offset_offsets_or_slice, Iterable):
|
|
445
|
-
offsets = np.array(offset_offsets_or_slice)
|
|
446
|
-
return self.index._compiled.get_keys_at_offsets(offsets)
|
|
447
|
-
|
|
448
|
-
else:
|
|
449
|
-
offset = int(offset_offsets_or_slice)
|
|
450
|
-
return self.index._compiled.get_key_at_offset(offset)
|
|
451
|
-
|
|
452
|
-
def __array__(self, dtype=None) -> np.ndarray:
|
|
453
|
-
if dtype is None:
|
|
454
|
-
dtype = Key
|
|
455
|
-
return self.index._compiled.get_keys_in_slice().astype(dtype)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
class Index:
|
|
459
|
-
"""Fast vector-search engine for dense equi-dimensional embeddings.
|
|
460
|
-
|
|
461
|
-
Vector keys must be integers.
|
|
462
|
-
Vectors must have the same number of dimensions within the index.
|
|
463
|
-
Supports Inner Product, Cosine Distance, L^n measures like the Euclidean metric,
|
|
464
|
-
as well as automatic downcasting to low-precision floating-point and integral
|
|
465
|
-
representations.
|
|
466
|
-
"""
|
|
467
|
-
|
|
468
|
-
def __init__(
|
|
469
|
-
self,
|
|
470
|
-
*,
|
|
471
|
-
ndim: int = 0,
|
|
472
|
-
metric: MetricLike = MetricKind.Cos,
|
|
473
|
-
dtype: Optional[DTypeLike] = None,
|
|
474
|
-
connectivity: Optional[int] = None,
|
|
475
|
-
expansion_add: Optional[int] = None,
|
|
476
|
-
expansion_search: Optional[int] = None,
|
|
477
|
-
multi: bool = False,
|
|
478
|
-
path: Optional[os.PathLike] = None,
|
|
479
|
-
view: bool = False,
|
|
480
|
-
enable_key_lookups: bool = True,
|
|
481
|
-
) -> None:
|
|
482
|
-
"""Construct the index and compiles the functions, if requested (expensive).
|
|
483
|
-
|
|
484
|
-
:param ndim: Number of vector dimensions
|
|
485
|
-
:type ndim: int
|
|
486
|
-
Required for some metrics, pre-set for others.
|
|
487
|
-
Haversine, for example, only applies to 2-dimensional latitude/longitude
|
|
488
|
-
coordinates. Angular (Cos) and Euclidean (L2sq), obviously, apply to
|
|
489
|
-
vectors with arbitrary number of dimensions.
|
|
490
|
-
|
|
491
|
-
:param metric: Distance function
|
|
492
|
-
:type metric: MetricLike, defaults to MetricKind.Cos
|
|
493
|
-
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
494
|
-
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
495
|
-
Hamming, Tanimoto, Sorensen.
|
|
496
|
-
|
|
497
|
-
:param dtype: Scalar type for internal vector storage
|
|
498
|
-
:type dtype: Optional[DTypeLike], defaults to None
|
|
499
|
-
For continuous metrics can be: f16, f32, f64, or i8.
|
|
500
|
-
For bitwise metrics it's implementation-defined, and can't change.
|
|
501
|
-
If nothing is provided, the optimal data type is selected based on the metric
|
|
502
|
-
kind and hardware support.
|
|
503
|
-
Example: you can use the `f16` index with `f32` vectors in Euclidean space,
|
|
504
|
-
which will be automatically downcasted. Moreover, if `dtype=None` is passed,
|
|
505
|
-
and hardware supports `f16` SIMD-instructions, this choice will be done for you.
|
|
506
|
-
You can later double-check the used representation with `index.dtype`.
|
|
507
|
-
|
|
508
|
-
:param connectivity: Connections per node in HNSW
|
|
509
|
-
:type connectivity: Optional[int], defaults to None
|
|
510
|
-
Hyper-parameter for the number of Graph connections
|
|
511
|
-
per layer of HNSW. The original paper calls it "M".
|
|
512
|
-
Optional, but can't be changed after construction.
|
|
513
|
-
|
|
514
|
-
:param expansion_add: Traversal depth on insertions
|
|
515
|
-
:type expansion_add: Optional[int], defaults to None
|
|
516
|
-
Hyper-parameter for the search depth when inserting new
|
|
517
|
-
vectors. The original paper calls it "efConstruction".
|
|
518
|
-
Can be changed afterwards, as the `.expansion_add`.
|
|
519
|
-
|
|
520
|
-
:param expansion_search: Traversal depth on queries
|
|
521
|
-
:type expansion_search: Optional[int], defaults to None
|
|
522
|
-
Hyper-parameter for the search depth when querying
|
|
523
|
-
nearest neighbors. The original paper calls it "ef".
|
|
524
|
-
Can be changed afterwards, as the `.expansion_search`.
|
|
525
|
-
|
|
526
|
-
:param multi: Allow multiple vectors with the same key
|
|
527
|
-
:type multi: bool, defaults to True
|
|
528
|
-
:param path: Where to store the index
|
|
529
|
-
:type path: Optional[os.PathLike], defaults to None
|
|
530
|
-
:param view: Are we simply viewing an immutable index
|
|
531
|
-
:type view: bool, defaults to False
|
|
532
|
-
"""
|
|
533
|
-
|
|
534
|
-
if connectivity is None:
|
|
535
|
-
connectivity = DEFAULT_CONNECTIVITY
|
|
536
|
-
if expansion_add is None:
|
|
537
|
-
expansion_add = DEFAULT_EXPANSION_ADD
|
|
538
|
-
if expansion_search is None:
|
|
539
|
-
expansion_search = DEFAULT_EXPANSION_SEARCH
|
|
540
|
-
|
|
541
|
-
assert isinstance(connectivity, int), "Expects integer `connectivity`"
|
|
542
|
-
assert isinstance(expansion_add, int), "Expects integer `expansion_add`"
|
|
543
|
-
assert isinstance(expansion_search, int), "Expects integer `expansion_search`"
|
|
544
|
-
|
|
545
|
-
metric = _normalize_metric(metric)
|
|
546
|
-
if isinstance(metric, MetricKind):
|
|
547
|
-
self._metric_kind = metric
|
|
548
|
-
self._metric_jit = None
|
|
549
|
-
self._metric_pointer = 0
|
|
550
|
-
self._metric_signature = MetricSignature.ArrayArraySize
|
|
551
|
-
elif isinstance(metric, CompiledMetric):
|
|
552
|
-
self._metric_jit = metric
|
|
553
|
-
self._metric_kind = metric.kind
|
|
554
|
-
self._metric_pointer = metric.pointer
|
|
555
|
-
self._metric_signature = metric.signature
|
|
556
|
-
else:
|
|
557
|
-
raise ValueError(
|
|
558
|
-
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
# Validate, that the right scalar type is defined
|
|
562
|
-
dtype = _normalize_dtype(dtype, ndim, self._metric_kind)
|
|
563
|
-
self._compiled = _CompiledIndex(
|
|
564
|
-
ndim=ndim,
|
|
565
|
-
dtype=dtype,
|
|
566
|
-
connectivity=connectivity,
|
|
567
|
-
expansion_add=expansion_add,
|
|
568
|
-
expansion_search=expansion_search,
|
|
569
|
-
multi=multi,
|
|
570
|
-
enable_key_lookups=enable_key_lookups,
|
|
571
|
-
metric_kind=self._metric_kind,
|
|
572
|
-
metric_pointer=self._metric_pointer,
|
|
573
|
-
metric_signature=self._metric_signature,
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
self.path = path
|
|
577
|
-
if path is not None and os.path.exists(path):
|
|
578
|
-
if view:
|
|
579
|
-
self.view(path)
|
|
580
|
-
else:
|
|
581
|
-
self.load(path)
|
|
582
|
-
|
|
583
|
-
@staticmethod
|
|
584
|
-
def metadata(path_or_buffer: PathOrBuffer) -> Optional[dict]:
|
|
585
|
-
try:
|
|
586
|
-
if isinstance(path_or_buffer, bytearray):
|
|
587
|
-
path_or_buffer = bytes(path_or_buffer)
|
|
588
|
-
if isinstance(path_or_buffer, bytes):
|
|
589
|
-
return _index_dense_metadata_from_buffer(path_or_buffer)
|
|
590
|
-
else:
|
|
591
|
-
path_or_buffer = os.fspath(path_or_buffer)
|
|
592
|
-
if not os.path.exists(path_or_buffer):
|
|
593
|
-
return None
|
|
594
|
-
return _index_dense_metadata_from_path(path_or_buffer)
|
|
595
|
-
except Exception as e:
|
|
596
|
-
raise e
|
|
597
|
-
|
|
598
|
-
@staticmethod
|
|
599
|
-
def restore(path_or_buffer: PathOrBuffer, view: bool = False) -> Optional[Index]:
|
|
600
|
-
meta = Index.metadata(path_or_buffer)
|
|
601
|
-
if not meta:
|
|
602
|
-
return None
|
|
603
|
-
|
|
604
|
-
index = Index(
|
|
605
|
-
ndim=meta["dimensions"],
|
|
606
|
-
dtype=meta["kind_scalar"],
|
|
607
|
-
metric=meta["kind_metric"],
|
|
608
|
-
)
|
|
609
|
-
|
|
610
|
-
if view:
|
|
611
|
-
index.view(path_or_buffer)
|
|
612
|
-
else:
|
|
613
|
-
index.load(path_or_buffer)
|
|
614
|
-
return index
|
|
615
|
-
|
|
616
|
-
def __len__(self) -> int:
|
|
617
|
-
return self._compiled.__len__()
|
|
618
|
-
|
|
619
|
-
def add(
|
|
620
|
-
self,
|
|
621
|
-
keys: KeyOrKeysLike,
|
|
622
|
-
vectors: VectorOrVectorsLike,
|
|
623
|
-
*,
|
|
624
|
-
copy: bool = True,
|
|
625
|
-
threads: int = 0,
|
|
626
|
-
log: Union[str, bool] = False,
|
|
627
|
-
progress: Optional[ProgressCallback] = None,
|
|
628
|
-
) -> Union[int, np.ndarray]:
|
|
629
|
-
"""Inserts one or move vectors into the index.
|
|
630
|
-
|
|
631
|
-
For maximal performance the `keys` and `vectors`
|
|
632
|
-
should conform to the Python's "buffer protocol" spec.
|
|
633
|
-
|
|
634
|
-
To index a single entry:
|
|
635
|
-
keys: int, vectors: np.ndarray.
|
|
636
|
-
To index many entries:
|
|
637
|
-
keys: np.ndarray, vectors: np.ndarray.
|
|
638
|
-
|
|
639
|
-
When working with extremely large indexes, you may want to
|
|
640
|
-
pass `copy=False`, if you can guarantee the lifetime of the
|
|
641
|
-
primary vectors store during the process of construction.
|
|
642
|
-
|
|
643
|
-
:param keys: Unique identifier(s) for passed vectors
|
|
644
|
-
:type keys: Optional[KeyOrKeysLike], can be `None`
|
|
645
|
-
:param vectors: Vector or a row-major matrix
|
|
646
|
-
:type vectors: VectorOrVectorsLike
|
|
647
|
-
:param copy: Should the index store a copy of vectors
|
|
648
|
-
:type copy: bool, defaults to True
|
|
649
|
-
:param threads: Optimal number of cores to use
|
|
650
|
-
:type threads: int, defaults to 0
|
|
651
|
-
:param log: Whether to print the progress bar
|
|
652
|
-
:type log: Union[str, bool], defaults to False
|
|
653
|
-
:param progress: Callback to report stats of the progress and control it
|
|
654
|
-
:type progress: Optional[ProgressCallback], defaults to None
|
|
655
|
-
:return: Inserted key or keys
|
|
656
|
-
:type: Union[int, np.ndarray]
|
|
657
|
-
"""
|
|
658
|
-
return _add_to_compiled(
|
|
659
|
-
self._compiled,
|
|
660
|
-
keys=keys,
|
|
661
|
-
vectors=vectors,
|
|
662
|
-
copy=copy,
|
|
663
|
-
threads=threads,
|
|
664
|
-
log=log,
|
|
665
|
-
progress=progress,
|
|
666
|
-
)
|
|
667
|
-
|
|
668
|
-
def search(
|
|
669
|
-
self,
|
|
670
|
-
vectors: VectorOrVectorsLike,
|
|
671
|
-
count: int = 10,
|
|
672
|
-
radius: float = math.inf,
|
|
673
|
-
*,
|
|
674
|
-
threads: int = 0,
|
|
675
|
-
exact: bool = False,
|
|
676
|
-
log: Union[str, bool] = False,
|
|
677
|
-
progress: Optional[ProgressCallback] = None,
|
|
678
|
-
) -> Union[Matches, BatchMatches]:
|
|
679
|
-
"""
|
|
680
|
-
Performs approximate nearest neighbors search for one or more queries.
|
|
681
|
-
|
|
682
|
-
:param vectors: Query vector or vectors.
|
|
683
|
-
:type vectors: VectorOrVectorsLike
|
|
684
|
-
:param count: Upper count on the number of matches to find
|
|
685
|
-
:type count: int, defaults to 10
|
|
686
|
-
:param threads: Optimal number of cores to use
|
|
687
|
-
:type threads: int, defaults to 0
|
|
688
|
-
:param exact: Perform exhaustive linear-time exact search
|
|
689
|
-
:type exact: bool, defaults to False
|
|
690
|
-
:param log: Whether to print the progress bar, default to False
|
|
691
|
-
:type log: Union[str, bool], optional
|
|
692
|
-
:param progress: Callback to report stats of the progress and control it
|
|
693
|
-
:type progress: Optional[ProgressCallback], defaults to None
|
|
694
|
-
:return: Matches for one or more queries
|
|
695
|
-
:rtype: Union[Matches, BatchMatches]
|
|
696
|
-
"""
|
|
697
|
-
|
|
698
|
-
return _search_in_compiled(
|
|
699
|
-
self._compiled.search_many,
|
|
700
|
-
vectors,
|
|
701
|
-
# Batch scheduling:
|
|
702
|
-
log=log,
|
|
703
|
-
# Search constraints:
|
|
704
|
-
count=count,
|
|
705
|
-
exact=exact,
|
|
706
|
-
threads=threads,
|
|
707
|
-
progress=progress,
|
|
708
|
-
)
|
|
709
|
-
|
|
710
|
-
def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
|
|
711
|
-
if isinstance(keys, Iterable):
|
|
712
|
-
return self._compiled.contains_many(np.array(keys, dtype=Key))
|
|
713
|
-
else:
|
|
714
|
-
return self._compiled.contains_one(int(keys))
|
|
715
|
-
|
|
716
|
-
def __contains__(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
|
|
717
|
-
return self.contains(keys)
|
|
718
|
-
|
|
719
|
-
def count(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
|
|
720
|
-
if isinstance(keys, Iterable):
|
|
721
|
-
return self._compiled.count_many(np.array(keys, dtype=Key))
|
|
722
|
-
else:
|
|
723
|
-
return self._compiled.count_one(int(keys))
|
|
724
|
-
|
|
725
|
-
def get(
|
|
726
|
-
self,
|
|
727
|
-
keys: KeyOrKeysLike,
|
|
728
|
-
dtype: Optional[DTypeLike] = None,
|
|
729
|
-
) -> Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]:
|
|
730
|
-
"""Looks up one or more keys from the `Index`, retrieving corresponding vectors.
|
|
731
|
-
|
|
732
|
-
Returns `None`, if one key is requested, and its not present.
|
|
733
|
-
Returns a (row) vector, if the key maps into a single vector.
|
|
734
|
-
Returns a (row-major) matrix, if the key maps into a multiple vectors.
|
|
735
|
-
If multiple keys are requested, composes many such responses into a `tuple`.
|
|
736
|
-
|
|
737
|
-
:param keys: One or more keys to lookup
|
|
738
|
-
:type keys: KeyOrKeysLike
|
|
739
|
-
:return: One or more keys lookup results
|
|
740
|
-
:rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
|
|
741
|
-
"""
|
|
742
|
-
if not dtype:
|
|
743
|
-
dtype = self.dtype
|
|
744
|
-
else:
|
|
745
|
-
dtype = _normalize_dtype(dtype)
|
|
746
|
-
|
|
747
|
-
view_dtype = _to_numpy_dtype(dtype)
|
|
748
|
-
|
|
749
|
-
def cast(result):
|
|
750
|
-
if result is not None:
|
|
751
|
-
return result.view(view_dtype)
|
|
752
|
-
return result
|
|
753
|
-
|
|
754
|
-
is_one = not isinstance(keys, Iterable)
|
|
755
|
-
if is_one:
|
|
756
|
-
keys = [keys]
|
|
757
|
-
if not isinstance(keys, np.ndarray):
|
|
758
|
-
keys = np.array(keys, dtype=Key)
|
|
759
|
-
else:
|
|
760
|
-
keys = keys.astype(Key)
|
|
761
|
-
|
|
762
|
-
results = self._compiled.get_many(keys, dtype)
|
|
763
|
-
results = [cast(result) for result in results]
|
|
764
|
-
return results[0] if is_one else results
|
|
765
|
-
|
|
766
|
-
def __getitem__(
|
|
767
|
-
self, keys: KeyOrKeysLike
|
|
768
|
-
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
|
769
|
-
"""Looks up one or more keys from the `Index`, retrieving corresponding vectors.
|
|
770
|
-
|
|
771
|
-
Returns `None`, if one key is requested, and its not present.
|
|
772
|
-
Returns a (row) vector, if the key maps into a single vector.
|
|
773
|
-
Returns a (row-major) matrix, if the key maps into a multiple vectors.
|
|
774
|
-
If multiple keys are requested, composes many such responses into a `tuple`.
|
|
775
|
-
|
|
776
|
-
:param keys: One or more keys to lookup
|
|
777
|
-
:type keys: KeyOrKeysLike
|
|
778
|
-
:return: One or more keys lookup results
|
|
779
|
-
:rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
|
|
780
|
-
"""
|
|
781
|
-
return self.get(keys)
|
|
782
|
-
|
|
783
|
-
def remove(
|
|
784
|
-
self,
|
|
785
|
-
keys: KeyOrKeysLike,
|
|
786
|
-
*,
|
|
787
|
-
compact: bool = False,
|
|
788
|
-
threads: int = 0,
|
|
789
|
-
) -> Union[int, np.ndarray]:
|
|
790
|
-
"""Removes one or move vectors from the index.
|
|
791
|
-
|
|
792
|
-
When working with extremely large indexes, you may want to
|
|
793
|
-
mark some entries deleted, instead of rebuilding a filtered index.
|
|
794
|
-
In other cases, rebuilding - is the recommended approach.
|
|
795
|
-
|
|
796
|
-
:param keys: Unique identifier for passed vectors, optional
|
|
797
|
-
:type keys: KeyOrKeysLike
|
|
798
|
-
:param compact: Removes links to removed nodes (expensive), defaults to False
|
|
799
|
-
:type compact: bool, optional
|
|
800
|
-
:param threads: Optimal number of cores to use, defaults to 0
|
|
801
|
-
:type threads: int, optional
|
|
802
|
-
:return: Array of integers for the number of removed vectors per key
|
|
803
|
-
:type: Union[int, np.ndarray]
|
|
804
|
-
"""
|
|
805
|
-
if not isinstance(keys, Iterable):
|
|
806
|
-
return self._compiled.remove_one(keys, compact=compact, threads=threads)
|
|
807
|
-
else:
|
|
808
|
-
keys = np.array(keys, dtype=Key)
|
|
809
|
-
return self._compiled.remove_many(keys, compact=compact, threads=threads)
|
|
810
|
-
|
|
811
|
-
def __delitem__(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
|
|
812
|
-
raise self.remove(keys)
|
|
813
|
-
|
|
814
|
-
def rename(
|
|
815
|
-
self,
|
|
816
|
-
from_: KeyOrKeysLike,
|
|
817
|
-
to: KeyOrKeysLike,
|
|
818
|
-
) -> Union[int, np.ndarray]:
|
|
819
|
-
"""Rename existing member vector or vectors.
|
|
820
|
-
|
|
821
|
-
May be used in iterative clustering procedures, where one would iteratively
|
|
822
|
-
relabel every vector with the name of the cluster an entry belongs to, until
|
|
823
|
-
the system converges.
|
|
824
|
-
|
|
825
|
-
:param from_: One or more keys to be renamed
|
|
826
|
-
:type from_: KeyOrKeysLike
|
|
827
|
-
:param to: New name or names (of identical length as `from_`)
|
|
828
|
-
:type to: KeyOrKeysLike
|
|
829
|
-
:return: Number of vectors that were found and renamed
|
|
830
|
-
:rtype: int
|
|
831
|
-
"""
|
|
832
|
-
if isinstance(from_, Iterable):
|
|
833
|
-
from_ = np.array(from_, dtype=Key)
|
|
834
|
-
if isinstance(to, Iterable):
|
|
835
|
-
to = np.array(to, dtype=Key)
|
|
836
|
-
return self._compiled.rename_many_to_many(from_, to)
|
|
837
|
-
|
|
838
|
-
else:
|
|
839
|
-
return self._compiled.rename_many_to_one(from_, int(to))
|
|
840
|
-
|
|
841
|
-
else:
|
|
842
|
-
return self._compiled.rename_one_to_one(int(from_), int(to))
|
|
843
|
-
|
|
844
|
-
@property
|
|
845
|
-
def jit(self) -> bool:
|
|
846
|
-
"""
|
|
847
|
-
:return: True, if the provided `metric` was JIT-ed
|
|
848
|
-
:rtype: bool
|
|
849
|
-
"""
|
|
850
|
-
return self._metric_jit is not None
|
|
851
|
-
|
|
852
|
-
@property
|
|
853
|
-
def hardware_acceleration(self) -> str:
|
|
854
|
-
"""Describes the kind of hardware-acceleration support used in
|
|
855
|
-
that exact instance of the `Index`, for that metric kind, and
|
|
856
|
-
the given number of dimensions.
|
|
857
|
-
|
|
858
|
-
:return: "auto", if nothing is available, ISA subset name otherwise
|
|
859
|
-
:rtype: str
|
|
860
|
-
"""
|
|
861
|
-
return self._compiled.hardware_acceleration
|
|
862
|
-
|
|
863
|
-
@property
|
|
864
|
-
def size(self) -> int:
|
|
865
|
-
return self._compiled.size
|
|
866
|
-
|
|
867
|
-
@property
|
|
868
|
-
def ndim(self) -> int:
|
|
869
|
-
return self._compiled.ndim
|
|
870
|
-
|
|
871
|
-
@property
|
|
872
|
-
def serialized_length(self) -> int:
|
|
873
|
-
return self._compiled.serialized_length
|
|
874
|
-
|
|
875
|
-
@property
|
|
876
|
-
def metric_kind(self) -> Union[MetricKind, CompiledMetric]:
|
|
877
|
-
return self._metric_jit.kind if self._metric_jit else self._metric_kind
|
|
878
|
-
|
|
879
|
-
@property
|
|
880
|
-
def metric(self) -> Union[MetricKind, CompiledMetric]:
|
|
881
|
-
return self._metric_jit if self._metric_jit else self._metric_kind
|
|
882
|
-
|
|
883
|
-
@metric.setter
|
|
884
|
-
def metric(self, metric: MetricLike):
|
|
885
|
-
metric = _normalize_metric(metric)
|
|
886
|
-
if isinstance(metric, MetricKind):
|
|
887
|
-
metric_kind = metric
|
|
888
|
-
metric_pointer = 0
|
|
889
|
-
metric_signature = MetricSignature.ArrayArraySize
|
|
890
|
-
elif isinstance(metric, CompiledMetric):
|
|
891
|
-
metric_kind = metric.kind
|
|
892
|
-
metric_pointer = metric.pointer
|
|
893
|
-
metric_signature = metric.signature
|
|
894
|
-
else:
|
|
895
|
-
raise ValueError(
|
|
896
|
-
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
897
|
-
)
|
|
898
|
-
|
|
899
|
-
return self._compiled.change_metric(
|
|
900
|
-
metric_kind=metric_kind,
|
|
901
|
-
metric_pointer=metric_pointer,
|
|
902
|
-
metric_signature=metric_signature,
|
|
903
|
-
)
|
|
904
|
-
|
|
905
|
-
@property
|
|
906
|
-
def dtype(self) -> ScalarKind:
|
|
907
|
-
return self._compiled.dtype
|
|
908
|
-
|
|
909
|
-
@property
|
|
910
|
-
def connectivity(self) -> int:
|
|
911
|
-
return self._compiled.connectivity
|
|
912
|
-
|
|
913
|
-
@property
|
|
914
|
-
def capacity(self) -> int:
|
|
915
|
-
return self._compiled.capacity
|
|
916
|
-
|
|
917
|
-
@property
|
|
918
|
-
def memory_usage(self) -> int:
|
|
919
|
-
return self._compiled.memory_usage
|
|
920
|
-
|
|
921
|
-
@property
|
|
922
|
-
def expansion_add(self) -> int:
|
|
923
|
-
return self._compiled.expansion_add
|
|
924
|
-
|
|
925
|
-
@property
|
|
926
|
-
def expansion_search(self) -> int:
|
|
927
|
-
return self._compiled.expansion_search
|
|
928
|
-
|
|
929
|
-
@expansion_add.setter
|
|
930
|
-
def expansion_add(self, v: int):
|
|
931
|
-
self._compiled.expansion_add = v
|
|
932
|
-
|
|
933
|
-
@expansion_search.setter
|
|
934
|
-
def expansion_search(self, v: int):
|
|
935
|
-
self._compiled.expansion_search = v
|
|
936
|
-
|
|
937
|
-
def save(
|
|
938
|
-
self,
|
|
939
|
-
path_or_buffer: Union[str, os.PathLike, NoneType] = None,
|
|
940
|
-
progress: Optional[ProgressCallback] = None,
|
|
941
|
-
) -> Optional[bytes]:
|
|
942
|
-
assert not progress or _match_signature(
|
|
943
|
-
progress, [int, int], bool
|
|
944
|
-
), "Invalid callback signature"
|
|
945
|
-
|
|
946
|
-
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
947
|
-
if path_or_buffer is None:
|
|
948
|
-
return self._compiled.save_index_to_buffer(progress)
|
|
949
|
-
else:
|
|
950
|
-
self._compiled.save_index_to_path(os.fspath(path_or_buffer), progress)
|
|
951
|
-
|
|
952
|
-
def load(
|
|
953
|
-
self,
|
|
954
|
-
path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None,
|
|
955
|
-
progress: Optional[ProgressCallback] = None,
|
|
956
|
-
):
|
|
957
|
-
assert not progress or _match_signature(
|
|
958
|
-
progress, [int, int], bool
|
|
959
|
-
), "Invalid callback signature"
|
|
960
|
-
|
|
961
|
-
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
962
|
-
if path_or_buffer is None:
|
|
963
|
-
raise Exception("Define the source")
|
|
964
|
-
if isinstance(path_or_buffer, bytearray):
|
|
965
|
-
path_or_buffer = bytes(path_or_buffer)
|
|
966
|
-
if isinstance(path_or_buffer, bytes):
|
|
967
|
-
self._compiled.load_index_from_buffer(path_or_buffer, progress)
|
|
968
|
-
else:
|
|
969
|
-
path_or_buffer = os.fspath(path_or_buffer)
|
|
970
|
-
if os.path.exists(path_or_buffer):
|
|
971
|
-
self._compiled.load_index_from_path(path_or_buffer, progress)
|
|
972
|
-
else:
|
|
973
|
-
raise RuntimeError("Missing file!")
|
|
974
|
-
|
|
975
|
-
def view(
|
|
976
|
-
self,
|
|
977
|
-
path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None,
|
|
978
|
-
progress: Optional[ProgressCallback] = None,
|
|
979
|
-
):
|
|
980
|
-
assert not progress or _match_signature(
|
|
981
|
-
progress, [int, int], bool
|
|
982
|
-
), "Invalid callback signature"
|
|
983
|
-
|
|
984
|
-
path_or_buffer = path_or_buffer if path_or_buffer else self.path
|
|
985
|
-
if path_or_buffer is None:
|
|
986
|
-
raise Exception("Define the source")
|
|
987
|
-
if isinstance(path_or_buffer, bytearray):
|
|
988
|
-
path_or_buffer = bytes(path_or_buffer)
|
|
989
|
-
if isinstance(path_or_buffer, bytes):
|
|
990
|
-
self._compiled.view_index_from_buffer(path_or_buffer, progress)
|
|
991
|
-
else:
|
|
992
|
-
self._compiled.view_index_from_path(os.fspath(path_or_buffer), progress)
|
|
993
|
-
|
|
994
|
-
def clear(self):
|
|
995
|
-
"""Erases all the vectors from the index, preserving the space for future insertions."""
|
|
996
|
-
self._compiled.clear()
|
|
997
|
-
|
|
998
|
-
def reset(self):
|
|
999
|
-
"""Erases all members from index, closing files, and returning RAM to OS."""
|
|
1000
|
-
if not hasattr(self, "_compiled"):
|
|
1001
|
-
return
|
|
1002
|
-
self._compiled.reset()
|
|
1003
|
-
|
|
1004
|
-
def __del__(self):
|
|
1005
|
-
self.reset()
|
|
1006
|
-
|
|
1007
|
-
def copy(self) -> Index:
|
|
1008
|
-
result = Index(
|
|
1009
|
-
ndim=self.ndim,
|
|
1010
|
-
metric=self.metric,
|
|
1011
|
-
dtype=self.dtype,
|
|
1012
|
-
connectivity=self.connectivity,
|
|
1013
|
-
expansion_add=self.expansion_add,
|
|
1014
|
-
expansion_search=self.expansion_search,
|
|
1015
|
-
path=self.path,
|
|
1016
|
-
)
|
|
1017
|
-
result._compiled = self._compiled.copy()
|
|
1018
|
-
return result
|
|
1019
|
-
|
|
1020
|
-
def join(
|
|
1021
|
-
self,
|
|
1022
|
-
other: Index,
|
|
1023
|
-
max_proposals: int = 0,
|
|
1024
|
-
exact: bool = False,
|
|
1025
|
-
progress: Optional[ProgressCallback] = None,
|
|
1026
|
-
) -> Dict[Key, Key]:
|
|
1027
|
-
"""Performs "Semantic Join" or pairwise matching between `self` & `other` index.
|
|
1028
|
-
Is different from `search`, as no collisions are allowed in resulting pairs.
|
|
1029
|
-
Uses the concept of "Stable Marriages" from Combinatorics, famous for the 2012
|
|
1030
|
-
Nobel Prize in Economics.
|
|
1031
|
-
|
|
1032
|
-
:param other: Another index.
|
|
1033
|
-
:type other: Index
|
|
1034
|
-
:param max_proposals: Limit on candidates evaluated per vector, defaults to 0
|
|
1035
|
-
:type max_proposals: int, optional
|
|
1036
|
-
:param exact: Controls if underlying `search` should be exact, defaults to False
|
|
1037
|
-
:type exact: bool, optional
|
|
1038
|
-
:param progress: Callback to report stats of the progress and control it
|
|
1039
|
-
:type progress: Optional[ProgressCallback], defaults to None
|
|
1040
|
-
:return: Mapping from keys of `self` to keys of `other`
|
|
1041
|
-
:rtype: Dict[Key, Key]
|
|
1042
|
-
"""
|
|
1043
|
-
assert not progress or _match_signature(
|
|
1044
|
-
progress, [int, int], bool
|
|
1045
|
-
), "Invalid callback signature"
|
|
1046
|
-
|
|
1047
|
-
return self._compiled.join(
|
|
1048
|
-
other=other._compiled,
|
|
1049
|
-
max_proposals=max_proposals,
|
|
1050
|
-
exact=exact,
|
|
1051
|
-
progress=progress,
|
|
1052
|
-
)
|
|
1053
|
-
|
|
1054
|
-
def cluster(
|
|
1055
|
-
self,
|
|
1056
|
-
*,
|
|
1057
|
-
vectors: Optional[np.ndarray] = None,
|
|
1058
|
-
keys: Optional[np.ndarray] = None,
|
|
1059
|
-
min_count: Optional[int] = None,
|
|
1060
|
-
max_count: Optional[int] = None,
|
|
1061
|
-
threads: int = 0,
|
|
1062
|
-
log: Union[str, bool] = False,
|
|
1063
|
-
progress: Optional[ProgressCallback] = None,
|
|
1064
|
-
) -> Clustering:
|
|
1065
|
-
"""
|
|
1066
|
-
Clusters already indexed or provided `vectors`, mapping them to various centroids.
|
|
1067
|
-
|
|
1068
|
-
:param vectors: .
|
|
1069
|
-
:type vectors: Optional[VectorOrVectorsLike]
|
|
1070
|
-
:param count: Upper bound on the number of clusters to produce
|
|
1071
|
-
:type count: Optional[int], defaults to None
|
|
1072
|
-
|
|
1073
|
-
:param threads: Optimal number of cores to use,
|
|
1074
|
-
:type threads: int, defaults to 0
|
|
1075
|
-
:param log: Whether to print the progress bar
|
|
1076
|
-
:type log: Union[str, bool], defaults to False
|
|
1077
|
-
:param progress: Callback to report stats of the progress and control it
|
|
1078
|
-
:type progress: Optional[ProgressCallback], defaults to None
|
|
1079
|
-
:return: Matches for one or more queries
|
|
1080
|
-
:rtype: Union[Matches, BatchMatches]
|
|
1081
|
-
"""
|
|
1082
|
-
assert not progress or _match_signature(
|
|
1083
|
-
progress, [int, int], bool
|
|
1084
|
-
), "Invalid callback signature"
|
|
1085
|
-
|
|
1086
|
-
if min_count is None:
|
|
1087
|
-
min_count = 0
|
|
1088
|
-
if max_count is None:
|
|
1089
|
-
max_count = 0
|
|
1090
|
-
|
|
1091
|
-
if vectors is not None:
|
|
1092
|
-
assert keys is None, "You can either cluster vectors or member keys"
|
|
1093
|
-
results = self._compiled.cluster_vectors(
|
|
1094
|
-
vectors,
|
|
1095
|
-
min_count=min_count,
|
|
1096
|
-
max_count=max_count,
|
|
1097
|
-
threads=threads,
|
|
1098
|
-
progress=progress,
|
|
1099
|
-
)
|
|
1100
|
-
else:
|
|
1101
|
-
if keys is None:
|
|
1102
|
-
keys = self._compiled.get_keys_in_slice()
|
|
1103
|
-
if not isinstance(keys, np.ndarray):
|
|
1104
|
-
keys = np.array(keys)
|
|
1105
|
-
keys = keys.astype(Key)
|
|
1106
|
-
results = self._compiled.cluster_keys(
|
|
1107
|
-
keys,
|
|
1108
|
-
min_count=min_count,
|
|
1109
|
-
max_count=max_count,
|
|
1110
|
-
threads=threads,
|
|
1111
|
-
progress=progress,
|
|
1112
|
-
)
|
|
1113
|
-
|
|
1114
|
-
batch_matches = BatchMatches(*results)
|
|
1115
|
-
return Clustering(self, batch_matches, keys)
|
|
1116
|
-
|
|
1117
|
-
def pairwise_distance(
|
|
1118
|
-
self, left: KeyOrKeysLike, right: KeyOrKeysLike
|
|
1119
|
-
) -> Union[np.ndarray, float]:
|
|
1120
|
-
assert isinstance(left, Iterable) == isinstance(right, Iterable)
|
|
1121
|
-
|
|
1122
|
-
if not isinstance(left, Iterable):
|
|
1123
|
-
return self._compiled.pairwise_distance(int(left), int(right))
|
|
1124
|
-
else:
|
|
1125
|
-
left = np.array(left).astype(Key)
|
|
1126
|
-
right = np.array(right).astype(Key)
|
|
1127
|
-
return self._compiled.pairwise_distances(left, right)
|
|
1128
|
-
|
|
1129
|
-
@property
|
|
1130
|
-
def keys(self) -> IndexedKeys:
|
|
1131
|
-
return IndexedKeys(self)
|
|
1132
|
-
|
|
1133
|
-
@property
|
|
1134
|
-
def vectors(self) -> np.ndarray:
|
|
1135
|
-
return self.get(self.keys, vstack=True)
|
|
1136
|
-
|
|
1137
|
-
@property
|
|
1138
|
-
def max_level(self) -> int:
|
|
1139
|
-
return self._compiled.max_level
|
|
1140
|
-
|
|
1141
|
-
@property
|
|
1142
|
-
def nlevels(self) -> int:
|
|
1143
|
-
return self._compiled.max_level + 1
|
|
1144
|
-
|
|
1145
|
-
@property
|
|
1146
|
-
def stats(self) -> _CompiledIndexStats:
|
|
1147
|
-
"""Get the accumulated statistics for the entire multi-level graph.
|
|
1148
|
-
|
|
1149
|
-
:return: Statistics for the entire multi-level graph.
|
|
1150
|
-
:rtype: _CompiledIndexStats
|
|
1151
|
-
|
|
1152
|
-
Statistics:
|
|
1153
|
-
- ``nodes`` (int): The number of nodes in that level.
|
|
1154
|
-
- ``edges`` (int): The number of edges in that level.
|
|
1155
|
-
- ``max_edges`` (int): The maximum possible number of edges in that level.
|
|
1156
|
-
- ``allocated_bytes`` (int): The amount of allocated memory for that level.
|
|
1157
|
-
"""
|
|
1158
|
-
return self._compiled.stats
|
|
1159
|
-
|
|
1160
|
-
@property
|
|
1161
|
-
def levels_stats(self) -> List[_CompiledIndexStats]:
|
|
1162
|
-
"""Get the accumulated statistics for every level graph.
|
|
1163
|
-
|
|
1164
|
-
:return: Statistics for every level graph.
|
|
1165
|
-
:rtype: List[_CompiledIndexStats]
|
|
1166
|
-
|
|
1167
|
-
Statistics:
|
|
1168
|
-
- ``nodes`` (int): The number of nodes in that level.
|
|
1169
|
-
- ``edges`` (int): The number of edges in that level.
|
|
1170
|
-
- ``max_edges`` (int): The maximum possible number of edges in that level.
|
|
1171
|
-
- ``allocated_bytes`` (int): The amount of allocated memory for that level.
|
|
1172
|
-
"""
|
|
1173
|
-
return self._compiled.levels_stats
|
|
1174
|
-
|
|
1175
|
-
def level_stats(self, level: int) -> _CompiledIndexStats:
|
|
1176
|
-
"""Get statistics for one level of the index - one graph.
|
|
1177
|
-
|
|
1178
|
-
:return: Statistics for one level of the index - one graph.
|
|
1179
|
-
:rtype: _CompiledIndexStats
|
|
1180
|
-
|
|
1181
|
-
Statistics:
|
|
1182
|
-
- ``nodes`` (int): The number of nodes in that level.
|
|
1183
|
-
- ``edges`` (int): The number of edges in that level.
|
|
1184
|
-
- ``max_edges`` (int): The maximum possible number of edges in that level.
|
|
1185
|
-
- ``allocated_bytes`` (int): The amount of allocated memory for that level.
|
|
1186
|
-
"""
|
|
1187
|
-
return self._compiled.level_stats(level)
|
|
1188
|
-
|
|
1189
|
-
@property
|
|
1190
|
-
def specs(self) -> Dict[str, Union[str, int, bool]]:
|
|
1191
|
-
if not hasattr(self, "_compiled"):
|
|
1192
|
-
return "usearch.Index(failed)"
|
|
1193
|
-
return {
|
|
1194
|
-
"type": "usearch.Index",
|
|
1195
|
-
"ndim": self.ndim,
|
|
1196
|
-
"connectivity": self.connectivity,
|
|
1197
|
-
"expansion_add": self.expansion_add,
|
|
1198
|
-
"expansion_search": self.expansion_search,
|
|
1199
|
-
"size": self.size,
|
|
1200
|
-
"jit": self.jit,
|
|
1201
|
-
"hardware_acceleration": self.hardware_acceleration,
|
|
1202
|
-
"metric_kind": self.metric_kind,
|
|
1203
|
-
"dtype": self.dtype,
|
|
1204
|
-
"path": self.path,
|
|
1205
|
-
"compiled_with_openmp": USES_OPENMP,
|
|
1206
|
-
"compiled_with_simsimd": USES_SIMSIMD,
|
|
1207
|
-
"compiled_with_native_f16": USES_NATIVE_F16,
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
def __repr__(self) -> str:
|
|
1211
|
-
if not hasattr(self, "_compiled"):
|
|
1212
|
-
return "usearch.Index(failed)"
|
|
1213
|
-
f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {:,} vectors in {} levels, {} hardware acceleration)"
|
|
1214
|
-
return f.format(
|
|
1215
|
-
self.dtype,
|
|
1216
|
-
self.ndim,
|
|
1217
|
-
self.metric_kind,
|
|
1218
|
-
self.connectivity,
|
|
1219
|
-
self.expansion_add,
|
|
1220
|
-
self.expansion_search,
|
|
1221
|
-
len(self),
|
|
1222
|
-
self.nlevels,
|
|
1223
|
-
self.hardware_acceleration,
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
def _repr_pretty_(self, printer, cycle) -> str:
|
|
1227
|
-
if not hasattr(self, "_compiled"):
|
|
1228
|
-
return "usearch.Index(failed)"
|
|
1229
|
-
level_stats = [
|
|
1230
|
-
f"--- {i}. {self.level_stats(i).nodes:,} nodes" for i in range(self.nlevels)
|
|
1231
|
-
]
|
|
1232
|
-
lines = "\n".join(
|
|
1233
|
-
[
|
|
1234
|
-
"usearch.Index",
|
|
1235
|
-
"- config",
|
|
1236
|
-
f"-- data type: {self.dtype}",
|
|
1237
|
-
f"-- dimensions: {self.ndim}",
|
|
1238
|
-
f"-- metric: {self.metric_kind}",
|
|
1239
|
-
f"-- connectivity: {self.connectivity}",
|
|
1240
|
-
f"-- expansion on addition:{self.expansion_add} candidates",
|
|
1241
|
-
f"-- expansion on search: {self.expansion_search} candidates",
|
|
1242
|
-
"- binary",
|
|
1243
|
-
f"-- uses OpenMP: {USES_OPENMP}",
|
|
1244
|
-
f"-- uses SimSIMD: {USES_SIMSIMD}",
|
|
1245
|
-
f"-- supports half-precision: {USES_NATIVE_F16}",
|
|
1246
|
-
f"-- uses hardware acceleration: {self.hardware_acceleration}",
|
|
1247
|
-
"- state",
|
|
1248
|
-
f"-- size: {self.size:,} vectors",
|
|
1249
|
-
f"-- memory usage: {self.memory_usage:,} bytes",
|
|
1250
|
-
f"-- max level: {self.max_level}",
|
|
1251
|
-
*level_stats,
|
|
1252
|
-
]
|
|
1253
|
-
)
|
|
1254
|
-
printer.text(lines)
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
class Indexes:
|
|
1258
|
-
def __init__(
|
|
1259
|
-
self,
|
|
1260
|
-
indexes: Iterable[Index] = [],
|
|
1261
|
-
paths: Iterable[os.PathLike] = [],
|
|
1262
|
-
view: bool = False,
|
|
1263
|
-
threads: int = 0,
|
|
1264
|
-
) -> None:
|
|
1265
|
-
self._compiled = _CompiledIndexes()
|
|
1266
|
-
for index in indexes:
|
|
1267
|
-
self._compiled.merge(index._compiled)
|
|
1268
|
-
self._compiled.merge_paths(paths, view=view, threads=threads)
|
|
1269
|
-
|
|
1270
|
-
def merge(self, index: Index):
|
|
1271
|
-
self._compiled.merge(index._compiled)
|
|
1272
|
-
|
|
1273
|
-
def merge_path(self, path: os.PathLike):
|
|
1274
|
-
self._compiled.merge_path(os.fspath(path))
|
|
1275
|
-
|
|
1276
|
-
def __len__(self) -> int:
|
|
1277
|
-
return self._compiled.__len__()
|
|
1278
|
-
|
|
1279
|
-
def search(
|
|
1280
|
-
self,
|
|
1281
|
-
vectors,
|
|
1282
|
-
count: int = 10,
|
|
1283
|
-
*,
|
|
1284
|
-
threads: int = 0,
|
|
1285
|
-
exact: bool = False,
|
|
1286
|
-
progress: Optional[ProgressCallback] = None,
|
|
1287
|
-
):
|
|
1288
|
-
return _search_in_compiled(
|
|
1289
|
-
self._compiled.search_many,
|
|
1290
|
-
vectors,
|
|
1291
|
-
# Batch scheduling:
|
|
1292
|
-
log=False,
|
|
1293
|
-
# Search constraints:
|
|
1294
|
-
count=count,
|
|
1295
|
-
exact=exact,
|
|
1296
|
-
threads=threads,
|
|
1297
|
-
progress=progress,
|
|
1298
|
-
)
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
def search(
|
|
1302
|
-
dataset: np.ndarray,
|
|
1303
|
-
query: np.ndarray,
|
|
1304
|
-
count: int = 10,
|
|
1305
|
-
metric: MetricLike = MetricKind.Cos,
|
|
1306
|
-
*,
|
|
1307
|
-
exact: bool = False,
|
|
1308
|
-
threads: int = 0,
|
|
1309
|
-
log: Union[str, bool] = False,
|
|
1310
|
-
progress: Optional[ProgressCallback] = None,
|
|
1311
|
-
) -> Union[Matches, BatchMatches]:
|
|
1312
|
-
"""Shortcut for search, that can avoid index construction. Particularly useful for
|
|
1313
|
-
tiny datasets, where brute-force exact search works fast enough.
|
|
1314
|
-
|
|
1315
|
-
:param dataset: Row-major matrix.
|
|
1316
|
-
:type dataset: np.ndarray
|
|
1317
|
-
:param query: Query vector or vectors (also row-major), to find in `dataset`.
|
|
1318
|
-
:type query: np.ndarray
|
|
1319
|
-
|
|
1320
|
-
:param count: Upper count on the number of matches to find, defaults to 10
|
|
1321
|
-
:type count: int, optional
|
|
1322
|
-
|
|
1323
|
-
:param metric: Distance function
|
|
1324
|
-
:type metric: MetricLike, defaults to MetricKind.Cos
|
|
1325
|
-
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
1326
|
-
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
1327
|
-
Hamming, Tanimoto, Sorensen.
|
|
1328
|
-
|
|
1329
|
-
:param threads: Optimal number of cores to use, defaults to 0
|
|
1330
|
-
:type threads: int, optional
|
|
1331
|
-
:param exact: Perform exhaustive linear-time exact search, defaults to False
|
|
1332
|
-
:type exact: bool, optional
|
|
1333
|
-
:param log: Whether to print the progress bar, default to False
|
|
1334
|
-
:type log: Union[str, bool], optional
|
|
1335
|
-
:param progress: Callback to report stats of the progress and control it
|
|
1336
|
-
:type progress: Optional[ProgressCallback], defaults to None
|
|
1337
|
-
:return: Matches for one or more queries
|
|
1338
|
-
:rtype: Union[Matches, BatchMatches]
|
|
1339
|
-
"""
|
|
1340
|
-
assert not progress or _match_signature(
|
|
1341
|
-
progress, [int, int], bool
|
|
1342
|
-
), "Invalid callback signature"
|
|
1343
|
-
assert dataset.ndim == 2, "Dataset must be a matrix, with a vector in each row"
|
|
1344
|
-
|
|
1345
|
-
if not exact:
|
|
1346
|
-
index = Index(
|
|
1347
|
-
dataset.shape[1],
|
|
1348
|
-
metric=metric,
|
|
1349
|
-
dtype=dataset.dtype,
|
|
1350
|
-
)
|
|
1351
|
-
index.add(
|
|
1352
|
-
None,
|
|
1353
|
-
dataset,
|
|
1354
|
-
threads=threads,
|
|
1355
|
-
log=log,
|
|
1356
|
-
progress=progress,
|
|
1357
|
-
)
|
|
1358
|
-
return index.search(
|
|
1359
|
-
query,
|
|
1360
|
-
count,
|
|
1361
|
-
threads=threads,
|
|
1362
|
-
log=log,
|
|
1363
|
-
progress=progress,
|
|
1364
|
-
)
|
|
1365
|
-
|
|
1366
|
-
metric = _normalize_metric(metric)
|
|
1367
|
-
if isinstance(metric, MetricKind):
|
|
1368
|
-
metric_kind = metric
|
|
1369
|
-
metric_pointer = 0
|
|
1370
|
-
metric_signature = MetricSignature.ArrayArraySize
|
|
1371
|
-
elif isinstance(metric, CompiledMetric):
|
|
1372
|
-
metric_kind = metric.kind
|
|
1373
|
-
metric_pointer = metric.pointer
|
|
1374
|
-
metric_signature = metric.signature
|
|
1375
|
-
else:
|
|
1376
|
-
raise ValueError("The `metric` must be a `CompiledMetric` or a `MetricKind`")
|
|
1377
|
-
|
|
1378
|
-
def search_batch(query, **kwargs):
|
|
1379
|
-
assert dataset.shape[1] == query.shape[1], "Number of dimensions differs"
|
|
1380
|
-
if dataset.dtype != query.dtype:
|
|
1381
|
-
query = query.astype(dataset.dtype)
|
|
1382
|
-
|
|
1383
|
-
return _exact_search(
|
|
1384
|
-
dataset,
|
|
1385
|
-
query,
|
|
1386
|
-
metric_kind=metric_kind,
|
|
1387
|
-
metric_signature=metric_signature,
|
|
1388
|
-
metric_pointer=metric_pointer,
|
|
1389
|
-
**kwargs,
|
|
1390
|
-
)
|
|
1391
|
-
|
|
1392
|
-
return _search_in_compiled(
|
|
1393
|
-
search_batch,
|
|
1394
|
-
query,
|
|
1395
|
-
# Batch scheduling:
|
|
1396
|
-
log=log,
|
|
1397
|
-
# Search constraints:
|
|
1398
|
-
count=count,
|
|
1399
|
-
threads=threads,
|
|
1400
|
-
progress=progress,
|
|
1401
|
-
)
|