usearch 0.20.0 → 0.21.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/.github/workflows/prerelease.yml +27 -0
- package/Cargo.toml +1 -1
- package/README.md +20 -1
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +6 -2
- package/cpp/bench.cpp +41 -4
- package/docs/benchmarks.md +14 -12
- package/include/usearch/index.hpp +475 -56
- package/include/usearch/index_punned_dense.hpp +68 -10
- package/include/usearch/index_punned_helpers.hpp +28 -3
- package/package.json +1 -1
- package/python/lib.cpp +57 -5
- package/python/scripts/join.py +151 -0
- package/python/scripts/test.py +19 -0
- package/python/usearch/eval.py +11 -3
- package/python/usearch/index.py +119 -10
- package/python/usearch/join.py +0 -98
|
@@ -170,6 +170,7 @@ class index_punned_dense_gt {
|
|
|
170
170
|
using search_result_t = typename index_t::search_result_t;
|
|
171
171
|
using add_result_t = typename index_t::add_result_t;
|
|
172
172
|
using serialization_result_t = typename index_t::serialization_result_t;
|
|
173
|
+
using join_result_t = typename index_t::join_result_t;
|
|
173
174
|
using stats_t = typename index_t::stats_t;
|
|
174
175
|
|
|
175
176
|
index_punned_dense_gt() = default;
|
|
@@ -184,12 +185,15 @@ class index_punned_dense_gt {
|
|
|
184
185
|
void swap(index_punned_dense_gt& other) {
|
|
185
186
|
std::swap(dimensions_, other.dimensions_);
|
|
186
187
|
std::swap(scalar_words_, other.scalar_words_);
|
|
188
|
+
std::swap(expansion_add_, other.expansion_add_);
|
|
189
|
+
std::swap(expansion_search_, other.expansion_search_);
|
|
187
190
|
std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
|
|
188
191
|
std::swap(typed_, other.typed_);
|
|
189
192
|
std::swap(cast_buffer_, other.cast_buffer_);
|
|
190
193
|
std::swap(casts_, other.casts_);
|
|
191
194
|
std::swap(root_metric_, other.root_metric_);
|
|
192
195
|
std::swap(available_threads_, other.available_threads_);
|
|
196
|
+
std::swap(lookup_table_, other.lookup_table_);
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
static index_config_t optimize(index_config_t config) { return index_t::optimize(config); }
|
|
@@ -199,6 +203,7 @@ class index_punned_dense_gt {
|
|
|
199
203
|
std::size_t connectivity() const { return typed_->connectivity(); }
|
|
200
204
|
std::size_t size() const { return typed_->size(); }
|
|
201
205
|
std::size_t capacity() const { return typed_->capacity(); }
|
|
206
|
+
std::size_t max_level() const noexcept { return typed_->max_level(); }
|
|
202
207
|
index_config_t const& config() const { return typed_->config(); }
|
|
203
208
|
index_limits_t const& limits() const { return typed_->limits(); }
|
|
204
209
|
void clear() { return typed_->clear(); }
|
|
@@ -323,23 +328,76 @@ class index_punned_dense_gt {
|
|
|
323
328
|
metric_t(metric), make_casts_(accuracy));
|
|
324
329
|
}
|
|
325
330
|
|
|
326
|
-
|
|
327
|
-
index_punned_dense_gt
|
|
331
|
+
struct copy_result_t {
|
|
332
|
+
index_punned_dense_gt index;
|
|
333
|
+
error_t error;
|
|
334
|
+
|
|
335
|
+
explicit operator bool() const noexcept { return !error; }
|
|
336
|
+
copy_result_t failed(error_t message) noexcept {
|
|
337
|
+
error = std::move(message);
|
|
338
|
+
return std::move(*this);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
copy_result_t copy(copy_config_t config = {}) const {
|
|
343
|
+
copy_result_t result = fork();
|
|
344
|
+
if (!result)
|
|
345
|
+
return result;
|
|
346
|
+
auto typed_result = typed_->copy(config);
|
|
347
|
+
if (!typed_result)
|
|
348
|
+
return result.failed(std::move(typed_result.error));
|
|
349
|
+
*result.index.typed_ = std::move(typed_result.index);
|
|
350
|
+
result.index.lookup_table_ = lookup_table_;
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
328
353
|
|
|
329
|
-
|
|
330
|
-
result
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
354
|
+
copy_result_t fork() const {
|
|
355
|
+
copy_result_t result;
|
|
356
|
+
index_punned_dense_gt& other = result.index;
|
|
357
|
+
|
|
358
|
+
other.dimensions_ = dimensions_;
|
|
359
|
+
other.scalar_words_ = scalar_words_;
|
|
360
|
+
other.expansion_add_ = expansion_add_;
|
|
361
|
+
other.expansion_search_ = expansion_search_;
|
|
362
|
+
other.casted_vector_bytes_ = casted_vector_bytes_;
|
|
363
|
+
other.cast_buffer_ = cast_buffer_;
|
|
364
|
+
other.casts_ = casts_;
|
|
365
|
+
|
|
366
|
+
other.root_metric_ = root_metric_;
|
|
367
|
+
other.available_threads_ = available_threads_;
|
|
334
368
|
|
|
335
|
-
result.root_metric_ = root_metric_;
|
|
336
369
|
index_t* raw = index_allocator_t{}.allocate(1);
|
|
337
|
-
|
|
338
|
-
|
|
370
|
+
if (!raw)
|
|
371
|
+
return result.failed("Can't allocate the index");
|
|
339
372
|
|
|
373
|
+
new (raw) index_t(config(), root_metric_);
|
|
374
|
+
other.typed_ = raw;
|
|
340
375
|
return result;
|
|
341
376
|
}
|
|
342
377
|
|
|
378
|
+
template < //
|
|
379
|
+
typename first_to_second_at = dummy_label_to_label_mapping_t, //
|
|
380
|
+
typename second_to_first_at = dummy_label_to_label_mapping_t, //
|
|
381
|
+
typename executor_at = dummy_executor_t, //
|
|
382
|
+
typename progress_at = dummy_progress_t //
|
|
383
|
+
>
|
|
384
|
+
static join_result_t join( //
|
|
385
|
+
index_punned_dense_gt const& first, //
|
|
386
|
+
index_punned_dense_gt const& second, //
|
|
387
|
+
join_config_t config = {}, //
|
|
388
|
+
first_to_second_at&& first_to_second = first_to_second_at{}, //
|
|
389
|
+
second_to_first_at&& second_to_first = second_to_first_at{}, //
|
|
390
|
+
executor_at&& executor = executor_at{}, //
|
|
391
|
+
progress_at&& progress = progress_at{}) noexcept {
|
|
392
|
+
|
|
393
|
+
return index_t::join( //
|
|
394
|
+
*first.typed_, *second.typed_, config, //
|
|
395
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
396
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
397
|
+
std::forward<executor_at>(executor), //
|
|
398
|
+
std::forward<progress_at>(progress));
|
|
399
|
+
}
|
|
400
|
+
|
|
343
401
|
private:
|
|
344
402
|
struct thread_lock_t {
|
|
345
403
|
index_punned_dense_gt const& parent;
|
|
@@ -313,9 +313,11 @@ class executor_stl_t {
|
|
|
313
313
|
std::size_t threads_count_{};
|
|
314
314
|
|
|
315
315
|
public:
|
|
316
|
-
executor_stl_t(std::size_t threads_count) noexcept
|
|
316
|
+
executor_stl_t(std::size_t threads_count = 0) noexcept
|
|
317
317
|
: threads_count_(threads_count ? threads_count : std::thread::hardware_concurrency()) {}
|
|
318
318
|
|
|
319
|
+
std::size_t size() const noexcept { return threads_count_; }
|
|
320
|
+
|
|
319
321
|
template <typename thread_aware_function_at>
|
|
320
322
|
void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
|
|
321
323
|
std::vector<std::thread> threads_pool;
|
|
@@ -330,22 +332,39 @@ class executor_stl_t {
|
|
|
330
332
|
for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
|
|
331
333
|
threads_pool[thread_idx].join();
|
|
332
334
|
}
|
|
335
|
+
|
|
336
|
+
template <typename thread_aware_function_at>
|
|
337
|
+
void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
|
|
338
|
+
std::vector<std::thread> threads_pool;
|
|
339
|
+
for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
|
|
340
|
+
threads_pool.emplace_back([=]() { thread_aware_function(thread_idx); });
|
|
341
|
+
for (std::size_t thread_idx = 0; thread_idx != threads_count_; ++thread_idx)
|
|
342
|
+
threads_pool[thread_idx].join();
|
|
343
|
+
}
|
|
333
344
|
};
|
|
334
345
|
|
|
335
346
|
#if USEARCH_USE_OPENMP
|
|
336
347
|
|
|
337
348
|
class executor_openmp_t {
|
|
338
349
|
public:
|
|
339
|
-
executor_openmp_t(std::size_t threads_count) noexcept {
|
|
350
|
+
executor_openmp_t(std::size_t threads_count = 0) noexcept {
|
|
340
351
|
omp_set_num_threads(threads_count ? threads_count : std::thread::hardware_concurrency());
|
|
341
352
|
}
|
|
342
353
|
|
|
354
|
+
std::size_t size() const noexcept { return omp_get_num_threads(); }
|
|
355
|
+
|
|
343
356
|
template <typename thread_aware_function_at>
|
|
344
357
|
void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept(false) {
|
|
345
358
|
#pragma omp parallel for schedule(dynamic)
|
|
346
|
-
for (std::size_t i = 0; i
|
|
359
|
+
for (std::size_t i = 0; i != tasks; ++i)
|
|
347
360
|
thread_aware_function(omp_get_thread_num(), i);
|
|
348
361
|
}
|
|
362
|
+
|
|
363
|
+
template <typename thread_aware_function_at>
|
|
364
|
+
void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept(false) {
|
|
365
|
+
#pragma omp parallel
|
|
366
|
+
{ thread_aware_function(omp_get_thread_num()); }
|
|
367
|
+
}
|
|
349
368
|
};
|
|
350
369
|
|
|
351
370
|
using executor_default_t = executor_openmp_t;
|
|
@@ -451,6 +470,12 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
|
|
|
451
470
|
return *this;
|
|
452
471
|
}
|
|
453
472
|
|
|
473
|
+
memory_mapping_allocator_gt(memory_mapping_allocator_gt const&) noexcept {}
|
|
474
|
+
memory_mapping_allocator_gt& operator=(memory_mapping_allocator_gt const&) noexcept {
|
|
475
|
+
reset();
|
|
476
|
+
return *this;
|
|
477
|
+
}
|
|
478
|
+
|
|
454
479
|
~memory_mapping_allocator_gt() noexcept { reset(); }
|
|
455
480
|
|
|
456
481
|
inline byte_t* allocate(std::size_t count_bytes) noexcept {
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
#define PY_SSIZE_T_CLEAN
|
|
19
19
|
#include <pybind11/numpy.h>
|
|
20
20
|
#include <pybind11/pybind11.h>
|
|
21
|
+
#include <pybind11/stl.h>
|
|
21
22
|
|
|
22
23
|
#include <usearch/index_punned_dense.hpp>
|
|
23
24
|
|
|
@@ -356,7 +357,6 @@ static py::tuple search_many_in_index( //
|
|
|
356
357
|
|
|
357
358
|
Py_ssize_t vectors_count = vectors_info.shape[0];
|
|
358
359
|
Py_ssize_t vectors_dimensions = vectors_info.shape[1];
|
|
359
|
-
char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
|
|
360
360
|
if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
|
|
361
361
|
throw std::invalid_argument("The number of vector dimensions doesn't match!");
|
|
362
362
|
|
|
@@ -381,10 +381,48 @@ static py::tuple search_many_in_index( //
|
|
|
381
381
|
return results;
|
|
382
382
|
}
|
|
383
383
|
|
|
384
|
+
static std::unordered_map<label_t, label_t> join_index( //
|
|
385
|
+
dense_index_py_t const& a, dense_index_py_t const& b, //
|
|
386
|
+
std::size_t max_proposals, bool exact) {
|
|
387
|
+
|
|
388
|
+
std::unordered_map<label_t, label_t> a_to_b;
|
|
389
|
+
a_to_b.reserve((std::min)(a.size(), b.size()));
|
|
390
|
+
|
|
391
|
+
using join_result_t = typename dense_index_py_t::join_result_t;
|
|
392
|
+
join_config_t config;
|
|
393
|
+
|
|
394
|
+
config.max_proposals = max_proposals;
|
|
395
|
+
config.exact = exact;
|
|
396
|
+
config.expansion = (std::max)(a.expansion_search(), b.expansion_search());
|
|
397
|
+
std::size_t threads = (std::min)(a.limits().threads(), b.limits().threads());
|
|
398
|
+
executor_default_t executor{threads};
|
|
399
|
+
join_result_t result = dense_index_py_t::join( //
|
|
400
|
+
a, b, config, //
|
|
401
|
+
a_to_b, //
|
|
402
|
+
dummy_label_to_label_mapping_t{}, //
|
|
403
|
+
executor);
|
|
404
|
+
result.error.raise();
|
|
405
|
+
return a_to_b;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
static dense_index_py_t copy_index(dense_index_py_t const& index) {
|
|
409
|
+
|
|
410
|
+
using copy_result_t = typename dense_index_py_t::copy_result_t;
|
|
411
|
+
copy_config_t config;
|
|
412
|
+
|
|
413
|
+
copy_result_t result = index.copy();
|
|
414
|
+
result.error.raise();
|
|
415
|
+
return std::move(result.index);
|
|
416
|
+
}
|
|
417
|
+
|
|
384
418
|
// clang-format off
|
|
385
419
|
template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
|
|
386
420
|
template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
|
|
387
421
|
template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
|
|
422
|
+
template <typename index_at> void clear_index(index_at& index) { index.clear(); }
|
|
423
|
+
template <typename index_at> std::size_t max_level(index_at const &index) { return index.max_level(); }
|
|
424
|
+
template <typename index_at> typename index_at::stats_t compute_stats(index_at const &index) { return index.stats(); }
|
|
425
|
+
template <typename index_at> typename index_at::stats_t compute_level_stats(index_at const &index, std::size_t level) { return index.stats(level); }
|
|
388
426
|
// clang-format on
|
|
389
427
|
|
|
390
428
|
template <typename internal_at, typename external_at = internal_at, typename index_at = void>
|
|
@@ -650,7 +688,21 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
650
688
|
i.def("save", &save_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
651
689
|
i.def("load", &load_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
652
690
|
i.def("view", &view_index<dense_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
|
|
653
|
-
i.def("clear", &dense_index_py_t
|
|
691
|
+
i.def("clear", &clear_index<dense_index_py_t>, py::call_guard<py::gil_scoped_release>());
|
|
692
|
+
i.def("copy", ©_index, py::call_guard<py::gil_scoped_release>());
|
|
693
|
+
i.def("join", &join_index, py::arg("other"), py::arg("max_proposals") = 0, py::arg("exact") = false,
|
|
694
|
+
py::call_guard<py::gil_scoped_release>());
|
|
695
|
+
|
|
696
|
+
using punned_index_stats_t = typename dense_index_py_t::stats_t;
|
|
697
|
+
auto i_stats = py::class_<punned_index_stats_t>(m, "IndexStats");
|
|
698
|
+
i_stats.def_readonly("nodes", &punned_index_stats_t::nodes);
|
|
699
|
+
i_stats.def_readonly("edges", &punned_index_stats_t::edges);
|
|
700
|
+
i_stats.def_readonly("max_edges", &punned_index_stats_t::max_edges);
|
|
701
|
+
i_stats.def_readonly("allocated_bytes", &punned_index_stats_t::allocated_bytes);
|
|
702
|
+
|
|
703
|
+
i.def_property_readonly("max_level", &max_level<dense_index_py_t>);
|
|
704
|
+
i.def_property_readonly("levels_stats", &compute_stats<dense_index_py_t>);
|
|
705
|
+
i.def("level_stats", &compute_level_stats<dense_index_py_t>, py::arg("level"));
|
|
654
706
|
|
|
655
707
|
auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
|
|
656
708
|
|
|
@@ -704,8 +756,8 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
704
756
|
si.def_property_readonly("connectivity", &sparse_index_py_t::connectivity);
|
|
705
757
|
si.def_property_readonly("capacity", &sparse_index_py_t::capacity);
|
|
706
758
|
|
|
707
|
-
si.def("save", &sparse_index_py_t
|
|
708
|
-
si.def("load", &sparse_index_py_t
|
|
709
|
-
si.def("view", &sparse_index_py_t
|
|
759
|
+
si.def("save", &save_index<sparse_index_py_t>, py::arg("path"));
|
|
760
|
+
si.def("load", &load_index<sparse_index_py_t>, py::arg("path"));
|
|
761
|
+
si.def("view", &view_index<sparse_index_py_t>, py::arg("path"));
|
|
710
762
|
si.def("clear", &sparse_index_py_t::clear);
|
|
711
763
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/clip_images.fbin -P datasets/cc_3M/
|
|
3
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/clip_texts.fbin -P datasets/cc_3M/
|
|
4
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/images.fbin -P datasets/cc_3M/
|
|
5
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/texts.fbin -P datasets/cc_3M/
|
|
6
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/uform_english_images.fbin -P datasets/cc_3M/
|
|
7
|
+
wget -nc https://huggingface.co/datasets/unum-cloud/ann-cc-3m/resolve/main/uform_english_texts.fbin -P datasets/cc_3M/
|
|
8
|
+
rm -rf datasets/cc_3M/*.usearch
|
|
9
|
+
python python/scripts/join.py
|
|
10
|
+
"""
|
|
11
|
+
from numpy import dot
|
|
12
|
+
from numpy.linalg import norm
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
from simsimd import cos_f32x4_neon, to_int
|
|
15
|
+
|
|
16
|
+
from usearch.index import Index, MetricKind, CompiledMetric, MetricSignature
|
|
17
|
+
from usearch.io import load_matrix
|
|
18
|
+
from usearch.eval import measure_seconds
|
|
19
|
+
|
|
20
|
+
k = 10
|
|
21
|
+
exact = False
|
|
22
|
+
batch_size = 1024 * 4
|
|
23
|
+
max_elements = 1000000
|
|
24
|
+
|
|
25
|
+
a_name = "cc_3M/texts"
|
|
26
|
+
b_name = "cc_3M/images"
|
|
27
|
+
|
|
28
|
+
a_mat = load_matrix(f"datasets/{a_name}.fbin", view=True)
|
|
29
|
+
b_mat = load_matrix(f"datasets/{b_name}.fbin", view=True)
|
|
30
|
+
|
|
31
|
+
a_mat = a_mat[:max_elements]
|
|
32
|
+
b_mat = b_mat[:max_elements]
|
|
33
|
+
|
|
34
|
+
print(f"Loaded two datasets of shape: {a_mat.shape}, {b_mat.shape}")
|
|
35
|
+
print("--------------------------------------")
|
|
36
|
+
print("---------------Indexing---------------")
|
|
37
|
+
print("--------------------------------------")
|
|
38
|
+
|
|
39
|
+
metric = CompiledMetric(
|
|
40
|
+
pointer=to_int(cos_f32x4_neon),
|
|
41
|
+
kind=MetricKind.Cos,
|
|
42
|
+
signature=MetricSignature.ArrayArraySize,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
a = Index(
|
|
46
|
+
a_mat.shape[1],
|
|
47
|
+
metric=metric,
|
|
48
|
+
path=f"datasets/{a_name}.f32.usearch",
|
|
49
|
+
dtype="f32",
|
|
50
|
+
)
|
|
51
|
+
b = Index(
|
|
52
|
+
b_mat.shape[1],
|
|
53
|
+
metric=metric,
|
|
54
|
+
path=f"datasets/{b_name}.f32.usearch",
|
|
55
|
+
dtype="f32",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if len(a) != a_mat.shape[0]:
|
|
59
|
+
a.clear()
|
|
60
|
+
a.add(None, a_mat, log=True, batch_size=batch_size)
|
|
61
|
+
a.save()
|
|
62
|
+
|
|
63
|
+
if len(b) != b_mat.shape[0]:
|
|
64
|
+
b.clear()
|
|
65
|
+
b.add(None, b_mat, log=True, batch_size=batch_size)
|
|
66
|
+
b.save()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
print(
|
|
70
|
+
f"Loaded two indexes of size: {len(a):,} for {a_name} and {len(b):,} for {b_name}"
|
|
71
|
+
)
|
|
72
|
+
min_elements = min(len(a), len(b))
|
|
73
|
+
|
|
74
|
+
run_diagnostics = input("Would you like to run diagnostics? [Y/n]: ")
|
|
75
|
+
if len(run_diagnostics) == 0 or run_diagnostics.lower() == "y":
|
|
76
|
+
print("--------------------------------------")
|
|
77
|
+
print("-------------Diagnostics--------------")
|
|
78
|
+
print("--------------------------------------")
|
|
79
|
+
|
|
80
|
+
mean_similarity = 0.0
|
|
81
|
+
mean_recovered_similarity = 0.0
|
|
82
|
+
|
|
83
|
+
for i in tqdm(range(min_elements), desc="Pairwise Similarity"):
|
|
84
|
+
a_vec = a_mat[i]
|
|
85
|
+
b_vec = b_mat[i]
|
|
86
|
+
cos_similarity = dot(a_vec, b_vec) / (norm(a_vec) * norm(b_vec))
|
|
87
|
+
mean_similarity += cos_similarity
|
|
88
|
+
|
|
89
|
+
a_vec = a[i]
|
|
90
|
+
b_vec = b[i]
|
|
91
|
+
cos_similarity = dot(a_vec, b_vec) / (norm(a_vec) * norm(b_vec))
|
|
92
|
+
mean_recovered_similarity += cos_similarity
|
|
93
|
+
|
|
94
|
+
mean_similarity /= min_elements
|
|
95
|
+
mean_recovered_similarity /= min_elements
|
|
96
|
+
print(
|
|
97
|
+
f"Average vector similarity is {mean_similarity:.4f} in original dataset, "
|
|
98
|
+
f"and {mean_recovered_similarity:.4f} in recovered state in index"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
dt = measure_seconds
|
|
102
|
+
args = dict(
|
|
103
|
+
k=k,
|
|
104
|
+
batch_size=batch_size,
|
|
105
|
+
log=True,
|
|
106
|
+
exact=exact,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
secs, a_self_recall = dt(lambda: a.search(a.vectors, **args).recall(a.labels))
|
|
110
|
+
print(
|
|
111
|
+
"Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
|
|
112
|
+
k, a_name, a_self_recall * 100, secs
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
secs, b_self_recall = dt(lambda: b.search(b.vectors, **args).recall(b.labels))
|
|
117
|
+
print(
|
|
118
|
+
"Self-recall @{} of {} index: {:.2f}%, took {:.2f}s".format(
|
|
119
|
+
k, b_name, b_self_recall * 100, secs
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
secs, ab_recall = dt(lambda: b.search(a.vectors, **args).recall(b.labels))
|
|
124
|
+
print(
|
|
125
|
+
"Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
|
|
126
|
+
k, a_name, b_name, ab_recall * 100, secs
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
secs, ba_recall = dt(lambda: a.search(b.vectors, **args).recall(a.labels))
|
|
131
|
+
print(
|
|
132
|
+
"Cross-recall @{} of {} in {}: {:.2f}%, took {:.2f}s".format(
|
|
133
|
+
k, b_name, a_name, ba_recall * 100, secs
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
print("--------------------------------------")
|
|
139
|
+
print("-----------------Join-----------------")
|
|
140
|
+
print("--------------------------------------")
|
|
141
|
+
|
|
142
|
+
secs, bimapping = measure_seconds(lambda: a.join(b, max_proposals=100))
|
|
143
|
+
mapping_size = len(bimapping)
|
|
144
|
+
recall = 0
|
|
145
|
+
for i, j in bimapping.items():
|
|
146
|
+
recall += i == j
|
|
147
|
+
|
|
148
|
+
recall *= 100.0 / min_elements
|
|
149
|
+
print(
|
|
150
|
+
f"Took {secs:.2f}s to find {mapping_size:,} pairings with {recall:.2f}% being exact"
|
|
151
|
+
)
|
package/python/scripts/test.py
CHANGED
|
@@ -115,6 +115,11 @@ def test_index(
|
|
|
115
115
|
assert matches[0] == 42
|
|
116
116
|
assert distances[0] == pytest.approx(0, abs=1e-3)
|
|
117
117
|
|
|
118
|
+
assert index.max_level >= 0
|
|
119
|
+
assert index.levels_stats.nodes >= 1
|
|
120
|
+
assert index.level_stats(0).nodes == 1
|
|
121
|
+
assert str(index).startswith("usearch.index.Index")
|
|
122
|
+
|
|
118
123
|
index.save("tmp.usearch")
|
|
119
124
|
index.clear()
|
|
120
125
|
assert len(index) == 0
|
|
@@ -127,6 +132,10 @@ def test_index(
|
|
|
127
132
|
assert len(index) == 1
|
|
128
133
|
assert len(index[42]) == ndim
|
|
129
134
|
|
|
135
|
+
index_copy = index.copy()
|
|
136
|
+
assert len(index_copy) == 1
|
|
137
|
+
assert len(index_copy[42]) == ndim
|
|
138
|
+
|
|
130
139
|
# Cleanup
|
|
131
140
|
os.remove("tmp.usearch")
|
|
132
141
|
|
|
@@ -157,6 +166,10 @@ def test_index_batch(
|
|
|
157
166
|
assert matches.counts.shape[0] == batch_size
|
|
158
167
|
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
159
168
|
|
|
169
|
+
assert index.max_level >= 0 # TODO: This should be 1
|
|
170
|
+
assert index.levels_stats.nodes >= batch_size
|
|
171
|
+
assert index.level_stats(0).nodes == batch_size
|
|
172
|
+
|
|
160
173
|
index.save("tmp.usearch")
|
|
161
174
|
index.clear()
|
|
162
175
|
assert len(index) == 0
|
|
@@ -205,6 +218,12 @@ def test_exact_recall(
|
|
|
205
218
|
for i in range(batch_size):
|
|
206
219
|
assert found_labels[i, 0] == i
|
|
207
220
|
|
|
221
|
+
# Match entries aginst themselves
|
|
222
|
+
index_copy: Index = index.copy()
|
|
223
|
+
mapping: dict = index.join(index_copy, exact=True)
|
|
224
|
+
for man, woman in mapping.items():
|
|
225
|
+
assert man == woman, "Stable marriage failed"
|
|
226
|
+
|
|
208
227
|
|
|
209
228
|
@pytest.mark.parametrize("ndim", dimensions)
|
|
210
229
|
@pytest.mark.parametrize("batch_size", batch_sizes)
|
package/python/usearch/eval.py
CHANGED
|
@@ -3,6 +3,7 @@ from time import time_ns
|
|
|
3
3
|
from typing import Tuple, Any, Callable, Union, Optional, List
|
|
4
4
|
from dataclasses import dataclass, asdict
|
|
5
5
|
from collections import defaultdict
|
|
6
|
+
from math import ceil
|
|
6
7
|
|
|
7
8
|
import numpy as np
|
|
8
9
|
|
|
@@ -47,7 +48,7 @@ def random_vectors(
|
|
|
47
48
|
return x
|
|
48
49
|
|
|
49
50
|
|
|
50
|
-
def recall_members(index: Index,
|
|
51
|
+
def recall_members(index: Index, sample: float = 1, **kwargs) -> float:
|
|
51
52
|
"""Simplest benchmark for a quality of search, which queries every
|
|
52
53
|
existing member of the index, to make sure approximate search finds
|
|
53
54
|
the point itself.
|
|
@@ -59,9 +60,16 @@ def recall_members(index: Index, *args, **kwargs) -> float:
|
|
|
59
60
|
"""
|
|
60
61
|
if len(index) == 0:
|
|
61
62
|
return 0
|
|
63
|
+
if "k" not in kwargs:
|
|
64
|
+
kwargs["k"] = 1
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
labels = index.labels
|
|
67
|
+
if sample != 1:
|
|
68
|
+
labels = np.random.choice(labels, int(ceil(len(labels) * sample)))
|
|
69
|
+
|
|
70
|
+
queries = index.get_vectors(labels, index.dtype)
|
|
71
|
+
matches: Matches = index.search(queries, **kwargs)
|
|
72
|
+
return matches.recall_first(labels)
|
|
65
73
|
|
|
66
74
|
|
|
67
75
|
def measure_seconds(f: Callable) -> Tuple[float, Any]:
|