usearch 0.17.1 → 0.18.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 +4 -4
- package/.github/workflows/release.yml +2 -0
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/conanfile.py +1 -1
- package/package.json +1 -1
- package/python/lib.cpp +14 -14
- package/python/usearch/eval.py +23 -26
- package/python/usearch/index.py +3 -3
- package/python/usearch/server.py +6 -7
|
@@ -42,13 +42,13 @@ jobs:
|
|
|
42
42
|
run: pytest python/scripts/test.py
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
name: Test Python 3.
|
|
45
|
+
test_python_37:
|
|
46
|
+
name: Test Python 3.7
|
|
47
47
|
runs-on: ${{ matrix.os }}
|
|
48
48
|
strategy:
|
|
49
49
|
matrix:
|
|
50
50
|
os: [ubuntu-20.04]
|
|
51
|
-
python-version: ["3.
|
|
51
|
+
python-version: ["3.7"]
|
|
52
52
|
|
|
53
53
|
steps:
|
|
54
54
|
- uses: actions/checkout@v3
|
|
@@ -62,7 +62,7 @@ jobs:
|
|
|
62
62
|
- name: Install dependencies
|
|
63
63
|
run: |
|
|
64
64
|
python -m pip install --upgrade pip
|
|
65
|
-
pip install pytest numpy
|
|
65
|
+
pip install pytest numpy
|
|
66
66
|
|
|
67
67
|
- name: Build locally
|
|
68
68
|
run: python -m pip install .
|
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -276,7 +276,7 @@ The difference would be in the shape of the tensors.
|
|
|
276
276
|
|
|
277
277
|
```py
|
|
278
278
|
n = 100
|
|
279
|
-
labels = np.
|
|
279
|
+
labels = np.arange(n)
|
|
280
280
|
vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
281
281
|
|
|
282
282
|
index.add(labels, vectors, threads=..., copy=...)
|
|
@@ -584,7 +584,7 @@ fingerprints = np.vstack([encoder.GetFingerprint(x) for x in molecules])
|
|
|
584
584
|
fingerprints = np.packbits(fingerprints, axis=1)
|
|
585
585
|
|
|
586
586
|
index = Index(ndim=2048, metric=MetricKind.BitwiseTanimoto)
|
|
587
|
-
labels = np.
|
|
587
|
+
labels = np.arange(len(molecules))
|
|
588
588
|
|
|
589
589
|
index.add(labels, fingerprints)
|
|
590
590
|
matches = index.search(fingerprints, 10)
|
package/conanfile.py
CHANGED
|
@@ -4,7 +4,7 @@ from conans import ConanFile
|
|
|
4
4
|
class USearchConan(ConanFile):
|
|
5
5
|
|
|
6
6
|
name = 'USearch'
|
|
7
|
-
version = '0.
|
|
7
|
+
version = '0.18.1'
|
|
8
8
|
license = 'Apache 2.0'
|
|
9
9
|
url = 'https://github.com/unum-cloud/usearch'
|
|
10
10
|
description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -26,7 +26,7 @@ using namespace unum;
|
|
|
26
26
|
namespace py = pybind11;
|
|
27
27
|
using py_shape_t = py::array::ShapeContainer;
|
|
28
28
|
|
|
29
|
-
using label_t =
|
|
29
|
+
using label_t = std::uint32_t;
|
|
30
30
|
using distance_t = punned_distance_t;
|
|
31
31
|
using metric_t = punned_metric_t;
|
|
32
32
|
using id_t = std::uint32_t;
|
|
@@ -132,11 +132,11 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
|
|
|
132
132
|
config.store_vector = copy;
|
|
133
133
|
|
|
134
134
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
135
|
-
if (vector_info.format == "B")
|
|
135
|
+
if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1")
|
|
136
136
|
index.add(label, reinterpret_cast<b1x8_t const*>(vector_data), config).error.raise();
|
|
137
|
-
else if (vector_info.format == "
|
|
137
|
+
else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1")
|
|
138
138
|
index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
|
|
139
|
-
else if (vector_info.format == "e")
|
|
139
|
+
else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2")
|
|
140
140
|
index.add(label, reinterpret_cast<f16_t const*>(vector_data), config).error.raise();
|
|
141
141
|
else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
|
|
142
142
|
index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
|
|
@@ -156,7 +156,7 @@ static void add_many_to_index( //
|
|
|
156
156
|
py::buffer_info labels_info = labels.request();
|
|
157
157
|
py::buffer_info vectors_info = vectors.request();
|
|
158
158
|
|
|
159
|
-
if (labels_info.
|
|
159
|
+
if (labels_info.itemsize != sizeof(label_t))
|
|
160
160
|
throw std::invalid_argument("Incompatible label type!");
|
|
161
161
|
|
|
162
162
|
if (labels_info.ndim != 1)
|
|
@@ -181,7 +181,7 @@ static void add_many_to_index( //
|
|
|
181
181
|
char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
|
|
182
182
|
|
|
183
183
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
184
|
-
if (vectors_info.format == "B")
|
|
184
|
+
if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
|
|
185
185
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
186
186
|
add_config_t config;
|
|
187
187
|
config.store_vector = copy;
|
|
@@ -190,7 +190,7 @@ static void add_many_to_index( //
|
|
|
190
190
|
b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
191
191
|
index.add(label, vector, config).error.raise();
|
|
192
192
|
});
|
|
193
|
-
else if (vectors_info.format == "
|
|
193
|
+
else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
|
|
194
194
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
195
195
|
add_config_t config;
|
|
196
196
|
config.store_vector = copy;
|
|
@@ -200,7 +200,7 @@ static void add_many_to_index( //
|
|
|
200
200
|
reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
201
201
|
index.add(label, vector, config).error.raise();
|
|
202
202
|
});
|
|
203
|
-
else if (vectors_info.format == "e")
|
|
203
|
+
else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
|
|
204
204
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
205
205
|
add_config_t config;
|
|
206
206
|
config.store_vector = copy;
|
|
@@ -249,15 +249,15 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
|
|
|
249
249
|
config.exact = exact;
|
|
250
250
|
|
|
251
251
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
252
|
-
if (vector_info.format == "B") {
|
|
252
|
+
if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1") {
|
|
253
253
|
punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
|
|
254
254
|
result.error.raise();
|
|
255
255
|
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
256
|
-
} else if (vector_info.format == "
|
|
256
|
+
} else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1") {
|
|
257
257
|
punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
|
|
258
258
|
result.error.raise();
|
|
259
259
|
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
260
|
-
} else if (vector_info.format == "e") {
|
|
260
|
+
} else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2") {
|
|
261
261
|
punned_search_result_t result = index.search(reinterpret_cast<f16_t const*>(vector_data), wanted, config);
|
|
262
262
|
result.error.raise();
|
|
263
263
|
count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
@@ -320,7 +320,7 @@ static py::tuple search_many_in_index( //
|
|
|
320
320
|
auto counts_py1d = counts_py.template mutable_unchecked<1>();
|
|
321
321
|
|
|
322
322
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
323
|
-
if (vectors_info.format == "B")
|
|
323
|
+
if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
|
|
324
324
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
325
325
|
search_config_t config;
|
|
326
326
|
config.thread = thread_idx;
|
|
@@ -331,7 +331,7 @@ static py::tuple search_many_in_index( //
|
|
|
331
331
|
counts_py1d(task_idx) =
|
|
332
332
|
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
333
333
|
});
|
|
334
|
-
else if (vectors_info.format == "
|
|
334
|
+
else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
|
|
335
335
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
336
336
|
search_config_t config;
|
|
337
337
|
config.thread = thread_idx;
|
|
@@ -342,7 +342,7 @@ static py::tuple search_many_in_index( //
|
|
|
342
342
|
counts_py1d(task_idx) =
|
|
343
343
|
static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
344
344
|
});
|
|
345
|
-
else if (vectors_info.format == "e")
|
|
345
|
+
else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
|
|
346
346
|
executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
347
347
|
search_config_t config;
|
|
348
348
|
config.thread = thread_idx;
|
package/python/usearch/eval.py
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
from
|
|
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
|
|
2
5
|
from collections import defaultdict
|
|
3
6
|
|
|
4
7
|
import numpy as np
|
|
5
8
|
|
|
6
9
|
from usearch.io import load_matrix
|
|
7
|
-
from usearch.index import Index, Matches, MetricKind, MetricKindBitwise
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
# For compatibility with Python 3.6
|
|
11
|
-
try:
|
|
12
|
-
from time import time_ns
|
|
13
|
-
except ImportError:
|
|
14
|
-
from datetime import datetime
|
|
15
|
-
|
|
16
|
-
def time_ns():
|
|
17
|
-
now = datetime.now()
|
|
18
|
-
return int(now.timestamp() * 1e9)
|
|
10
|
+
from usearch.index import Index, Matches, MetricKind, MetricKindBitwise, Label
|
|
19
11
|
|
|
20
12
|
|
|
21
13
|
def random_vectors(
|
|
@@ -89,7 +81,8 @@ def measure_seconds(f: Callable) -> Tuple[float, Any]:
|
|
|
89
81
|
return secs, result
|
|
90
82
|
|
|
91
83
|
|
|
92
|
-
|
|
84
|
+
@dataclass
|
|
85
|
+
class Dataset:
|
|
93
86
|
|
|
94
87
|
labels: np.ndarray
|
|
95
88
|
vectors: np.ndarray
|
|
@@ -134,7 +127,7 @@ class Dataset(NamedTuple):
|
|
|
134
127
|
count = min(
|
|
135
128
|
d.vectors.shape[0], count) if count is not None else d.vectors.shape[0]
|
|
136
129
|
d.vectors = d.vectors[:count, :]
|
|
137
|
-
d.labels = np.arange(count, dtype=
|
|
130
|
+
d.labels = np.arange(count, dtype=Label)
|
|
138
131
|
|
|
139
132
|
if queries is not None:
|
|
140
133
|
d.queries = load_matrix(queries)
|
|
@@ -157,13 +150,14 @@ class Dataset(NamedTuple):
|
|
|
157
150
|
|
|
158
151
|
d.vectors = random_vectors(count=count, ndim=ndim)
|
|
159
152
|
d.queries = d.vectors
|
|
160
|
-
d.labels = np.arange(count, dtype=
|
|
153
|
+
d.labels = np.arange(count, dtype=Label)
|
|
161
154
|
d.neighbors = np.reshape(d.labels, (count, 1))
|
|
162
155
|
|
|
163
156
|
return d
|
|
164
157
|
|
|
165
158
|
|
|
166
|
-
|
|
159
|
+
@dataclass
|
|
160
|
+
class TaskResult:
|
|
167
161
|
|
|
168
162
|
add_operations: Optional[int] = None
|
|
169
163
|
add_per_second: Optional[float] = None
|
|
@@ -190,7 +184,7 @@ class TaskResult(NamedTuple):
|
|
|
190
184
|
def search_seconds(self) -> float:
|
|
191
185
|
return self.search_operations / self.search_per_second
|
|
192
186
|
|
|
193
|
-
def __add__(self, other):
|
|
187
|
+
def __add__(self, other: TaskResult):
|
|
194
188
|
result = TaskResult()
|
|
195
189
|
if self.add_operations and other.add_operations:
|
|
196
190
|
result.add_operations = self.add_operations + other.add_operations
|
|
@@ -217,7 +211,8 @@ class TaskResult(NamedTuple):
|
|
|
217
211
|
return result
|
|
218
212
|
|
|
219
213
|
|
|
220
|
-
|
|
214
|
+
@dataclass
|
|
215
|
+
class AddTask:
|
|
221
216
|
|
|
222
217
|
labels: np.ndarray
|
|
223
218
|
vectors: np.ndarray
|
|
@@ -251,7 +246,7 @@ class AddTask(NamedTuple):
|
|
|
251
246
|
self.labels = self.labels[new_order]
|
|
252
247
|
self.vectors = self.vectors[new_order, :]
|
|
253
248
|
|
|
254
|
-
def slices(self, batch_size: int) ->
|
|
249
|
+
def slices(self, batch_size: int) -> List[AddTask]:
|
|
255
250
|
"""Splits this dataset into smaller chunks."""
|
|
256
251
|
|
|
257
252
|
return [
|
|
@@ -260,7 +255,7 @@ class AddTask(NamedTuple):
|
|
|
260
255
|
vectors=self.vectors[start_row:start_row+batch_size, :],
|
|
261
256
|
) for start_row in range(0, self.count, batch_size)]
|
|
262
257
|
|
|
263
|
-
def clusters(self, number_of_clusters: int) ->
|
|
258
|
+
def clusters(self, number_of_clusters: int) -> List[AddTask]:
|
|
264
259
|
"""Splits this dataset into smaller chunks."""
|
|
265
260
|
|
|
266
261
|
from sklearn.cluster import KMeans
|
|
@@ -280,7 +275,8 @@ class AddTask(NamedTuple):
|
|
|
280
275
|
) for rows in partitioning.values()]
|
|
281
276
|
|
|
282
277
|
|
|
283
|
-
|
|
278
|
+
@dataclass
|
|
279
|
+
class SearchTask:
|
|
284
280
|
|
|
285
281
|
queries: np.ndarray
|
|
286
282
|
neighbors: np.ndarray
|
|
@@ -295,7 +291,7 @@ class SearchTask(NamedTuple):
|
|
|
295
291
|
recall_at_one=results.recall_first(self.neighbors[:, 0].flatten()),
|
|
296
292
|
)
|
|
297
293
|
|
|
298
|
-
def slices(self, batch_size: int) ->
|
|
294
|
+
def slices(self, batch_size: int) -> List[SearchTask]:
|
|
299
295
|
"""Splits this dataset into smaller chunks."""
|
|
300
296
|
|
|
301
297
|
return [
|
|
@@ -305,7 +301,8 @@ class SearchTask(NamedTuple):
|
|
|
305
301
|
) for start_row in range(0, self.queries.shape[0], batch_size)]
|
|
306
302
|
|
|
307
303
|
|
|
308
|
-
|
|
304
|
+
@dataclass
|
|
305
|
+
class Evaluation:
|
|
309
306
|
|
|
310
307
|
tasks: List[Union[AddTask, SearchTask]]
|
|
311
308
|
count: int
|
|
@@ -315,7 +312,7 @@ class Evaluation(NamedTuple):
|
|
|
315
312
|
def for_dataset(
|
|
316
313
|
dataset: Dataset,
|
|
317
314
|
batch_size: int = 0,
|
|
318
|
-
clusters: int = 1):
|
|
315
|
+
clusters: int = 1) -> Evaluation:
|
|
319
316
|
|
|
320
317
|
tasks = []
|
|
321
318
|
add = AddTask(
|
|
@@ -356,5 +353,5 @@ class Evaluation(NamedTuple):
|
|
|
356
353
|
index.clear()
|
|
357
354
|
return {
|
|
358
355
|
**index.specs,
|
|
359
|
-
**task_result
|
|
356
|
+
**asdict(task_result),
|
|
360
357
|
}
|
package/python/usearch/index.py
CHANGED
|
@@ -29,7 +29,7 @@ MetricKindBitwise = (
|
|
|
29
29
|
|
|
30
30
|
SparseIndex = _CompiledSetsIndex
|
|
31
31
|
|
|
32
|
-
Label = np.
|
|
32
|
+
Label = np.uint32
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def _normalize_dtype(dtype) -> ScalarKind:
|
|
@@ -295,9 +295,9 @@ class Index:
|
|
|
295
295
|
else:
|
|
296
296
|
labels = start_id
|
|
297
297
|
if isinstance(labels, np.ndarray):
|
|
298
|
-
labels = labels.astype(
|
|
298
|
+
labels = labels.astype(Label)
|
|
299
299
|
elif isinstance(labels, Iterable):
|
|
300
|
-
labels = np.array(labels, dtype=
|
|
300
|
+
labels = np.array(labels, dtype=Label)
|
|
301
301
|
|
|
302
302
|
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
303
303
|
|
package/python/usearch/server.py
CHANGED
|
@@ -7,7 +7,7 @@ import numpy as np
|
|
|
7
7
|
from typing import List
|
|
8
8
|
|
|
9
9
|
from ucall.rich_posix import Server
|
|
10
|
-
from usearch.index import Index,
|
|
10
|
+
from usearch.index import Index, Matches, Label
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _ascii_to_vector(string: str) -> np.ndarray:
|
|
@@ -58,26 +58,25 @@ def serve(
|
|
|
58
58
|
@server
|
|
59
59
|
def add_one(label: int, vector: np.ndarray):
|
|
60
60
|
print('adding', label, vector)
|
|
61
|
-
labels = np.array([label], dtype=
|
|
61
|
+
labels = np.array([label], dtype=Label)
|
|
62
62
|
vectors = vector.flatten().reshape(vector.shape[0], 1)
|
|
63
63
|
index.add(labels, vectors)
|
|
64
64
|
|
|
65
65
|
@server
|
|
66
66
|
def add_many(labels: np.ndarray, vectors: np.ndarray):
|
|
67
|
-
labels = labels.astype(np.longlong)
|
|
68
67
|
index.add(labels, vectors, threads=threads)
|
|
69
68
|
|
|
70
69
|
@server
|
|
71
70
|
def search_one(vector: np.ndarray, count: int) -> List[dict]:
|
|
72
71
|
print('search', vector, count)
|
|
73
72
|
vectors = vector.reshape(vector.shape[0], 1)
|
|
74
|
-
results = index.search(vectors, count)
|
|
75
|
-
return
|
|
73
|
+
results: Matches = index.search(vectors, count)
|
|
74
|
+
return results.to_list()
|
|
76
75
|
|
|
77
76
|
@server
|
|
78
77
|
def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
|
|
79
|
-
results = index.search(vectors, count)
|
|
80
|
-
return
|
|
78
|
+
results: Matches = index.search(vectors, count)
|
|
79
|
+
return results.to_list()
|
|
81
80
|
|
|
82
81
|
@server
|
|
83
82
|
def add_ascii(label: int, string: str):
|