usearch 0.19.0 → 0.19.2
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 +11 -34
- 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 +4 -4
- package/docs/compilation.md +1 -0
- package/include/usearch/index.hpp +22 -5
- package/include/usearch/index_punned_dense.hpp +5 -3
- package/include/usearch/index_punned_helpers.hpp +2 -0
- package/objc/USearchObjective.mm +4 -0
- package/objc/include/USearchObjective.h +5 -0
- package/package.json +2 -2
- package/python/lib.cpp +125 -166
- package/python/scripts/bench.py +10 -10
- package/python/scripts/test.py +65 -17
- package/python/usearch/index.py +158 -89
- package/python/usearch/io.py +2 -2
- package/python/usearch/numba.py +28 -22
- package/setup.py +56 -59
- package/swift/Test.swift +13 -9
package/python/usearch/index.py
CHANGED
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
from typing import Optional, Union, NamedTuple, List, Iterable
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
|
+
from tqdm import tqdm
|
|
11
12
|
|
|
12
13
|
from usearch.compiled import Index as _CompiledIndex, IndexMetadata
|
|
13
14
|
from usearch.compiled import SparseIndex as _CompiledSetsIndex
|
|
@@ -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
|
-
"
|
|
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,
|
|
49
98
|
}
|
|
50
|
-
return _normalize[
|
|
99
|
+
return _normalize[metric.lower()]
|
|
51
100
|
|
|
52
|
-
|
|
53
|
-
_normalize = {
|
|
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
|
-
return dtype
|
|
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,7 +119,7 @@ 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
|
-
if not self.
|
|
122
|
+
if not self.is_multiple:
|
|
83
123
|
assert row is None, "Exporting a single sequence is only for batch requests"
|
|
84
124
|
labels = self.labels
|
|
85
125
|
distances = self.distances
|
|
@@ -98,13 +138,13 @@ class Matches(NamedTuple):
|
|
|
98
138
|
]
|
|
99
139
|
|
|
100
140
|
def recall_first(self, expected: np.ndarray) -> float:
|
|
101
|
-
best_matches = self.labels if not self.
|
|
141
|
+
best_matches = self.labels if not self.is_multiple else self.labels[:, 0]
|
|
102
142
|
return np.sum(best_matches == expected) / len(expected)
|
|
103
143
|
|
|
104
144
|
def __repr__(self) -> str:
|
|
105
145
|
return (
|
|
106
146
|
f"usearch.Matches({self.total_matches})"
|
|
107
|
-
if self.
|
|
147
|
+
if self.is_multiple
|
|
108
148
|
else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
|
|
109
149
|
)
|
|
110
150
|
|
|
@@ -127,10 +167,9 @@ class Index:
|
|
|
127
167
|
|
|
128
168
|
def __init__(
|
|
129
169
|
self,
|
|
130
|
-
ndim: int,
|
|
170
|
+
ndim: int = 0,
|
|
131
171
|
metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
|
|
132
|
-
dtype: Optional[str] = None,
|
|
133
|
-
jit: bool = False,
|
|
172
|
+
dtype: Optional[Union[str, ScalarKind]] = None,
|
|
134
173
|
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
135
174
|
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
136
175
|
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
@@ -142,6 +181,10 @@ class Index:
|
|
|
142
181
|
|
|
143
182
|
:param ndim: Number of vector dimensions
|
|
144
183
|
:type ndim: int
|
|
184
|
+
Required for some metrics, optional for others.
|
|
185
|
+
Haversine, for example, only applies to 2-dimensional latitude/longitude
|
|
186
|
+
coordinates. Angular (Cos) and Euclidean (L2sq), obviously, apply to
|
|
187
|
+
vectors with arbitrary number of dimensions.
|
|
145
188
|
|
|
146
189
|
:param metric: Distance function, defaults to MetricKind.IP
|
|
147
190
|
:type metric: Union[MetricKind, Callable, str], optional
|
|
@@ -151,18 +194,12 @@ class Index:
|
|
|
151
194
|
Not every kind is JIT-able. For Jaccard distance, use `SparseIndex`.
|
|
152
195
|
|
|
153
196
|
:param dtype: Scalar type for internal vector storage, defaults to None
|
|
154
|
-
:type dtype: str, optional
|
|
197
|
+
:type dtype: Optional[Union[str, ScalarKind]], optional
|
|
155
198
|
For continuous metrics can be: f16, f32, f64, or f8.
|
|
156
199
|
For bitwise metrics it's implementation-defined, and can't change.
|
|
157
200
|
Example: you can use the `f16` index with `f32` vectors in Euclidean space,
|
|
158
201
|
which will be automatically downcasted.
|
|
159
202
|
|
|
160
|
-
:param jit: Enable Numba to JIT compile the metric, defaults to False
|
|
161
|
-
:type jit: bool, optional
|
|
162
|
-
This can result in up-to 3x performance difference on very large vectors
|
|
163
|
-
and very recent hardware, as the Python module is compiled with high
|
|
164
|
-
compatibility in mind and avoids very fancy assembly instructions.
|
|
165
|
-
|
|
166
203
|
:param connectivity: Connections per node in HNSW, defaults to None
|
|
167
204
|
:type connectivity: Optional[int], optional
|
|
168
205
|
Hyper-parameter for the number of Graph connections
|
|
@@ -190,43 +227,7 @@ class Index:
|
|
|
190
227
|
:type view: bool, optional
|
|
191
228
|
"""
|
|
192
229
|
|
|
193
|
-
|
|
194
|
-
assert dtype is None or dtype == ScalarKind.B1
|
|
195
|
-
dtype = ScalarKind.B1
|
|
196
|
-
else:
|
|
197
|
-
dtype = _normalize_dtype(dtype)
|
|
198
|
-
|
|
199
|
-
if metric is None:
|
|
200
|
-
metric = MetricKind.IP
|
|
201
|
-
|
|
202
|
-
if isinstance(metric, str):
|
|
203
|
-
_normalize = {
|
|
204
|
-
"cos": MetricKind.Cos,
|
|
205
|
-
"ip": MetricKind.IP,
|
|
206
|
-
"l2_sq": MetricKind.L2sq,
|
|
207
|
-
"haversine": MetricKind.Haversine,
|
|
208
|
-
"perason": MetricKind.Pearson,
|
|
209
|
-
"hamming": MetricKind.Hamming,
|
|
210
|
-
"tanimoto": MetricKind.Tanimoto,
|
|
211
|
-
"sorensen": MetricKind.Sorensen,
|
|
212
|
-
}
|
|
213
|
-
metric = _normalize[metric.lower()]
|
|
214
|
-
|
|
215
|
-
if isinstance(metric, MetricKind) and jit:
|
|
216
|
-
try:
|
|
217
|
-
from usearch.numba import jit
|
|
218
|
-
except ImportError:
|
|
219
|
-
raise ModuleNotFoundError(
|
|
220
|
-
"To use JIT install Numba with `pip install numba`."
|
|
221
|
-
"Alternatively, reinstall with `pip install usearch[jit]`"
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
metric = jit(
|
|
225
|
-
ndim=ndim,
|
|
226
|
-
metric=metric,
|
|
227
|
-
dtype=dtype,
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
+
metric = _normalize_metric(metric)
|
|
230
231
|
if isinstance(metric, MetricKind):
|
|
231
232
|
self._metric_kind = metric
|
|
232
233
|
self._metric_jit = None
|
|
@@ -242,6 +243,8 @@ class Index:
|
|
|
242
243
|
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
243
244
|
)
|
|
244
245
|
|
|
246
|
+
# Validate, that the right scalar type is defined
|
|
247
|
+
dtype = _normalize_dtype(dtype, self._metric_kind)
|
|
245
248
|
self._compiled = _CompiledIndex(
|
|
246
249
|
ndim=ndim,
|
|
247
250
|
dtype=dtype,
|
|
@@ -285,7 +288,14 @@ class Index:
|
|
|
285
288
|
)
|
|
286
289
|
|
|
287
290
|
def add(
|
|
288
|
-
self,
|
|
291
|
+
self,
|
|
292
|
+
labels,
|
|
293
|
+
vectors,
|
|
294
|
+
*,
|
|
295
|
+
copy: bool = True,
|
|
296
|
+
threads: int = 0,
|
|
297
|
+
log: Union[str, bool] = False,
|
|
298
|
+
batch_size: int = 0,
|
|
289
299
|
) -> Union[int, np.ndarray]:
|
|
290
300
|
"""Inserts one or move vectors into the index.
|
|
291
301
|
|
|
@@ -315,27 +325,67 @@ class Index:
|
|
|
315
325
|
"""
|
|
316
326
|
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
317
327
|
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
318
|
-
|
|
319
|
-
|
|
328
|
+
count_vectors = vectors.shape[0] if vectors.ndim == 2 else 1
|
|
329
|
+
is_multiple = count_vectors > 1
|
|
330
|
+
if not is_multiple:
|
|
331
|
+
vectors = vectors.flatten()
|
|
320
332
|
|
|
321
|
-
#
|
|
333
|
+
# Validate of generate teh labels
|
|
334
|
+
generate_labels = labels is None
|
|
322
335
|
if generate_labels:
|
|
323
336
|
start_id = len(self._compiled)
|
|
324
|
-
if
|
|
325
|
-
labels = np.arange(start_id, start_id +
|
|
337
|
+
if is_multiple:
|
|
338
|
+
labels = np.arange(start_id, start_id + count_vectors, dtype=Label)
|
|
326
339
|
else:
|
|
327
340
|
labels = start_id
|
|
328
|
-
|
|
329
|
-
labels
|
|
330
|
-
|
|
331
|
-
|
|
341
|
+
else:
|
|
342
|
+
if isinstance(labels, np.ndarray):
|
|
343
|
+
if not is_multiple:
|
|
344
|
+
labels = int(labels[0])
|
|
345
|
+
else:
|
|
346
|
+
labels = labels.astype(Label)
|
|
347
|
+
count_labels = len(labels) if isinstance(labels, Iterable) else 1
|
|
348
|
+
assert count_labels == count_vectors
|
|
349
|
+
|
|
350
|
+
# Split into batches and log progress, if needed
|
|
351
|
+
if batch_size:
|
|
352
|
+
labels = [
|
|
353
|
+
labels[start_row : start_row + batch_size]
|
|
354
|
+
for start_row in range(0, count_vectors, batch_size)
|
|
355
|
+
]
|
|
356
|
+
vectors = [
|
|
357
|
+
vectors[start_row : start_row + batch_size, :]
|
|
358
|
+
for start_row in range(0, count_vectors, batch_size)
|
|
359
|
+
]
|
|
360
|
+
tasks = zip(labels, vectors)
|
|
361
|
+
name = log if isinstance(log, str) else "Add"
|
|
362
|
+
pbar = tqdm(
|
|
363
|
+
tasks,
|
|
364
|
+
desc=name,
|
|
365
|
+
total=count_vectors,
|
|
366
|
+
unit="Vector",
|
|
367
|
+
disable=log is False,
|
|
368
|
+
)
|
|
369
|
+
for labels, vectors in tasks:
|
|
370
|
+
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
371
|
+
pbar.update(len(labels))
|
|
332
372
|
|
|
333
|
-
|
|
373
|
+
pbar.close()
|
|
374
|
+
|
|
375
|
+
else:
|
|
376
|
+
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
334
377
|
|
|
335
378
|
return labels
|
|
336
379
|
|
|
337
380
|
def search(
|
|
338
|
-
self,
|
|
381
|
+
self,
|
|
382
|
+
vectors,
|
|
383
|
+
k: int = 10,
|
|
384
|
+
*,
|
|
385
|
+
threads: int = 0,
|
|
386
|
+
exact: bool = False,
|
|
387
|
+
log: Union[str, bool] = False,
|
|
388
|
+
batch_size: int = 0,
|
|
339
389
|
) -> Matches:
|
|
340
390
|
"""Performs approximate nearest neighbors search for one or more queries.
|
|
341
391
|
|
|
@@ -427,18 +477,30 @@ class Index:
|
|
|
427
477
|
def expansion_search(self, v: int):
|
|
428
478
|
self._compiled.expansion_search = v
|
|
429
479
|
|
|
430
|
-
def save(self, path: os.PathLike):
|
|
480
|
+
def save(self, path: Optional[os.PathLike] = None):
|
|
481
|
+
path = path if path else self.path
|
|
482
|
+
if path is None:
|
|
483
|
+
raise Exception("Define `path` argument")
|
|
431
484
|
self._compiled.save(path)
|
|
432
485
|
|
|
433
|
-
def load(self, path: os.PathLike):
|
|
486
|
+
def load(self, path: Optional[os.PathLike] = None):
|
|
487
|
+
path = path if path else self.path
|
|
488
|
+
if path is None:
|
|
489
|
+
raise Exception("Define `path` argument")
|
|
434
490
|
self._compiled.load(path)
|
|
435
491
|
|
|
436
|
-
def view(self, path: os.PathLike):
|
|
492
|
+
def view(self, path: Optional[os.PathLike] = None):
|
|
493
|
+
path = path if path else self.path
|
|
494
|
+
if path is None:
|
|
495
|
+
raise Exception("Define `path` argument")
|
|
437
496
|
self._compiled.view(path)
|
|
438
497
|
|
|
439
498
|
def clear(self):
|
|
440
499
|
self._compiled.clear()
|
|
441
500
|
|
|
501
|
+
def close(self):
|
|
502
|
+
self._compiled.close()
|
|
503
|
+
|
|
442
504
|
@property
|
|
443
505
|
def labels(self) -> np.ndarray:
|
|
444
506
|
return self._compiled.labels
|
|
@@ -446,12 +508,15 @@ class Index:
|
|
|
446
508
|
def get_vectors(
|
|
447
509
|
self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
|
|
448
510
|
) -> np.ndarray:
|
|
449
|
-
dtype = _normalize_dtype(dtype)
|
|
450
|
-
|
|
511
|
+
dtype = _normalize_dtype(dtype, self._metric_kind)
|
|
512
|
+
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
513
|
+
vectors = np.vstack([self._compiled.__getitem__(l, get_dtype) for l in labels])
|
|
514
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
515
|
+
return vectors.view(view_dtype)
|
|
451
516
|
|
|
452
517
|
@property
|
|
453
518
|
def vectors(self) -> np.ndarray:
|
|
454
|
-
return self.get_vectors(self.labels)
|
|
519
|
+
return self.get_vectors(self.labels, self.dtype)
|
|
455
520
|
|
|
456
521
|
def __delitem__(self, label: int):
|
|
457
522
|
raise NotImplementedError()
|
|
@@ -460,4 +525,8 @@ class Index:
|
|
|
460
525
|
return self._compiled.__contains__(label)
|
|
461
526
|
|
|
462
527
|
def __getitem__(self, label: int) -> np.ndarray:
|
|
463
|
-
|
|
528
|
+
dtype = self.dtype
|
|
529
|
+
get_dtype = _to_numpy_compatible_dtype(dtype)
|
|
530
|
+
vector = self._compiled.__getitem__(label, get_dtype)
|
|
531
|
+
view_dtype = _to_numpy_dtype(dtype)
|
|
532
|
+
return None if vector is None else vector.view(view_dtype)
|
package/python/usearch/io.py
CHANGED
|
@@ -31,7 +31,7 @@ def load_matrix(
|
|
|
31
31
|
dtype = np.int32
|
|
32
32
|
scalar_size = 4
|
|
33
33
|
elif filename.endswith(".bbin"):
|
|
34
|
-
dtype = np.
|
|
34
|
+
dtype = np.uint8
|
|
35
35
|
scalar_size = 1
|
|
36
36
|
else:
|
|
37
37
|
raise Exception("Unknown file type")
|
|
@@ -69,7 +69,7 @@ def save_matrix(vectors: np.ndarray, filename: str):
|
|
|
69
69
|
elif filename.endswith(".ibin"):
|
|
70
70
|
dtype = np.int32
|
|
71
71
|
elif filename.endswith(".bbin"):
|
|
72
|
-
dtype = np.
|
|
72
|
+
dtype = np.uint8
|
|
73
73
|
else:
|
|
74
74
|
raise Exception("Unknown file type")
|
|
75
75
|
|
package/python/usearch/numba.py
CHANGED
|
@@ -4,31 +4,19 @@
|
|
|
4
4
|
# into the primary `Index` class, connecting USearch with Numba.
|
|
5
5
|
from math import sqrt
|
|
6
6
|
|
|
7
|
-
from numba import cfunc, types, carray
|
|
8
|
-
|
|
9
7
|
from usearch.index import MetricKind, ScalarKind, MetricSignature, CompiledMetric
|
|
10
8
|
|
|
11
9
|
|
|
12
|
-
signature_i8args = types.float32(types.CPointer(types.int8), types.CPointer(types.int8))
|
|
13
|
-
|
|
14
|
-
signature_f16args = types.float32(
|
|
15
|
-
types.CPointer(types.float16), types.CPointer(types.float16)
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
signature_f32args = types.float32(
|
|
19
|
-
types.CPointer(types.float32), types.CPointer(types.float32)
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
signature_f64args = types.float32(
|
|
23
|
-
types.CPointer(types.float64), types.CPointer(types.float64)
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
10
|
def jit(
|
|
28
|
-
ndim: int,
|
|
11
|
+
ndim: int,
|
|
12
|
+
metric: MetricKind = MetricKind.Cos,
|
|
13
|
+
dtype: ScalarKind = ScalarKind.F32,
|
|
29
14
|
) -> CompiledMetric:
|
|
30
|
-
"""JIT-compiles
|
|
31
|
-
|
|
15
|
+
"""JIT-compiles the metric for target hardware and number of dimensions.
|
|
16
|
+
|
|
17
|
+
This can result in up-to 3x performance difference on very large vectors
|
|
18
|
+
and very recent hardware, as the Python module is compiled with high
|
|
19
|
+
compatibility in mind and avoids very fancy assembly instructions.
|
|
32
20
|
|
|
33
21
|
Uses Numba `cfunc` functionality, annotating it with Numba `types` instead
|
|
34
22
|
of `ctypes` to support half-precision.
|
|
@@ -37,6 +25,24 @@ def jit(
|
|
|
37
25
|
assert isinstance(metric, MetricKind)
|
|
38
26
|
assert isinstance(dtype, ScalarKind)
|
|
39
27
|
|
|
28
|
+
from numba import cfunc, types, carray
|
|
29
|
+
|
|
30
|
+
signature_i8args = types.float32(
|
|
31
|
+
types.CPointer(types.int8), types.CPointer(types.int8)
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
signature_f16args = types.float32(
|
|
35
|
+
types.CPointer(types.float16), types.CPointer(types.float16)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
signature_f32args = types.float32(
|
|
39
|
+
types.CPointer(types.float32), types.CPointer(types.float32)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
signature_f64args = types.float32(
|
|
43
|
+
types.CPointer(types.float64), types.CPointer(types.float64)
|
|
44
|
+
)
|
|
45
|
+
|
|
40
46
|
numba_supported_types = (
|
|
41
47
|
ScalarKind.F8,
|
|
42
48
|
# Half-precision is still unsupported
|
|
@@ -46,7 +52,7 @@ def jit(
|
|
|
46
52
|
ScalarKind.F64,
|
|
47
53
|
)
|
|
48
54
|
if dtype not in numba_supported_types:
|
|
49
|
-
return
|
|
55
|
+
return metric
|
|
50
56
|
|
|
51
57
|
scalar_kind_to_accumulator_type = {
|
|
52
58
|
ScalarKind.F8: types.int32,
|
|
@@ -109,7 +115,7 @@ def jit(
|
|
|
109
115
|
|
|
110
116
|
pointer = cfunc(scalar_kind_to_signature[dtype])(metric_kind_to_function[metric])
|
|
111
117
|
return CompiledMetric(
|
|
112
|
-
pointer=pointer,
|
|
118
|
+
pointer=pointer.address,
|
|
113
119
|
kind=metric,
|
|
114
120
|
signature=MetricSignature.ArrayArray,
|
|
115
121
|
)
|
package/setup.py
CHANGED
|
@@ -5,97 +5,94 @@ from setuptools import setup
|
|
|
5
5
|
from pybind11.setup_helpers import Pybind11Extension
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
compile_args = [
|
|
8
|
+
compile_args = []
|
|
9
9
|
link_args = []
|
|
10
10
|
macros_args = [
|
|
11
|
-
(
|
|
12
|
-
(
|
|
11
|
+
("USEARCH_USE_NATIVE_F16", "0"),
|
|
12
|
+
("USEARCH_USE_SIMSIMD", "0"),
|
|
13
13
|
]
|
|
14
14
|
|
|
15
|
-
if sys.platform ==
|
|
16
|
-
compile_args.append(
|
|
17
|
-
|
|
18
|
-
compile_args.append(
|
|
19
|
-
macros_args.append(('USEARCH_USE_OPENMP', '1'))
|
|
15
|
+
if sys.platform == "linux":
|
|
16
|
+
compile_args.append("-std=c++11")
|
|
17
|
+
compile_args.append("-O3")
|
|
18
|
+
compile_args.append("-Wno-unknown-pragmas")
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
macros_args.append(("USEARCH_USE_OPENMP", "1"))
|
|
21
|
+
compile_args.append("-fopenmp")
|
|
22
|
+
link_args.append("-lgomp")
|
|
23
|
+
|
|
24
|
+
if sys.platform == "darwin":
|
|
22
25
|
# MacOS 10.15 or higher is needed for `aligned_alloc` support.
|
|
23
26
|
# https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
|
|
24
|
-
compile_args.append(
|
|
25
|
-
compile_args.append(
|
|
27
|
+
compile_args.append("-mmacosx-version-min=10.15")
|
|
28
|
+
compile_args.append("-std=c++11")
|
|
29
|
+
compile_args.append("-O3")
|
|
30
|
+
compile_args.append("-Wno-unknown-pragmas")
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
# Linking OpenMP requires additional preparion in CIBuildWheel
|
|
33
|
+
# macros_args.append(("USEARCH_USE_OPENMP", "1"))
|
|
34
|
+
# compile_args.append("-Xpreprocessor -fopenmp")
|
|
35
|
+
# link_args.append("-Xpreprocessor -lomp")
|
|
29
36
|
|
|
37
|
+
if sys.platform == "win32":
|
|
38
|
+
compile_args.append("/std:c++14")
|
|
39
|
+
compile_args.append("/O2")
|
|
30
40
|
|
|
31
41
|
ext_modules = [
|
|
32
42
|
Pybind11Extension(
|
|
33
|
-
|
|
34
|
-
[
|
|
43
|
+
"usearch.compiled",
|
|
44
|
+
["python/lib.cpp"],
|
|
35
45
|
extra_compile_args=compile_args,
|
|
36
46
|
extra_link_args=link_args,
|
|
37
47
|
define_macros=macros_args,
|
|
38
48
|
),
|
|
39
49
|
]
|
|
40
50
|
|
|
41
|
-
__version__ = open(
|
|
42
|
-
__lib_name__ =
|
|
51
|
+
__version__ = open("VERSION", "r").read().strip()
|
|
52
|
+
__lib_name__ = "usearch"
|
|
43
53
|
|
|
44
54
|
|
|
45
55
|
this_directory = os.path.abspath(os.path.dirname(__file__))
|
|
46
|
-
with open(os.path.join(this_directory,
|
|
56
|
+
with open(os.path.join(this_directory, "README.md")) as f:
|
|
47
57
|
long_description = f.read()
|
|
48
58
|
|
|
49
59
|
|
|
50
60
|
setup(
|
|
51
|
-
|
|
52
61
|
name=__lib_name__,
|
|
53
62
|
version=__version__,
|
|
54
|
-
packages=[
|
|
55
|
-
package_dir={
|
|
56
|
-
|
|
57
|
-
description='Smaller & Faster Single-File Vector Search Engine from Unum',
|
|
63
|
+
packages=["usearch"],
|
|
64
|
+
package_dir={"usearch": "python/usearch"},
|
|
65
|
+
description="Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
58
66
|
long_description=long_description,
|
|
59
|
-
long_description_content_type=
|
|
60
|
-
license=
|
|
61
|
-
|
|
67
|
+
long_description_content_type="text/markdown",
|
|
68
|
+
license="Apache-2.0",
|
|
62
69
|
classifiers=[
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
'Topic :: System :: Clustering',
|
|
84
|
-
'Topic :: Database :: Database Engines/Servers',
|
|
85
|
-
'Topic :: Scientific/Engineering :: Artificial Intelligence',
|
|
70
|
+
"Development Status :: 5 - Production/Stable",
|
|
71
|
+
"Natural Language :: English",
|
|
72
|
+
"Intended Audience :: Developers",
|
|
73
|
+
"Intended Audience :: Information Technology",
|
|
74
|
+
"License :: OSI Approved :: Apache Software License",
|
|
75
|
+
"Programming Language :: C++",
|
|
76
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
77
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
78
|
+
"Programming Language :: Java",
|
|
79
|
+
"Programming Language :: JavaScript",
|
|
80
|
+
"Programming Language :: Objective C",
|
|
81
|
+
"Programming Language :: Rust",
|
|
82
|
+
"Programming Language :: Other",
|
|
83
|
+
"Operating System :: MacOS",
|
|
84
|
+
"Operating System :: Unix",
|
|
85
|
+
"Operating System :: Microsoft :: Windows",
|
|
86
|
+
"Topic :: System :: Clustering",
|
|
87
|
+
"Topic :: Database :: Database Engines/Servers",
|
|
88
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
86
89
|
],
|
|
87
|
-
|
|
88
|
-
include_dirs=[
|
|
89
|
-
'include',
|
|
90
|
-
'fp16/include', 'robin-map/include', 'simsimd/include'],
|
|
91
|
-
|
|
90
|
+
include_dirs=["include", "fp16/include", "robin-map/include", "simsimd/include"],
|
|
92
91
|
ext_modules=ext_modules,
|
|
93
92
|
install_requires=[
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
"numpy",
|
|
94
|
+
"pandas",
|
|
95
|
+
"tqdm",
|
|
96
96
|
'ucall; python_version >= "3.9"',
|
|
97
97
|
],
|
|
98
|
-
extras_require={
|
|
99
|
-
'jit': ['numba', 'cppyy'],
|
|
100
|
-
},
|
|
101
98
|
)
|
package/swift/Test.swift
CHANGED
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
//
|
|
7
7
|
|
|
8
8
|
import Foundation
|
|
9
|
+
import XCTest
|
|
9
10
|
import USearch
|
|
10
11
|
|
|
11
12
|
@available(iOS 13, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
class Test: XCTestCase {
|
|
14
|
+
func testUnit() throws {
|
|
15
|
+
let index = USearchIndex.make(metric:USearchMetric.l2sq, dimensions: 4, connectivity: 8, quantization:USearchScalar.F32)
|
|
16
|
+
let vectorA: [Float32] = [0.3, 0.5, 1.2, 1.4]
|
|
17
|
+
let vectorB: [Float32] = [0.4, 0.2, 1.2, 1.1]
|
|
18
|
+
index.reserve(2)
|
|
19
|
+
index.add(label: 42, vector: vectorA[...])
|
|
20
|
+
index.add(label: 43, vector: vectorB[...])
|
|
21
|
+
|
|
22
|
+
let results = index.search(vector: vectorA[...], count: 10)
|
|
23
|
+
assert(results.0[0] == 42)
|
|
24
|
+
}
|
|
21
25
|
}
|