usearch 1.1.1 → 1.2.0
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/.github/workflows/prerelease.yml +3 -3
- package/.github/workflows/release.yml +3 -3
- package/.github/workflows/update_version.sh +1 -1
- package/.gitmodules +1 -4
- package/Cargo.toml +1 -1
- package/Package.swift +2 -1
- package/README.md +86 -42
- package/binding.gyp +16 -12
- package/build.gradle +1 -1
- package/build.rs +0 -1
- package/c/CMakeLists.txt +11 -7
- package/c/lib.cpp +36 -24
- package/c/usearch.h +42 -12
- package/cluster.ipynb +167 -0
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +8 -3
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +42 -25
- package/docs/benchmarks.md +8 -0
- package/docs/compilation.md +7 -7
- package/docs/conf.py +27 -24
- package/docs/javascript/reference.rst +3 -0
- package/golang/README.md +31 -7
- package/golang/lib.go +1 -1
- package/include/usearch/index.hpp +348 -69
- package/include/usearch/index_dense.hpp +587 -179
- package/include/usearch/index_plugins.hpp +194 -16
- package/java/cloud/unum/usearch/Index.java +13 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +40 -9
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +16 -0
- package/javascript/docs.js +111 -0
- package/javascript/lib.cpp +29 -9
- package/javascript/usearch.d.ts +9 -1
- package/objc/USearchObjective.mm +11 -11
- package/package.json +2 -2
- package/pyproject.toml +1 -1
- package/python/lib.cpp +488 -158
- package/python/scripts/index_faiss.py +3 -3
- package/python/scripts/join.py +6 -6
- package/python/scripts/test_index.py +219 -0
- package/python/scripts/test_sparse.py +21 -21
- package/python/scripts/test_tooling.py +111 -0
- package/python/usearch/eval.py +9 -5
- package/python/usearch/index.py +491 -216
- package/rust/lib.cpp +6 -24
- package/rust/lib.hpp +1 -3
- package/rust/lib.rs +89 -0
- package/setup.py +5 -6
- package/wasm/CMakeLists.txt +0 -1
- package/wasmer.toml +1 -1
- package/python/scripts/test.py +0 -364
- package/robin-map/.codecov.yml +0 -5
- package/robin-map/.github/workflows/ci.yml +0 -136
- package/robin-map/CMakeLists.txt +0 -86
- package/robin-map/LICENSE +0 -21
- package/robin-map/README.md +0 -483
- package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +0 -9
- package/robin-map/doxygen.conf +0 -2483
- package/robin-map/include/tsl/robin_growth_policy.h +0 -406
- package/robin-map/include/tsl/robin_hash.h +0 -1639
- package/robin-map/include/tsl/robin_map.h +0 -807
- package/robin-map/include/tsl/robin_set.h +0 -660
- package/robin-map/tests/CMakeLists.txt +0 -26
- package/robin-map/tests/custom_allocator_tests.cpp +0 -138
- package/robin-map/tests/main.cpp +0 -26
- package/robin-map/tests/policy_tests.cpp +0 -97
- package/robin-map/tests/robin_map_tests.cpp +0 -1451
- package/robin-map/tests/robin_set_tests.cpp +0 -164
- package/robin-map/tests/utils.h +0 -443
- package/robin-map/tsl-robin-map.natvis +0 -78
package/python/usearch/index.py
CHANGED
|
@@ -6,8 +6,17 @@ from __future__ import annotations
|
|
|
6
6
|
# into the primary `Index` class, connecting USearch with Numba.
|
|
7
7
|
import os
|
|
8
8
|
import math
|
|
9
|
-
from typing import Optional, Union, NamedTuple, List, Iterable
|
|
10
9
|
from dataclasses import dataclass
|
|
10
|
+
from typing import (
|
|
11
|
+
Optional,
|
|
12
|
+
Union,
|
|
13
|
+
NamedTuple,
|
|
14
|
+
List,
|
|
15
|
+
Iterable,
|
|
16
|
+
Tuple,
|
|
17
|
+
Dict,
|
|
18
|
+
Callable,
|
|
19
|
+
)
|
|
11
20
|
|
|
12
21
|
import numpy as np
|
|
13
22
|
from tqdm import tqdm
|
|
@@ -34,8 +43,23 @@ MetricKindBitwise = (
|
|
|
34
43
|
MetricKind.Sorensen,
|
|
35
44
|
)
|
|
36
45
|
|
|
46
|
+
|
|
47
|
+
class CompiledMetric(NamedTuple):
|
|
48
|
+
pointer: int
|
|
49
|
+
kind: MetricKind
|
|
50
|
+
signature: MetricSignature
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
Key = np.uint64
|
|
38
54
|
|
|
55
|
+
KeyOrKeysLike = Union[Key, Iterable[Key], int, Iterable[int], np.ndarray, memoryview]
|
|
56
|
+
|
|
57
|
+
VectorOrVectorsLike = Union[np.ndarray, Iterable[np.ndarray], memoryview]
|
|
58
|
+
|
|
59
|
+
DTypeLike = Union[str, ScalarKind]
|
|
60
|
+
|
|
61
|
+
MetricLike = Union[str, MetricKind, CompiledMetric]
|
|
62
|
+
|
|
39
63
|
|
|
40
64
|
def _normalize_dtype(dtype, metric: MetricKind = MetricKind.Cos) -> ScalarKind:
|
|
41
65
|
if dtype is None or dtype == "":
|
|
@@ -58,19 +82,11 @@ def _normalize_dtype(dtype, metric: MetricKind = MetricKind.Cos) -> ScalarKind:
|
|
|
58
82
|
"float32": ScalarKind.F32,
|
|
59
83
|
"float16": ScalarKind.F16,
|
|
60
84
|
"int8": ScalarKind.I8,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def _to_numpy_compatible_dtype(dtype: ScalarKind) -> ScalarKind:
|
|
68
|
-
_normalize = {
|
|
69
|
-
ScalarKind.F64: ScalarKind.F64,
|
|
70
|
-
ScalarKind.F32: ScalarKind.F32,
|
|
71
|
-
ScalarKind.F16: ScalarKind.F16,
|
|
72
|
-
ScalarKind.I8: ScalarKind.F16,
|
|
73
|
-
ScalarKind.B1: ScalarKind.B1,
|
|
85
|
+
np.float64: ScalarKind.F64,
|
|
86
|
+
np.float32: ScalarKind.F32,
|
|
87
|
+
np.float16: ScalarKind.F16,
|
|
88
|
+
np.int8: ScalarKind.I8,
|
|
89
|
+
np.uint8: ScalarKind.B1,
|
|
74
90
|
}
|
|
75
91
|
return _normalize[dtype]
|
|
76
92
|
|
|
@@ -80,13 +96,15 @@ def _to_numpy_dtype(dtype: ScalarKind):
|
|
|
80
96
|
ScalarKind.F64: np.float64,
|
|
81
97
|
ScalarKind.F32: np.float32,
|
|
82
98
|
ScalarKind.F16: np.float16,
|
|
83
|
-
ScalarKind.I8: np.
|
|
99
|
+
ScalarKind.I8: np.int8,
|
|
84
100
|
ScalarKind.B1: np.uint8,
|
|
85
101
|
}
|
|
102
|
+
if dtype in _normalize.values():
|
|
103
|
+
return dtype
|
|
86
104
|
return _normalize[dtype]
|
|
87
105
|
|
|
88
106
|
|
|
89
|
-
def _normalize_metric(metric):
|
|
107
|
+
def _normalize_metric(metric) -> MetricKind:
|
|
90
108
|
if metric is None:
|
|
91
109
|
return MetricKind.Cos
|
|
92
110
|
|
|
@@ -111,14 +129,12 @@ def _normalize_metric(metric):
|
|
|
111
129
|
|
|
112
130
|
|
|
113
131
|
def _search_in_compiled(
|
|
114
|
-
|
|
115
|
-
compiled: Union[_CompiledIndex, _CompiledIndexes],
|
|
132
|
+
compiled_callable: Callable,
|
|
116
133
|
vectors: np.ndarray,
|
|
117
|
-
|
|
118
|
-
threads: int,
|
|
119
|
-
exact: bool,
|
|
134
|
+
*,
|
|
120
135
|
log: Union[str, bool],
|
|
121
136
|
batch_size: int,
|
|
137
|
+
**kwargs,
|
|
122
138
|
) -> Union[Matches, BatchMatches]:
|
|
123
139
|
#
|
|
124
140
|
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
@@ -150,12 +166,7 @@ def _search_in_compiled(
|
|
|
150
166
|
disable=log is False,
|
|
151
167
|
)
|
|
152
168
|
for vectors in tasks:
|
|
153
|
-
tuple_ =
|
|
154
|
-
vectors,
|
|
155
|
-
k,
|
|
156
|
-
exact=exact,
|
|
157
|
-
threads=threads,
|
|
158
|
-
)
|
|
169
|
+
tuple_ = compiled_callable(vectors, **kwargs)
|
|
159
170
|
tasks_matches.append(BatchMatches(*tuple_))
|
|
160
171
|
pbar.update(vectors.shape[0])
|
|
161
172
|
|
|
@@ -165,22 +176,19 @@ def _search_in_compiled(
|
|
|
165
176
|
keys=np.vstack([m.keys for m in tasks_matches]),
|
|
166
177
|
distances=np.vstack([m.distances for m in tasks_matches]),
|
|
167
178
|
counts=np.concatenate([m.counts for m in tasks_matches], axis=None),
|
|
179
|
+
visited_members=sum([m.visited_members for m in tasks_matches]),
|
|
180
|
+
computed_distances=sum([m.computed_distances for m in tasks_matches]),
|
|
168
181
|
)
|
|
169
182
|
)
|
|
170
183
|
|
|
171
184
|
else:
|
|
172
|
-
tuple_ =
|
|
173
|
-
vectors,
|
|
174
|
-
k,
|
|
175
|
-
exact=exact,
|
|
176
|
-
threads=threads,
|
|
177
|
-
)
|
|
185
|
+
tuple_ = compiled_callable(vectors, **kwargs)
|
|
178
186
|
return distil_batch(BatchMatches(*tuple_))
|
|
179
187
|
|
|
180
188
|
|
|
181
189
|
def _add_to_compiled(
|
|
182
|
-
*,
|
|
183
190
|
compiled,
|
|
191
|
+
*,
|
|
184
192
|
keys,
|
|
185
193
|
vectors,
|
|
186
194
|
copy: bool,
|
|
@@ -231,13 +239,13 @@ def _add_to_compiled(
|
|
|
231
239
|
disable=log is False,
|
|
232
240
|
)
|
|
233
241
|
for keys, vectors in tasks:
|
|
234
|
-
compiled.
|
|
242
|
+
compiled.add_many(keys, vectors, copy=copy, threads=threads)
|
|
235
243
|
pbar.update(len(keys))
|
|
236
244
|
|
|
237
245
|
pbar.close()
|
|
238
246
|
|
|
239
247
|
else:
|
|
240
|
-
compiled.
|
|
248
|
+
compiled.add_many(keys, vectors, copy=copy, threads=threads)
|
|
241
249
|
|
|
242
250
|
return keys
|
|
243
251
|
|
|
@@ -249,6 +257,9 @@ class Match:
|
|
|
249
257
|
key: int
|
|
250
258
|
distance: float
|
|
251
259
|
|
|
260
|
+
def to_tuple(self) -> tuple:
|
|
261
|
+
return self.key, self.distance
|
|
262
|
+
|
|
252
263
|
|
|
253
264
|
@dataclass
|
|
254
265
|
class Matches:
|
|
@@ -258,8 +269,8 @@ class Matches:
|
|
|
258
269
|
keys: np.ndarray
|
|
259
270
|
distances: np.ndarray
|
|
260
271
|
|
|
261
|
-
visited_members: int
|
|
262
|
-
computed_distances: int
|
|
272
|
+
visited_members: int = 0
|
|
273
|
+
computed_distances: int = 0
|
|
263
274
|
|
|
264
275
|
def __len__(self) -> int:
|
|
265
276
|
return len(self.keys)
|
|
@@ -293,8 +304,8 @@ class BatchMatches:
|
|
|
293
304
|
distances: np.ndarray
|
|
294
305
|
counts: np.ndarray
|
|
295
306
|
|
|
296
|
-
visited_members: int
|
|
297
|
-
computed_distances: int
|
|
307
|
+
visited_members: int = 0
|
|
308
|
+
computed_distances: int = 0
|
|
298
309
|
|
|
299
310
|
def __len__(self) -> int:
|
|
300
311
|
return len(self.counts)
|
|
@@ -313,58 +324,140 @@ class BatchMatches:
|
|
|
313
324
|
def to_list(self) -> List[List[tuple]]:
|
|
314
325
|
"""Convert the result for each query to the list of tuples with information about its matches."""
|
|
315
326
|
list_of_matches = [self.__getitem__(row) for row in range(self.__len__())]
|
|
316
|
-
return [match.
|
|
327
|
+
return [match.to_tuple() for matches in list_of_matches for match in matches]
|
|
317
328
|
|
|
318
|
-
def mean_recall(self, expected: np.ndarray,
|
|
329
|
+
def mean_recall(self, expected: np.ndarray, count: Optional[int] = None) -> float:
|
|
319
330
|
"""Measures recall [0, 1] as of `Matches` that contain the corresponding
|
|
320
331
|
`expected` entry anywhere among results."""
|
|
321
|
-
return self.count_matches(expected,
|
|
332
|
+
return self.count_matches(expected, count=count) / len(expected)
|
|
322
333
|
|
|
323
|
-
def count_matches(self, expected: np.ndarray,
|
|
334
|
+
def count_matches(self, expected: np.ndarray, count: Optional[int] = None) -> int:
|
|
324
335
|
"""Measures recall [0, len(expected)] as of `Matches` that contain the corresponding
|
|
325
336
|
`expected` entry anywhere among results.
|
|
326
337
|
"""
|
|
327
338
|
assert len(expected) == len(self)
|
|
328
339
|
recall = 0
|
|
329
|
-
if
|
|
330
|
-
|
|
340
|
+
if count is None:
|
|
341
|
+
count = self.keys.shape[1]
|
|
331
342
|
|
|
332
|
-
if
|
|
343
|
+
if count == 1:
|
|
333
344
|
recall = np.sum(self.keys[:, 0] == expected)
|
|
334
345
|
else:
|
|
335
346
|
for i in range(len(self)):
|
|
336
|
-
recall += expected[i] in self.keys[i, :
|
|
347
|
+
recall += expected[i] in self.keys[i, :count]
|
|
337
348
|
return recall
|
|
338
349
|
|
|
339
350
|
def __repr__(self) -> str:
|
|
340
351
|
return f"usearch.BatchMatches({np.sum(self.counts)} across {len(self)} queries)"
|
|
341
352
|
|
|
342
353
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
354
|
+
@dataclass
|
|
355
|
+
class Clustering:
|
|
356
|
+
def __init__(
|
|
357
|
+
self,
|
|
358
|
+
index: Index,
|
|
359
|
+
matches: BatchMatches,
|
|
360
|
+
queries: Optional[np.ndarray] = None,
|
|
361
|
+
) -> None:
|
|
362
|
+
if queries is None:
|
|
363
|
+
queries = index._compiled.get_keys_in_slice()
|
|
364
|
+
self.index = index
|
|
365
|
+
self.queries = queries
|
|
366
|
+
self.matches = matches
|
|
367
|
+
|
|
368
|
+
def __repr__(self) -> str:
|
|
369
|
+
return f"usearch.Clustering(for {len(self.queries)} queries)"
|
|
370
|
+
|
|
371
|
+
@property
|
|
372
|
+
def centroids_popularity(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
373
|
+
return np.unique(self.matches.keys, return_counts=True)
|
|
374
|
+
|
|
375
|
+
def members_of(self, centroid: Key) -> np.ndarray:
|
|
376
|
+
return self.queries[self.matches.keys.flatten() == centroid]
|
|
377
|
+
|
|
378
|
+
def subcluster(self, centroid: Key, **clustering_kwards) -> Clustering:
|
|
379
|
+
sub_keys = self.members_of(centroid)
|
|
380
|
+
return self.index.cluster(keys=sub_keys, **clustering_kwards)
|
|
381
|
+
|
|
382
|
+
def plot_centroids_popularity(self):
|
|
383
|
+
from matplotlib import pyplot as plt
|
|
384
|
+
|
|
385
|
+
_, sizes = self.centroids_popularity
|
|
386
|
+
plt.yscale("log")
|
|
387
|
+
plt.plot(sorted(sizes), np.arange(len(sizes)))
|
|
388
|
+
plt.show()
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def network(self):
|
|
392
|
+
import networkx as nx
|
|
393
|
+
|
|
394
|
+
keys, sizes = self.centroids_popularity
|
|
395
|
+
|
|
396
|
+
g = nx.Graph()
|
|
397
|
+
for key, size in zip(keys, sizes):
|
|
398
|
+
g.add_node(key, size=size)
|
|
399
|
+
|
|
400
|
+
for i, i_key in enumerate(keys):
|
|
401
|
+
for j_key in keys[:i]:
|
|
402
|
+
d = self.index.pairwise_distance(i_key, j_key)
|
|
403
|
+
g.add_edge(i_key, j_key, distance=d)
|
|
404
|
+
|
|
405
|
+
return g
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class IndexedKeys:
|
|
409
|
+
"""Smart-reference for the range of keys present in a specific `Index`"""
|
|
410
|
+
|
|
411
|
+
def __init__(self, index: Index) -> None:
|
|
412
|
+
self.index = index
|
|
413
|
+
|
|
414
|
+
def __len__(self) -> int:
|
|
415
|
+
return len(self.index)
|
|
416
|
+
|
|
417
|
+
def __getitem__(
|
|
418
|
+
self,
|
|
419
|
+
offset_offsets_or_slice: Union[int, np.ndarray, slice],
|
|
420
|
+
) -> Union[Key, np.ndarray]:
|
|
421
|
+
if isinstance(offset_offsets_or_slice, slice):
|
|
422
|
+
start, stop, step = offset_offsets_or_slice.indices(len(self))
|
|
423
|
+
if step:
|
|
424
|
+
raise
|
|
425
|
+
return self.index._compiled.get_keys_in_slice(start, stop - start)
|
|
426
|
+
|
|
427
|
+
elif isinstance(offset_offsets_or_slice, Iterable):
|
|
428
|
+
offsets = np.array(offset_offsets_or_slice)
|
|
429
|
+
return self.index._compiled.get_keys_at_offsets(offsets)
|
|
430
|
+
|
|
431
|
+
else:
|
|
432
|
+
offset = int(offset_offsets_or_slice)
|
|
433
|
+
return self.index._compiled.get_key_at_offset(offset)
|
|
434
|
+
|
|
435
|
+
def __array__(self, dtype=None) -> np.ndarray:
|
|
436
|
+
if dtype is None:
|
|
437
|
+
dtype = Key
|
|
438
|
+
return self.index._compiled.get_keys_in_slice().astype(dtype)
|
|
347
439
|
|
|
348
440
|
|
|
349
441
|
class Index:
|
|
350
|
-
"""Fast
|
|
442
|
+
"""Fast vector-search engine for dense equi-dimensional embeddings.
|
|
351
443
|
|
|
352
444
|
Vector keys must be integers.
|
|
353
445
|
Vectors must have the same number of dimensions within the index.
|
|
354
|
-
Supports Inner Product, Cosine Distance,
|
|
355
|
-
|
|
356
|
-
|
|
446
|
+
Supports Inner Product, Cosine Distance, L^n measures like the Euclidean metric,
|
|
447
|
+
as well as automatic downcasting to low-precision floating-point and integral
|
|
448
|
+
representations.
|
|
357
449
|
"""
|
|
358
450
|
|
|
359
451
|
def __init__(
|
|
360
452
|
self,
|
|
361
453
|
*,
|
|
362
454
|
ndim: int = 0,
|
|
363
|
-
metric:
|
|
364
|
-
dtype: Optional[
|
|
455
|
+
metric: MetricLike = MetricKind.Cos,
|
|
456
|
+
dtype: Optional[DTypeLike] = None,
|
|
365
457
|
connectivity: Optional[int] = None,
|
|
366
458
|
expansion_add: Optional[int] = None,
|
|
367
459
|
expansion_search: Optional[int] = None,
|
|
460
|
+
multi: bool = False,
|
|
368
461
|
path: Optional[os.PathLike] = None,
|
|
369
462
|
view: bool = False,
|
|
370
463
|
) -> None:
|
|
@@ -377,41 +470,43 @@ class Index:
|
|
|
377
470
|
coordinates. Angular (Cos) and Euclidean (L2sq), obviously, apply to
|
|
378
471
|
vectors with arbitrary number of dimensions.
|
|
379
472
|
|
|
380
|
-
:param metric: Distance function
|
|
381
|
-
:type metric:
|
|
473
|
+
:param metric: Distance function
|
|
474
|
+
:type metric: MetricLike, defaults to MetricKind.Cos
|
|
382
475
|
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
383
476
|
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
384
477
|
Hamming, Tanimoto, Sorensen.
|
|
385
478
|
|
|
386
|
-
:param dtype: Scalar type for internal vector storage
|
|
387
|
-
:type dtype: Optional[
|
|
479
|
+
:param dtype: Scalar type for internal vector storage
|
|
480
|
+
:type dtype: Optional[DTypeLike], defaults to None
|
|
388
481
|
For continuous metrics can be: f16, f32, f64, or i8.
|
|
389
482
|
For bitwise metrics it's implementation-defined, and can't change.
|
|
390
483
|
Example: you can use the `f16` index with `f32` vectors in Euclidean space,
|
|
391
484
|
which will be automatically downcasted.
|
|
392
485
|
|
|
393
|
-
:param connectivity: Connections per node in HNSW
|
|
394
|
-
:type connectivity: Optional[int],
|
|
486
|
+
:param connectivity: Connections per node in HNSW
|
|
487
|
+
:type connectivity: Optional[int], defaults to None
|
|
395
488
|
Hyper-parameter for the number of Graph connections
|
|
396
489
|
per layer of HNSW. The original paper calls it "M".
|
|
397
490
|
Optional, but can't be changed after construction.
|
|
398
491
|
|
|
399
|
-
:param expansion_add: Traversal depth on insertions
|
|
400
|
-
:type expansion_add: Optional[int],
|
|
492
|
+
:param expansion_add: Traversal depth on insertions
|
|
493
|
+
:type expansion_add: Optional[int], defaults to None
|
|
401
494
|
Hyper-parameter for the search depth when inserting new
|
|
402
495
|
vectors. The original paper calls it "efConstruction".
|
|
403
496
|
Can be changed afterwards, as the `.expansion_add`.
|
|
404
497
|
|
|
405
|
-
:param expansion_search: Traversal depth on queries
|
|
406
|
-
:type expansion_search: Optional[int],
|
|
498
|
+
:param expansion_search: Traversal depth on queries
|
|
499
|
+
:type expansion_search: Optional[int], defaults to None
|
|
407
500
|
Hyper-parameter for the search depth when querying
|
|
408
501
|
nearest neighbors. The original paper calls it "ef".
|
|
409
502
|
Can be changed afterwards, as the `.expansion_search`.
|
|
410
503
|
|
|
411
|
-
:param
|
|
412
|
-
:type
|
|
413
|
-
:param
|
|
414
|
-
:type
|
|
504
|
+
:param multi: Allow multiple vectors with the same key
|
|
505
|
+
:type multi: bool, defaults to True
|
|
506
|
+
:param path: Where to store the index
|
|
507
|
+
:type path: Optional[os.PathLike], defaults to None
|
|
508
|
+
:param view: Are we simply viewing an immutable index
|
|
509
|
+
:type view: bool, defaults to False
|
|
415
510
|
"""
|
|
416
511
|
|
|
417
512
|
if connectivity is None:
|
|
@@ -449,6 +544,7 @@ class Index:
|
|
|
449
544
|
connectivity=connectivity,
|
|
450
545
|
expansion_add=expansion_add,
|
|
451
546
|
expansion_search=expansion_search,
|
|
547
|
+
multi=multi,
|
|
452
548
|
metric_kind=self._metric_kind,
|
|
453
549
|
metric_pointer=self._metric_pointer,
|
|
454
550
|
metric_signature=self._metric_signature,
|
|
@@ -456,6 +552,7 @@ class Index:
|
|
|
456
552
|
|
|
457
553
|
self.path = path
|
|
458
554
|
if path and os.path.exists(path):
|
|
555
|
+
path = os.fspath(path)
|
|
459
556
|
if view:
|
|
460
557
|
self._compiled.view(path)
|
|
461
558
|
else:
|
|
@@ -463,6 +560,7 @@ class Index:
|
|
|
463
560
|
|
|
464
561
|
@staticmethod
|
|
465
562
|
def metadata(path: os.PathLike) -> Optional[dict]:
|
|
563
|
+
path = os.fspath(path)
|
|
466
564
|
if not os.path.exists(path):
|
|
467
565
|
return None
|
|
468
566
|
try:
|
|
@@ -472,6 +570,7 @@ class Index:
|
|
|
472
570
|
|
|
473
571
|
@staticmethod
|
|
474
572
|
def restore(path: os.PathLike, view: bool = False) -> Optional[Index]:
|
|
573
|
+
path = os.fspath(path)
|
|
475
574
|
meta = Index.metadata(path)
|
|
476
575
|
if not meta:
|
|
477
576
|
return None
|
|
@@ -483,10 +582,13 @@ class Index:
|
|
|
483
582
|
view=view,
|
|
484
583
|
)
|
|
485
584
|
|
|
585
|
+
def __len__(self) -> int:
|
|
586
|
+
return self._compiled.__len__()
|
|
587
|
+
|
|
486
588
|
def add(
|
|
487
589
|
self,
|
|
488
|
-
keys,
|
|
489
|
-
vectors,
|
|
590
|
+
keys: KeyOrKeysLike,
|
|
591
|
+
vectors: VectorOrVectorsLike,
|
|
490
592
|
*,
|
|
491
593
|
copy: bool = True,
|
|
492
594
|
threads: int = 0,
|
|
@@ -507,23 +609,23 @@ class Index:
|
|
|
507
609
|
pass `copy=False`, if you can guarantee the lifetime of the
|
|
508
610
|
primary vectors store during the process of construction.
|
|
509
611
|
|
|
510
|
-
:param keys: Unique identifier(s) for passed vectors
|
|
511
|
-
:type keys:
|
|
612
|
+
:param keys: Unique identifier(s) for passed vectors
|
|
613
|
+
:type keys: Optional[KeyOrKeysLike], can be `None`
|
|
512
614
|
:param vectors: Vector or a row-major matrix
|
|
513
|
-
:type vectors:
|
|
514
|
-
:param copy: Should the index store a copy of vectors
|
|
515
|
-
:type copy: bool,
|
|
516
|
-
:param threads: Optimal number of cores to use
|
|
517
|
-
:type threads: int,
|
|
518
|
-
:param log: Whether to print the progress bar
|
|
519
|
-
:type log: Union[str, bool],
|
|
520
|
-
:param batch_size: Number of vectors to process at once
|
|
521
|
-
:type batch_size: int,
|
|
615
|
+
:type vectors: VectorOrVectorsLike
|
|
616
|
+
:param copy: Should the index store a copy of vectors
|
|
617
|
+
:type copy: bool, defaults to True
|
|
618
|
+
:param threads: Optimal number of cores to use
|
|
619
|
+
:type threads: int, defaults to 0
|
|
620
|
+
:param log: Whether to print the progress bar
|
|
621
|
+
:type log: Union[str, bool], defaults to False
|
|
622
|
+
:param batch_size: Number of vectors to process at once
|
|
623
|
+
:type batch_size: int, defaults to 0
|
|
522
624
|
:return: Inserted key or keys
|
|
523
625
|
:type: Union[int, np.ndarray]
|
|
524
626
|
"""
|
|
525
627
|
return _add_to_compiled(
|
|
526
|
-
|
|
628
|
+
self._compiled,
|
|
527
629
|
keys=keys,
|
|
528
630
|
vectors=vectors,
|
|
529
631
|
copy=copy,
|
|
@@ -534,8 +636,9 @@ class Index:
|
|
|
534
636
|
|
|
535
637
|
def search(
|
|
536
638
|
self,
|
|
537
|
-
vectors,
|
|
538
|
-
|
|
639
|
+
vectors: VectorOrVectorsLike,
|
|
640
|
+
count: int = 10,
|
|
641
|
+
radius: float = math.inf,
|
|
539
642
|
*,
|
|
540
643
|
threads: int = 0,
|
|
541
644
|
exact: bool = False,
|
|
@@ -546,38 +649,113 @@ class Index:
|
|
|
546
649
|
Performs approximate nearest neighbors search for one or more queries.
|
|
547
650
|
|
|
548
651
|
:param vectors: Query vector or vectors.
|
|
549
|
-
:type vectors:
|
|
550
|
-
:param
|
|
551
|
-
:type
|
|
552
|
-
:param threads: Optimal number of cores to use
|
|
553
|
-
:type threads: int,
|
|
554
|
-
:param exact: Perform exhaustive linear-time exact search
|
|
555
|
-
:type exact: bool,
|
|
652
|
+
:type vectors: VectorOrVectorsLike
|
|
653
|
+
:param count: Upper count on the number of matches to find
|
|
654
|
+
:type count: int, defaults to 10
|
|
655
|
+
:param threads: Optimal number of cores to use
|
|
656
|
+
:type threads: int, defaults to 0
|
|
657
|
+
:param exact: Perform exhaustive linear-time exact search
|
|
658
|
+
:type exact: bool, defaults to False
|
|
556
659
|
:param log: Whether to print the progress bar, default to False
|
|
557
660
|
:type log: Union[str, bool], optional
|
|
558
|
-
:param batch_size: Number of vectors to process at once
|
|
559
|
-
:type batch_size: int,
|
|
661
|
+
:param batch_size: Number of vectors to process at once
|
|
662
|
+
:type batch_size: int, defaults to 0
|
|
560
663
|
:return: Matches for one or more queries
|
|
561
664
|
:rtype: Union[Matches, BatchMatches]
|
|
562
665
|
"""
|
|
563
666
|
|
|
564
667
|
return _search_in_compiled(
|
|
565
|
-
|
|
566
|
-
vectors
|
|
567
|
-
|
|
568
|
-
exact=exact,
|
|
569
|
-
threads=threads,
|
|
668
|
+
self._compiled.search_many,
|
|
669
|
+
vectors,
|
|
670
|
+
# Batch scheduling:
|
|
570
671
|
log=log,
|
|
571
672
|
batch_size=batch_size,
|
|
673
|
+
# Search constraints:
|
|
674
|
+
count=count,
|
|
675
|
+
exact=exact,
|
|
676
|
+
threads=threads,
|
|
572
677
|
)
|
|
573
678
|
|
|
679
|
+
def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
|
|
680
|
+
if isinstance(keys, Iterable):
|
|
681
|
+
return self._compiled.contains_many(np.array(keys, dtype=Key))
|
|
682
|
+
else:
|
|
683
|
+
return self._compiled.contains_one(int(keys))
|
|
684
|
+
|
|
685
|
+
def __contains__(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
|
|
686
|
+
return self.contains(keys)
|
|
687
|
+
|
|
688
|
+
def count(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
|
|
689
|
+
if isinstance(keys, Iterable):
|
|
690
|
+
return self._compiled.count_many(np.array(keys, dtype=Key))
|
|
691
|
+
else:
|
|
692
|
+
return self._compiled.count_one(int(keys))
|
|
693
|
+
|
|
694
|
+
def get(
|
|
695
|
+
self,
|
|
696
|
+
keys: KeyOrKeysLike,
|
|
697
|
+
dtype: Optional[DTypeLike] = None,
|
|
698
|
+
) -> Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]:
|
|
699
|
+
"""Looks up one or more keys from the `Index`, retrieving corresponding vectors.
|
|
700
|
+
|
|
701
|
+
Returns `None`, if one key is requested, and its not present.
|
|
702
|
+
Returns a (row) vector, if the key maps into a single vector.
|
|
703
|
+
Returns a (row-major) matrix, if the key maps into a multiple vectors.
|
|
704
|
+
If multiple keys are requested, composes many such responses into a `tuple`.
|
|
705
|
+
|
|
706
|
+
:param keys: One or more keys to lookup
|
|
707
|
+
:type keys: KeyOrKeysLike
|
|
708
|
+
:return: One or more keys lookup results
|
|
709
|
+
:rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
|
|
710
|
+
"""
|
|
711
|
+
if not dtype:
|
|
712
|
+
dtype = self.dtype
|
|
713
|
+
else:
|
|
714
|
+
dtype = _normalize_dtype(dtype)
|
|
715
|
+
|
|
716
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
717
|
+
|
|
718
|
+
def cast(result):
|
|
719
|
+
if result is not None:
|
|
720
|
+
return result.view(view_dtype)
|
|
721
|
+
return result
|
|
722
|
+
|
|
723
|
+
is_one = not isinstance(keys, Iterable)
|
|
724
|
+
if is_one:
|
|
725
|
+
keys = [keys]
|
|
726
|
+
if not isinstance(keys, np.ndarray):
|
|
727
|
+
keys = np.array(keys, dtype=Key)
|
|
728
|
+
else:
|
|
729
|
+
keys = keys.astype(Key)
|
|
730
|
+
|
|
731
|
+
results = self._compiled.get_many(keys, dtype)
|
|
732
|
+
results = [cast(result) for result in results]
|
|
733
|
+
return results[0] if is_one else results
|
|
734
|
+
|
|
735
|
+
def __getitem__(
|
|
736
|
+
self, keys: KeyOrKeysLike
|
|
737
|
+
) -> Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]:
|
|
738
|
+
"""Looks up one or more keys from the `Index`, retrieving corresponding vectors.
|
|
739
|
+
|
|
740
|
+
Returns `None`, if one key is requested, and its not present.
|
|
741
|
+
Returns a (row) vector, if the key maps into a single vector.
|
|
742
|
+
Returns a (row-major) matrix, if the key maps into a multiple vectors.
|
|
743
|
+
If multiple keys are requested, composes many such responses into a `tuple`.
|
|
744
|
+
|
|
745
|
+
:param keys: One or more keys to lookup
|
|
746
|
+
:type keys: KeyOrKeysLike
|
|
747
|
+
:return: One or more keys lookup results
|
|
748
|
+
:rtype: Union[Optional[np.ndarray], Tuple[Optional[np.ndarray]]]
|
|
749
|
+
"""
|
|
750
|
+
return self.get(keys)
|
|
751
|
+
|
|
574
752
|
def remove(
|
|
575
753
|
self,
|
|
576
|
-
keys:
|
|
754
|
+
keys: KeyOrKeysLike,
|
|
577
755
|
*,
|
|
578
756
|
compact: bool = False,
|
|
579
757
|
threads: int = 0,
|
|
580
|
-
) -> Union[
|
|
758
|
+
) -> Union[int, np.ndarray]:
|
|
581
759
|
"""Removes one or move vectors from the index.
|
|
582
760
|
|
|
583
761
|
When working with extremely large indexes, you may want to
|
|
@@ -585,57 +763,72 @@ class Index:
|
|
|
585
763
|
In other cases, rebuilding - is the recommended approach.
|
|
586
764
|
|
|
587
765
|
:param keys: Unique identifier for passed vectors, optional
|
|
588
|
-
:type keys:
|
|
766
|
+
:type keys: KeyOrKeysLike
|
|
589
767
|
:param compact: Removes links to removed nodes (expensive), defaults to False
|
|
590
768
|
:type compact: bool, optional
|
|
591
769
|
:param threads: Optimal number of cores to use, defaults to 0
|
|
592
770
|
:type threads: int, optional
|
|
593
|
-
:return:
|
|
594
|
-
:type: Union[
|
|
771
|
+
:return: Array of integers for the number of removed vectors per key
|
|
772
|
+
:type: Union[int, np.ndarray]
|
|
595
773
|
"""
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
@property
|
|
603
|
-
def specs(self) -> dict:
|
|
604
|
-
return {
|
|
605
|
-
"Class": "usearch.Index",
|
|
606
|
-
"Connectivity": self.connectivity,
|
|
607
|
-
"Size": self.size,
|
|
608
|
-
"Dimensions": self.ndim,
|
|
609
|
-
"Expansion@Add": self.expansion_add,
|
|
610
|
-
"Expansion@Search": self.expansion_search,
|
|
611
|
-
"OpenMP": USES_OPENMP,
|
|
612
|
-
"SimSIMD": USES_SIMSIMD,
|
|
613
|
-
"NativeF16": USES_NATIVE_F16,
|
|
614
|
-
"JIT": self.jit,
|
|
615
|
-
"DType": self.dtype,
|
|
616
|
-
"Path": self.path,
|
|
617
|
-
}
|
|
774
|
+
if not isinstance(keys, Iterable):
|
|
775
|
+
return self._compiled.remove_one(keys, compact=compact, threads=threads)
|
|
776
|
+
else:
|
|
777
|
+
keys = np.array(keys, dtype=Key)
|
|
778
|
+
return self._compiled.remove_many(keys, compact=compact, threads=threads)
|
|
618
779
|
|
|
619
|
-
def
|
|
620
|
-
|
|
780
|
+
def __delitem__(self, keys: KeyOrKeysLike) -> Union[int, np.ndarray]:
|
|
781
|
+
raise self.remove(keys)
|
|
621
782
|
|
|
622
|
-
def
|
|
623
|
-
|
|
783
|
+
def rename(
|
|
784
|
+
self,
|
|
785
|
+
from_: KeyOrKeysLike,
|
|
786
|
+
to: KeyOrKeysLike,
|
|
787
|
+
) -> Union[int, np.ndarray]:
|
|
788
|
+
"""Rename existing member vector or vectors.
|
|
789
|
+
|
|
790
|
+
May be used in iterative clustering procedures, where one would iteratively
|
|
791
|
+
relabel every vector with the name of the cluster an entry belongs to, until
|
|
792
|
+
the system converges.
|
|
793
|
+
|
|
794
|
+
:param from_: One or more keys to be renamed
|
|
795
|
+
:type from_: KeyOrKeysLike
|
|
796
|
+
:param to: New name or names (of identical length as `from_`)
|
|
797
|
+
:type to: KeyOrKeysLike
|
|
798
|
+
:return: Number of vectors that were found and renamed
|
|
799
|
+
:rtype: int
|
|
800
|
+
"""
|
|
801
|
+
if isinstance(from_, Iterable):
|
|
802
|
+
from_ = np.array(from_, dtype=Key)
|
|
803
|
+
if isinstance(to, Iterable):
|
|
804
|
+
to = np.array(to, dtype=Key)
|
|
805
|
+
return self._compiled.rename_many_to_many(from_, to)
|
|
624
806
|
|
|
625
|
-
|
|
626
|
-
|
|
807
|
+
else:
|
|
808
|
+
return self._compiled.rename_many_to_one(from_, int(to))
|
|
627
809
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
631
|
-
vector = self._compiled.__getitem__(key, get_dtype)
|
|
632
|
-
view_dtype = _to_numpy_dtype(dtype)
|
|
633
|
-
return None if vector is None else vector.view(view_dtype)
|
|
810
|
+
else:
|
|
811
|
+
return self._compiled.rename_one_to_one(int(from_), int(to))
|
|
634
812
|
|
|
635
813
|
@property
|
|
636
814
|
def jit(self) -> bool:
|
|
815
|
+
"""
|
|
816
|
+
:return: True, if the provided `metric` was JIT-ed
|
|
817
|
+
:rtype: bool
|
|
818
|
+
"""
|
|
637
819
|
return self._metric_jit is not None
|
|
638
820
|
|
|
821
|
+
@property
|
|
822
|
+
def hardware_acceleration(self) -> str:
|
|
823
|
+
"""Describes the kind of hardware-acceleration support used in
|
|
824
|
+
that exact instance of the `Index`, for that metric kind, and
|
|
825
|
+
the given number of dimensions.
|
|
826
|
+
|
|
827
|
+
:return: "auto", if nothing is available, ISA subset name otherwise
|
|
828
|
+
:rtype: str
|
|
829
|
+
"""
|
|
830
|
+
return self._compiled.hardware_acceleration
|
|
831
|
+
|
|
639
832
|
@property
|
|
640
833
|
def size(self) -> int:
|
|
641
834
|
return self._compiled.size
|
|
@@ -649,7 +842,7 @@ class Index:
|
|
|
649
842
|
return self._metric_jit if self._metric_jit else self._metric_kind
|
|
650
843
|
|
|
651
844
|
@metric.setter
|
|
652
|
-
def metric(self, metric:
|
|
845
|
+
def metric(self, metric: MetricLike):
|
|
653
846
|
metric = _normalize_metric(metric)
|
|
654
847
|
if isinstance(metric, MetricKind):
|
|
655
848
|
metric_kind = metric
|
|
@@ -728,6 +921,9 @@ class Index:
|
|
|
728
921
|
"""Erases all members from index, closing files, and returning RAM to OS."""
|
|
729
922
|
self._compiled.reset()
|
|
730
923
|
|
|
924
|
+
def __del__(self):
|
|
925
|
+
self.reset()
|
|
926
|
+
|
|
731
927
|
def copy(self) -> Index:
|
|
732
928
|
result = Index(
|
|
733
929
|
ndim=self.ndim,
|
|
@@ -741,7 +937,12 @@ class Index:
|
|
|
741
937
|
result._compiled = self._compiled.copy()
|
|
742
938
|
return result
|
|
743
939
|
|
|
744
|
-
def join(
|
|
940
|
+
def join(
|
|
941
|
+
self,
|
|
942
|
+
other: Index,
|
|
943
|
+
max_proposals: int = 0,
|
|
944
|
+
exact: bool = False,
|
|
945
|
+
) -> Dict[Key, Key]:
|
|
745
946
|
"""Performs "Semantic Join" or pairwise matching between `self` & `other` index.
|
|
746
947
|
Is different from `search`, as no collisions are allowed in resulting pairs.
|
|
747
948
|
Uses the concept of "Stable Marriages" from Combinatorics, famous for the 2012
|
|
@@ -754,7 +955,7 @@ class Index:
|
|
|
754
955
|
:param exact: Controls if underlying `search` should be exact, defaults to False
|
|
755
956
|
:type exact: bool, optional
|
|
756
957
|
:return: Mapping from keys of `self` to keys of `other`
|
|
757
|
-
:rtype:
|
|
958
|
+
:rtype: Dict[Key, Key]
|
|
758
959
|
"""
|
|
759
960
|
return self._compiled.join(
|
|
760
961
|
other=other._compiled,
|
|
@@ -762,44 +963,91 @@ class Index:
|
|
|
762
963
|
exact=exact,
|
|
763
964
|
)
|
|
764
965
|
|
|
765
|
-
def
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
966
|
+
def cluster(
|
|
967
|
+
self,
|
|
968
|
+
*,
|
|
969
|
+
vectors: Optional[np.ndarray] = None,
|
|
970
|
+
keys: Optional[np.ndarray] = None,
|
|
971
|
+
min_count: Optional[int] = None,
|
|
972
|
+
max_count: Optional[int] = None,
|
|
973
|
+
threads: int = 0,
|
|
974
|
+
log: Union[str, bool] = False,
|
|
975
|
+
batch_size: int = 0,
|
|
976
|
+
) -> Clustering:
|
|
977
|
+
"""
|
|
978
|
+
Clusters already indexed or provided `vectors`, mapping them to various centroids.
|
|
769
979
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
980
|
+
:param vectors: .
|
|
981
|
+
:type vectors: Optional[VectorOrVectorsLike]
|
|
982
|
+
:param count: Upper bound on the number of clusters to produce
|
|
983
|
+
:type count: Optional[int], defaults to None
|
|
773
984
|
|
|
774
|
-
:
|
|
775
|
-
:
|
|
985
|
+
:param threads: Optimal number of cores to use,
|
|
986
|
+
:type threads: int, defaults to 0
|
|
987
|
+
:param log: Whether to print the progress bar
|
|
988
|
+
:type log: Union[str, bool], defaults to False
|
|
989
|
+
:param batch_size: Number of vectors to process at once, defaults to 0
|
|
990
|
+
:type batch_size: int, defaults to 0
|
|
991
|
+
:return: Matches for one or more queries
|
|
992
|
+
:rtype: Union[Matches, BatchMatches]
|
|
776
993
|
"""
|
|
777
|
-
|
|
994
|
+
if min_count is None:
|
|
995
|
+
min_count = 0
|
|
996
|
+
if max_count is None:
|
|
997
|
+
max_count = 0
|
|
998
|
+
|
|
999
|
+
if vectors is not None:
|
|
1000
|
+
assert keys is None, "You can either cluster vectors or member keys"
|
|
1001
|
+
results = self._compiled.cluster_vectors(
|
|
1002
|
+
vectors,
|
|
1003
|
+
min_count=min_count,
|
|
1004
|
+
max_count=max_count,
|
|
1005
|
+
threads=threads,
|
|
1006
|
+
)
|
|
1007
|
+
else:
|
|
1008
|
+
if keys is None:
|
|
1009
|
+
keys = self._compiled.get_keys_in_slice()
|
|
1010
|
+
if not isinstance(keys, np.ndarray):
|
|
1011
|
+
keys = np.array(keys)
|
|
1012
|
+
keys = keys.astype(Key)
|
|
1013
|
+
results = self._compiled.cluster_keys(
|
|
1014
|
+
keys,
|
|
1015
|
+
min_count=min_count,
|
|
1016
|
+
max_count=max_count,
|
|
1017
|
+
threads=threads,
|
|
1018
|
+
)
|
|
778
1019
|
|
|
779
|
-
|
|
780
|
-
self,
|
|
781
|
-
keys: np.ndarray,
|
|
782
|
-
dtype: ScalarKind = ScalarKind.F32,
|
|
783
|
-
) -> np.ndarray:
|
|
784
|
-
"""Retrieves vectors associated with given `keys`
|
|
1020
|
+
batch_matches = BatchMatches(*results)
|
|
1021
|
+
return Clustering(self, batch_matches, keys)
|
|
785
1022
|
|
|
786
|
-
|
|
787
|
-
:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1023
|
+
def pairwise_distance(
|
|
1024
|
+
self, left: KeyOrKeysLike, right: KeyOrKeysLike
|
|
1025
|
+
) -> Union[np.ndarray, float]:
|
|
1026
|
+
assert isinstance(left, Iterable) == isinstance(right, Iterable)
|
|
1027
|
+
|
|
1028
|
+
if not isinstance(left, Iterable):
|
|
1029
|
+
return self._compiled.pairwise_distance(int(left), int(right))
|
|
1030
|
+
else:
|
|
1031
|
+
left = np.array(left).astype(Key)
|
|
1032
|
+
right = np.array(right).astype(Key)
|
|
1033
|
+
return self._compiled.pairwise_distances(left, right)
|
|
1034
|
+
|
|
1035
|
+
@property
|
|
1036
|
+
def keys(self) -> IndexedKeys:
|
|
1037
|
+
return IndexedKeys(self)
|
|
794
1038
|
|
|
795
1039
|
@property
|
|
796
1040
|
def vectors(self) -> np.ndarray:
|
|
797
|
-
return self.
|
|
1041
|
+
return self.get(self.keys, vstack=True)
|
|
798
1042
|
|
|
799
1043
|
@property
|
|
800
1044
|
def max_level(self) -> int:
|
|
801
1045
|
return self._compiled.max_level
|
|
802
1046
|
|
|
1047
|
+
@property
|
|
1048
|
+
def nlevels(self) -> int:
|
|
1049
|
+
return self._compiled.max_level + 1
|
|
1050
|
+
|
|
803
1051
|
@property
|
|
804
1052
|
def levels_stats(self) -> _CompiledIndexStats:
|
|
805
1053
|
"""Get the accumulated statistics for the entire multi-level graph.
|
|
@@ -829,37 +1077,63 @@ class Index:
|
|
|
829
1077
|
"""
|
|
830
1078
|
return self._compiled.level_stats(level)
|
|
831
1079
|
|
|
1080
|
+
@property
|
|
1081
|
+
def specs(self) -> Dict[str, Union[str, int, bool]]:
|
|
1082
|
+
return {
|
|
1083
|
+
"Class": "usearch.Index",
|
|
1084
|
+
"Connectivity": self.connectivity,
|
|
1085
|
+
"Size": self.size,
|
|
1086
|
+
"Dimensions": self.ndim,
|
|
1087
|
+
"Expansion@Add": self.expansion_add,
|
|
1088
|
+
"Expansion@Search": self.expansion_search,
|
|
1089
|
+
"OpenMP": USES_OPENMP,
|
|
1090
|
+
"SimSIMD": USES_SIMSIMD,
|
|
1091
|
+
"NativeF16": USES_NATIVE_F16,
|
|
1092
|
+
"JIT": self.jit,
|
|
1093
|
+
"DType": self.dtype,
|
|
1094
|
+
"Path": self.path,
|
|
1095
|
+
}
|
|
1096
|
+
|
|
832
1097
|
def __repr__(self) -> str:
|
|
833
|
-
f = "usearch.Index({} x {}, {}, expansion: {} & {}, {} vectors in {} levels)"
|
|
1098
|
+
f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {} vectors in {} levels)"
|
|
834
1099
|
return f.format(
|
|
835
1100
|
self.dtype,
|
|
836
1101
|
self.ndim,
|
|
837
1102
|
self.metric,
|
|
1103
|
+
self.connectivity,
|
|
838
1104
|
self.expansion_add,
|
|
839
1105
|
self.expansion_search,
|
|
840
1106
|
len(self),
|
|
841
|
-
self.
|
|
1107
|
+
self.nlevels,
|
|
842
1108
|
)
|
|
843
1109
|
|
|
844
|
-
def _repr_pretty_(self) -> str:
|
|
1110
|
+
def _repr_pretty_(self, printer, cycle) -> str:
|
|
845
1111
|
level_stats = [
|
|
846
|
-
f"--- {i}. {self.level_stats(i).nodes} nodes" for i in range(self.
|
|
1112
|
+
f"--- {i}. {self.level_stats(i).nodes:,} nodes" for i in range(self.nlevels)
|
|
847
1113
|
]
|
|
848
|
-
|
|
1114
|
+
lines = "\n".join(
|
|
849
1115
|
[
|
|
850
1116
|
"usearch.Index",
|
|
851
|
-
"- config"
|
|
1117
|
+
"- config",
|
|
1118
|
+
f"-- data type: {self.dtype}",
|
|
852
1119
|
f"-- dimensions: {self.ndim}",
|
|
853
1120
|
f"-- metric: {self.metric}",
|
|
1121
|
+
f"-- connectivity: {self.connectivity}",
|
|
854
1122
|
f"-- expansion on addition:{self.expansion_add} candidates",
|
|
855
1123
|
f"-- expansion on search: {self.expansion_search} candidates",
|
|
1124
|
+
"- binary",
|
|
1125
|
+
f"-- uses OpenMP: {USES_OPENMP}",
|
|
1126
|
+
f"-- uses SimSIMD: {USES_SIMSIMD}",
|
|
1127
|
+
f"-- supports half-precision: {USES_NATIVE_F16}",
|
|
1128
|
+
f"-- uses hardware acceletion: {self.hardware_acceleration}",
|
|
856
1129
|
"- state",
|
|
857
|
-
f"-- size: {self.size} vectors",
|
|
858
|
-
f"-- memory usage: {self.memory_usage} bytes",
|
|
1130
|
+
f"-- size: {self.size:,} vectors",
|
|
1131
|
+
f"-- memory usage: {self.memory_usage:,} bytes",
|
|
859
1132
|
f"-- max level: {self.max_level}",
|
|
860
1133
|
*level_stats,
|
|
861
1134
|
]
|
|
862
1135
|
)
|
|
1136
|
+
printer.text(lines)
|
|
863
1137
|
|
|
864
1138
|
|
|
865
1139
|
class Indexes:
|
|
@@ -872,11 +1146,14 @@ class Indexes:
|
|
|
872
1146
|
) -> None:
|
|
873
1147
|
self._compiled = _CompiledIndexes()
|
|
874
1148
|
for index in indexes:
|
|
875
|
-
self._compiled.
|
|
876
|
-
self._compiled.
|
|
1149
|
+
self._compiled.merge(index._compiled)
|
|
1150
|
+
self._compiled.merge_paths(paths, view=view, threads=threads)
|
|
1151
|
+
|
|
1152
|
+
def merge(self, index: Index):
|
|
1153
|
+
self._compiled.merge(index._compiled)
|
|
877
1154
|
|
|
878
|
-
def
|
|
879
|
-
self._compiled.
|
|
1155
|
+
def merge_path(self, path: os.PathLike):
|
|
1156
|
+
self._compiled.merge_path(os.fspath(path))
|
|
880
1157
|
|
|
881
1158
|
def __len__(self) -> int:
|
|
882
1159
|
return self._compiled.__len__()
|
|
@@ -884,27 +1161,29 @@ class Indexes:
|
|
|
884
1161
|
def search(
|
|
885
1162
|
self,
|
|
886
1163
|
vectors,
|
|
887
|
-
|
|
1164
|
+
count: int = 10,
|
|
888
1165
|
*,
|
|
889
1166
|
threads: int = 0,
|
|
890
1167
|
exact: bool = False,
|
|
891
1168
|
):
|
|
892
1169
|
return _search_in_compiled(
|
|
893
|
-
|
|
894
|
-
vectors
|
|
895
|
-
|
|
896
|
-
exact=exact,
|
|
897
|
-
threads=threads,
|
|
1170
|
+
self._compiled.search_many,
|
|
1171
|
+
vectors,
|
|
1172
|
+
# Batch scheduling:
|
|
898
1173
|
log=False,
|
|
899
1174
|
batch_size=None,
|
|
1175
|
+
# Search constraints:
|
|
1176
|
+
count=count,
|
|
1177
|
+
exact=exact,
|
|
1178
|
+
threads=threads,
|
|
900
1179
|
)
|
|
901
1180
|
|
|
902
1181
|
|
|
903
1182
|
def search(
|
|
904
1183
|
dataset: np.ndarray,
|
|
905
1184
|
query: np.ndarray,
|
|
906
|
-
|
|
907
|
-
metric:
|
|
1185
|
+
count: int = 10,
|
|
1186
|
+
metric: MetricLike = MetricKind.Cos,
|
|
908
1187
|
*,
|
|
909
1188
|
exact: bool = False,
|
|
910
1189
|
threads: int = 0,
|
|
@@ -919,11 +1198,11 @@ def search(
|
|
|
919
1198
|
:param query: Query vector or vectors (also row-major), to find in `dataset`.
|
|
920
1199
|
:type query: np.ndarray
|
|
921
1200
|
|
|
922
|
-
:param
|
|
923
|
-
:type
|
|
1201
|
+
:param count: Upper count on the number of matches to find, defaults to 10
|
|
1202
|
+
:type count: int, optional
|
|
924
1203
|
|
|
925
|
-
:param metric: Distance function
|
|
926
|
-
:type metric:
|
|
1204
|
+
:param metric: Distance function
|
|
1205
|
+
:type metric: MetricLike, defaults to MetricKind.Cos
|
|
927
1206
|
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
928
1207
|
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
929
1208
|
Hamming, Tanimoto, Sorensen.
|
|
@@ -957,7 +1236,7 @@ def search(
|
|
|
957
1236
|
)
|
|
958
1237
|
return index.search(
|
|
959
1238
|
query,
|
|
960
|
-
|
|
1239
|
+
count,
|
|
961
1240
|
threads=threads,
|
|
962
1241
|
log=log,
|
|
963
1242
|
batch_size=batch_size,
|
|
@@ -975,31 +1254,27 @@ def search(
|
|
|
975
1254
|
else:
|
|
976
1255
|
raise ValueError("The `metric` must be a `CompiledMetric` or a `MetricKind`")
|
|
977
1256
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
def search(self, query, k, **kwargs):
|
|
983
|
-
kwargs.pop("exact")
|
|
984
|
-
kwargs.update(
|
|
985
|
-
dict(
|
|
986
|
-
metric_kind=metric_kind,
|
|
987
|
-
metric_pointer=metric_pointer,
|
|
988
|
-
metric_signature=metric_signature,
|
|
989
|
-
)
|
|
990
|
-
)
|
|
991
|
-
assert dataset.shape[1] == query.shape[1], "Number of dimensions differs"
|
|
992
|
-
if dataset.dtype != query.dtype:
|
|
993
|
-
query = query.astype(dataset.dtype)
|
|
1257
|
+
def search_batch(query, **kwargs):
|
|
1258
|
+
assert dataset.shape[1] == query.shape[1], "Number of dimensions differs"
|
|
1259
|
+
if dataset.dtype != query.dtype:
|
|
1260
|
+
query = query.astype(dataset.dtype)
|
|
994
1261
|
|
|
995
|
-
|
|
1262
|
+
return _exact_search(
|
|
1263
|
+
dataset,
|
|
1264
|
+
query,
|
|
1265
|
+
metric_kind=metric_kind,
|
|
1266
|
+
metric_pointer=metric_pointer,
|
|
1267
|
+
metric_signature=metric_signature,
|
|
1268
|
+
**kwargs,
|
|
1269
|
+
)
|
|
996
1270
|
|
|
997
1271
|
return _search_in_compiled(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
threads=threads,
|
|
1002
|
-
exact=True,
|
|
1272
|
+
search_batch,
|
|
1273
|
+
query,
|
|
1274
|
+
# Batch scheduling:
|
|
1003
1275
|
log=log,
|
|
1004
1276
|
batch_size=batch_size,
|
|
1277
|
+
# Search constraints:
|
|
1278
|
+
count=count,
|
|
1279
|
+
threads=threads,
|
|
1005
1280
|
)
|