usearch 0.18.8 → 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 +176 -6
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +2 -2
- package/cpp/test.cpp +11 -9
- package/include/usearch/index.hpp +269 -50
- package/include/usearch/index_punned_dense.hpp +123 -108
- package/include/usearch/index_punned_helpers.hpp +0 -12
- package/package.json +1 -1
- package/python/lib.cpp +116 -40
- package/python/scripts/bench.ipynb +13 -2113
- package/python/scripts/bench.py +45 -29
- 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 +112 -83
- package/python/usearch/io.py +23 -23
- package/python/usearch/join.py +2 -4
- package/python/usearch/numba.py +22 -18
- 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 +2 -1
- 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
|
|
|
@@ -190,58 +196,58 @@ class Index:
|
|
|
190
196
|
else:
|
|
191
197
|
dtype = _normalize_dtype(dtype)
|
|
192
198
|
|
|
193
|
-
|
|
194
199
|
if metric is None:
|
|
195
200
|
metric = MetricKind.IP
|
|
196
|
-
|
|
201
|
+
|
|
202
|
+
if isinstance(metric, str):
|
|
197
203
|
_normalize = {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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,
|
|
206
212
|
}
|
|
207
213
|
metric = _normalize[metric.lower()]
|
|
208
214
|
|
|
209
|
-
if isinstance(metric,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
try:
|
|
218
|
-
from usearch.numba import jit
|
|
219
|
-
except ImportError:
|
|
220
|
-
raise ModuleNotFoundError(
|
|
221
|
-
'To use JIT install Numba with `pip install numba`.'
|
|
222
|
-
'Alternatively, reinstall usearch with `pip install usearch[jit]`')
|
|
223
|
-
|
|
224
|
-
self._metric_kind = metric
|
|
225
|
-
self._metric_jit = jit(
|
|
226
|
-
ndim=ndim,
|
|
227
|
-
metric=metric,
|
|
228
|
-
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]`"
|
|
229
222
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
223
|
+
|
|
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(
|
|
238
|
-
|
|
242
|
+
"The `metric` must be a `CompiledMetric` or a `MetricKind`"
|
|
243
|
+
)
|
|
239
244
|
|
|
240
245
|
self._compiled = _CompiledIndex(
|
|
241
246
|
ndim=ndim,
|
|
247
|
+
dtype=dtype,
|
|
242
248
|
metric=self._metric_kind,
|
|
243
249
|
metric_pointer=self._metric_pointer,
|
|
244
|
-
|
|
250
|
+
metric_signature=self._metric_signature,
|
|
245
251
|
connectivity=connectivity,
|
|
246
252
|
expansion_add=expansion_add,
|
|
247
253
|
expansion_search=expansion_search,
|
|
@@ -255,17 +261,40 @@ class Index:
|
|
|
255
261
|
else:
|
|
256
262
|
self._compiled.load(path)
|
|
257
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
|
+
|
|
258
287
|
def add(
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
self, labels, vectors, *, copy: bool = True, threads: int = 0
|
|
289
|
+
) -> Union[int, np.ndarray]:
|
|
261
290
|
"""Inserts one or move vectors into the index.
|
|
262
291
|
|
|
263
292
|
For maximal performance the `labels` and `vectors`
|
|
264
293
|
should conform to the Python's "buffer protocol" spec.
|
|
265
294
|
|
|
266
|
-
To index a single entry:
|
|
295
|
+
To index a single entry:
|
|
267
296
|
labels: int, vectors: np.ndarray.
|
|
268
|
-
To index many entries:
|
|
297
|
+
To index many entries:
|
|
269
298
|
labels: np.ndarray, vectors: np.ndarray.
|
|
270
299
|
|
|
271
300
|
When working with extremely large indexes, you may want to
|
|
@@ -284,8 +313,8 @@ class Index:
|
|
|
284
313
|
:return: Inserted label or labels
|
|
285
314
|
:type: Union[int, np.ndarray]
|
|
286
315
|
"""
|
|
287
|
-
assert isinstance(vectors, np.ndarray),
|
|
288
|
-
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"
|
|
289
318
|
is_batch = vectors.ndim == 2
|
|
290
319
|
generate_labels = labels is None
|
|
291
320
|
|
|
@@ -306,8 +335,8 @@ class Index:
|
|
|
306
335
|
return labels
|
|
307
336
|
|
|
308
337
|
def search(
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
self, vectors, k: int = 10, *, threads: int = 0, exact: bool = False
|
|
339
|
+
) -> Matches:
|
|
311
340
|
"""Performs approximate nearest neighbors search for one or more queries.
|
|
312
341
|
|
|
313
342
|
:param vectors: Query vector or vectors.
|
|
@@ -323,26 +352,28 @@ class Index:
|
|
|
323
352
|
:rtype: Matches
|
|
324
353
|
"""
|
|
325
354
|
tuple_ = self._compiled.search(
|
|
326
|
-
vectors,
|
|
327
|
-
|
|
355
|
+
vectors,
|
|
356
|
+
k,
|
|
357
|
+
exact=exact,
|
|
358
|
+
threads=threads,
|
|
328
359
|
)
|
|
329
360
|
return Matches(*tuple_)
|
|
330
361
|
|
|
331
362
|
@property
|
|
332
363
|
def specs(self) -> dict:
|
|
333
364
|
return {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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,
|
|
346
377
|
}
|
|
347
378
|
|
|
348
379
|
def __len__(self) -> int:
|
|
@@ -413,10 +444,8 @@ class Index:
|
|
|
413
444
|
return self._compiled.labels
|
|
414
445
|
|
|
415
446
|
def get_vectors(
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
dtype: ScalarKind = ScalarKind.F32) -> np.ndarray:
|
|
419
|
-
|
|
447
|
+
self, labels: np.ndarray, dtype: ScalarKind = ScalarKind.F32
|
|
448
|
+
) -> np.ndarray:
|
|
420
449
|
dtype = _normalize_dtype(dtype)
|
|
421
450
|
return np.vstack([self._compiled.__getitem__(l, dtype) for l in labels])
|
|
422
451
|
|