usearch 0.14.0 → 0.15.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/release.yml +56 -4
- package/.github/workflows/update_version.sh +6 -1
- package/.vscode/launch.json +21 -22
- package/.vscode/settings.json +7 -1
- package/CITATION.cff +4 -4
- package/CMakeLists.txt +1 -1
- package/Cargo.toml +1 -1
- package/Package.swift +0 -1
- package/README.md +100 -27
- package/binding.gyp +0 -1
- package/build.gradle +1 -1
- package/build.rs +3 -3
- package/c/lib.cpp +32 -9
- package/c/usearch.h +67 -36
- package/conanfile.py +1 -1
- package/{src → cpp}/bench.cpp +52 -36
- package/{src → cpp}/test.cpp +17 -7
- package/docs/benchmarks.md +33 -10
- package/docs/c/index.rst +11 -0
- package/docs/c/reference.rst +7 -0
- package/docs/compilation.md +24 -9
- package/docs/conf.dox +6 -6
- package/docs/conf.py +3 -1
- package/docs/cpp/index.rst +11 -0
- package/docs/cpp/reference.rst +7 -0
- package/docs/index.rst +10 -0
- package/docs/java/index.rst +11 -0
- package/docs/java/reference.rst +7 -0
- package/docs/javascript/index.rst +11 -0
- package/docs/javascript/reference.rst +9 -0
- package/docs/python/index.rst +11 -0
- package/docs/python/reference.rst +35 -0
- package/golang/lib.go +1 -1
- package/include/usearch/{usearch.hpp → index.hpp} +152 -148
- package/include/usearch/index_punned_dense.hpp +633 -0
- package/include/usearch/index_punned_helpers.hpp +528 -0
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
- package/javascript/lib.cpp +26 -50
- package/javascript/usearch.js +117 -2
- package/objc/USearchObjective.mm +145 -49
- package/objc/include/USearchObjective.h +106 -30
- package/package.json +2 -2
- package/python/lib.cpp +134 -331
- package/python/scripts/bench.ipynb +170 -0
- package/python/scripts/bench.py +87 -175
- package/python/scripts/index_faiss.py +100 -0
- package/python/scripts/test.py +41 -37
- package/python/usearch/eval.py +353 -0
- package/python/usearch/index.py +117 -113
- package/python/usearch/io.py +7 -2
- package/python/usearch/join.py +100 -0
- package/python/usearch/numba.py +101 -0
- package/rust/lib.cpp +27 -14
- package/rust/lib.hpp +4 -10
- package/rust/lib.rs +50 -17
- package/setup.py +12 -2
- package/simsimd/include/simsimd/simsimd.h +32 -3
- package/swift/Index+Sugar.swift +55 -29
- package/swift/Test.swift +1 -1
- package/wolfram/lib.cpp +4 -4
- package/python/scripts/bench_params.py +0 -201
- package/python/usearch/synthetic.py +0 -31
- package/src/punned.hpp +0 -906
- /package/{src → cpp}/CMakeLists.txt +0 -0
package/python/scripts/test.py
CHANGED
|
@@ -3,9 +3,9 @@ import pytest
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from usearch.io import load_matrix, save_matrix
|
|
6
|
-
from usearch.
|
|
6
|
+
from usearch.eval import recall_members, random_vectors
|
|
7
7
|
|
|
8
|
-
from usearch.index import Index,
|
|
8
|
+
from usearch.index import Index, SparseIndex, MetricKind, ScalarKind, Matches
|
|
9
9
|
from usearch.index import (
|
|
10
10
|
DEFAULT_CONNECTIVITY,
|
|
11
11
|
DEFAULT_EXPANSION_ADD,
|
|
@@ -13,10 +13,14 @@ from usearch.index import (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
dimensions = [
|
|
17
|
-
batch_sizes = [1,
|
|
18
|
-
index_types = [
|
|
19
|
-
|
|
16
|
+
dimensions = [3, 97, 256]
|
|
17
|
+
batch_sizes = [1, 33]
|
|
18
|
+
index_types = [
|
|
19
|
+
ScalarKind.F32, ScalarKind.F64,
|
|
20
|
+
ScalarKind.F16, ScalarKind.F8,
|
|
21
|
+
]
|
|
22
|
+
numpy_types = [np.float32, np.float64, np.float16]
|
|
23
|
+
|
|
20
24
|
connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
|
|
21
25
|
jit_options = [False]
|
|
22
26
|
continuous_metrics = [
|
|
@@ -24,9 +28,9 @@ continuous_metrics = [
|
|
|
24
28
|
MetricKind.L2sq,
|
|
25
29
|
]
|
|
26
30
|
hash_metrics = [
|
|
27
|
-
MetricKind.
|
|
28
|
-
MetricKind.
|
|
29
|
-
MetricKind.
|
|
31
|
+
MetricKind.Hamming,
|
|
32
|
+
MetricKind.Tanimoto,
|
|
33
|
+
MetricKind.Sorensen,
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
|
|
@@ -77,9 +81,15 @@ def test_index(
|
|
|
77
81
|
|
|
78
82
|
limit = 0.7 if numpy_type != np.byte else 70
|
|
79
83
|
vector = np.random.uniform(0, limit, (index.ndim)).astype(numpy_type)
|
|
84
|
+
|
|
80
85
|
index.add(42, vector)
|
|
81
|
-
matches, distances, count = index.search(vector, 10)
|
|
82
86
|
|
|
87
|
+
assert 42 in index
|
|
88
|
+
assert 42 in index.labels
|
|
89
|
+
if numpy_type != np.byte:
|
|
90
|
+
assert np.allclose(index[42], vector, atol=0.1)
|
|
91
|
+
|
|
92
|
+
matches, distances, count = index.search(vector, 10)
|
|
83
93
|
assert len(index) == 1
|
|
84
94
|
assert len(matches) == count
|
|
85
95
|
assert len(distances) == count
|
|
@@ -87,11 +97,6 @@ def test_index(
|
|
|
87
97
|
assert matches[0] == 42
|
|
88
98
|
assert distances[0] == pytest.approx(0, abs=1e-3)
|
|
89
99
|
|
|
90
|
-
assert 42 in index
|
|
91
|
-
assert 42 in index.labels
|
|
92
|
-
if numpy_type != np.byte:
|
|
93
|
-
assert np.allclose(index[42], vector, atol=0.1)
|
|
94
|
-
|
|
95
100
|
index.save('tmp.usearch')
|
|
96
101
|
index.clear()
|
|
97
102
|
assert len(index) == 0
|
|
@@ -111,14 +116,15 @@ def test_index_batch(
|
|
|
111
116
|
|
|
112
117
|
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
vectors =
|
|
116
|
-
0, limit, (batch_size, index.ndim)).astype(numpy_type)
|
|
117
|
-
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
119
|
+
labels = np.arange(batch_size)
|
|
120
|
+
vectors = random_vectors(count=batch_size, ndim=ndim, dtype=numpy_type)
|
|
118
121
|
|
|
119
122
|
index.add(labels, vectors)
|
|
120
|
-
|
|
123
|
+
assert len(index) == batch_size
|
|
124
|
+
assert np.allclose(index.get_vectors(
|
|
125
|
+
labels).astype(numpy_type), vectors, atol=0.1)
|
|
121
126
|
|
|
127
|
+
matches: Matches = index.search(vectors, 10)
|
|
122
128
|
assert matches.labels.shape[0] == matches.distances.shape[0]
|
|
123
129
|
assert matches.counts.shape[0] == batch_size
|
|
124
130
|
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
@@ -128,7 +134,7 @@ def test_index_batch(
|
|
|
128
134
|
|
|
129
135
|
@pytest.mark.parametrize('ndim', dimensions)
|
|
130
136
|
@pytest.mark.parametrize('batch_size', batch_sizes)
|
|
131
|
-
def
|
|
137
|
+
def test_index_numba(ndim: int, batch_size: int):
|
|
132
138
|
|
|
133
139
|
try:
|
|
134
140
|
from numba import cfunc, types, carray
|
|
@@ -137,37 +143,35 @@ def test_index_udf(ndim: int, batch_size: int):
|
|
|
137
143
|
|
|
138
144
|
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
139
145
|
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
140
|
-
signature = types.float32(
|
|
141
|
-
types.CPointer(types.float32)
|
|
142
|
-
types.CPointer(types.float32),
|
|
143
|
-
types.uint64, types.uint64)
|
|
146
|
+
signature = types.float32(types.CPointer(
|
|
147
|
+
types.float32), types.CPointer(types.float32))
|
|
144
148
|
|
|
145
149
|
@cfunc(signature)
|
|
146
|
-
def python_dot(a, b
|
|
147
|
-
a_array = carray(a,
|
|
148
|
-
b_array = carray(b,
|
|
150
|
+
def python_dot(a, b):
|
|
151
|
+
a_array = carray(a, ndim)
|
|
152
|
+
b_array = carray(b, ndim)
|
|
149
153
|
c = 0.0
|
|
150
|
-
for i in range(
|
|
154
|
+
for i in range(ndim):
|
|
151
155
|
c += a_array[i] * b_array[i]
|
|
152
|
-
return c
|
|
156
|
+
return 1 - c
|
|
153
157
|
|
|
154
158
|
index = Index(ndim=ndim, metric=python_dot)
|
|
155
159
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
160
|
+
labels = np.arange(batch_size)
|
|
161
|
+
vectors = random_vectors(count=batch_size, ndim=ndim)
|
|
159
162
|
|
|
160
163
|
index.add(labels, vectors)
|
|
161
164
|
matches, distances, count = index.search(vectors, 10)
|
|
162
|
-
|
|
163
165
|
assert matches.shape[0] == distances.shape[0]
|
|
164
166
|
assert count.shape[0] == batch_size
|
|
165
167
|
|
|
168
|
+
assert recall_members(index, exact=True) == 1
|
|
169
|
+
|
|
166
170
|
|
|
167
171
|
@pytest.mark.parametrize('connectivity', connectivity_options)
|
|
168
172
|
def test_sets_index(connectivity: int):
|
|
169
173
|
|
|
170
|
-
index =
|
|
174
|
+
index = SparseIndex(connectivity=connectivity)
|
|
171
175
|
index.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
172
176
|
index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
173
177
|
results = index.search(np.array([12, 15], dtype=np.uint32), 10)
|
|
@@ -178,13 +182,13 @@ def test_sets_index(connectivity: int):
|
|
|
178
182
|
@pytest.mark.parametrize('metric', hash_metrics)
|
|
179
183
|
@pytest.mark.parametrize('connectivity', connectivity_options)
|
|
180
184
|
@pytest.mark.parametrize('batch_size', batch_sizes)
|
|
181
|
-
def
|
|
185
|
+
def test_bitwise_index(bits: int, metric: MetricKind, connectivity: int, batch_size: int):
|
|
182
186
|
|
|
183
187
|
index = Index(ndim=bits, metric=metric, connectivity=connectivity)
|
|
184
188
|
|
|
189
|
+
labels = np.arange(batch_size)
|
|
185
190
|
bit_vectors = np.random.randint(2, size=(batch_size, bits))
|
|
186
191
|
bit_vectors = np.packbits(bit_vectors, axis=1)
|
|
187
|
-
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
188
192
|
|
|
189
193
|
index.add(labels, bit_vectors)
|
|
190
194
|
index.search(bit_vectors, 10)
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from time import time_ns
|
|
3
|
+
from typing import Tuple, Any, Callable, Union, Optional, List
|
|
4
|
+
from dataclasses import dataclass, asdict
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from usearch.io import load_matrix
|
|
10
|
+
from usearch.index import Index, Matches, MetricKind, MetricKindBitwise
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def random_vectors(
|
|
14
|
+
count: int,
|
|
15
|
+
metric: MetricKind = MetricKind.IP,
|
|
16
|
+
dtype: np.generic = np.float32,
|
|
17
|
+
ndim: Optional[int] = None,
|
|
18
|
+
index: Optional[Index] = None,
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
"""Produces a collection of random vectors normalized for the provided `metric`
|
|
21
|
+
and matching wanted `dtype`, which can both be inferred from an existing `index`.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Infer default parameters from the `index`, if passed
|
|
25
|
+
if index is not None:
|
|
26
|
+
if not isinstance(index, Index):
|
|
27
|
+
raise ValueError('Unsupported `index` type')
|
|
28
|
+
|
|
29
|
+
ndim = index.ndim
|
|
30
|
+
dtype = index.numpy_dtype
|
|
31
|
+
metric = index.metric
|
|
32
|
+
|
|
33
|
+
# Produce data
|
|
34
|
+
if metric in MetricKindBitwise:
|
|
35
|
+
bit_vectors = np.random.randint(2, size=(count, ndim))
|
|
36
|
+
bit_vectors = np.packbits(bit_vectors, axis=1)
|
|
37
|
+
return bit_vectors
|
|
38
|
+
|
|
39
|
+
else:
|
|
40
|
+
|
|
41
|
+
x = np.random.rand(count, ndim)
|
|
42
|
+
if dtype == np.int8:
|
|
43
|
+
x = (x * 100).astype(np.int8)
|
|
44
|
+
else:
|
|
45
|
+
x = x.astype(dtype)
|
|
46
|
+
if metric == MetricKind.IP:
|
|
47
|
+
return x / np.linalg.norm(x, axis=1, keepdims=True)
|
|
48
|
+
return x
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def recall_members(index: Index, *args, **kwargs) -> float:
|
|
52
|
+
"""Simplest benchmark for a quality of search, which queries every
|
|
53
|
+
existing member of the index, to make sure approximate search finds
|
|
54
|
+
the point itself.
|
|
55
|
+
|
|
56
|
+
:param index: Non-empty pre-constructed index
|
|
57
|
+
:type index: Index
|
|
58
|
+
:return: Value from 0 to 1, for the share of found self-references
|
|
59
|
+
:rtype: float
|
|
60
|
+
"""
|
|
61
|
+
if len(index) == 0:
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
matches: Matches = index.search(index.vectors, 1, *args, **kwargs)
|
|
65
|
+
return matches.recall_first(index.labels)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def measure_seconds(f: Callable) -> Tuple[float, Any]:
|
|
69
|
+
"""Simple function profiling decorator.
|
|
70
|
+
|
|
71
|
+
:param f: Function to be profiled
|
|
72
|
+
:type f: Callable
|
|
73
|
+
:return: Time elapsed in seconds and the result of the execution
|
|
74
|
+
:rtype: Tuple[float, Any]
|
|
75
|
+
"""
|
|
76
|
+
a = time_ns()
|
|
77
|
+
result = f()
|
|
78
|
+
b = time_ns()
|
|
79
|
+
c = b - a
|
|
80
|
+
secs = c / (10 ** 9)
|
|
81
|
+
return secs, result
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Dataset:
|
|
86
|
+
|
|
87
|
+
labels: np.ndarray
|
|
88
|
+
vectors: np.ndarray
|
|
89
|
+
queries: np.ndarray
|
|
90
|
+
neighbors: np.ndarray
|
|
91
|
+
|
|
92
|
+
def crop_neighbors(self, k: int):
|
|
93
|
+
self.neighbors = self.neighbors[:, k]
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def build(
|
|
97
|
+
vectors: Optional[str] = None,
|
|
98
|
+
queries: Optional[str] = None,
|
|
99
|
+
neighbors: Optional[str] = None,
|
|
100
|
+
count: Optional[int] = None,
|
|
101
|
+
ndim: Optional[int] = None,
|
|
102
|
+
k: Optional[int] = None,
|
|
103
|
+
):
|
|
104
|
+
"""Either loads an existing dataset from disk, or generates one on the fly.
|
|
105
|
+
|
|
106
|
+
:param vectors: _description_, defaults to None
|
|
107
|
+
:type vectors: Optional[str], optional
|
|
108
|
+
:param queries: _description_, defaults to None
|
|
109
|
+
:type queries: Optional[str], optional
|
|
110
|
+
:param neighbors: _description_, defaults to None
|
|
111
|
+
:type neighbors: Optional[str], optional
|
|
112
|
+
:param count: _description_, defaults to None
|
|
113
|
+
:type count: Optional[int], optional
|
|
114
|
+
:param ndim: _description_, defaults to None
|
|
115
|
+
:type ndim: Optional[int], optional
|
|
116
|
+
:param k: _description_, defaults to None
|
|
117
|
+
:type k: Optional[int], optional
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
d = Dataset(None, None, None, None)
|
|
121
|
+
|
|
122
|
+
if vectors is not None:
|
|
123
|
+
assert ndim is None
|
|
124
|
+
|
|
125
|
+
d.vectors = load_matrix(vectors)
|
|
126
|
+
ndim = d.vectors.shape[1]
|
|
127
|
+
count = min(
|
|
128
|
+
d.vectors.shape[0], count) if count is not None else d.vectors.shape[0]
|
|
129
|
+
d.vectors = d.vectors[:count, :]
|
|
130
|
+
d.labels = np.arange(count, dtype=np.longlong)
|
|
131
|
+
|
|
132
|
+
if queries is not None:
|
|
133
|
+
d.queries = load_matrix(queries)
|
|
134
|
+
else:
|
|
135
|
+
d.queries = d.vectors
|
|
136
|
+
|
|
137
|
+
if neighbors is not None:
|
|
138
|
+
d.neighbors = load_matrix(neighbors)
|
|
139
|
+
if k is not None:
|
|
140
|
+
d.neighbors = d.neighbors[:, :k]
|
|
141
|
+
else:
|
|
142
|
+
assert k is None, 'Cant ovveride `k`, will retrieve one neighbor'
|
|
143
|
+
d.neighbors = np.reshape(d.labels, (count, 1))
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
|
|
147
|
+
assert ndim is not None
|
|
148
|
+
assert count is not None
|
|
149
|
+
assert k is None, 'Cant ovveride `k`, will retrieve one neighbor'
|
|
150
|
+
|
|
151
|
+
d.vectors = random_vectors(count=count, ndim=ndim)
|
|
152
|
+
d.queries = d.vectors
|
|
153
|
+
d.labels = np.arange(count, dtype=np.longlong)
|
|
154
|
+
d.neighbors = np.reshape(d.labels, (count, 1))
|
|
155
|
+
|
|
156
|
+
return d
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class TaskResult:
|
|
161
|
+
|
|
162
|
+
add_operations: Optional[int] = None
|
|
163
|
+
add_per_second: Optional[float] = None
|
|
164
|
+
|
|
165
|
+
search_operations: Optional[int] = None
|
|
166
|
+
search_per_second: Optional[float] = None
|
|
167
|
+
recall_at_one: Optional[float] = None
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
parts = []
|
|
171
|
+
if self.add_per_second:
|
|
172
|
+
parts.append(f'{self.add_per_second:.2f} add/s')
|
|
173
|
+
if self.search_per_second:
|
|
174
|
+
parts.append(f'{self.search_per_second:.2f} search/s')
|
|
175
|
+
if self.recall_at_one:
|
|
176
|
+
parts.append(f'{self.recall_at_one * 100:.2f}% recall@1')
|
|
177
|
+
return ', '.join(parts)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def add_seconds(self) -> float:
|
|
181
|
+
return self.add_operations / self.add_per_second
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def search_seconds(self) -> float:
|
|
185
|
+
return self.search_operations / self.search_per_second
|
|
186
|
+
|
|
187
|
+
def __add__(self, other: TaskResult, /):
|
|
188
|
+
result = TaskResult()
|
|
189
|
+
if self.add_operations and other.add_operations:
|
|
190
|
+
result.add_operations = self.add_operations + other.add_operations
|
|
191
|
+
result.add_per_second = result.add_operations / \
|
|
192
|
+
(self.add_seconds + other.add_seconds)
|
|
193
|
+
else:
|
|
194
|
+
base = self if self.add_operations else other
|
|
195
|
+
result.add_operations = base.add_operations
|
|
196
|
+
result.add_per_second = base.add_per_second
|
|
197
|
+
|
|
198
|
+
if self.search_operations and other.search_operations:
|
|
199
|
+
result.search_operations = self.search_operations + other.search_operations
|
|
200
|
+
result.recall_at_one = (
|
|
201
|
+
self.recall_at_one * self.search_operations + other.recall_at_one *
|
|
202
|
+
other.search_operations) / (self.search_operations + other.search_operations)
|
|
203
|
+
result.search_per_second = result.search_operations / \
|
|
204
|
+
(self.search_seconds + other.search_seconds)
|
|
205
|
+
else:
|
|
206
|
+
base = self if self.search_operations else other
|
|
207
|
+
result.search_operations = base.search_operations
|
|
208
|
+
result.search_per_second = base.search_per_second
|
|
209
|
+
result.recall_at_one = base.recall_at_one
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class AddTask:
|
|
216
|
+
|
|
217
|
+
labels: np.ndarray
|
|
218
|
+
vectors: np.ndarray
|
|
219
|
+
|
|
220
|
+
def __call__(self, index: Index) -> TaskResult:
|
|
221
|
+
|
|
222
|
+
batch_size: int = self.vectors.shape[0]
|
|
223
|
+
old_size: int = len(index)
|
|
224
|
+
dt, _ = measure_seconds(
|
|
225
|
+
lambda: index.add(self.labels, self.vectors))
|
|
226
|
+
|
|
227
|
+
assert len(index) == old_size + batch_size
|
|
228
|
+
return TaskResult(
|
|
229
|
+
add_operations=batch_size,
|
|
230
|
+
add_per_second=batch_size/dt,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def ndim(self):
|
|
235
|
+
return self.vectors.shape[1]
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def count(self):
|
|
239
|
+
return self.vectors.shape[0]
|
|
240
|
+
|
|
241
|
+
def inplace_shuffle(self):
|
|
242
|
+
"""Rorders the `vectors` and `labels`. Often used for robustness benchmarks."""
|
|
243
|
+
|
|
244
|
+
new_order = np.arange(self.count)
|
|
245
|
+
np.random.shuffle(new_order)
|
|
246
|
+
self.labels = self.labels[new_order]
|
|
247
|
+
self.vectors = self.vectors[new_order, :]
|
|
248
|
+
|
|
249
|
+
def slices(self, batch_size: int) -> List[AddTask]:
|
|
250
|
+
"""Splits this dataset into smaller chunks."""
|
|
251
|
+
|
|
252
|
+
return [
|
|
253
|
+
AddTask(
|
|
254
|
+
labels=self.labels[start_row:start_row+batch_size],
|
|
255
|
+
vectors=self.vectors[start_row:start_row+batch_size, :],
|
|
256
|
+
) for start_row in range(0, self.count, batch_size)]
|
|
257
|
+
|
|
258
|
+
def clusters(self, number_of_clusters: int) -> List[AddTask]:
|
|
259
|
+
"""Splits this dataset into smaller chunks."""
|
|
260
|
+
|
|
261
|
+
from sklearn.cluster import KMeans
|
|
262
|
+
clustering = KMeans(
|
|
263
|
+
n_clusters=number_of_clusters,
|
|
264
|
+
random_state=0, n_init='auto',
|
|
265
|
+
).fit(self.vectors)
|
|
266
|
+
|
|
267
|
+
partitioning = defaultdict(list)
|
|
268
|
+
for row, cluster in enumerate(clustering.labels_):
|
|
269
|
+
partitioning[cluster].append(row)
|
|
270
|
+
|
|
271
|
+
return [
|
|
272
|
+
AddTask(
|
|
273
|
+
labels=self.labels[rows],
|
|
274
|
+
vectors=self.vectors[rows, :],
|
|
275
|
+
) for rows in partitioning.values()]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@dataclass
|
|
279
|
+
class SearchTask:
|
|
280
|
+
|
|
281
|
+
queries: np.ndarray
|
|
282
|
+
neighbors: np.ndarray
|
|
283
|
+
|
|
284
|
+
def __call__(self, index: Index) -> TaskResult:
|
|
285
|
+
|
|
286
|
+
dt, results = measure_seconds(
|
|
287
|
+
lambda: index.search(self.queries, self.neighbors.shape[1]))
|
|
288
|
+
|
|
289
|
+
return TaskResult(
|
|
290
|
+
search_per_second=self.queries.shape[0]/dt,
|
|
291
|
+
recall_at_one=results.recall_first(self.neighbors[:, 0].flatten()),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
def slices(self, batch_size: int) -> List[SearchTask]:
|
|
295
|
+
"""Splits this dataset into smaller chunks."""
|
|
296
|
+
|
|
297
|
+
return [
|
|
298
|
+
SearchTask(
|
|
299
|
+
queries=self.queries[start_row:start_row+batch_size, :],
|
|
300
|
+
neighbors=self.neighbors[start_row:start_row+batch_size, :],
|
|
301
|
+
) for start_row in range(0, self.queries.shape[0], batch_size)]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@dataclass
|
|
305
|
+
class Evaluation:
|
|
306
|
+
|
|
307
|
+
tasks: List[Union[AddTask, SearchTask]]
|
|
308
|
+
count: int
|
|
309
|
+
ndim: int
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def for_dataset(dataset: Dataset, batch_size: int = 0, clusters: int = 1) -> Evaluation:
|
|
313
|
+
tasks = []
|
|
314
|
+
add = AddTask(
|
|
315
|
+
vectors=dataset.vectors,
|
|
316
|
+
labels=dataset.labels)
|
|
317
|
+
search = SearchTask(
|
|
318
|
+
queries=dataset.queries,
|
|
319
|
+
neighbors=dataset.neighbors)
|
|
320
|
+
|
|
321
|
+
if batch_size:
|
|
322
|
+
tasks.extend(add.slices(batch_size))
|
|
323
|
+
tasks.extend(search.slices(batch_size))
|
|
324
|
+
elif clusters != 1:
|
|
325
|
+
tasks.extend(add.clusters(clusters))
|
|
326
|
+
print(tasks)
|
|
327
|
+
tasks.append(search)
|
|
328
|
+
else:
|
|
329
|
+
tasks.append(add)
|
|
330
|
+
tasks.append(search)
|
|
331
|
+
|
|
332
|
+
return Evaluation(
|
|
333
|
+
tasks=tasks,
|
|
334
|
+
count=add.count,
|
|
335
|
+
ndim=add.ndim,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def __call__(self, index: Index, post_clean: bool = True) -> dict:
|
|
339
|
+
|
|
340
|
+
task_result = TaskResult()
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
for task in self.tasks:
|
|
344
|
+
task_result = task_result + task(index)
|
|
345
|
+
except KeyboardInterrupt:
|
|
346
|
+
pass
|
|
347
|
+
|
|
348
|
+
if post_clean:
|
|
349
|
+
index.clear()
|
|
350
|
+
return {
|
|
351
|
+
**index.specs,
|
|
352
|
+
**asdict(task_result),
|
|
353
|
+
}
|