usearch 0.18.6 → 0.19.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/Cargo.toml +1 -1
- package/README.md +184 -11
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +11 -9
- package/docs/compilation.md +4 -2
- package/include/usearch/index.hpp +269 -50
- package/include/usearch/index_punned_dense.hpp +123 -108
- package/include/usearch/index_punned_helpers.hpp +5 -13
- package/package.json +1 -1
- package/python/lib.cpp +116 -40
- package/python/scripts/bench.ipynb +4 -4
- package/python/scripts/bench.py +51 -28
- package/python/scripts/index_faiss.py +19 -19
- package/python/scripts/test.py +275 -56
- package/python/usearch/eval.py +100 -49
- package/python/usearch/index.py +118 -88
- package/python/usearch/io.py +23 -23
- package/python/usearch/join.py +2 -4
- package/python/usearch/numba.py +44 -30
- package/python/usearch/server.py +38 -26
- package/simsimd/.vscode/settings.json +2 -1
- package/simsimd/CMakeLists.txt +1 -1
- package/simsimd/README.md +14 -2
- package/simsimd/bench.cxx +44 -27
- package/simsimd/include/simsimd/simsimd.h +22 -7
- package/simsimd/include/simsimd/simsimd.hpp +0 -443
package/python/usearch/eval.py
CHANGED
|
@@ -24,7 +24,7 @@ def random_vectors(
|
|
|
24
24
|
# Infer default parameters from the `index`, if passed
|
|
25
25
|
if index is not None:
|
|
26
26
|
if not isinstance(index, Index):
|
|
27
|
-
raise ValueError(
|
|
27
|
+
raise ValueError("Unsupported `index` type")
|
|
28
28
|
|
|
29
29
|
ndim = index.ndim
|
|
30
30
|
dtype = index.numpy_dtype
|
|
@@ -37,7 +37,6 @@ def random_vectors(
|
|
|
37
37
|
return bit_vectors
|
|
38
38
|
|
|
39
39
|
else:
|
|
40
|
-
|
|
41
40
|
x = np.random.rand(count, ndim)
|
|
42
41
|
if dtype == np.int8:
|
|
43
42
|
x = (x * 100).astype(np.int8)
|
|
@@ -77,13 +76,65 @@ def measure_seconds(f: Callable) -> Tuple[float, Any]:
|
|
|
77
76
|
result = f()
|
|
78
77
|
b = time_ns()
|
|
79
78
|
c = b - a
|
|
80
|
-
secs = c / (10
|
|
79
|
+
secs = c / (10**9)
|
|
81
80
|
return secs, result
|
|
82
81
|
|
|
83
82
|
|
|
83
|
+
def dcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
|
|
84
|
+
"""Calculate DCG (Discounted Cumulative Gain) up to position k.
|
|
85
|
+
|
|
86
|
+
:param relevances: List of true relevance scores (in the order as they are ranked)
|
|
87
|
+
:type relevances: list
|
|
88
|
+
:param k: Position up to which DCG is computed
|
|
89
|
+
:type k: int
|
|
90
|
+
:return: The DCG score at position k
|
|
91
|
+
:rtype: float
|
|
92
|
+
"""
|
|
93
|
+
if k:
|
|
94
|
+
relevances = np.asarray(relevances)[:k]
|
|
95
|
+
|
|
96
|
+
n_relevances = len(relevances)
|
|
97
|
+
if n_relevances == 0:
|
|
98
|
+
return 0.0
|
|
99
|
+
|
|
100
|
+
discounts = np.log2(np.arange(n_relevances) + 2)
|
|
101
|
+
return np.sum(relevances / discounts)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def ndcg(relevances: np.ndarray, k: Optional[int] = None) -> np.ndarray:
|
|
105
|
+
"""Calculate NDCG (Normalized Discounted Cumulative Gain) at position k.
|
|
106
|
+
|
|
107
|
+
:param relevances: List of true relevance scores (in the order as they are ranked)
|
|
108
|
+
:type relevances: list
|
|
109
|
+
:param k: Position up to which NDCG is computed
|
|
110
|
+
:type k: int
|
|
111
|
+
:return: The NDCG score at position k
|
|
112
|
+
:rtype: float
|
|
113
|
+
"""
|
|
114
|
+
best_dcg = dcg(sorted(relevances, reverse=True), k)
|
|
115
|
+
if best_dcg == 0:
|
|
116
|
+
return 0.0
|
|
117
|
+
|
|
118
|
+
return dcg(relevances, k) / best_dcg
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def relevance(
|
|
122
|
+
expected: np.ndarray, predicted: np.ndarray, k: Optional[int] = None
|
|
123
|
+
) -> np.ndarray:
|
|
124
|
+
"""Calculate relevance scores. Binary relevance scores
|
|
125
|
+
|
|
126
|
+
:param expected: ground-truth labels
|
|
127
|
+
:type expected: np.ndarray
|
|
128
|
+
:param predicted: predicted labels
|
|
129
|
+
:type predicted: np.ndarray
|
|
130
|
+
"""
|
|
131
|
+
expected = expected[:k]
|
|
132
|
+
predicted = predicted[:k]
|
|
133
|
+
return [1 if i in expected else 0 for i in predicted]
|
|
134
|
+
|
|
135
|
+
|
|
84
136
|
@dataclass
|
|
85
137
|
class Dataset:
|
|
86
|
-
|
|
87
138
|
labels: np.ndarray
|
|
88
139
|
vectors: np.ndarray
|
|
89
140
|
queries: np.ndarray
|
|
@@ -124,8 +175,11 @@ class Dataset:
|
|
|
124
175
|
|
|
125
176
|
d.vectors = load_matrix(vectors)
|
|
126
177
|
ndim = d.vectors.shape[1]
|
|
127
|
-
count =
|
|
128
|
-
d.vectors.shape[0], count)
|
|
178
|
+
count = (
|
|
179
|
+
min(d.vectors.shape[0], count)
|
|
180
|
+
if count is not None
|
|
181
|
+
else d.vectors.shape[0]
|
|
182
|
+
)
|
|
129
183
|
d.vectors = d.vectors[:count, :]
|
|
130
184
|
d.labels = np.arange(count, dtype=Label)
|
|
131
185
|
|
|
@@ -139,14 +193,13 @@ class Dataset:
|
|
|
139
193
|
if k is not None:
|
|
140
194
|
d.neighbors = d.neighbors[:, :k]
|
|
141
195
|
else:
|
|
142
|
-
assert k is None,
|
|
196
|
+
assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
|
|
143
197
|
d.neighbors = np.reshape(d.labels, (count, 1))
|
|
144
198
|
|
|
145
199
|
else:
|
|
146
|
-
|
|
147
200
|
assert ndim is not None
|
|
148
201
|
assert count is not None
|
|
149
|
-
assert k is None,
|
|
202
|
+
assert k is None, "Cant ovveride `k`, will retrieve one neighbor"
|
|
150
203
|
|
|
151
204
|
d.vectors = random_vectors(count=count, ndim=ndim)
|
|
152
205
|
d.queries = d.vectors
|
|
@@ -158,7 +211,6 @@ class Dataset:
|
|
|
158
211
|
|
|
159
212
|
@dataclass
|
|
160
213
|
class TaskResult:
|
|
161
|
-
|
|
162
214
|
add_operations: Optional[int] = None
|
|
163
215
|
add_per_second: Optional[float] = None
|
|
164
216
|
|
|
@@ -169,12 +221,12 @@ class TaskResult:
|
|
|
169
221
|
def __repr__(self) -> str:
|
|
170
222
|
parts = []
|
|
171
223
|
if self.add_per_second:
|
|
172
|
-
parts.append(f
|
|
224
|
+
parts.append(f"{self.add_per_second:.2f} add/s")
|
|
173
225
|
if self.search_per_second:
|
|
174
|
-
parts.append(f
|
|
226
|
+
parts.append(f"{self.search_per_second:.2f} search/s")
|
|
175
227
|
if self.recall_at_one:
|
|
176
|
-
parts.append(f
|
|
177
|
-
return
|
|
228
|
+
parts.append(f"{self.recall_at_one * 100:.2f}% recall@1")
|
|
229
|
+
return ", ".join(parts)
|
|
178
230
|
|
|
179
231
|
@property
|
|
180
232
|
def add_seconds(self) -> float:
|
|
@@ -188,8 +240,9 @@ class TaskResult:
|
|
|
188
240
|
result = TaskResult()
|
|
189
241
|
if self.add_operations and other.add_operations:
|
|
190
242
|
result.add_operations = self.add_operations + other.add_operations
|
|
191
|
-
result.add_per_second = result.add_operations /
|
|
192
|
-
|
|
243
|
+
result.add_per_second = result.add_operations / (
|
|
244
|
+
self.add_seconds + other.add_seconds
|
|
245
|
+
)
|
|
193
246
|
else:
|
|
194
247
|
base = self if self.add_operations else other
|
|
195
248
|
result.add_operations = base.add_operations
|
|
@@ -198,10 +251,12 @@ class TaskResult:
|
|
|
198
251
|
if self.search_operations and other.search_operations:
|
|
199
252
|
result.search_operations = self.search_operations + other.search_operations
|
|
200
253
|
result.recall_at_one = (
|
|
201
|
-
self.recall_at_one * self.search_operations
|
|
202
|
-
other.
|
|
203
|
-
|
|
204
|
-
|
|
254
|
+
self.recall_at_one * self.search_operations
|
|
255
|
+
+ other.recall_at_one * other.search_operations
|
|
256
|
+
) / (self.search_operations + other.search_operations)
|
|
257
|
+
result.search_per_second = result.search_operations / (
|
|
258
|
+
self.search_seconds + other.search_seconds
|
|
259
|
+
)
|
|
205
260
|
else:
|
|
206
261
|
base = self if self.search_operations else other
|
|
207
262
|
result.search_operations = base.search_operations
|
|
@@ -213,21 +268,18 @@ class TaskResult:
|
|
|
213
268
|
|
|
214
269
|
@dataclass
|
|
215
270
|
class AddTask:
|
|
216
|
-
|
|
217
271
|
labels: np.ndarray
|
|
218
272
|
vectors: np.ndarray
|
|
219
273
|
|
|
220
274
|
def __call__(self, index: Index) -> TaskResult:
|
|
221
|
-
|
|
222
275
|
batch_size: int = self.vectors.shape[0]
|
|
223
276
|
old_size: int = len(index)
|
|
224
|
-
dt, _ = measure_seconds(
|
|
225
|
-
lambda: index.add(self.labels, self.vectors))
|
|
277
|
+
dt, _ = measure_seconds(lambda: index.add(self.labels, self.vectors))
|
|
226
278
|
|
|
227
279
|
assert len(index) == old_size + batch_size
|
|
228
280
|
return TaskResult(
|
|
229
281
|
add_operations=batch_size,
|
|
230
|
-
add_per_second=batch_size/dt,
|
|
282
|
+
add_per_second=batch_size / dt,
|
|
231
283
|
)
|
|
232
284
|
|
|
233
285
|
@property
|
|
@@ -251,17 +303,21 @@ class AddTask:
|
|
|
251
303
|
|
|
252
304
|
return [
|
|
253
305
|
AddTask(
|
|
254
|
-
labels=self.labels[start_row:start_row+batch_size],
|
|
255
|
-
vectors=self.vectors[start_row:start_row+batch_size, :],
|
|
256
|
-
)
|
|
306
|
+
labels=self.labels[start_row : start_row + batch_size],
|
|
307
|
+
vectors=self.vectors[start_row : start_row + batch_size, :],
|
|
308
|
+
)
|
|
309
|
+
for start_row in range(0, self.count, batch_size)
|
|
310
|
+
]
|
|
257
311
|
|
|
258
312
|
def clusters(self, number_of_clusters: int) -> List[AddTask]:
|
|
259
313
|
"""Splits this dataset into smaller chunks."""
|
|
260
314
|
|
|
261
315
|
from sklearn.cluster import KMeans
|
|
316
|
+
|
|
262
317
|
clustering = KMeans(
|
|
263
318
|
n_clusters=number_of_clusters,
|
|
264
|
-
random_state=0,
|
|
319
|
+
random_state=0,
|
|
320
|
+
n_init="auto",
|
|
265
321
|
).fit(self.vectors)
|
|
266
322
|
|
|
267
323
|
partitioning = defaultdict(list)
|
|
@@ -272,22 +328,23 @@ class AddTask:
|
|
|
272
328
|
AddTask(
|
|
273
329
|
labels=self.labels[rows],
|
|
274
330
|
vectors=self.vectors[rows, :],
|
|
275
|
-
)
|
|
331
|
+
)
|
|
332
|
+
for rows in partitioning.values()
|
|
333
|
+
]
|
|
276
334
|
|
|
277
335
|
|
|
278
336
|
@dataclass
|
|
279
337
|
class SearchTask:
|
|
280
|
-
|
|
281
338
|
queries: np.ndarray
|
|
282
339
|
neighbors: np.ndarray
|
|
283
340
|
|
|
284
341
|
def __call__(self, index: Index) -> TaskResult:
|
|
285
|
-
|
|
286
342
|
dt, results = measure_seconds(
|
|
287
|
-
lambda: index.search(self.queries, self.neighbors.shape[1])
|
|
343
|
+
lambda: index.search(self.queries, self.neighbors.shape[1])
|
|
344
|
+
)
|
|
288
345
|
|
|
289
346
|
return TaskResult(
|
|
290
|
-
search_per_second=self.queries.shape[0]/dt,
|
|
347
|
+
search_per_second=self.queries.shape[0] / dt,
|
|
291
348
|
recall_at_one=results.recall_first(self.neighbors[:, 0].flatten()),
|
|
292
349
|
)
|
|
293
350
|
|
|
@@ -296,31 +353,26 @@ class SearchTask:
|
|
|
296
353
|
|
|
297
354
|
return [
|
|
298
355
|
SearchTask(
|
|
299
|
-
queries=self.queries[start_row:start_row+batch_size, :],
|
|
300
|
-
neighbors=self.neighbors[start_row:start_row+batch_size, :],
|
|
301
|
-
)
|
|
356
|
+
queries=self.queries[start_row : start_row + batch_size, :],
|
|
357
|
+
neighbors=self.neighbors[start_row : start_row + batch_size, :],
|
|
358
|
+
)
|
|
359
|
+
for start_row in range(0, self.queries.shape[0], batch_size)
|
|
360
|
+
]
|
|
302
361
|
|
|
303
362
|
|
|
304
363
|
@dataclass
|
|
305
364
|
class Evaluation:
|
|
306
|
-
|
|
307
365
|
tasks: List[Union[AddTask, SearchTask]]
|
|
308
366
|
count: int
|
|
309
367
|
ndim: int
|
|
310
368
|
|
|
311
369
|
@staticmethod
|
|
312
370
|
def for_dataset(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
clusters: int = 1) -> Evaluation:
|
|
316
|
-
|
|
371
|
+
dataset: Dataset, batch_size: int = 0, clusters: int = 1
|
|
372
|
+
) -> Evaluation:
|
|
317
373
|
tasks = []
|
|
318
|
-
add = AddTask(
|
|
319
|
-
|
|
320
|
-
labels=dataset.labels)
|
|
321
|
-
search = SearchTask(
|
|
322
|
-
queries=dataset.queries,
|
|
323
|
-
neighbors=dataset.neighbors)
|
|
374
|
+
add = AddTask(vectors=dataset.vectors, labels=dataset.labels)
|
|
375
|
+
search = SearchTask(queries=dataset.queries, neighbors=dataset.neighbors)
|
|
324
376
|
|
|
325
377
|
if batch_size:
|
|
326
378
|
tasks.extend(add.slices(batch_size))
|
|
@@ -340,7 +392,6 @@ class Evaluation:
|
|
|
340
392
|
)
|
|
341
393
|
|
|
342
394
|
def __call__(self, index: Index, post_clean: bool = True) -> dict:
|
|
343
|
-
|
|
344
395
|
task_result = TaskResult()
|
|
345
396
|
|
|
346
397
|
try:
|
package/python/usearch/index.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
# The purpose of this file is to provide Pythonic wrapper on top
|
|
2
4
|
# the native precompiled CPython module. It improves compatibility
|
|
3
5
|
# Python tooling, linters, and static analyzers. It also embeds JIT
|
|
4
6
|
# into the primary `Index` class, connecting USearch with Numba.
|
|
5
7
|
import os
|
|
6
|
-
from
|
|
7
|
-
from typing import Optional, Callable, Union, NamedTuple, List, Iterable
|
|
8
|
+
from typing import Optional, Union, NamedTuple, List, Iterable
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
10
11
|
|
|
11
|
-
from usearch.compiled import Index as _CompiledIndex
|
|
12
|
+
from usearch.compiled import Index as _CompiledIndex, IndexMetadata
|
|
12
13
|
from usearch.compiled import SparseIndex as _CompiledSetsIndex
|
|
13
14
|
|
|
14
|
-
from usearch.compiled import MetricKind, ScalarKind
|
|
15
|
+
from usearch.compiled import MetricKind, ScalarKind, MetricSignature
|
|
15
16
|
from usearch.compiled import (
|
|
16
17
|
DEFAULT_CONNECTIVITY,
|
|
17
18
|
DEFAULT_EXPANSION_ADD,
|
|
18
19
|
DEFAULT_EXPANSION_SEARCH,
|
|
19
|
-
|
|
20
20
|
USES_OPENMP,
|
|
21
21
|
USES_SIMSIMD,
|
|
22
22
|
USES_NATIVE_F16,
|
|
@@ -41,11 +41,11 @@ def _normalize_dtype(dtype) -> ScalarKind:
|
|
|
41
41
|
|
|
42
42
|
if isinstance(dtype, str):
|
|
43
43
|
_normalize = {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
"f64": ScalarKind.F64,
|
|
45
|
+
"f32": ScalarKind.F32,
|
|
46
|
+
"f16": ScalarKind.F16,
|
|
47
|
+
"f8": ScalarKind.F8,
|
|
48
|
+
"b1": ScalarKind.B1,
|
|
49
49
|
}
|
|
50
50
|
return _normalize[dtype.lower()]
|
|
51
51
|
|
|
@@ -79,9 +79,8 @@ class Matches(NamedTuple):
|
|
|
79
79
|
return np.sum(self.counts)
|
|
80
80
|
|
|
81
81
|
def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
|
|
82
|
-
|
|
83
82
|
if not self.is_batch:
|
|
84
|
-
assert row is None,
|
|
83
|
+
assert row is None, "Exporting a single sequence is only for batch requests"
|
|
85
84
|
labels = self.labels
|
|
86
85
|
distances = self.distances
|
|
87
86
|
|
|
@@ -94,7 +93,7 @@ class Matches(NamedTuple):
|
|
|
94
93
|
distances = self.distances[row, :count]
|
|
95
94
|
|
|
96
95
|
return [
|
|
97
|
-
{
|
|
96
|
+
{"label": int(label), "distance": float(distance)}
|
|
98
97
|
for label, distance in zip(labels, distances)
|
|
99
98
|
]
|
|
100
99
|
|
|
@@ -103,8 +102,17 @@ class Matches(NamedTuple):
|
|
|
103
102
|
return np.sum(best_matches == expected) / len(expected)
|
|
104
103
|
|
|
105
104
|
def __repr__(self) -> str:
|
|
106
|
-
return
|
|
107
|
-
f
|
|
105
|
+
return (
|
|
106
|
+
f"usearch.Matches({self.total_matches})"
|
|
107
|
+
if self.is_batch
|
|
108
|
+
else f"usearch.Matches({self.total_matches} across {self.batch_size} queries)"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CompiledMetric(NamedTuple):
|
|
113
|
+
pointer: int
|
|
114
|
+
kind: MetricKind
|
|
115
|
+
signature: MetricSignature
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
class Index:
|
|
@@ -120,15 +128,13 @@ class Index:
|
|
|
120
128
|
def __init__(
|
|
121
129
|
self,
|
|
122
130
|
ndim: int,
|
|
123
|
-
metric: Union[
|
|
131
|
+
metric: Union[str, MetricKind, CompiledMetric] = MetricKind.IP,
|
|
124
132
|
dtype: Optional[str] = None,
|
|
125
133
|
jit: bool = False,
|
|
126
|
-
|
|
127
134
|
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
128
135
|
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
129
136
|
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
130
137
|
tune: bool = False,
|
|
131
|
-
|
|
132
138
|
path: Optional[os.PathLike] = None,
|
|
133
139
|
view: bool = False,
|
|
134
140
|
) -> None:
|
|
@@ -171,7 +177,7 @@ class Index:
|
|
|
171
177
|
|
|
172
178
|
:param expansion_search: Traversal depth on queries, defaults to None
|
|
173
179
|
:type expansion_search: Optional[int], optional
|
|
174
|
-
Hyper-parameter for the search depth when querying
|
|
180
|
+
Hyper-parameter for the search depth when querying
|
|
175
181
|
nearest neighbors. The original paper calls it "ef".
|
|
176
182
|
Can be changed afterwards, as the `.expansion_search`.
|
|
177
183
|
|
|
@@ -184,63 +190,64 @@ class Index:
|
|
|
184
190
|
:type view: bool, optional
|
|
185
191
|
"""
|
|
186
192
|
|
|
193
|
+
if metric in MetricKindBitwise:
|
|
194
|
+
assert dtype is None or dtype == ScalarKind.B1
|
|
195
|
+
dtype = ScalarKind.B1
|
|
196
|
+
else:
|
|
197
|
+
dtype = _normalize_dtype(dtype)
|
|
198
|
+
|
|
187
199
|
if metric is None:
|
|
188
200
|
metric = MetricKind.IP
|
|
189
|
-
|
|
201
|
+
|
|
202
|
+
if isinstance(metric, str):
|
|
190
203
|
_normalize = {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
199
212
|
}
|
|
200
213
|
metric = _normalize[metric.lower()]
|
|
201
214
|
|
|
202
|
-
if isinstance(metric,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
try:
|
|
211
|
-
from usearch.numba import jit
|
|
212
|
-
except ImportError:
|
|
213
|
-
raise ModuleNotFoundError(
|
|
214
|
-
'To use JIT install Numba with `pip install numba`.'
|
|
215
|
-
'Alternatively, reinstall usearch with `pip install usearch[jit]`')
|
|
216
|
-
|
|
217
|
-
self._metric_kind = metric
|
|
218
|
-
self._metric_jit = jit(
|
|
219
|
-
ndim=ndim,
|
|
220
|
-
metric=metric,
|
|
221
|
-
dtype=dtype,
|
|
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
222
|
)
|
|
223
|
-
self._metric_pointer = self._metric_jit.address if \
|
|
224
|
-
self._metric_jit else 0
|
|
225
|
-
else:
|
|
226
|
-
self._metric_kind = metric
|
|
227
|
-
self._metric_jit = None
|
|
228
|
-
self._metric_pointer = 0
|
|
229
|
-
else:
|
|
230
|
-
raise ValueError(
|
|
231
|
-
'The `metric` must be Numba callback or a `MetricKind`')
|
|
232
223
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
224
|
+
metric = jit(
|
|
225
|
+
ndim=ndim,
|
|
226
|
+
metric=metric,
|
|
227
|
+
dtype=dtype,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if isinstance(metric, MetricKind):
|
|
231
|
+
self._metric_kind = metric
|
|
232
|
+
self._metric_jit = None
|
|
233
|
+
self._metric_pointer = 0
|
|
234
|
+
self._metric_signature = MetricSignature.ArrayArraySize
|
|
235
|
+
elif isinstance(metric, CompiledMetric):
|
|
236
|
+
self._metric_jit = metric
|
|
237
|
+
self._metric_kind = metric.kind
|
|
238
|
+
self._metric_pointer = metric.pointer
|
|
239
|
+
self._metric_signature = metric.signature
|
|
236
240
|
else:
|
|
237
|
-
|
|
241
|
+
raise ValueError(
|
|
242
|
+
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
243
|
+
)
|
|
238
244
|
|
|
239
245
|
self._compiled = _CompiledIndex(
|
|
240
246
|
ndim=ndim,
|
|
247
|
+
dtype=dtype,
|
|
241
248
|
metric=self._metric_kind,
|
|
242
249
|
metric_pointer=self._metric_pointer,
|
|
243
|
-
|
|
250
|
+
metric_signature=self._metric_signature,
|
|
244
251
|
connectivity=connectivity,
|
|
245
252
|
expansion_add=expansion_add,
|
|
246
253
|
expansion_search=expansion_search,
|
|
@@ -254,17 +261,40 @@ class Index:
|
|
|
254
261
|
else:
|
|
255
262
|
self._compiled.load(path)
|
|
256
263
|
|
|
264
|
+
@staticmethod
|
|
265
|
+
def metadata(path: os.PathLike) -> IndexMetadata:
|
|
266
|
+
return IndexMetadata(path)
|
|
267
|
+
|
|
268
|
+
@staticmethod
|
|
269
|
+
def restore(path: os.PathLike, view: bool = False) -> Index:
|
|
270
|
+
meta = Index.metadata(path)
|
|
271
|
+
bits_per_scalar = {
|
|
272
|
+
ScalarKind.F8: 8,
|
|
273
|
+
ScalarKind.F16: 16,
|
|
274
|
+
ScalarKind.F32: 32,
|
|
275
|
+
ScalarKind.F64: 64,
|
|
276
|
+
ScalarKind.B1: 1,
|
|
277
|
+
}[meta.scalar_kind]
|
|
278
|
+
ndim = meta.bytes_for_vectors * 8 // meta.size // bits_per_scalar
|
|
279
|
+
return Index(
|
|
280
|
+
ndim=ndim,
|
|
281
|
+
connectivity=meta.connectivity,
|
|
282
|
+
metric=meta.metric,
|
|
283
|
+
path=path,
|
|
284
|
+
view=view,
|
|
285
|
+
)
|
|
286
|
+
|
|
257
287
|
def add(
|
|
258
|
-
|
|
259
|
-
|
|
288
|
+
self, labels, vectors, *, copy: bool = True, threads: int = 0
|
|
289
|
+
) -> Union[int, np.ndarray]:
|
|
260
290
|
"""Inserts one or move vectors into the index.
|
|
261
291
|
|
|
262
292
|
For maximal performance the `labels` and `vectors`
|
|
263
293
|
should conform to the Python's "buffer protocol" spec.
|
|
264
294
|
|
|
265
|
-
To index a single entry:
|
|
295
|
+
To index a single entry:
|
|
266
296
|
labels: int, vectors: np.ndarray.
|
|
267
|
-
To index many entries:
|
|
297
|
+
To index many entries:
|
|
268
298
|
labels: np.ndarray, vectors: np.ndarray.
|
|
269
299
|
|
|
270
300
|
When working with extremely large indexes, you may want to
|
|
@@ -283,8 +313,8 @@ class Index:
|
|
|
283
313
|
:return: Inserted label or labels
|
|
284
314
|
:type: Union[int, np.ndarray]
|
|
285
315
|
"""
|
|
286
|
-
assert isinstance(vectors, np.ndarray),
|
|
287
|
-
assert vectors.ndim == 1 or vectors.ndim == 2,
|
|
316
|
+
assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
|
|
317
|
+
assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
|
|
288
318
|
is_batch = vectors.ndim == 2
|
|
289
319
|
generate_labels = labels is None
|
|
290
320
|
|
|
@@ -305,8 +335,8 @@ class Index:
|
|
|
305
335
|
return labels
|
|
306
336
|
|
|
307
337
|
def search(
|
|
308
|
-
|
|
309
|
-
|
|
338
|
+
self, vectors, k: int = 10, *, threads: int = 0, exact: bool = False
|
|
339
|
+
) -> Matches:
|
|
310
340
|
"""Performs approximate nearest neighbors search for one or more queries.
|
|
311
341
|
|
|
312
342
|
:param vectors: Query vector or vectors.
|
|
@@ -322,26 +352,28 @@ class Index:
|
|
|
322
352
|
:rtype: Matches
|
|
323
353
|
"""
|
|
324
354
|
tuple_ = self._compiled.search(
|
|
325
|
-
vectors,
|
|
326
|
-
|
|
355
|
+
vectors,
|
|
356
|
+
k,
|
|
357
|
+
exact=exact,
|
|
358
|
+
threads=threads,
|
|
327
359
|
)
|
|
328
360
|
return Matches(*tuple_)
|
|
329
361
|
|
|
330
362
|
@property
|
|
331
363
|
def specs(self) -> dict:
|
|
332
364
|
return {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
365
|
+
"Class": "usearch.Index",
|
|
366
|
+
"Connectivity": self.connectivity,
|
|
367
|
+
"Size": self.size,
|
|
368
|
+
"Dimensions": self.ndim,
|
|
369
|
+
"Expansion@Add": self.expansion_add,
|
|
370
|
+
"Expansion@Search": self.expansion_search,
|
|
371
|
+
"OpenMP": USES_OPENMP,
|
|
372
|
+
"SimSIMD": USES_SIMSIMD,
|
|
373
|
+
"NativeF16": USES_NATIVE_F16,
|
|
374
|
+
"JIT": self.jit,
|
|
375
|
+
"DType": self.dtype,
|
|
376
|
+
"Path": self.path,
|
|
345
377
|
}
|
|
346
378
|
|
|
347
379
|
def __len__(self) -> int:
|
|
@@ -412,10 +444,8 @@ class Index:
|
|
|
412
444
|
return self._compiled.labels
|
|
413
445
|
|
|
414
446
|
def get_vectors(
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
dtype: ScalarKind = ScalarKind.F32) -> np.ndarray:
|
|
418
|
-
|
|
447
|
+
self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
|
|
448
|
+
) -> np.ndarray:
|
|
419
449
|
dtype = _normalize_dtype(dtype)
|
|
420
450
|
return np.vstack([self._compiled.__getitem__(l, dtype) for l in labels])
|
|
421
451
|
|