usearch 0.22.3 → 1.1.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/.releaserc +10 -2
- package/.vscode/launch.json +10 -10
- package/.vscode/settings.json +9 -1
- package/.vscode/tasks.json +4 -4
- package/CMakeLists.txt +6 -1
- package/Cargo.toml +2 -2
- package/README.md +35 -41
- package/build.rs +2 -2
- package/c/CMakeLists.txt +48 -0
- package/c/README.md +34 -0
- package/c/lib.cpp +83 -72
- package/c/test.c +20 -20
- package/c/usearch.h +132 -32
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +3 -1
- package/cpp/README.md +16 -16
- package/cpp/bench.cpp +50 -126
- package/cpp/test.cpp +164 -80
- package/docs/benchmarks.md +26 -5
- package/docs/c/reference.rst +3 -3
- package/docs/compilation.md +31 -49
- package/docs/cpp/index.rst +1 -1
- package/docs/cpp/reference.rst +4 -4
- package/golang/lib.go +275 -141
- package/golang/lib_test.go +105 -0
- package/include/usearch/index.hpp +1681 -1656
- package/include/usearch/index_dense.hpp +1400 -0
- package/include/usearch/index_plugins.hpp +1263 -0
- package/java/README.md +1 -1
- package/java/cloud/unum/usearch/Index.java +10 -10
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +34 -37
- package/java/test/IndexTest.java +1 -1
- package/javascript/README.md +6 -6
- package/javascript/lib.cpp +156 -55
- package/javascript/test.js +26 -11
- package/javascript/usearch.d.ts +102 -0
- package/javascript/usearch.js +2 -118
- package/objc/USearchObjective.mm +57 -31
- package/objc/include/USearchObjective.h +32 -22
- package/package.json +5 -3
- package/python/README.md +23 -14
- package/python/lib.cpp +468 -444
- package/python/scripts/bench.ipynb +3 -2
- package/python/scripts/bench.py +4 -4
- package/python/scripts/index_faiss.py +5 -5
- package/python/scripts/join.py +4 -4
- package/python/scripts/test.py +152 -61
- package/python/scripts/test_jit.py +10 -12
- package/python/scripts/test_sparse.py +32 -0
- package/python/usearch/client.py +26 -28
- package/python/usearch/eval.py +73 -23
- package/python/usearch/index.py +520 -228
- package/python/usearch/io.py +57 -25
- package/python/usearch/numba.py +4 -4
- package/python/usearch/server.py +10 -10
- package/rust/lib.cpp +34 -30
- package/rust/lib.hpp +14 -12
- package/rust/lib.rs +8 -7
- package/setup.py +5 -2
- package/simsimd/.github/workflows/prerelease.yml +73 -0
- package/simsimd/.github/workflows/release.yml +110 -0
- package/simsimd/.github/workflows/update_version.sh +3 -0
- package/simsimd/.releaserc +77 -0
- package/simsimd/.vscode/launch.json +27 -1
- package/simsimd/.vscode/settings.json +22 -1
- package/simsimd/.vscode/tasks.json +6 -0
- package/simsimd/CMakeLists.txt +3 -2
- package/simsimd/README.md +82 -46
- package/simsimd/bench.cxx +63 -24
- package/simsimd/include/simsimd/simsimd.h +88 -66
- package/simsimd/include/simsimd/simsimd_chem.h +126 -0
- package/simsimd/package.json +8 -0
- package/simsimd/pyproject.toml +42 -0
- package/simsimd/python/python.c +81 -0
- package/simsimd/python/run.sh +1 -0
- package/simsimd/python/test.c +109 -0
- package/simsimd/python/test.py +23 -0
- package/simsimd/setup.py +44 -0
- package/swift/Index+Sugar.swift +75 -23
- package/swift/README.md +26 -2
- package/swift/Test.swift +19 -7
- package/swift/USearch.swift +2 -2
- package/tsconfig.json +8 -0
- package/wasm/CMakeLists.txt +1 -0
- package/wasmer.toml +1 -1
- package/wolfram/CMakeLists.txt +1 -0
- package/wolfram/lib.cpp +11 -13
- package/golang/usearch.h +0 -9
- package/golang/usearch_par_test.go +0 -40
- package/golang/usearch_test.go +0 -82
- package/include/usearch/index_punned_dense.hpp +0 -1001
- package/include/usearch/index_punned_helpers.hpp +0 -676
package/python/usearch/eval.py
CHANGED
|
@@ -8,7 +8,14 @@ from math import ceil
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
|
|
10
10
|
from usearch.io import load_matrix
|
|
11
|
-
from usearch.index import
|
|
11
|
+
from usearch.index import (
|
|
12
|
+
Index,
|
|
13
|
+
Matches,
|
|
14
|
+
BatchMatches,
|
|
15
|
+
MetricKind,
|
|
16
|
+
MetricKindBitwise,
|
|
17
|
+
Key,
|
|
18
|
+
)
|
|
12
19
|
|
|
13
20
|
|
|
14
21
|
def random_vectors(
|
|
@@ -48,28 +55,71 @@ def random_vectors(
|
|
|
48
55
|
return x
|
|
49
56
|
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
@dataclass
|
|
59
|
+
class SearchStats:
|
|
60
|
+
"""
|
|
61
|
+
Contains statistics for one or more search runs, including the number of
|
|
62
|
+
internal nodes that were fetched (`visited_members`) and the number
|
|
63
|
+
of times the distance metric was invoked (`computed_distances`).
|
|
64
|
+
|
|
65
|
+
Other derivative metrics include the `mean_recall` and `mean_efficiency`.
|
|
66
|
+
Recall is the share of queried vectors, that were successfully found.
|
|
67
|
+
Efficiency describes the number of distances that had to be computed for
|
|
68
|
+
each query, normalized to size of the `index`. Highest efficiency is 0.(9),
|
|
69
|
+
lowest is zero. Highest is achieved, when the distance metric was computed
|
|
70
|
+
just once per query. Lowest happens during exact search, when every distance
|
|
71
|
+
to every present vector had to be computed.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
index_size: int
|
|
75
|
+
count_queries: int
|
|
76
|
+
count_matches: int
|
|
77
|
+
|
|
78
|
+
visited_members: int
|
|
79
|
+
computed_distances: int
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def mean_efficiency(self) -> float:
|
|
83
|
+
return 1 - float(self.computed_distances) / (
|
|
84
|
+
self.count_queries * self.index_size
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def mean_recall(self) -> float:
|
|
89
|
+
return self.count_matches / self.count_queries
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def self_recall(index: Index, sample: float = 1, **kwargs) -> SearchStats:
|
|
52
93
|
"""Simplest benchmark for a quality of search, which queries every
|
|
53
94
|
existing member of the index, to make sure approximate search finds
|
|
54
95
|
the point itself.
|
|
55
96
|
|
|
56
97
|
:param index: Non-empty pre-constructed index
|
|
57
98
|
:type index: Index
|
|
58
|
-
:
|
|
59
|
-
:
|
|
99
|
+
:param sample: Share of vectors to search, defaults to 1
|
|
100
|
+
:type sample: float
|
|
101
|
+
:return: Evaluation report with key metrics
|
|
102
|
+
:rtype: SearchStats
|
|
60
103
|
"""
|
|
61
104
|
if len(index) == 0:
|
|
62
105
|
return 0
|
|
63
106
|
if "k" not in kwargs:
|
|
64
107
|
kwargs["k"] = 1
|
|
65
108
|
|
|
66
|
-
|
|
109
|
+
keys = index.keys
|
|
67
110
|
if sample != 1:
|
|
68
|
-
|
|
111
|
+
keys = np.random.choice(keys, int(ceil(len(keys) * sample)))
|
|
69
112
|
|
|
70
|
-
queries = index.get_vectors(
|
|
71
|
-
matches:
|
|
72
|
-
|
|
113
|
+
queries = index.get_vectors(keys, index.dtype)
|
|
114
|
+
matches: BatchMatches = index.search(queries, **kwargs)
|
|
115
|
+
count_matches: float = matches.count_matches(keys)
|
|
116
|
+
return SearchStats(
|
|
117
|
+
index_size=len(index),
|
|
118
|
+
count_queries=len(keys),
|
|
119
|
+
count_matches=count_matches,
|
|
120
|
+
visited_members=matches.visited_members,
|
|
121
|
+
computed_distances=matches.computed_distances,
|
|
122
|
+
)
|
|
73
123
|
|
|
74
124
|
|
|
75
125
|
def measure_seconds(f: Callable) -> Tuple[float, Any]:
|
|
@@ -131,9 +181,9 @@ def relevance(
|
|
|
131
181
|
) -> np.ndarray:
|
|
132
182
|
"""Calculate relevance scores. Binary relevance scores
|
|
133
183
|
|
|
134
|
-
:param expected: ground-truth
|
|
184
|
+
:param expected: ground-truth keys
|
|
135
185
|
:type expected: np.ndarray
|
|
136
|
-
:param predicted: predicted
|
|
186
|
+
:param predicted: predicted keys
|
|
137
187
|
:type predicted: np.ndarray
|
|
138
188
|
"""
|
|
139
189
|
expected = expected[:k]
|
|
@@ -143,7 +193,7 @@ def relevance(
|
|
|
143
193
|
|
|
144
194
|
@dataclass
|
|
145
195
|
class Dataset:
|
|
146
|
-
|
|
196
|
+
keys: np.ndarray
|
|
147
197
|
vectors: np.ndarray
|
|
148
198
|
queries: np.ndarray
|
|
149
199
|
neighbors: np.ndarray
|
|
@@ -189,7 +239,7 @@ class Dataset:
|
|
|
189
239
|
else d.vectors.shape[0]
|
|
190
240
|
)
|
|
191
241
|
d.vectors = d.vectors[:count, :]
|
|
192
|
-
d.
|
|
242
|
+
d.keys = np.arange(count, dtype=Key)
|
|
193
243
|
|
|
194
244
|
if queries is not None:
|
|
195
245
|
d.queries = load_matrix(queries)
|
|
@@ -202,7 +252,7 @@ class Dataset:
|
|
|
202
252
|
d.neighbors = d.neighbors[:, :k]
|
|
203
253
|
else:
|
|
204
254
|
assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
|
|
205
|
-
d.neighbors = np.reshape(d.
|
|
255
|
+
d.neighbors = np.reshape(d.keys, (count, 1))
|
|
206
256
|
|
|
207
257
|
else:
|
|
208
258
|
assert ndim is not None
|
|
@@ -211,8 +261,8 @@ class Dataset:
|
|
|
211
261
|
|
|
212
262
|
d.vectors = random_vectors(count=count, ndim=ndim)
|
|
213
263
|
d.queries = d.vectors
|
|
214
|
-
d.
|
|
215
|
-
d.neighbors = np.reshape(d.
|
|
264
|
+
d.keys = np.arange(count, dtype=Key)
|
|
265
|
+
d.neighbors = np.reshape(d.keys, (count, 1))
|
|
216
266
|
|
|
217
267
|
return d
|
|
218
268
|
|
|
@@ -276,13 +326,13 @@ class TaskResult:
|
|
|
276
326
|
|
|
277
327
|
@dataclass
|
|
278
328
|
class AddTask:
|
|
279
|
-
|
|
329
|
+
keys: np.ndarray
|
|
280
330
|
vectors: np.ndarray
|
|
281
331
|
|
|
282
332
|
def __call__(self, index: Index) -> TaskResult:
|
|
283
333
|
batch_size: int = self.vectors.shape[0]
|
|
284
334
|
old_size: int = len(index)
|
|
285
|
-
dt, _ = measure_seconds(lambda: index.add(self.
|
|
335
|
+
dt, _ = measure_seconds(lambda: index.add(self.keys, self.vectors))
|
|
286
336
|
|
|
287
337
|
assert len(index) == old_size + batch_size
|
|
288
338
|
return TaskResult(
|
|
@@ -299,11 +349,11 @@ class AddTask:
|
|
|
299
349
|
return self.vectors.shape[0]
|
|
300
350
|
|
|
301
351
|
def inplace_shuffle(self):
|
|
302
|
-
"""Rorders the `vectors` and `
|
|
352
|
+
"""Rorders the `vectors` and `keys`. Often used for robustness benchmarks."""
|
|
303
353
|
|
|
304
354
|
new_order = np.arange(self.count)
|
|
305
355
|
np.random.shuffle(new_order)
|
|
306
|
-
self.
|
|
356
|
+
self.keys = self.keys[new_order]
|
|
307
357
|
self.vectors = self.vectors[new_order, :]
|
|
308
358
|
|
|
309
359
|
def slices(self, batch_size: int) -> List[AddTask]:
|
|
@@ -311,7 +361,7 @@ class AddTask:
|
|
|
311
361
|
|
|
312
362
|
return [
|
|
313
363
|
AddTask(
|
|
314
|
-
|
|
364
|
+
keys=self.keys[start_row : start_row + batch_size],
|
|
315
365
|
vectors=self.vectors[start_row : start_row + batch_size, :],
|
|
316
366
|
)
|
|
317
367
|
for start_row in range(0, self.count, batch_size)
|
|
@@ -334,7 +384,7 @@ class AddTask:
|
|
|
334
384
|
|
|
335
385
|
return [
|
|
336
386
|
AddTask(
|
|
337
|
-
|
|
387
|
+
keys=self.keys[rows],
|
|
338
388
|
vectors=self.vectors[rows, :],
|
|
339
389
|
)
|
|
340
390
|
for rows in partitioning.values()
|
|
@@ -379,7 +429,7 @@ class Evaluation:
|
|
|
379
429
|
dataset: Dataset, batch_size: int = 0, clusters: int = 1
|
|
380
430
|
) -> Evaluation:
|
|
381
431
|
tasks = []
|
|
382
|
-
add = AddTask(vectors=dataset.vectors,
|
|
432
|
+
add = AddTask(vectors=dataset.vectors, keys=dataset.keys)
|
|
383
433
|
search = SearchTask(queries=dataset.queries, neighbors=dataset.neighbors)
|
|
384
434
|
|
|
385
435
|
if batch_size:
|