usearch 0.10.0 → 0.11.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 +1 -1
- package/.gitmodules +3 -0
- package/.vscode/settings.json +2 -0
- package/Cargo.toml +1 -1
- package/Package.swift +1 -0
- package/README.md +1 -1
- package/VERSION +1 -1
- package/binding.gyp +4 -3
- package/build.gradle +1 -1
- package/build.rs +1 -0
- package/conanfile.py +1 -1
- package/docs/compilation.md +6 -3
- package/include/usearch/usearch.hpp +133 -43
- package/package.json +1 -1
- package/python/lib.cpp +36 -24
- package/python/scripts/bench.py +2 -2
- package/python/scripts/test.py +1 -1
- package/python/usearch/client.py +8 -9
- package/python/usearch/index.py +306 -0
- package/python/usearch/server.py +6 -16
- package/robin-map/.codecov.yml +5 -0
- package/robin-map/.github/workflows/ci.yml +136 -0
- package/robin-map/CMakeLists.txt +86 -0
- package/robin-map/LICENSE +21 -0
- package/robin-map/README.md +483 -0
- package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +9 -0
- package/robin-map/doxygen.conf +2483 -0
- package/robin-map/include/tsl/robin_growth_policy.h +406 -0
- package/robin-map/include/tsl/robin_hash.h +1639 -0
- package/robin-map/include/tsl/robin_map.h +807 -0
- package/robin-map/include/tsl/robin_set.h +660 -0
- package/robin-map/tests/CMakeLists.txt +26 -0
- package/robin-map/tests/custom_allocator_tests.cpp +138 -0
- package/robin-map/tests/main.cpp +26 -0
- package/robin-map/tests/policy_tests.cpp +97 -0
- package/robin-map/tests/robin_map_tests.cpp +1451 -0
- package/robin-map/tests/robin_set_tests.cpp +164 -0
- package/robin-map/tests/utils.h +443 -0
- package/robin-map/tsl-robin-map.natvis +78 -0
- package/setup.py +4 -2
- package/src/CMakeLists.txt +1 -0
- package/src/punned.hpp +50 -6
- package/src/test.cpp +29 -8
package/python/lib.cpp
CHANGED
|
@@ -169,7 +169,7 @@ static std::unique_ptr<hash_index_py_t> make_hash_index( //
|
|
|
169
169
|
return std::unique_ptr<hash_index_py_t>(new hash_index_py_t(config, bits));
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy) {
|
|
172
|
+
static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
|
|
173
173
|
|
|
174
174
|
py::buffer_info vector_info = vector.request();
|
|
175
175
|
if (vector_info.ndim != 1)
|
|
@@ -191,9 +191,9 @@ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vecto
|
|
|
191
191
|
index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config);
|
|
192
192
|
else if (vector_info.format == "e")
|
|
193
193
|
index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config);
|
|
194
|
-
else if (vector_info.format == "f")
|
|
194
|
+
else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
|
|
195
195
|
index.add(label, reinterpret_cast<float const*>(vector_data), config);
|
|
196
|
-
else if (vector_info.format == "d")
|
|
196
|
+
else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
|
|
197
197
|
index.add(label, reinterpret_cast<double const*>(vector_data), config);
|
|
198
198
|
else
|
|
199
199
|
throw std::invalid_argument("Incompatible scalars in the vector!");
|
|
@@ -201,7 +201,10 @@ static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vecto
|
|
|
201
201
|
|
|
202
202
|
static void add_many_to_index( //
|
|
203
203
|
punned_py_t& index, py::buffer labels, py::buffer vectors, //
|
|
204
|
-
bool copy, std::size_t threads
|
|
204
|
+
bool copy, std::size_t threads) {
|
|
205
|
+
|
|
206
|
+
if (index.config().max_threads_add < threads)
|
|
207
|
+
throw std::invalid_argument("Can't use that many threads!");
|
|
205
208
|
|
|
206
209
|
py::buffer_info labels_info = labels.request();
|
|
207
210
|
py::buffer_info vectors_info = vectors.request();
|
|
@@ -232,7 +235,7 @@ static void add_many_to_index( //
|
|
|
232
235
|
|
|
233
236
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
234
237
|
if (vectors_info.format == "c" || vectors_info.format == "b")
|
|
235
|
-
multithreaded(
|
|
238
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
236
239
|
add_config_t config;
|
|
237
240
|
config.store_vector = copy;
|
|
238
241
|
config.thread = thread_idx;
|
|
@@ -242,7 +245,7 @@ static void add_many_to_index( //
|
|
|
242
245
|
index.add(label, vector, config);
|
|
243
246
|
});
|
|
244
247
|
else if (vectors_info.format == "e")
|
|
245
|
-
multithreaded(
|
|
248
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
246
249
|
add_config_t config;
|
|
247
250
|
config.store_vector = copy;
|
|
248
251
|
config.thread = thread_idx;
|
|
@@ -251,8 +254,8 @@ static void add_many_to_index( //
|
|
|
251
254
|
reinterpret_cast<f16_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
252
255
|
index.add(label, vector, config);
|
|
253
256
|
});
|
|
254
|
-
else if (vectors_info.format == "f")
|
|
255
|
-
multithreaded(
|
|
257
|
+
else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
|
|
258
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
256
259
|
add_config_t config;
|
|
257
260
|
config.store_vector = copy;
|
|
258
261
|
config.thread = thread_idx;
|
|
@@ -260,8 +263,8 @@ static void add_many_to_index( //
|
|
|
260
263
|
float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
261
264
|
index.add(label, vector, config);
|
|
262
265
|
});
|
|
263
|
-
else if (vectors_info.format == "d")
|
|
264
|
-
multithreaded(
|
|
266
|
+
else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
|
|
267
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
265
268
|
add_config_t config;
|
|
266
269
|
config.store_vector = copy;
|
|
267
270
|
config.thread = thread_idx;
|
|
@@ -299,11 +302,11 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
|
|
|
299
302
|
count = index //
|
|
300
303
|
.search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config)
|
|
301
304
|
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
302
|
-
else if (vector_info.format == "f")
|
|
305
|
+
else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
|
|
303
306
|
count = index //
|
|
304
307
|
.search(reinterpret_cast<float const*>(vector_data), wanted, config)
|
|
305
308
|
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
306
|
-
else if (vector_info.format == "d")
|
|
309
|
+
else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
|
|
307
310
|
count = index //
|
|
308
311
|
.search(reinterpret_cast<double const*>(vector_data), wanted, config)
|
|
309
312
|
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
@@ -329,11 +332,15 @@ static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std:
|
|
|
329
332
|
* 2. matrix of distances,
|
|
330
333
|
* 3. array with match counts.
|
|
331
334
|
*/
|
|
332
|
-
static py::tuple search_many_in_index(
|
|
335
|
+
static py::tuple search_many_in_index( //
|
|
336
|
+
punned_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
|
|
333
337
|
|
|
334
338
|
if (wanted == 0)
|
|
335
339
|
return py::tuple(3);
|
|
336
340
|
|
|
341
|
+
if (index.config().max_threads_add < threads)
|
|
342
|
+
throw std::invalid_argument("Can't use that many threads!");
|
|
343
|
+
|
|
337
344
|
py::buffer_info vectors_info = vectors.request();
|
|
338
345
|
if (vectors_info.ndim == 1)
|
|
339
346
|
return search_one_in_index(index, vectors, wanted, exact);
|
|
@@ -355,7 +362,7 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
|
|
|
355
362
|
|
|
356
363
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
357
364
|
if (vectors_info.format == "c" || vectors_info.format == "b")
|
|
358
|
-
multithreaded(
|
|
365
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
359
366
|
search_config_t config;
|
|
360
367
|
config.thread = thread_idx;
|
|
361
368
|
config.exact = exact;
|
|
@@ -364,7 +371,7 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
|
|
|
364
371
|
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
365
372
|
});
|
|
366
373
|
else if (vectors_info.format == "e")
|
|
367
|
-
multithreaded(
|
|
374
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
368
375
|
search_config_t config;
|
|
369
376
|
config.thread = thread_idx;
|
|
370
377
|
config.exact = exact;
|
|
@@ -372,8 +379,8 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
|
|
|
372
379
|
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
373
380
|
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
374
381
|
});
|
|
375
|
-
else if (vectors_info.format == "f")
|
|
376
|
-
multithreaded(
|
|
382
|
+
else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
|
|
383
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
377
384
|
search_config_t config;
|
|
378
385
|
config.thread = thread_idx;
|
|
379
386
|
config.exact = exact;
|
|
@@ -381,8 +388,8 @@ static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, st
|
|
|
381
388
|
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
382
389
|
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
383
390
|
});
|
|
384
|
-
else if (vectors_info.format == "d")
|
|
385
|
-
multithreaded(
|
|
391
|
+
else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
|
|
392
|
+
multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
386
393
|
search_config_t config;
|
|
387
394
|
config.thread = thread_idx;
|
|
388
395
|
config.exact = exact;
|
|
@@ -407,7 +414,6 @@ template <typename index_at> void view_index(index_at& index, std::string const&
|
|
|
407
414
|
template <typename index_at> void clear_index(index_at& index) { index.clear(); }
|
|
408
415
|
template <typename index_at> std::size_t get_expansion_add(index_at const &index) { return index.config().expansion_add; }
|
|
409
416
|
template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
|
|
410
|
-
|
|
411
417
|
// clang-format on
|
|
412
418
|
|
|
413
419
|
template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
|
|
@@ -476,8 +482,12 @@ void hash_buffer(hash_index_py_t& index, py::buffer vector) {
|
|
|
476
482
|
throw std::invalid_argument("Array elements must be 16, 32, or 64 bit hashable integers!");
|
|
477
483
|
}
|
|
478
484
|
|
|
479
|
-
PYBIND11_MODULE(
|
|
480
|
-
m.doc() = "
|
|
485
|
+
PYBIND11_MODULE(compiled, m) {
|
|
486
|
+
m.doc() = "Smaller & Faster Single-File Vector Search Engine from Unum";
|
|
487
|
+
|
|
488
|
+
m.attr("DEFAULT_CONNECTIVITY") = py::int_(default_connectivity());
|
|
489
|
+
m.attr("DEFAULT_EXPANSION_ADD") = py::int_(default_expansion_add());
|
|
490
|
+
m.attr("DEFAULT_EXPANSION_SEARCH") = py::int_(default_expansion_search());
|
|
481
491
|
|
|
482
492
|
auto i = py::class_<punned_py_t>(m, "Index");
|
|
483
493
|
|
|
@@ -508,14 +518,16 @@ PYBIND11_MODULE(index, m) {
|
|
|
508
518
|
py::arg("label"), //
|
|
509
519
|
py::arg("vector"), //
|
|
510
520
|
py::kw_only(), //
|
|
511
|
-
py::arg("copy") = true
|
|
521
|
+
py::arg("copy") = true, //
|
|
522
|
+
py::arg("threads") = 0 //
|
|
512
523
|
);
|
|
513
524
|
|
|
514
525
|
i.def( //
|
|
515
526
|
"search", &search_many_in_index, //
|
|
516
527
|
py::arg("query"), //
|
|
517
528
|
py::arg("count") = 10, //
|
|
518
|
-
py::arg("exact") = false
|
|
529
|
+
py::arg("exact") = false, //
|
|
530
|
+
py::arg("threads") = 0 //
|
|
519
531
|
);
|
|
520
532
|
|
|
521
533
|
i.def("__len__", &punned_py_t::size);
|
package/python/scripts/bench.py
CHANGED
|
@@ -142,7 +142,7 @@ def main(
|
|
|
142
142
|
expansion_add=expansion_add,
|
|
143
143
|
expansion_search=expansion_search,
|
|
144
144
|
connectivity=connectivity,
|
|
145
|
-
|
|
145
|
+
metric=inner_product_f32.address,
|
|
146
146
|
)
|
|
147
147
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
148
148
|
|
|
@@ -153,7 +153,7 @@ def main(
|
|
|
153
153
|
expansion_add=expansion_add,
|
|
154
154
|
expansion_search=expansion_search,
|
|
155
155
|
connectivity=connectivity,
|
|
156
|
-
|
|
156
|
+
metric=cos_f8.address,
|
|
157
157
|
)
|
|
158
158
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
159
159
|
except ImportError:
|
package/python/scripts/test.py
CHANGED
|
@@ -86,7 +86,7 @@ def test_user_defined_function(ndim: int, size: int):
|
|
|
86
86
|
c += a_array[i] * b_array[i]
|
|
87
87
|
return c
|
|
88
88
|
|
|
89
|
-
index = Index(ndim=ndim,
|
|
89
|
+
index = Index(ndim=ndim, metric=python_dot.address)
|
|
90
90
|
|
|
91
91
|
vectors = np.random.uniform(
|
|
92
92
|
0, 0.3, (size, index.ndim)).astype(np.float32)
|
package/python/usearch/client.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from typing import Union, Optional
|
|
1
|
+
from typing import Union, Optional, List
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ucall.client import Client
|
|
5
5
|
|
|
6
|
+
from usearch.index import Matches
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
|
|
8
10
|
if vector.dtype != np.int8 and vector.dtype != np.uint8 and vector.dtype != np.byte:
|
|
@@ -18,9 +20,6 @@ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
|
|
|
18
20
|
return ascii
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
22
|
-
|
|
23
|
-
|
|
24
23
|
class IndexClient:
|
|
25
24
|
|
|
26
25
|
def __init__(
|
|
@@ -53,8 +52,8 @@ class IndexClient:
|
|
|
53
52
|
else:
|
|
54
53
|
return self.add_many(labels, vectors)
|
|
55
54
|
|
|
56
|
-
def search_one(self, vector: np.ndarray, count: int) ->
|
|
57
|
-
matches:
|
|
55
|
+
def search_one(self, vector: np.ndarray, count: int) -> Matches:
|
|
56
|
+
matches: List[dict] = []
|
|
58
57
|
vector = vector.flatten()
|
|
59
58
|
ascii = _vector_to_ascii(vector)
|
|
60
59
|
if ascii:
|
|
@@ -75,9 +74,9 @@ class IndexClient:
|
|
|
75
74
|
|
|
76
75
|
return labels, distances, counts
|
|
77
76
|
|
|
78
|
-
def search_many(self, vectors: np.ndarray, count: int) ->
|
|
77
|
+
def search_many(self, vectors: np.ndarray, count: int) -> Matches:
|
|
79
78
|
batch_size: int = vectors.shape[0]
|
|
80
|
-
list_of_matches:
|
|
79
|
+
list_of_matches: List[List[dict]] = self.client.search_many(
|
|
81
80
|
vectors=vectors, count=count)
|
|
82
81
|
|
|
83
82
|
labels = np.array((batch_size, count), dtype=np.uint32)
|
|
@@ -91,7 +90,7 @@ class IndexClient:
|
|
|
91
90
|
|
|
92
91
|
return labels, distances, counts
|
|
93
92
|
|
|
94
|
-
def search(self, vectors: np.ndarray, count: int) ->
|
|
93
|
+
def search(self, vectors: np.ndarray, count: int) -> Matches:
|
|
95
94
|
if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
|
|
96
95
|
return self.search_one(vectors, count)
|
|
97
96
|
else:
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# The purpose of this file is to provide Pythonic wrapper on top
|
|
2
|
+
# the native precompiled CPython module. It improves compatibility
|
|
3
|
+
# Python tooling, linters, and static analyzers. It also embeds JIT
|
|
4
|
+
# into the primary `Index` class, connecting USearch with Numba.
|
|
5
|
+
import os
|
|
6
|
+
from math import sqrt
|
|
7
|
+
from typing import Optional, Callable, Union, NamedTuple, List
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from usearch.compiled import Index as _CompiledIndex
|
|
12
|
+
from usearch.compiled import SetsIndex as _CompiledSetsIndex
|
|
13
|
+
from usearch.compiled import HashIndex as _CompiledHashIndex
|
|
14
|
+
from usearch.compiled import DEFAULT_CONNECTIVITY, DEFAULT_EXPANSION_ADD, DEFAULT_EXPANSION_SEARCH
|
|
15
|
+
|
|
16
|
+
SetsIndex = _CompiledSetsIndex
|
|
17
|
+
HashIndex = _CompiledHashIndex
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Matches(NamedTuple):
|
|
21
|
+
labels: np.ndarray
|
|
22
|
+
distances: np.ndarray
|
|
23
|
+
counts: np.ndarray
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def list_matches(results: Matches, row: int) -> List[dict]:
|
|
27
|
+
|
|
28
|
+
count = results[2][row]
|
|
29
|
+
labels = results[0][row, :count]
|
|
30
|
+
distances = results[1][row, :count]
|
|
31
|
+
return [
|
|
32
|
+
{'label': int(label), 'distance': float(distance)}
|
|
33
|
+
for label, distance in zip(labels, distances)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from numba import cfunc, types, carray
|
|
41
|
+
except ImportError:
|
|
42
|
+
raise ModuleNotFoundError(
|
|
43
|
+
'To use JIT install Numba with `pip install numba`.'
|
|
44
|
+
'Alternatively, reinstall usearch with `pip install usearch[jit]`')
|
|
45
|
+
|
|
46
|
+
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
47
|
+
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
48
|
+
|
|
49
|
+
if accuracy == 'f32':
|
|
50
|
+
signature = types.float32(
|
|
51
|
+
types.CPointer(types.float32),
|
|
52
|
+
types.CPointer(types.float32),
|
|
53
|
+
types.size_t, types.size_t)
|
|
54
|
+
|
|
55
|
+
if metric_name == 'ip':
|
|
56
|
+
|
|
57
|
+
def numba_ip(a, b, _n, _m):
|
|
58
|
+
a_array = carray(a, ndim)
|
|
59
|
+
b_array = carray(b, ndim)
|
|
60
|
+
ab = 0.0
|
|
61
|
+
for i in range(ndim):
|
|
62
|
+
ab += a_array[i] * b_array[i]
|
|
63
|
+
return ab
|
|
64
|
+
|
|
65
|
+
return cfunc(numba_ip, signature)
|
|
66
|
+
|
|
67
|
+
if metric_name == 'cos':
|
|
68
|
+
|
|
69
|
+
def numba_cos(a, b, _n, _m):
|
|
70
|
+
a_array = carray(a, ndim)
|
|
71
|
+
b_array = carray(b, ndim)
|
|
72
|
+
ab = 0.0
|
|
73
|
+
a_sq = 0.0
|
|
74
|
+
b_sq = 0.0
|
|
75
|
+
for i in range(ndim):
|
|
76
|
+
ab += a_array[i] * b_array[i]
|
|
77
|
+
a_sq += a_array[i] * a_array[i]
|
|
78
|
+
b_sq += b_array[i] * b_array[i]
|
|
79
|
+
return ab / (sqrt(a_sq) * sqrt(b_sq))
|
|
80
|
+
|
|
81
|
+
return cfunc(numba_cos, signature)
|
|
82
|
+
|
|
83
|
+
if metric_name == 'l2sq':
|
|
84
|
+
|
|
85
|
+
def numba_l2sq(a, b, _n, _m):
|
|
86
|
+
a_array = carray(a, ndim)
|
|
87
|
+
b_array = carray(b, ndim)
|
|
88
|
+
ab_delta_sq = 0.0
|
|
89
|
+
for i in range(ndim):
|
|
90
|
+
ab_delta_sq += (a_array[i] - b_array[i]) * \
|
|
91
|
+
(a_array[i] - b_array[i])
|
|
92
|
+
return ab_delta_sq
|
|
93
|
+
|
|
94
|
+
return cfunc(numba_l2sq, signature)
|
|
95
|
+
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Index:
|
|
100
|
+
"""Fast JIT-compiled index for equi-dimensional embeddings.
|
|
101
|
+
|
|
102
|
+
Vector labels must be integers.
|
|
103
|
+
Vectors must have the same number of dimensions within the index.
|
|
104
|
+
Supports Inner Product, Cosine Distance, Ln measures
|
|
105
|
+
like the Euclidean metric, as well as automatic downcasting
|
|
106
|
+
and quantization.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def __init__(
|
|
110
|
+
self,
|
|
111
|
+
ndim: int,
|
|
112
|
+
dtype: str = 'f32',
|
|
113
|
+
metric: Union[str, int] = 'ip',
|
|
114
|
+
jit: bool = False,
|
|
115
|
+
|
|
116
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
117
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
118
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
119
|
+
tune: bool = False,
|
|
120
|
+
|
|
121
|
+
path: Optional[os.PathLike] = None,
|
|
122
|
+
view: bool = False,
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Construct the index and compiles the functions, if requested (expensive).
|
|
125
|
+
|
|
126
|
+
:param ndim: Number of vector dimensions
|
|
127
|
+
:type ndim: int
|
|
128
|
+
:param dtype: Scalar type for vectors, defaults to 'f32'
|
|
129
|
+
:type dtype: str, optional
|
|
130
|
+
Example: you can use the `f16` index with `f32` vectors,
|
|
131
|
+
which will be automatically downcasted.
|
|
132
|
+
|
|
133
|
+
:param metric: Distance function, defaults to 'ip'
|
|
134
|
+
:type metric: Union[str, int], optional
|
|
135
|
+
Name of distance function, or the address of the
|
|
136
|
+
Numba `cfunc` JIT-compiled object.
|
|
137
|
+
|
|
138
|
+
:param jit: Enable Numba to JIT compile the metric, defaults to False
|
|
139
|
+
:type jit: bool, optional
|
|
140
|
+
This can result in up-to 3x performance difference on very large vectors
|
|
141
|
+
and very recent hardware, as the Python module is compiled with high
|
|
142
|
+
compatibility in mind and avoids very fancy assembly instructions.
|
|
143
|
+
|
|
144
|
+
:param connectivity: Connections per node in HNSW, defaults to None
|
|
145
|
+
:type connectivity: Optional[int], optional
|
|
146
|
+
Hyper-parameter for the number of Graph connections
|
|
147
|
+
per layer of HNSW. The original paper calls it "M".
|
|
148
|
+
Optional, but can't be changed after construction.
|
|
149
|
+
|
|
150
|
+
:param expansion_add: Traversal depth on insertions, defaults to None
|
|
151
|
+
:type expansion_add: Optional[int], optional
|
|
152
|
+
Hyper-parameter for the search depth when inserting new
|
|
153
|
+
vectors. The original paper calls it "efConstruction".
|
|
154
|
+
Can be changed afterwards, as the `.expansion_add`.
|
|
155
|
+
|
|
156
|
+
:param expansion_search: Traversal depth on queries, defaults to None
|
|
157
|
+
:type expansion_search: Optional[int], optional
|
|
158
|
+
Hyper-parameter for the search depth when querying
|
|
159
|
+
nearest neighbors. The original paper calls it "ef".
|
|
160
|
+
Can be changed afterwards, as the `.expansion_search`.
|
|
161
|
+
|
|
162
|
+
:param tune: Automatically adjusts hyper-parameters, defaults to False
|
|
163
|
+
:type tune: bool, optional
|
|
164
|
+
|
|
165
|
+
:param path: Where to store the index, defaults to None
|
|
166
|
+
:type path: Optional[os.PathLike], optional
|
|
167
|
+
:param view: Are we simply viewing an immutable index, defaults to False
|
|
168
|
+
:type view: bool, optional
|
|
169
|
+
"""
|
|
170
|
+
if jit:
|
|
171
|
+
assert isinstance(metric, str), 'Name the metric to JIT'
|
|
172
|
+
self._metric_name = metric
|
|
173
|
+
self._metric_jit = jit_metric(
|
|
174
|
+
ndim=ndim,
|
|
175
|
+
metric_name=metric,
|
|
176
|
+
dtype=dtype,
|
|
177
|
+
)
|
|
178
|
+
self._metric_pointer = self._metric_jit.address if \
|
|
179
|
+
self._metric_jit else 0
|
|
180
|
+
|
|
181
|
+
elif isinstance(metric, int):
|
|
182
|
+
self._metric_name = ''
|
|
183
|
+
self._metric_jit = None
|
|
184
|
+
self._metric_pointer = metric
|
|
185
|
+
|
|
186
|
+
else:
|
|
187
|
+
if metric is None:
|
|
188
|
+
metric = 'ip'
|
|
189
|
+
assert isinstance(metric, str)
|
|
190
|
+
self._metric_name = metric
|
|
191
|
+
self._metric_jit = None
|
|
192
|
+
self._metric_pointer = 0
|
|
193
|
+
|
|
194
|
+
self._compiled = _CompiledIndex(
|
|
195
|
+
ndim=ndim,
|
|
196
|
+
metric=self._metric_name,
|
|
197
|
+
metric_pointer=self._metric_pointer,
|
|
198
|
+
dtype=dtype,
|
|
199
|
+
connectivity=connectivity,
|
|
200
|
+
expansion_add=expansion_add,
|
|
201
|
+
expansion_search=expansion_search,
|
|
202
|
+
tune=tune,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.path = path
|
|
206
|
+
if path and os.path.exists(path):
|
|
207
|
+
if view:
|
|
208
|
+
self._compiled.view(path)
|
|
209
|
+
else:
|
|
210
|
+
self._compiled.load(path)
|
|
211
|
+
|
|
212
|
+
def add(
|
|
213
|
+
self, labels, vectors, *,
|
|
214
|
+
copy: bool = True, threads: int = 0):
|
|
215
|
+
"""Inserts one or move vectors into the index.
|
|
216
|
+
|
|
217
|
+
For maximal performance the `labels` and `vectors`
|
|
218
|
+
should conform to the Python's "buffer protocol" spec.
|
|
219
|
+
|
|
220
|
+
To index a single entry:
|
|
221
|
+
labels: int, vectors: np.ndarray.
|
|
222
|
+
To index many entries:
|
|
223
|
+
labels: np.ndarray, vectors: np.ndarray.
|
|
224
|
+
|
|
225
|
+
When working with extremely large indexes, you may want to
|
|
226
|
+
pass `copy=False`, if you can guarantee the lifetime of the
|
|
227
|
+
primary vectors store during the process of construction.
|
|
228
|
+
|
|
229
|
+
:param labels: Unique identifier for passed vectors.
|
|
230
|
+
:type labels: Buffer
|
|
231
|
+
:param vectors: Collection of vectors.
|
|
232
|
+
:type vectors: Buffer
|
|
233
|
+
:param copy: Should the index store a copy of vectors, defaults to True
|
|
234
|
+
:type copy: bool, optional
|
|
235
|
+
:param threads: Optimal number of cores to use, defaults to 0
|
|
236
|
+
:type threads: int, optional
|
|
237
|
+
"""
|
|
238
|
+
if isinstance(labels, np.ndarray):
|
|
239
|
+
labels = labels.astype(np.longlong)
|
|
240
|
+
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
241
|
+
|
|
242
|
+
def search(
|
|
243
|
+
self, vectors, k: int = 10, *,
|
|
244
|
+
threads: int = 0, exact: bool = False) -> Matches:
|
|
245
|
+
tuple_ = self._compiled.search(
|
|
246
|
+
vectors, k,
|
|
247
|
+
exact=exact, threads=threads)
|
|
248
|
+
return Matches(*tuple_)
|
|
249
|
+
|
|
250
|
+
def __len__(self) -> int:
|
|
251
|
+
return len(self._compiled)
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def size(self) -> int:
|
|
255
|
+
return self._compiled.size
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def ndim(self) -> int:
|
|
259
|
+
return self._compiled.ndim
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def dtype(self) -> str:
|
|
263
|
+
return self._compiled.dtype
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def connectivity(self) -> int:
|
|
267
|
+
return self._compiled.connectivity
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def capacity(self) -> int:
|
|
271
|
+
return self._compiled.capacity
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def memory_usage(self) -> int:
|
|
275
|
+
return self._compiled.memory_usage
|
|
276
|
+
|
|
277
|
+
@property
|
|
278
|
+
def expansion_add(self) -> int:
|
|
279
|
+
return self._compiled.expansion_add
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def expansion_search(self) -> int:
|
|
283
|
+
return self._compiled.expansion_search
|
|
284
|
+
|
|
285
|
+
@expansion_add.setter
|
|
286
|
+
def change_expansion_add(self, v: int):
|
|
287
|
+
self._compiled.expansion_add = v
|
|
288
|
+
|
|
289
|
+
@expansion_search.setter
|
|
290
|
+
def change_expansion_search(self, v: int):
|
|
291
|
+
self._compiled.expansion_search = v
|
|
292
|
+
|
|
293
|
+
def save(self, path: os.PathLike):
|
|
294
|
+
self._compiled.save(path)
|
|
295
|
+
|
|
296
|
+
def load(self, path: os.PathLike):
|
|
297
|
+
self._compiled.load(path)
|
|
298
|
+
|
|
299
|
+
def view(self, path: os.PathLike):
|
|
300
|
+
self._compiled.view(path)
|
|
301
|
+
|
|
302
|
+
def clear(self):
|
|
303
|
+
self._compiled.clear()
|
|
304
|
+
|
|
305
|
+
def remove(self, label: int):
|
|
306
|
+
pass
|
package/python/usearch/server.py
CHANGED
|
@@ -4,20 +4,10 @@
|
|
|
4
4
|
import os
|
|
5
5
|
import argparse
|
|
6
6
|
import numpy as np
|
|
7
|
+
from typing import List
|
|
7
8
|
|
|
8
9
|
from ucall.rich_posix import Server
|
|
9
|
-
from usearch.index import Index
|
|
10
|
-
|
|
11
|
-
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _results_to_json(results: Triplet, row: int) -> list[dict]:
|
|
15
|
-
|
|
16
|
-
count = results[2][row]
|
|
17
|
-
labels = results[0][row, :count]
|
|
18
|
-
distances = results[1][row, :count]
|
|
19
|
-
return [{'label': int(label), 'distance': float(distance)}
|
|
20
|
-
for label, distance in zip(labels, distances)]
|
|
10
|
+
from usearch.index import Index, list_matches
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
def _ascii_to_vector(string: str) -> np.ndarray:
|
|
@@ -78,16 +68,16 @@ def serve(
|
|
|
78
68
|
index.add(labels, vectors, threads=threads)
|
|
79
69
|
|
|
80
70
|
@server
|
|
81
|
-
def search_one(vector: np.ndarray, count: int) ->
|
|
71
|
+
def search_one(vector: np.ndarray, count: int) -> List[dict]:
|
|
82
72
|
print('search', vector, count)
|
|
83
73
|
vectors = vector.reshape(vector.shape[0], 1)
|
|
84
74
|
results = index.search(vectors, count)
|
|
85
|
-
return
|
|
75
|
+
return list_matches(results, 0)
|
|
86
76
|
|
|
87
77
|
@server
|
|
88
|
-
def search_many(vectors: np.ndarray, count: int) ->
|
|
78
|
+
def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
|
|
89
79
|
results = index.search(vectors, count)
|
|
90
|
-
return [
|
|
80
|
+
return [list_matches(results, i) for i in range(vectors.shape[0])]
|
|
91
81
|
|
|
92
82
|
@server
|
|
93
83
|
def add_ascii(label: int, string: str):
|