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.
Files changed (70) hide show
  1. package/.github/workflows/prerelease.yml +2 -21
  2. package/.github/workflows/release.yml +4 -36
  3. package/.github/workflows/update_version.sh +1 -1
  4. package/.gitmodules +1 -4
  5. package/Cargo.toml +1 -1
  6. package/Package.swift +2 -1
  7. package/README.md +86 -42
  8. package/binding.gyp +16 -12
  9. package/build.gradle +1 -1
  10. package/build.rs +0 -1
  11. package/c/CMakeLists.txt +11 -7
  12. package/c/lib.cpp +36 -24
  13. package/c/usearch.h +42 -12
  14. package/cluster.ipynb +167 -0
  15. package/conanfile.py +1 -1
  16. package/cpp/CMakeLists.txt +8 -3
  17. package/cpp/bench.cpp +2 -2
  18. package/cpp/test.cpp +42 -25
  19. package/docs/benchmarks.md +8 -0
  20. package/docs/compilation.md +7 -7
  21. package/docs/conf.py +27 -24
  22. package/docs/javascript/reference.rst +3 -0
  23. package/golang/README.md +31 -7
  24. package/golang/lib.go +1 -1
  25. package/include/usearch/index.hpp +348 -69
  26. package/include/usearch/index_dense.hpp +587 -179
  27. package/include/usearch/index_plugins.hpp +194 -16
  28. package/java/cloud/unum/usearch/Index.java +13 -0
  29. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +40 -9
  30. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +16 -0
  31. package/javascript/docs.js +111 -0
  32. package/javascript/lib.cpp +29 -9
  33. package/javascript/usearch.d.ts +9 -1
  34. package/objc/USearchObjective.mm +11 -11
  35. package/package.json +2 -2
  36. package/pyproject.toml +2 -2
  37. package/python/lib.cpp +488 -158
  38. package/python/scripts/index_faiss.py +3 -3
  39. package/python/scripts/join.py +6 -6
  40. package/python/scripts/test_index.py +219 -0
  41. package/python/scripts/test_sparse.py +21 -21
  42. package/python/scripts/test_tooling.py +111 -0
  43. package/python/usearch/eval.py +9 -5
  44. package/python/usearch/index.py +491 -216
  45. package/rust/lib.cpp +6 -24
  46. package/rust/lib.hpp +1 -3
  47. package/rust/lib.rs +89 -0
  48. package/setup.py +5 -6
  49. package/wasm/CMakeLists.txt +0 -1
  50. package/wasmer.toml +1 -1
  51. package/python/scripts/test.py +0 -364
  52. package/robin-map/.codecov.yml +0 -5
  53. package/robin-map/.github/workflows/ci.yml +0 -136
  54. package/robin-map/CMakeLists.txt +0 -86
  55. package/robin-map/LICENSE +0 -21
  56. package/robin-map/README.md +0 -483
  57. package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +0 -9
  58. package/robin-map/doxygen.conf +0 -2483
  59. package/robin-map/include/tsl/robin_growth_policy.h +0 -406
  60. package/robin-map/include/tsl/robin_hash.h +0 -1639
  61. package/robin-map/include/tsl/robin_map.h +0 -807
  62. package/robin-map/include/tsl/robin_set.h +0 -660
  63. package/robin-map/tests/CMakeLists.txt +0 -26
  64. package/robin-map/tests/custom_allocator_tests.cpp +0 -138
  65. package/robin-map/tests/main.cpp +0 -26
  66. package/robin-map/tests/policy_tests.cpp +0 -97
  67. package/robin-map/tests/robin_map_tests.cpp +0 -1451
  68. package/robin-map/tests/robin_set_tests.cpp +0 -164
  69. package/robin-map/tests/utils.h +0 -443
  70. 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) -> Matches:
48
+ def search(self, queries, k: int) -> BatchMatches:
49
49
  distances, keys = self._faiss.search(queries, k)
50
- return Matches(keys, distances, np.array([k] * queries.shape[0]))
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
@@ -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
- k = 10
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
- k=k,
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
- k, a_name, a_self_recall * 100, secs
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
- k, b_name, b_self_recall * 100, secs
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
- k, a_name, b_name, ab_recall * 100, secs
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
- k, b_name, a_name, ba_recall * 100, secs
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
- SparseIndex,
8
- )
9
- from usearch.index import (
10
- DEFAULT_CONNECTIVITY,
11
- DEFAULT_EXPANSION_ADD,
12
- DEFAULT_EXPANSION_SEARCH,
5
+ Index,
6
+ MetricKind,
7
+ ScalarKind,
13
8
  )
14
9
 
15
10
 
16
- dimensions = [3, 97, 256]
17
- batch_sizes = [1, 77]
18
- connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
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
- @pytest.mark.parametrize("connectivity", connectivity_options)
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
- if __name__ == "__main__":
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
@@ -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 "k" not in kwargs:
107
- kwargs["k"] = 1
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.get_vectors(keys, index.dtype)
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.recall_first(self.neighbors[:, 0].flatten()),
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]: