usearch 1.1.1 → 1.2.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/.github/workflows/prerelease.yml +2 -21
- package/.github/workflows/release.yml +4 -36
- 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 +2 -2
- 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
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
from faiss import IndexHNSWFlat, IndexIVFPQ, read_index
|
|
6
6
|
|
|
7
|
-
from usearch.index import Matches
|
|
7
|
+
from usearch.index import Matches, BatchMatches
|
|
8
8
|
from usearch.index import (
|
|
9
9
|
DEFAULT_CONNECTIVITY,
|
|
10
10
|
DEFAULT_EXPANSION_ADD,
|
|
@@ -45,9 +45,9 @@ class IndexFAISS:
|
|
|
45
45
|
# self._faiss.add_with_ids(vectors, keys)
|
|
46
46
|
self._faiss.add(vectors)
|
|
47
47
|
|
|
48
|
-
def search(self, queries, k: int) ->
|
|
48
|
+
def search(self, queries, k: int) -> BatchMatches:
|
|
49
49
|
distances, keys = self._faiss.search(queries, k)
|
|
50
|
-
return
|
|
50
|
+
return BatchMatches(keys, distances, np.array([k] * queries.shape[0]))
|
|
51
51
|
|
|
52
52
|
def __len__(self) -> int:
|
|
53
53
|
return self._faiss.ntotal
|
package/python/scripts/join.py
CHANGED
|
@@ -21,7 +21,7 @@ from usearch.index import Index, MetricKind, CompiledMetric, MetricSignature
|
|
|
21
21
|
from usearch.io import load_matrix
|
|
22
22
|
from usearch.eval import measure_seconds
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
count = 10
|
|
25
25
|
exact = False
|
|
26
26
|
batch_size = 1024 * 4
|
|
27
27
|
max_elements = 1000000
|
|
@@ -104,7 +104,7 @@ if len(run_diagnostics) == 0 or run_diagnostics.lower() == "y":
|
|
|
104
104
|
|
|
105
105
|
dt = measure_seconds
|
|
106
106
|
args = dict(
|
|
107
|
-
|
|
107
|
+
count=count,
|
|
108
108
|
batch_size=batch_size,
|
|
109
109
|
log=True,
|
|
110
110
|
exact=exact,
|
|
@@ -113,28 +113,28 @@ if len(run_diagnostics) == 0 or run_diagnostics.lower() == "y":
|
|
|
113
113
|
secs, a_self_recall = dt(lambda: a.search(a.vectors, **args).recall(a.keys))
|
|
114
114
|
print(
|
|
115
115
|
"Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
|
|
116
|
-
|
|
116
|
+
count, a_name, a_self_recall * 100, secs
|
|
117
117
|
)
|
|
118
118
|
)
|
|
119
119
|
|
|
120
120
|
secs, b_self_recall = dt(lambda: b.search(b.vectors, **args).recall(b.keys))
|
|
121
121
|
print(
|
|
122
122
|
"Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
|
|
123
|
-
|
|
123
|
+
count, b_name, b_self_recall * 100, secs
|
|
124
124
|
)
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
secs, ab_recall = dt(lambda: b.search(a.vectors, **args).recall(b.keys))
|
|
128
128
|
print(
|
|
129
129
|
"Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
|
|
130
|
-
|
|
130
|
+
count, a_name, b_name, ab_recall * 100, secs
|
|
131
131
|
)
|
|
132
132
|
)
|
|
133
133
|
|
|
134
134
|
secs, ba_recall = dt(lambda: a.search(b.vectors, **args).recall(a.keys))
|
|
135
135
|
print(
|
|
136
136
|
"Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
|
|
137
|
-
|
|
137
|
+
count, b_name, a_name, ba_recall * 100, secs
|
|
138
138
|
)
|
|
139
139
|
)
|
|
140
140
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from usearch.io import load_matrix, save_matrix
|
|
7
|
+
from usearch.eval import random_vectors
|
|
8
|
+
from usearch.index import search
|
|
9
|
+
|
|
10
|
+
from usearch.index import (
|
|
11
|
+
Index,
|
|
12
|
+
MetricKind,
|
|
13
|
+
ScalarKind,
|
|
14
|
+
Match,
|
|
15
|
+
Matches,
|
|
16
|
+
BatchMatches,
|
|
17
|
+
Clustering,
|
|
18
|
+
)
|
|
19
|
+
from usearch.index import (
|
|
20
|
+
DEFAULT_CONNECTIVITY,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ndims = [3, 97, 256]
|
|
25
|
+
batch_sizes = [1, 11, 77]
|
|
26
|
+
quantizations = [
|
|
27
|
+
ScalarKind.F32,
|
|
28
|
+
ScalarKind.F64,
|
|
29
|
+
ScalarKind.F16,
|
|
30
|
+
ScalarKind.I8,
|
|
31
|
+
]
|
|
32
|
+
dtypes = [np.float32, np.float64, np.float16]
|
|
33
|
+
threads = 2
|
|
34
|
+
|
|
35
|
+
connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
|
|
36
|
+
continuous_metrics = [MetricKind.Cos, MetricKind.L2sq]
|
|
37
|
+
hash_metrics = [
|
|
38
|
+
MetricKind.Hamming,
|
|
39
|
+
MetricKind.Tanimoto,
|
|
40
|
+
MetricKind.Sorensen,
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("ndim", [3, 97, 256])
|
|
45
|
+
@pytest.mark.parametrize("metric", [MetricKind.Cos, MetricKind.L2sq])
|
|
46
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
47
|
+
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
48
|
+
@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.float16])
|
|
49
|
+
def test_index_initialization_and_addition(
|
|
50
|
+
ndim, metric, quantization, dtype, batch_size
|
|
51
|
+
):
|
|
52
|
+
index = Index(ndim=ndim, metric=metric, dtype=quantization, multi=False)
|
|
53
|
+
keys = np.arange(batch_size)
|
|
54
|
+
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=dtype)
|
|
55
|
+
index.add(keys, vectors, threads=threads)
|
|
56
|
+
assert len(index) == batch_size
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("ndim", [3, 97, 256])
|
|
60
|
+
@pytest.mark.parametrize("metric", [MetricKind.Cos, MetricKind.L2sq])
|
|
61
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
62
|
+
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
63
|
+
@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.float16])
|
|
64
|
+
def test_index_retrieval(ndim, metric, quantization, dtype, batch_size):
|
|
65
|
+
index = Index(ndim=ndim, metric=metric, dtype=quantization, multi=False)
|
|
66
|
+
keys = np.arange(batch_size)
|
|
67
|
+
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=dtype)
|
|
68
|
+
index.add(keys, vectors, threads=threads)
|
|
69
|
+
vectors_retrived = np.vstack(index.get(keys, dtype))
|
|
70
|
+
assert np.allclose(vectors_retrived, vectors, atol=0.1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize("ndim", [3, 97, 256])
|
|
74
|
+
@pytest.mark.parametrize("metric", [MetricKind.Cos, MetricKind.L2sq])
|
|
75
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
76
|
+
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
77
|
+
@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.float16])
|
|
78
|
+
def test_index_search(ndim, metric, quantization, dtype, batch_size):
|
|
79
|
+
index = Index(ndim=ndim, metric=metric, dtype=quantization, multi=False)
|
|
80
|
+
keys = np.arange(batch_size)
|
|
81
|
+
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=dtype)
|
|
82
|
+
index.add(keys, vectors, threads=threads)
|
|
83
|
+
|
|
84
|
+
if batch_size == 1:
|
|
85
|
+
matches: Matches = index.search(vectors, 10, threads=threads)
|
|
86
|
+
assert matches.keys.ndim == 1
|
|
87
|
+
assert matches.keys.shape[0] == matches.distances.shape[0]
|
|
88
|
+
assert len(matches) == batch_size
|
|
89
|
+
assert np.all(np.sort(index.keys) == np.sort(keys))
|
|
90
|
+
|
|
91
|
+
else:
|
|
92
|
+
matches: BatchMatches = index.search(vectors, 10, threads=threads)
|
|
93
|
+
assert matches.keys.ndim == 2
|
|
94
|
+
assert matches.keys.shape[0] == matches.distances.shape[0]
|
|
95
|
+
assert len(matches) == batch_size
|
|
96
|
+
assert np.all(np.sort(index.keys) == np.sort(keys))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
100
|
+
def test_index_duplicates(batch_size):
|
|
101
|
+
ndim = 8
|
|
102
|
+
index = Index(ndim=ndim, multi=False)
|
|
103
|
+
keys = np.arange(batch_size)
|
|
104
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
105
|
+
index.add(keys, vectors, threads=threads)
|
|
106
|
+
with pytest.raises(Exception):
|
|
107
|
+
index.add(keys, vectors, threads=threads)
|
|
108
|
+
|
|
109
|
+
index = Index(ndim=ndim, multi=True)
|
|
110
|
+
keys = np.arange(batch_size)
|
|
111
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
112
|
+
index.add(keys, vectors, threads=threads)
|
|
113
|
+
index.add(keys, vectors, threads=threads)
|
|
114
|
+
assert len(index) == batch_size * 2
|
|
115
|
+
|
|
116
|
+
two_per_key = index.get(keys)
|
|
117
|
+
assert np.vstack(two_per_key).shape == (2 * batch_size, ndim)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
121
|
+
def test_index_stats(batch_size):
|
|
122
|
+
ndim = 8
|
|
123
|
+
index = Index(ndim=ndim, multi=False)
|
|
124
|
+
keys = np.arange(batch_size)
|
|
125
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
126
|
+
index.add(keys, vectors, threads=threads)
|
|
127
|
+
|
|
128
|
+
assert index.max_level >= 0
|
|
129
|
+
assert index.levels_stats.nodes >= batch_size
|
|
130
|
+
assert index.level_stats(0).nodes == batch_size
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize("ndim", [1, 3, 8, 32, 256, 4096])
|
|
134
|
+
@pytest.mark.parametrize("batch_size", [1, 7, 1024])
|
|
135
|
+
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
136
|
+
def test_index_save_load_restore_copy(ndim, quantization, batch_size):
|
|
137
|
+
index = Index(ndim=ndim, dtype=quantization, multi=False)
|
|
138
|
+
keys = np.arange(batch_size)
|
|
139
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
140
|
+
index.add(keys, vectors, threads=threads)
|
|
141
|
+
|
|
142
|
+
index.save("tmp.usearch")
|
|
143
|
+
index.clear()
|
|
144
|
+
assert len(index) == 0
|
|
145
|
+
|
|
146
|
+
index.load("tmp.usearch")
|
|
147
|
+
assert len(index) == batch_size
|
|
148
|
+
assert len(index[0].flatten()) == ndim
|
|
149
|
+
|
|
150
|
+
index = Index.restore("tmp.usearch", view=True)
|
|
151
|
+
assert len(index) == batch_size
|
|
152
|
+
assert len(index[0].flatten()) == ndim
|
|
153
|
+
|
|
154
|
+
copied_index = index.copy()
|
|
155
|
+
assert len(copied_index) == len(index)
|
|
156
|
+
assert np.allclose(np.vstack(copied_index.get(keys)), np.vstack(index.get(keys)))
|
|
157
|
+
|
|
158
|
+
index.reset()
|
|
159
|
+
os.remove("tmp.usearch")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize("batch_size", [32])
|
|
163
|
+
def test_index_contains_remove_rename(batch_size):
|
|
164
|
+
if batch_size <= 1:
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
ndim = 8
|
|
168
|
+
index = Index(ndim=ndim, multi=False)
|
|
169
|
+
keys = np.arange(batch_size)
|
|
170
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
171
|
+
|
|
172
|
+
index.add(keys, vectors, threads=threads)
|
|
173
|
+
assert np.all(index.contains(keys))
|
|
174
|
+
assert np.all(index.count(keys) == np.ones(batch_size))
|
|
175
|
+
|
|
176
|
+
removed_keys = keys[: batch_size // 2]
|
|
177
|
+
remaining_keys = keys[batch_size // 2 :]
|
|
178
|
+
index.remove(removed_keys)
|
|
179
|
+
assert len(index) == (len(keys) - len(removed_keys))
|
|
180
|
+
assert np.sum(index.contains(keys)) == len(remaining_keys)
|
|
181
|
+
assert np.sum(index.count(keys)) == len(remaining_keys)
|
|
182
|
+
assert np.sum(index.count(removed_keys)) == 0
|
|
183
|
+
|
|
184
|
+
assert keys[0] not in index
|
|
185
|
+
assert keys[-1] in index
|
|
186
|
+
|
|
187
|
+
renamed_counts = index.rename(removed_keys, removed_keys)
|
|
188
|
+
assert np.sum(index.count(renamed_counts)) == 0
|
|
189
|
+
|
|
190
|
+
renamed_counts = index.rename(remaining_keys, removed_keys)
|
|
191
|
+
assert np.sum(index.count(removed_keys)) == len(index)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@pytest.mark.parametrize("ndim", [3, 97, 256])
|
|
195
|
+
@pytest.mark.parametrize("metric", [MetricKind.Cos, MetricKind.L2sq])
|
|
196
|
+
@pytest.mark.parametrize("batch_size", [500, 1024])
|
|
197
|
+
@pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
|
|
198
|
+
@pytest.mark.parametrize("dtype", [np.float32, np.float64, np.float16])
|
|
199
|
+
def test_index_clustering(ndim, metric, quantization, dtype, batch_size):
|
|
200
|
+
index = Index(ndim=ndim, metric=metric, dtype=quantization, multi=False)
|
|
201
|
+
keys = np.arange(batch_size)
|
|
202
|
+
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=dtype)
|
|
203
|
+
index.add(keys, vectors, threads=threads)
|
|
204
|
+
|
|
205
|
+
clusters: Clustering = index.cluster(vectors=vectors, threads=threads)
|
|
206
|
+
assert len(clusters.matches.keys) == batch_size
|
|
207
|
+
|
|
208
|
+
# If no argument is provided, we cluster the present entries
|
|
209
|
+
clusters: Clustering = index.cluster(threads=threads)
|
|
210
|
+
assert len(clusters.matches.keys) == batch_size
|
|
211
|
+
|
|
212
|
+
# If no argument is provided, we cluster the present entries
|
|
213
|
+
clusters: Clustering = index.cluster(keys=keys[:50], threads=threads)
|
|
214
|
+
assert len(clusters.matches.keys) == 50
|
|
215
|
+
|
|
216
|
+
# If no argument is provided, we cluster the present entries
|
|
217
|
+
clusters: Clustering = index.cluster(min_count=3, max_count=10, threads=threads)
|
|
218
|
+
unique_clusters = set(clusters.matches.keys.flatten().tolist())
|
|
219
|
+
assert len(unique_clusters) >= 3 and len(unique_clusters) <= 10
|
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
1
|
import pytest
|
|
4
2
|
import numpy as np
|
|
5
3
|
|
|
6
4
|
from usearch.index import (
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
DEFAULT_CONNECTIVITY,
|
|
11
|
-
DEFAULT_EXPANSION_ADD,
|
|
12
|
-
DEFAULT_EXPANSION_SEARCH,
|
|
5
|
+
Index,
|
|
6
|
+
MetricKind,
|
|
7
|
+
ScalarKind,
|
|
13
8
|
)
|
|
14
9
|
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
@pytest.mark.parametrize("bits", [3, 97, 256, 4097])
|
|
12
|
+
@pytest.mark.parametrize("metric", [MetricKind.Tanimoto])
|
|
13
|
+
@pytest.mark.parametrize("connectivity", [3, 13, 50])
|
|
14
|
+
@pytest.mark.parametrize("batch_size", [3, 77])
|
|
15
|
+
def test_bitwise_index(
|
|
16
|
+
bits: int,
|
|
17
|
+
metric: MetricKind,
|
|
18
|
+
connectivity: int,
|
|
19
|
+
batch_size: int,
|
|
20
|
+
):
|
|
21
|
+
index = Index(ndim=bits, metric=metric, connectivity=connectivity)
|
|
19
22
|
|
|
23
|
+
keys = np.arange(batch_size)
|
|
24
|
+
byte_vectors = np.random.randint(2, size=(batch_size, bits))
|
|
25
|
+
bit_vectors = np.packbits(byte_vectors, axis=1)
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
@pytest.mark.skipif(os.name == "nt", reason="Spurious behaviour on windows")
|
|
23
|
-
def test_sets_index(connectivity: int):
|
|
24
|
-
index = SparseIndex(connectivity=connectivity)
|
|
25
|
-
index.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
26
|
-
index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
27
|
-
results = index.search(np.array([12, 15], dtype=np.uint32), 10)
|
|
28
|
-
assert list(results) == [10, 11]
|
|
27
|
+
index.add(keys, bit_vectors)
|
|
29
28
|
|
|
29
|
+
byte_vectors_retrieved = np.vstack(index.get(keys, ScalarKind.B1))
|
|
30
|
+
assert np.all(byte_vectors_retrieved == bit_vectors)
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
pytest.main(args=["python/scripts/test.py", "-s", "-x", "-v"])
|
|
32
|
+
index.search(bit_vectors, 10)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from usearch.io import load_matrix, save_matrix
|
|
7
|
+
from usearch.index import search
|
|
8
|
+
from usearch.eval import random_vectors
|
|
9
|
+
|
|
10
|
+
from usearch.index import Match, Matches, BatchMatches, Index, Indexes
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
dimensions = [3, 97, 256]
|
|
14
|
+
batch_sizes = [1, 77]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize("rows", batch_sizes)
|
|
18
|
+
@pytest.mark.parametrize("cols", dimensions)
|
|
19
|
+
def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
20
|
+
"""
|
|
21
|
+
Test the serialization of floating point binary matrix.
|
|
22
|
+
|
|
23
|
+
:param int rows: The number of rows in the matrix.
|
|
24
|
+
:param int cols: The number of columns in the matrix.
|
|
25
|
+
"""
|
|
26
|
+
original = np.random.rand(rows, cols).astype(np.float32)
|
|
27
|
+
save_matrix(original, "tmp.fbin")
|
|
28
|
+
reconstructed = load_matrix("tmp.fbin")
|
|
29
|
+
assert np.allclose(original, reconstructed)
|
|
30
|
+
os.remove("tmp.fbin")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.parametrize("rows", batch_sizes)
|
|
34
|
+
@pytest.mark.parametrize("cols", dimensions)
|
|
35
|
+
def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
36
|
+
"""
|
|
37
|
+
Test the serialization of integer binary matrix.
|
|
38
|
+
|
|
39
|
+
:param int rows: The number of rows in the matrix.
|
|
40
|
+
:param int cols: The number of columns in the matrix.
|
|
41
|
+
"""
|
|
42
|
+
original = np.random.randint(0, rows + 1, size=(rows, cols)).astype(np.int32)
|
|
43
|
+
save_matrix(original, "tmp.ibin")
|
|
44
|
+
reconstructed = load_matrix("tmp.ibin")
|
|
45
|
+
assert np.allclose(original, reconstructed)
|
|
46
|
+
os.remove("tmp.ibin")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("rows", batch_sizes)
|
|
50
|
+
@pytest.mark.parametrize("cols", dimensions)
|
|
51
|
+
def test_exact_search(rows: int, cols: int):
|
|
52
|
+
"""
|
|
53
|
+
Test exact search.
|
|
54
|
+
|
|
55
|
+
:param int rows: The number of rows in the matrix.
|
|
56
|
+
:param int cols: The number of columns in the matrix.
|
|
57
|
+
"""
|
|
58
|
+
original = np.random.rand(rows, cols)
|
|
59
|
+
matches: BatchMatches = search(original, original, 10, exact=True)
|
|
60
|
+
top_matches = (
|
|
61
|
+
[int(m.keys[0]) for m in matches] if rows > 1 else int(matches.keys[0])
|
|
62
|
+
)
|
|
63
|
+
assert np.all(top_matches == np.arange(rows))
|
|
64
|
+
|
|
65
|
+
matches: Matches = search(original, original[0], 10, exact=True)
|
|
66
|
+
top_match = int(matches.keys[0])
|
|
67
|
+
assert top_match == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_matches_creation_and_methods():
|
|
71
|
+
matches = Matches(
|
|
72
|
+
keys=np.array([1, 2]),
|
|
73
|
+
distances=np.array([0.5, 0.6]),
|
|
74
|
+
visited_members=2,
|
|
75
|
+
computed_distances=2,
|
|
76
|
+
)
|
|
77
|
+
assert len(matches) == 2
|
|
78
|
+
assert matches[0] == Match(key=1, distance=0.5)
|
|
79
|
+
assert matches.to_list() == [(1, 0.5), (2, 0.6)]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_batch_matches_creation_and_methods():
|
|
83
|
+
keys = np.array([[1, 2], [3, 4]])
|
|
84
|
+
distances = np.array([[0.5, 0.6], [0.7, 0.8]])
|
|
85
|
+
counts = np.array([2, 2])
|
|
86
|
+
batch_matches = BatchMatches(
|
|
87
|
+
keys=keys,
|
|
88
|
+
distances=distances,
|
|
89
|
+
counts=counts,
|
|
90
|
+
visited_members=2,
|
|
91
|
+
computed_distances=2,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
assert len(batch_matches) == 2
|
|
95
|
+
assert batch_matches[0].keys.tolist() == [1, 2]
|
|
96
|
+
assert batch_matches[0].distances.tolist() == [0.5, 0.6]
|
|
97
|
+
assert batch_matches.to_list() == [(1, 0.5), (2, 0.6), (3, 0.7), (4, 0.8)]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_multi_index():
|
|
101
|
+
ndim = 10
|
|
102
|
+
index_a = Index(ndim=ndim)
|
|
103
|
+
index_b = Index(ndim=ndim)
|
|
104
|
+
|
|
105
|
+
vectors = random_vectors(count=3, ndim=ndim)
|
|
106
|
+
index_a.add(42, vectors[0])
|
|
107
|
+
index_b.add(43, vectors[1])
|
|
108
|
+
|
|
109
|
+
indexes = Indexes([index_a, index_b])
|
|
110
|
+
matches = indexes.search(vectors[2], 10)
|
|
111
|
+
assert len(matches) == 2
|
package/python/usearch/eval.py
CHANGED
|
@@ -103,14 +103,18 @@ def self_recall(index: Index, sample: float = 1, **kwargs) -> SearchStats:
|
|
|
103
103
|
"""
|
|
104
104
|
if len(index) == 0:
|
|
105
105
|
return 0
|
|
106
|
-
if "
|
|
107
|
-
kwargs["
|
|
106
|
+
if "count" not in kwargs:
|
|
107
|
+
kwargs["count"] = 1
|
|
108
|
+
|
|
109
|
+
if "keys" in kwargs:
|
|
110
|
+
keys = kwargs["keys"]
|
|
111
|
+
else:
|
|
112
|
+
keys = np.array(index.keys)
|
|
108
113
|
|
|
109
|
-
keys = index.keys
|
|
110
114
|
if sample != 1:
|
|
111
115
|
keys = np.random.choice(keys, int(ceil(len(keys) * sample)))
|
|
112
116
|
|
|
113
|
-
queries = index.
|
|
117
|
+
queries = index.get(keys, index.dtype)
|
|
114
118
|
matches: BatchMatches = index.search(queries, **kwargs)
|
|
115
119
|
count_matches: float = matches.count_matches(keys)
|
|
116
120
|
return SearchStats(
|
|
@@ -403,7 +407,7 @@ class SearchTask:
|
|
|
403
407
|
|
|
404
408
|
return TaskResult(
|
|
405
409
|
search_per_second=self.queries.shape[0] / dt,
|
|
406
|
-
recall_at_one=results.
|
|
410
|
+
recall_at_one=results.mean_recall(self.neighbors[:, 0].flatten()),
|
|
407
411
|
)
|
|
408
412
|
|
|
409
413
|
def slices(self, batch_size: int) -> List[SearchTask]:
|