usearch 0.18.8 → 0.19.1
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/Cargo.toml +1 -1
- package/README.md +187 -40
- package/c/lib.cpp +71 -51
- package/c/test.c +182 -0
- package/c/usearch.h +11 -5
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +6 -6
- package/cpp/test.cpp +11 -9
- package/docs/compilation.md +1 -0
- package/include/usearch/index.hpp +290 -54
- package/include/usearch/index_punned_dense.hpp +125 -108
- package/include/usearch/index_punned_helpers.hpp +2 -12
- package/objc/USearchObjective.mm +4 -0
- package/objc/include/USearchObjective.h +5 -0
- package/package.json +2 -2
- package/python/lib.cpp +234 -199
- package/python/scripts/bench.ipynb +13 -2113
- package/python/scripts/bench.py +45 -29
- package/python/scripts/index_faiss.py +19 -19
- package/python/scripts/test.py +320 -61
- package/python/usearch/eval.py +100 -49
- package/python/usearch/index.py +245 -129
- package/python/usearch/io.py +25 -25
- package/python/usearch/join.py +2 -4
- package/python/usearch/numba.py +22 -18
- package/python/usearch/server.py +38 -26
- package/setup.py +56 -59
- package/simsimd/.vscode/settings.json +2 -1
- package/simsimd/CMakeLists.txt +1 -1
- package/simsimd/README.md +14 -2
- package/simsimd/bench.cxx +44 -27
- package/simsimd/include/simsimd/simsimd.h +2 -1
- package/swift/Test.swift +13 -9
- package/simsimd/include/simsimd/simsimd.hpp +0 -443
package/python/usearch/index.py
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
# The purpose of this file is to provide Pythonic wrapper on top
|
|
2
4
|
# the native precompiled CPython module. It improves compatibility
|
|
3
5
|
# Python tooling, linters, and static analyzers. It also embeds JIT
|
|
4
6
|
# into the primary `Index` class, connecting USearch with Numba.
|
|
5
7
|
import os
|
|
6
|
-
from
|
|
7
|
-
from typing import Optional, Callable, Union, NamedTuple, List, Iterable
|
|
8
|
+
from typing import Optional, Union, NamedTuple, List, Iterable
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
11
|
+
from tqdm import tqdm
|
|
10
12
|
|
|
11
|
-
from usearch.compiled import Index as _CompiledIndex
|
|
13
|
+
from usearch.compiled import Index as _CompiledIndex, IndexMetadata
|
|
12
14
|
from usearch.compiled import SparseIndex as _CompiledSetsIndex
|
|
13
15
|
|
|
14
|
-
from usearch.compiled import MetricKind, ScalarKind
|
|
16
|
+
from usearch.compiled import MetricKind, ScalarKind, MetricSignature
|
|
15
17
|
from usearch.compiled import (
|
|
16
18
|
DEFAULT_CONNECTIVITY,
|
|
17
19
|
DEFAULT_EXPANSION_ADD,
|
|
18
20
|
DEFAULT_EXPANSION_SEARCH,
|
|
19
|
-
|
|
20
21
|
USES_OPENMP,
|
|
21
22
|
USES_SIMSIMD,
|
|
22
23
|
USES_NATIVE_F16,
|
|
@@ -33,32 +34,71 @@ SparseIndex = _CompiledSetsIndex
|
|
|
33
34
|
Label = np.uint32
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def _normalize_dtype(dtype) -> ScalarKind:
|
|
37
|
-
if dtype is None:
|
|
38
|
-
return ScalarKind.F32
|
|
37
|
+
def _normalize_dtype(dtype, metric: MetricKind = MetricKind.IP) -> ScalarKind:
|
|
38
|
+
if dtype is None or dtype == "":
|
|
39
|
+
return ScalarKind.B1 if metric in MetricKindBitwise else ScalarKind.F32
|
|
40
|
+
|
|
39
41
|
if isinstance(dtype, ScalarKind):
|
|
40
42
|
return dtype
|
|
41
43
|
|
|
42
44
|
if isinstance(dtype, str):
|
|
45
|
+
dtype = dtype.lower()
|
|
46
|
+
|
|
47
|
+
_normalize = {
|
|
48
|
+
"f64": ScalarKind.F64,
|
|
49
|
+
"f32": ScalarKind.F32,
|
|
50
|
+
"f16": ScalarKind.F16,
|
|
51
|
+
"f8": ScalarKind.F8,
|
|
52
|
+
"b1": ScalarKind.B1,
|
|
53
|
+
"b1x8": ScalarKind.B1,
|
|
54
|
+
np.float64: ScalarKind.F64,
|
|
55
|
+
np.float32: ScalarKind.F32,
|
|
56
|
+
np.float16: ScalarKind.F16,
|
|
57
|
+
np.int8: ScalarKind.F8,
|
|
58
|
+
}
|
|
59
|
+
return _normalize[dtype]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _to_numpy_compatible_dtype(dtype: ScalarKind) -> ScalarKind:
|
|
63
|
+
_normalize = {
|
|
64
|
+
ScalarKind.F64: ScalarKind.F64,
|
|
65
|
+
ScalarKind.F32: ScalarKind.F32,
|
|
66
|
+
ScalarKind.F16: ScalarKind.F16,
|
|
67
|
+
ScalarKind.F8: ScalarKind.F16,
|
|
68
|
+
ScalarKind.B1: ScalarKind.B1,
|
|
69
|
+
}
|
|
70
|
+
return _normalize[dtype]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _to_numpy_dtype(dtype: ScalarKind):
|
|
74
|
+
_normalize = {
|
|
75
|
+
ScalarKind.F64: np.float64,
|
|
76
|
+
ScalarKind.F32: np.float32,
|
|
77
|
+
ScalarKind.F16: np.float16,
|
|
78
|
+
ScalarKind.F8: np.float16,
|
|
79
|
+
ScalarKind.B1: np.uint8,
|
|
80
|
+
}
|
|
81
|
+
return _normalize[dtype]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_metric(metric):
|
|
85
|
+
if metric is None:
|
|
86
|
+
return MetricKind.IP
|
|
87
|
+
|
|
88
|
+
if isinstance(metric, str):
|
|
43
89
|
_normalize = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if not isinstance(dtype, ScalarKind):
|
|
53
|
-
_normalize = {
|
|
54
|
-
np.float64: ScalarKind.F64,
|
|
55
|
-
np.float32: ScalarKind.F32,
|
|
56
|
-
np.float16: ScalarKind.F16,
|
|
57
|
-
np.int8: ScalarKind.F8,
|
|
90
|
+
"cos": MetricKind.Cos,
|
|
91
|
+
"ip": MetricKind.IP,
|
|
92
|
+
"l2_sq": MetricKind.L2sq,
|
|
93
|
+
"haversine": MetricKind.Haversine,
|
|
94
|
+
"perason": MetricKind.Pearson,
|
|
95
|
+
"hamming": MetricKind.Hamming,
|
|
96
|
+
"tanimoto": MetricKind.Tanimoto,
|
|
97
|
+
"sorensen": MetricKind.Sorensen,
|
|
58
98
|
}
|
|
59
|
-
return _normalize[
|
|
99
|
+
return _normalize[metric.lower()]
|
|
60
100
|
|
|
61
|
-
return
|
|
101
|
+
return metric
|
|
62
102
|
|
|
63
103
|
|
|
64
104
|
class Matches(NamedTuple):
|
|
@@ -67,7 +107,7 @@ class Matches(NamedTuple):
|
|
|
67
107
|
counts: Union[np.ndarray, int]
|
|
68
108
|
|
|
69
109
|
@property
|
|
70
|
-
def
|
|
110
|
+
def is_multiple(self) -> bool:
|
|
71
111
|
return isinstance(self.counts, np.ndarray)
|
|
72
112
|
|
|
73
113
|
@property
|
|
@@ -79,9 +119,8 @@ class Matches(NamedTuple):
|
|
|
79
119
|
return np.sum(self.counts)
|
|
80
120
|
|
|
81
121
|
def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
assert row is None, 'Exporting a single sequence is only for batch requests'
|
|
122
|
+
if not self.is_multiple:
|
|
123
|
+
assert row is None, "Exporting a single sequence is only for batch requests"
|
|
85
124
|
labels = self.labels
|
|
86
125
|
distances = self.distances
|
|
87
126
|
|
|
@@ -94,17 +133,26 @@ class Matches(NamedTuple):
|
|
|
94
133
|
distances = self.distances[row, :count]
|
|
95
134
|
|
|
96
135
|
return [
|
|
97
|
-
{
|
|
136
|
+
{"label": int(label), "distance": float(distance)}
|
|
98
137
|
for label, distance in zip(labels, distances)
|
|
99
138
|
]
|
|
100
139
|
|
|
101
140
|
def recall_first(self, expected: np.ndarray) -> float:
|
|
102
|
-
best_matches = self.labels if not self.
|
|
141
|
+
best_matches = self.labels if not self.is_multiple else self.labels[:, 0]
|
|
103
142
|
return np.sum(best_matches == expected) / len(expected)
|
|
104
143
|
|
|
105
144
|
def __repr__(self) -> str:
|
|
106
|
-
return
|
|
107
|
-
f
|
|
145
|
+
return (
|
|
146
|
+
f"usearch.Matches({self.total_matches})"
|
|
147
|
+
if self.is_multiple
|
|
148
|
+
else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CompiledMetric(NamedTuple):
|
|
153
|
+
pointer: int
|
|
154
|
+
kind: MetricKind
|
|
155
|
+
signature: MetricSignature
|
|
108
156
|
|
|
109
157
|
|
|
110
158
|
class Index:
|
|
@@ -120,15 +168,13 @@ class Index:
|
|
|
120
168
|
def __init__(
|
|
121
169
|
self,
|
|
122
170
|
ndim: int,
|
|
123
|
-
metric: Union[
|
|
171
|
+
metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
|
|
124
172
|
dtype: Optional[str] = None,
|
|
125
173
|
jit: bool = False,
|
|
126
|
-
|
|
127
174
|
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
128
175
|
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
129
176
|
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
130
177
|
tune: bool = False,
|
|
131
|
-
|
|
132
178
|
path: Optional[os.PathLike] = None,
|
|
133
179
|
view: bool = False,
|
|
134
180
|
) -> None:
|
|
@@ -171,7 +217,7 @@ class Index:
|
|
|
171
217
|
|
|
172
218
|
:param expansion_search: Traversal depth on queries, defaults to None
|
|
173
219
|
:type expansion_search: Optional[int], optional
|
|
174
|
-
Hyper-parameter for the search depth when querying
|
|
220
|
+
Hyper-parameter for the search depth when querying
|
|
175
221
|
nearest neighbors. The original paper calls it "ef".
|
|
176
222
|
Can be changed afterwards, as the `.expansion_search`.
|
|
177
223
|
|
|
@@ -184,64 +230,45 @@ class Index:
|
|
|
184
230
|
:type view: bool, optional
|
|
185
231
|
"""
|
|
186
232
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
metric = MetricKind.IP
|
|
196
|
-
elif isinstance(metric, str):
|
|
197
|
-
_normalize = {
|
|
198
|
-
'cos': MetricKind.Cos,
|
|
199
|
-
'ip': MetricKind.IP,
|
|
200
|
-
'l2_sq': MetricKind.L2sq,
|
|
201
|
-
'haversine': MetricKind.Haversine,
|
|
202
|
-
'perason': MetricKind.Pearson,
|
|
203
|
-
'hamming': MetricKind.Hamming,
|
|
204
|
-
'tanimoto': MetricKind.Tanimoto,
|
|
205
|
-
'sorensen': MetricKind.Sorensen,
|
|
206
|
-
}
|
|
207
|
-
metric = _normalize[metric.lower()]
|
|
208
|
-
|
|
209
|
-
if isinstance(metric, Callable):
|
|
210
|
-
self._metric_kind = MetricKind.Unknown
|
|
211
|
-
self._metric_jit = metric
|
|
212
|
-
self._metric_pointer = int(metric.address)
|
|
213
|
-
|
|
214
|
-
elif isinstance(metric, MetricKind):
|
|
215
|
-
if jit:
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
from usearch.numba import jit
|
|
219
|
-
except ImportError:
|
|
220
|
-
raise ModuleNotFoundError(
|
|
221
|
-
'To use JIT install Numba with `pip install numba`.'
|
|
222
|
-
'Alternatively, reinstall usearch with `pip install usearch[jit]`')
|
|
223
|
-
|
|
224
|
-
self._metric_kind = metric
|
|
225
|
-
self._metric_jit = jit(
|
|
226
|
-
ndim=ndim,
|
|
227
|
-
metric=metric,
|
|
228
|
-
dtype=dtype,
|
|
233
|
+
metric = _normalize_metric(metric)
|
|
234
|
+
if isinstance(metric, MetricKind) and jit:
|
|
235
|
+
try:
|
|
236
|
+
from usearch.numba import jit
|
|
237
|
+
except ImportError:
|
|
238
|
+
raise ModuleNotFoundError(
|
|
239
|
+
"To use JIT install Numba with `pip install numba`."
|
|
240
|
+
"Alternatively, reinstall with `pip install usearch[jit]`"
|
|
229
241
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
242
|
+
|
|
243
|
+
metric = jit(
|
|
244
|
+
ndim=ndim,
|
|
245
|
+
metric=metric,
|
|
246
|
+
dtype=_normalize_dtype(dtype, metric),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if isinstance(metric, MetricKind):
|
|
250
|
+
self._metric_kind = metric
|
|
251
|
+
self._metric_jit = None
|
|
252
|
+
self._metric_pointer = 0
|
|
253
|
+
self._metric_signature = MetricSignature.ArrayArraySize
|
|
254
|
+
elif isinstance(metric, CompiledMetric):
|
|
255
|
+
self._metric_jit = metric
|
|
256
|
+
self._metric_kind = metric.kind
|
|
257
|
+
self._metric_pointer = metric.pointer
|
|
258
|
+
self._metric_signature = metric.signature
|
|
236
259
|
else:
|
|
237
260
|
raise ValueError(
|
|
238
|
-
|
|
261
|
+
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
262
|
+
)
|
|
239
263
|
|
|
264
|
+
# Validate, that the right scalar type is defined
|
|
265
|
+
dtype = _normalize_dtype(dtype, self._metric_kind)
|
|
240
266
|
self._compiled = _CompiledIndex(
|
|
241
267
|
ndim=ndim,
|
|
268
|
+
dtype=dtype,
|
|
242
269
|
metric=self._metric_kind,
|
|
243
270
|
metric_pointer=self._metric_pointer,
|
|
244
|
-
|
|
271
|
+
metric_signature=self._metric_signature,
|
|
245
272
|
connectivity=connectivity,
|
|
246
273
|
expansion_add=expansion_add,
|
|
247
274
|
expansion_search=expansion_search,
|
|
@@ -255,17 +282,47 @@ class Index:
|
|
|
255
282
|
else:
|
|
256
283
|
self._compiled.load(path)
|
|
257
284
|
|
|
285
|
+
@staticmethod
|
|
286
|
+
def metadata(path: os.PathLike) -> IndexMetadata:
|
|
287
|
+
return IndexMetadata(path)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def restore(path: os.PathLike, view: bool = False) -> Index:
|
|
291
|
+
meta = Index.metadata(path)
|
|
292
|
+
bits_per_scalar = {
|
|
293
|
+
ScalarKind.F8: 8,
|
|
294
|
+
ScalarKind.F16: 16,
|
|
295
|
+
ScalarKind.F32: 32,
|
|
296
|
+
ScalarKind.F64: 64,
|
|
297
|
+
ScalarKind.B1: 1,
|
|
298
|
+
}[meta.scalar_kind]
|
|
299
|
+
ndim = meta.bytes_for_vectors * 8 // meta.size // bits_per_scalar
|
|
300
|
+
return Index(
|
|
301
|
+
ndim=ndim,
|
|
302
|
+
connectivity=meta.connectivity,
|
|
303
|
+
metric=meta.metric,
|
|
304
|
+
path=path,
|
|
305
|
+
view=view,
|
|
306
|
+
)
|
|
307
|
+
|
|
258
308
|
def add(
|
|
259
|
-
|
|
260
|
-
|
|
309
|
+
self,
|
|
310
|
+
labels,
|
|
311
|
+
vectors,
|
|
312
|
+
*,
|
|
313
|
+
copy: bool = True,
|
|
314
|
+
threads: int = 0,
|
|
315
|
+
log: Union[str, bool] = False,
|
|
316
|
+
batch_size: int = 0,
|
|
317
|
+
) -> Union[int, np.ndarray]:
|
|
261
318
|
"""Inserts one or move vectors into the index.
|
|
262
319
|
|
|
263
320
|
For maximal performance the `labels` and `vectors`
|
|
264
321
|
should conform to the Python's "buffer protocol" spec.
|
|
265
322
|
|
|
266
|
-
To index a single entry:
|
|
323
|
+
To index a single entry:
|
|
267
324
|
labels: int, vectors: np.ndarray.
|
|
268
|
-
To index many entries:
|
|
325
|
+
To index many entries:
|
|
269
326
|
labels: np.ndarray, vectors: np.ndarray.
|
|
270
327
|
|
|
271
328
|
When working with extremely large indexes, you may want to
|
|
@@ -284,30 +341,70 @@ class Index:
|
|
|
284
341
|
:return: Inserted label or labels
|
|
285
342
|
:type: Union[int, np.ndarray]
|
|
286
343
|
"""
|
|
287
|
-
assert isinstance(vectors, np.ndarray),
|
|
288
|
-
assert vectors.ndim == 1 or vectors.ndim == 2,
|
|
289
|
-
|
|
344
|
+
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
345
|
+
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
346
|
+
count_vectors = vectors.shape[0] if vectors.ndim == 2 else 1
|
|
347
|
+
is_multiple = count_vectors > 1
|
|
348
|
+
if not is_multiple:
|
|
349
|
+
vectors = vectors.flatten()
|
|
350
|
+
|
|
351
|
+
# Validate of generate teh labels
|
|
290
352
|
generate_labels = labels is None
|
|
291
|
-
|
|
292
|
-
# If no `labels` were provided, generate some
|
|
293
353
|
if generate_labels:
|
|
294
354
|
start_id = len(self._compiled)
|
|
295
|
-
if
|
|
296
|
-
labels = np.arange(start_id, start_id +
|
|
355
|
+
if is_multiple:
|
|
356
|
+
labels = np.arange(start_id, start_id + count_vectors, dtype=Label)
|
|
297
357
|
else:
|
|
298
358
|
labels = start_id
|
|
299
|
-
|
|
300
|
-
labels
|
|
301
|
-
|
|
302
|
-
|
|
359
|
+
else:
|
|
360
|
+
if isinstance(labels, np.ndarray):
|
|
361
|
+
if not is_multiple:
|
|
362
|
+
labels = int(labels[0])
|
|
363
|
+
else:
|
|
364
|
+
labels = labels.astype(Label)
|
|
365
|
+
count_labels = len(labels) if isinstance(labels, Iterable) else 1
|
|
366
|
+
assert count_labels == count_vectors
|
|
367
|
+
|
|
368
|
+
# Split into batches and log progress, if needed
|
|
369
|
+
if batch_size:
|
|
370
|
+
labels = [
|
|
371
|
+
labels[start_row : start_row + batch_size]
|
|
372
|
+
for start_row in range(0, count_vectors, batch_size)
|
|
373
|
+
]
|
|
374
|
+
vectors = [
|
|
375
|
+
vectors[start_row : start_row + batch_size, :]
|
|
376
|
+
for start_row in range(0, count_vectors, batch_size)
|
|
377
|
+
]
|
|
378
|
+
tasks = zip(labels, vectors)
|
|
379
|
+
name = log if isinstance(log, str) else "Add"
|
|
380
|
+
pbar = tqdm(
|
|
381
|
+
tasks,
|
|
382
|
+
desc=name,
|
|
383
|
+
total=count_vectors,
|
|
384
|
+
unit="Vector",
|
|
385
|
+
disable=log is False,
|
|
386
|
+
)
|
|
387
|
+
for labels, vectors in tasks:
|
|
388
|
+
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
389
|
+
pbar.update(len(labels))
|
|
390
|
+
|
|
391
|
+
pbar.close()
|
|
303
392
|
|
|
304
|
-
|
|
393
|
+
else:
|
|
394
|
+
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
305
395
|
|
|
306
396
|
return labels
|
|
307
397
|
|
|
308
398
|
def search(
|
|
309
|
-
|
|
310
|
-
|
|
399
|
+
self,
|
|
400
|
+
vectors,
|
|
401
|
+
k: int = 10,
|
|
402
|
+
*,
|
|
403
|
+
threads: int = 0,
|
|
404
|
+
exact: bool = False,
|
|
405
|
+
log: Union[str, bool] = False,
|
|
406
|
+
batch_size: int = 0,
|
|
407
|
+
) -> Matches:
|
|
311
408
|
"""Performs approximate nearest neighbors search for one or more queries.
|
|
312
409
|
|
|
313
410
|
:param vectors: Query vector or vectors.
|
|
@@ -323,26 +420,28 @@ class Index:
|
|
|
323
420
|
:rtype: Matches
|
|
324
421
|
"""
|
|
325
422
|
tuple_ = self._compiled.search(
|
|
326
|
-
vectors,
|
|
327
|
-
|
|
423
|
+
vectors,
|
|
424
|
+
k,
|
|
425
|
+
exact=exact,
|
|
426
|
+
threads=threads,
|
|
328
427
|
)
|
|
329
428
|
return Matches(*tuple_)
|
|
330
429
|
|
|
331
430
|
@property
|
|
332
431
|
def specs(self) -> dict:
|
|
333
432
|
return {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
433
|
+
"Class": "usearch.Index",
|
|
434
|
+
"Connectivity": self.connectivity,
|
|
435
|
+
"Size": self.size,
|
|
436
|
+
"Dimensions": self.ndim,
|
|
437
|
+
"Expansion@Add": self.expansion_add,
|
|
438
|
+
"Expansion@Search": self.expansion_search,
|
|
439
|
+
"OpenMP": USES_OPENMP,
|
|
440
|
+
"SimSIMD": USES_SIMSIMD,
|
|
441
|
+
"NativeF16": USES_NATIVE_F16,
|
|
442
|
+
"JIT": self.jit,
|
|
443
|
+
"DType": self.dtype,
|
|
444
|
+
"Path": self.path,
|
|
346
445
|
}
|
|
347
446
|
|
|
348
447
|
def __len__(self) -> int:
|
|
@@ -396,33 +495,46 @@ class Index:
|
|
|
396
495
|
def expansion_search(self, v: int):
|
|
397
496
|
self._compiled.expansion_search = v
|
|
398
497
|
|
|
399
|
-
def save(self, path: os.PathLike):
|
|
498
|
+
def save(self, path: Optional[os.PathLike] = None):
|
|
499
|
+
path = path if path else self.path
|
|
500
|
+
if path is None:
|
|
501
|
+
raise Exception("Define `path` argument")
|
|
400
502
|
self._compiled.save(path)
|
|
401
503
|
|
|
402
|
-
def load(self, path: os.PathLike):
|
|
504
|
+
def load(self, path: Optional[os.PathLike] = None):
|
|
505
|
+
path = path if path else self.path
|
|
506
|
+
if path is None:
|
|
507
|
+
raise Exception("Define `path` argument")
|
|
403
508
|
self._compiled.load(path)
|
|
404
509
|
|
|
405
|
-
def view(self, path: os.PathLike):
|
|
510
|
+
def view(self, path: Optional[os.PathLike] = None):
|
|
511
|
+
path = path if path else self.path
|
|
512
|
+
if path is None:
|
|
513
|
+
raise Exception("Define `path` argument")
|
|
406
514
|
self._compiled.view(path)
|
|
407
515
|
|
|
408
516
|
def clear(self):
|
|
409
517
|
self._compiled.clear()
|
|
410
518
|
|
|
519
|
+
def close(self):
|
|
520
|
+
self._compiled.close()
|
|
521
|
+
|
|
411
522
|
@property
|
|
412
523
|
def labels(self) -> np.ndarray:
|
|
413
524
|
return self._compiled.labels
|
|
414
525
|
|
|
415
526
|
def get_vectors(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
527
|
+
self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
|
|
528
|
+
) -> np.ndarray:
|
|
529
|
+
dtype = _normalize_dtype(dtype, self._metric_kind)
|
|
530
|
+
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
531
|
+
vectors = np.vstack([self._compiled.__getitem__(l, get_dtype) for l in labels])
|
|
532
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
533
|
+
return vectors.view(view_dtype)
|
|
422
534
|
|
|
423
535
|
@property
|
|
424
536
|
def vectors(self) -> np.ndarray:
|
|
425
|
-
return self.get_vectors(self.labels)
|
|
537
|
+
return self.get_vectors(self.labels, self.dtype)
|
|
426
538
|
|
|
427
539
|
def __delitem__(self, label: int):
|
|
428
540
|
raise NotImplementedError()
|
|
@@ -431,4 +543,8 @@ class Index:
|
|
|
431
543
|
return self._compiled.__contains__(label)
|
|
432
544
|
|
|
433
545
|
def __getitem__(self, label: int) -> np.ndarray:
|
|
434
|
-
|
|
546
|
+
dtype = self.dtype
|
|
547
|
+
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
548
|
+
vector = self._compiled.__getitem__(label, get_dtype)
|
|
549
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
550
|
+
return None if vector is None else vector.view(view_dtype)
|
package/python/usearch/io.py
CHANGED
|
@@ -4,9 +4,8 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def load_matrix(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
view: bool = False) -> np.ndarray:
|
|
7
|
+
filename: str, start_row: int = 0, count_rows: int = None, view: bool = False
|
|
8
|
+
) -> np.ndarray:
|
|
10
9
|
"""Read *.ibin, *.bbib, *.hbin, *.fbin, *.dbin files with matrices.
|
|
11
10
|
|
|
12
11
|
:param filename: path to the matrix file
|
|
@@ -19,36 +18,37 @@ def load_matrix(
|
|
|
19
18
|
"""
|
|
20
19
|
dtype = np.float32
|
|
21
20
|
scalar_size = 4
|
|
22
|
-
if filename.endswith(
|
|
21
|
+
if filename.endswith(".fbin"):
|
|
23
22
|
dtype = np.float32
|
|
24
23
|
scalar_size = 4
|
|
25
|
-
elif filename.endswith(
|
|
24
|
+
elif filename.endswith(".dbin"):
|
|
26
25
|
dtype = np.float64
|
|
27
26
|
scalar_size = 8
|
|
28
|
-
elif filename.endswith(
|
|
27
|
+
elif filename.endswith(".hbin"):
|
|
29
28
|
dtype = np.float16
|
|
30
29
|
scalar_size = 2
|
|
31
|
-
elif filename.endswith(
|
|
30
|
+
elif filename.endswith(".ibin"):
|
|
32
31
|
dtype = np.int32
|
|
33
32
|
scalar_size = 4
|
|
34
|
-
elif filename.endswith(
|
|
35
|
-
dtype = np.
|
|
33
|
+
elif filename.endswith(".bbin"):
|
|
34
|
+
dtype = np.uint8
|
|
36
35
|
scalar_size = 1
|
|
37
36
|
else:
|
|
38
|
-
raise Exception(
|
|
37
|
+
raise Exception("Unknown file type")
|
|
39
38
|
|
|
40
|
-
with open(filename,
|
|
39
|
+
with open(filename, "rb") as f:
|
|
41
40
|
rows, cols = np.fromfile(f, count=2, dtype=np.int32)
|
|
42
41
|
rows = (rows - start_row) if count_rows is None else count_rows
|
|
43
42
|
row_offset = start_row * scalar_size * cols
|
|
44
43
|
|
|
45
44
|
if view:
|
|
46
45
|
return np.memmap(
|
|
47
|
-
f, dtype=dtype, mode=
|
|
46
|
+
f, dtype=dtype, mode="r", offset=8 + row_offset, shape=(rows, cols)
|
|
47
|
+
)
|
|
48
48
|
else:
|
|
49
49
|
return np.fromfile(
|
|
50
|
-
f, count=rows * cols,
|
|
51
|
-
|
|
50
|
+
f, count=rows * cols, dtype=dtype, offset=row_offset
|
|
51
|
+
).reshape(rows, cols)
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def save_matrix(vectors: np.ndarray, filename: str):
|
|
@@ -60,22 +60,22 @@ def save_matrix(vectors: np.ndarray, filename: str):
|
|
|
60
60
|
:type filename: str
|
|
61
61
|
"""
|
|
62
62
|
dtype = np.float32
|
|
63
|
-
if filename.endswith(
|
|
63
|
+
if filename.endswith(".fbin"):
|
|
64
64
|
dtype = np.float32
|
|
65
|
-
elif filename.endswith(
|
|
65
|
+
elif filename.endswith(".dbin"):
|
|
66
66
|
dtype = np.float64
|
|
67
|
-
elif filename.endswith(
|
|
67
|
+
elif filename.endswith(".hbin"):
|
|
68
68
|
dtype = np.float16
|
|
69
|
-
elif filename.endswith(
|
|
69
|
+
elif filename.endswith(".ibin"):
|
|
70
70
|
dtype = np.int32
|
|
71
|
-
elif filename.endswith(
|
|
72
|
-
dtype = np.
|
|
71
|
+
elif filename.endswith(".bbin"):
|
|
72
|
+
dtype = np.uint8
|
|
73
73
|
else:
|
|
74
|
-
raise Exception(
|
|
74
|
+
raise Exception("Unknown file type")
|
|
75
75
|
|
|
76
|
-
assert len(vectors.shape) == 2,
|
|
77
|
-
with open(filename,
|
|
76
|
+
assert len(vectors.shape) == 2, "Input array must have 2 dimensions"
|
|
77
|
+
with open(filename, "wb") as f:
|
|
78
78
|
count, dim = vectors.shape
|
|
79
|
-
f.write(struct.pack(
|
|
80
|
-
f.write(struct.pack(
|
|
79
|
+
f.write(struct.pack("<i", count))
|
|
80
|
+
f.write(struct.pack("<i", dim))
|
|
81
81
|
vectors.astype(dtype).flatten().tofile(f)
|
package/python/usearch/join.py
CHANGED
|
@@ -7,7 +7,6 @@ from index import Index, Matches, Label
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class bidict(dict):
|
|
10
|
-
|
|
11
10
|
def __init__(self, *args, **kwargs):
|
|
12
11
|
super(bidict, self).__init__(*args, **kwargs)
|
|
13
12
|
self.inverse = {}
|
|
@@ -37,10 +36,10 @@ def index_in_array(array: np.ndarray, item: Label) -> int:
|
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
def semantic_join(a: Index, b: Index, resolution: int = 10) -> bidict:
|
|
40
|
-
"""Performs a Semantic Join, matching one entry from `a` with
|
|
39
|
+
"""Performs a Semantic Join, matching one entry from `a` with
|
|
41
40
|
one entry from `b`, converging towards a Stable Marriage.
|
|
42
41
|
Assuming the collections can be different in size, classical solution
|
|
43
|
-
doesn't provide
|
|
42
|
+
doesn't provide
|
|
44
43
|
|
|
45
44
|
:param resolution: Approximate matches per member to consider, defaults to 10
|
|
46
45
|
:type resolution: int, optional
|
|
@@ -65,7 +64,6 @@ def semantic_join(a: Index, b: Index, resolution: int = 10) -> bidict:
|
|
|
65
64
|
idle_cycles: int = 0
|
|
66
65
|
hopeless_men_count = len(men) - len(women)
|
|
67
66
|
while len(free_men) > hopeless_men_count:
|
|
68
|
-
|
|
69
67
|
# In the worst case scenario we may need to match more candidates for every
|
|
70
68
|
# remaining man. This, however, may drastically increase the runtime.
|
|
71
69
|
if len(free_men) == idle_cycles:
|