usearch 2.6.0 → 2.7.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.
Files changed (55) hide show
  1. package/. npmignore +14 -0
  2. package/.cmake-format.py +19 -0
  3. package/.github/workflows/Dockerfile.libs +2 -2
  4. package/.github/workflows/prerelease.yml +85 -64
  5. package/.github/workflows/release.yml +24 -15
  6. package/.gitignore +1 -1
  7. package/.vscode/launch.json +47 -14
  8. package/.vscode/settings.json +7 -0
  9. package/.vscode/tasks.json +44 -8
  10. package/CITATION.cff +1 -1
  11. package/CMakeLists.txt +218 -67
  12. package/Cargo.toml +1 -1
  13. package/README.md +1 -1
  14. package/binding.gyp +3 -2
  15. package/build.gradle +19 -10
  16. package/c/CMakeLists.txt +21 -48
  17. package/c/README.md +5 -5
  18. package/c/lib.cpp +52 -7
  19. package/c/test.c +137 -94
  20. package/c/usearch.h +42 -0
  21. package/cmake/clipp.cmake +0 -1
  22. package/cmake/jemalloc.cmake +21 -37
  23. package/conanfile.py +1 -1
  24. package/cpp/CMakeLists.txt +14 -123
  25. package/cpp/README.md +26 -22
  26. package/cpp/bench.cpp +0 -12
  27. package/cpp/test.cpp +29 -1
  28. package/csharp/nuget/nuget-package.props +1 -1
  29. package/docs/compilation.md +23 -16
  30. package/docs/conf.py +1 -1
  31. package/golang/README.md +52 -51
  32. package/golang/lib.go +1 -1
  33. package/golang/lib_test.go +4 -1
  34. package/include/usearch/index.hpp +2 -2
  35. package/include/usearch/index_plugins.hpp +345 -160
  36. package/javascript/lib.cpp +204 -250
  37. package/javascript/usearch.js +440 -2
  38. package/javascript/usearch.test.js +37 -0
  39. package/package.json +10 -5
  40. package/python/lib.cpp +95 -224
  41. package/python/scripts/bench_exact.py +2 -2
  42. package/python/scripts/test_jit.py +2 -13
  43. package/python/scripts/test_sparse.py +1 -1
  44. package/python/scripts/test_tooling.py +15 -5
  45. package/rust/lib.cpp +7 -4
  46. package/rust/lib.hpp +2 -0
  47. package/rust/lib.rs +128 -8
  48. package/setup.py +14 -4
  49. package/wasm/CMakeLists.txt +4 -12
  50. package/wasm/lib.cpp +1 -1
  51. package/wasmer.toml +1 -1
  52. package/javascript/docs.js +0 -111
  53. package/javascript/test.js +0 -32
  54. package/javascript/usearch.d.ts +0 -110
  55. package/tsconfig.json +0 -8
package/python/lib.cpp CHANGED
@@ -32,17 +32,6 @@
32
32
  using namespace unum::usearch;
33
33
  using namespace unum;
34
34
 
35
- /**
36
- * @brief The signature of the user-defined function.
37
- * Can be just two array pointers, precompiled for a specific array length,
38
- * or include one or two array sizes as 64-bit unsigned integers.
39
- */
40
- enum class metric_signature_t {
41
- array_array_k = 0,
42
- array_array_size_k,
43
- array_size_array_size_k,
44
- };
45
-
46
35
  namespace py = pybind11;
47
36
  using py_shape_t = py::array::ShapeContainer;
48
37
 
@@ -59,6 +48,7 @@ using dense_clustering_result_t = typename index_dense_t::clustering_result_t;
59
48
  using progress_func_t = std::function<bool(std::size_t /*processed*/, std::size_t /*total*/)>;
60
49
 
61
50
  struct progress_t {
51
+ inline progress_t(nullptr_t = nullptr) : func_(&dummy_progress) {}
62
52
  inline progress_t(progress_func_t const& func) : func_(func ? func : &dummy_progress) {}
63
53
  inline bool operator()(std::size_t processed, std::size_t total) const noexcept { return func_(processed, total); }
64
54
 
@@ -114,70 +104,16 @@ struct dense_indexes_py_t {
114
104
  }
115
105
  };
116
106
 
117
- template <typename scalar_at>
118
- metric_t wrap_typed_user_defined_metric( //
119
- metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, //
120
- scalar_kind_t scalar_kind, std::size_t dimensions) {
121
- //
122
- using stl_function_t = metric_t::stl_function_t;
123
- stl_function_t stl_function;
124
- std::size_t scalar_words =
125
- divide_round_up(dimensions * bits_per_scalar(scalar_kind), bits_per_scalar_word(scalar_kind));
126
- switch (signature) {
127
- case metric_signature_t::array_array_k:
128
- stl_function = [metric_uintptr](byte_t const* a, byte_t const* b) -> distance_t {
129
- using metric_raw_t = distance_punned_t (*)(scalar_at const*, scalar_at const*);
130
- metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
131
- return metric_ptr((scalar_at const*)a, (scalar_at const*)b);
132
- };
133
- break;
134
- case metric_signature_t::array_array_size_k:
135
- stl_function = [metric_uintptr, scalar_words](byte_t const* a, byte_t const* b) -> distance_t {
136
- using metric_raw_t = distance_punned_t (*)(scalar_at const*, scalar_at const*, size_t);
137
- metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
138
- return metric_ptr((scalar_at const*)a, (scalar_at const*)b, scalar_words);
139
- };
140
- break;
141
- case metric_signature_t::array_size_array_size_k:
142
- stl_function = [metric_uintptr, scalar_words](byte_t const* a, byte_t const* b) -> distance_t {
143
- using metric_raw_t = distance_punned_t (*)(scalar_at const*, size_t, scalar_at const*, size_t);
144
- metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
145
- return metric_ptr((scalar_at const*)a, scalar_words, (scalar_at const*)b, scalar_words);
146
- };
147
- break;
148
- }
149
- return metric_t(stl_function, dimensions, kind, scalar_kind);
150
- }
151
-
152
- metric_t wrap_user_defined_metric( //
153
- metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, //
154
- scalar_kind_t scalar_kind, std::size_t dimensions) {
155
-
156
- switch (scalar_kind) {
157
- case scalar_kind_t::b1x8_k:
158
- return wrap_typed_user_defined_metric<b1x8_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
159
- case scalar_kind_t::i8_k:
160
- return wrap_typed_user_defined_metric<i8_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
161
- case scalar_kind_t::f16_k:
162
- return wrap_typed_user_defined_metric<f16_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
163
- case scalar_kind_t::f32_k:
164
- return wrap_typed_user_defined_metric<f32_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
165
- case scalar_kind_t::f64_k:
166
- return wrap_typed_user_defined_metric<f64_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
167
- default: return {};
168
- }
169
- }
170
-
171
- static dense_index_py_t make_index( //
172
- std::size_t dimensions, //
173
- scalar_kind_t scalar_kind, //
174
- std::size_t connectivity, //
175
- std::size_t expansion_add, //
176
- std::size_t expansion_search, //
177
- metric_kind_t metric_kind, //
178
- metric_signature_t metric_signature, //
179
- std::uintptr_t metric_uintptr, //
180
- bool multi, //
107
+ static dense_index_py_t make_index( //
108
+ std::size_t dimensions, //
109
+ scalar_kind_t scalar_kind, //
110
+ std::size_t connectivity, //
111
+ std::size_t expansion_add, //
112
+ std::size_t expansion_search, //
113
+ metric_kind_t metric_kind, //
114
+ metric_punned_signature_t metric_signature, //
115
+ std::uintptr_t metric_uintptr, //
116
+ bool multi, //
181
117
  bool enable_key_lookups) {
182
118
 
183
119
  index_dense_config_t config(connectivity, expansion_add, expansion_search);
@@ -186,7 +122,7 @@ static dense_index_py_t make_index( //
186
122
 
187
123
  metric_t metric = //
188
124
  metric_uintptr //
189
- ? wrap_user_defined_metric(metric_kind, metric_signature, metric_uintptr, scalar_kind, dimensions)
125
+ ? metric_t(dimensions, metric_uintptr, metric_signature, metric_kind, scalar_kind)
190
126
  : metric_t(dimensions, metric_kind, scalar_kind);
191
127
  return index_dense_t::make(metric, config);
192
128
  }
@@ -509,105 +445,13 @@ static py::tuple search_many_in_index( //
509
445
  return results;
510
446
  }
511
447
 
512
- template <typename scalar_at>
513
- static void search_typed_brute_force( //
514
- py::buffer_info& dataset_info, py::buffer_info& queries_info, //
515
- std::size_t wanted, std::size_t threads, metric_t const& metric, //
516
- py::array_t<dense_key_t>& keys_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py,
517
- progress_func_t const& progress) {
518
-
519
- auto keys_py2d = keys_py.template mutable_unchecked<2>();
520
- auto distances_py2d = distances_py.template mutable_unchecked<2>();
521
- auto counts_py1d = counts_py.template mutable_unchecked<1>();
522
-
523
- std::size_t dataset_count = static_cast<std::size_t>(dataset_info.shape[0]);
524
- std::size_t queries_count = static_cast<std::size_t>(queries_info.shape[0]);
525
-
526
- byte_t const* dataset_data = reinterpret_cast<byte_t const*>(dataset_info.ptr);
527
- byte_t const* queries_data = reinterpret_cast<byte_t const*>(queries_info.ptr);
528
- for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx)
529
- counts_py1d(query_idx) = wanted;
530
-
531
- if (!threads)
532
- threads = std::thread::hardware_concurrency();
533
-
534
- std::size_t tasks_count = static_cast<std::size_t>(dataset_count * queries_count);
535
- executor_default_t executor{threads};
536
-
537
- // Progress status
538
- progress_t progress_{progress};
539
- std::atomic<std::size_t> processed{0};
540
-
541
- struct dense_key_and_distance_t {
542
- u32_t offset;
543
- f32_t distance;
544
- };
545
- auto less = [](dense_key_and_distance_t const& a, dense_key_and_distance_t const& b) {
546
- return a.distance < b.distance;
547
- };
548
-
549
- // Allocate temporary memory to store the distance matrix
550
- // Previous version didn't need temporary memory, but the performance was much lower.
551
- // In the new design we keep two buffers - original and transposed, as in-place transpositions
552
- // of non-rectangular matrixes is expensive.
553
- std::vector<dense_key_and_distance_t> keys_and_distances(tasks_count * 2);
554
- dense_key_and_distance_t* keys_and_distances_per_dataset = keys_and_distances.data();
555
- dense_key_and_distance_t* keys_and_distances_per_query = keys_and_distances_per_dataset + tasks_count;
556
- executor.dynamic(dataset_count, [&](std::size_t thread_idx, std::size_t dataset_idx) {
557
- byte_t const* dataset = dataset_data + dataset_idx * dataset_info.strides[0];
558
- for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
559
- byte_t const* query = queries_data + query_idx * queries_info.strides[0];
560
- distance_t distance = metric(dataset, query);
561
- std::size_t task_idx = dataset_idx * queries_count + query_idx;
562
- keys_and_distances_per_dataset[task_idx].offset = static_cast<u32_t>(dataset_idx);
563
- keys_and_distances_per_dataset[task_idx].distance = static_cast<f32_t>(distance);
564
- }
565
-
566
- // We don't want to check for signals from multiple threads
567
- processed += queries_count;
568
- if (thread_idx == 0)
569
- if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), tasks_count))
570
- return false;
571
- return true;
572
- });
573
- if (processed.load() != tasks_count)
574
- return;
575
-
576
- // Transpose in a single thread to avoid contention writing into the same memory buffers
577
- for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
578
- for (std::size_t dataset_idx = 0; dataset_idx != dataset_count; ++dataset_idx) {
579
- std::size_t from_idx = queries_count * dataset_idx + query_idx;
580
- std::size_t to_idx = dataset_count * query_idx + dataset_idx;
581
- keys_and_distances_per_query[to_idx] = keys_and_distances_per_dataset[from_idx];
582
- }
583
- }
584
-
585
- // Partial-sort every query result
586
- executor.fixed(queries_count, [&](std::size_t, std::size_t query_idx) {
587
- dense_key_t* keys = &keys_py2d(query_idx, 0);
588
- distance_t* distances = &distances_py2d(query_idx, 0);
589
- auto start = keys_and_distances_per_query + dataset_count * query_idx;
590
- if (wanted > 1) {
591
- std::partial_sort(start, start + wanted, start + dataset_count, less);
592
- for (std::size_t i = 0; i != wanted; ++i)
593
- keys[i] = static_cast<dense_key_t>(start[i].offset), distances[i] = start[i].distance;
594
- } else {
595
- auto max_it = std::max_element(start, start + dataset_count, less);
596
- keys[0] = static_cast<dense_key_t>(max_it->offset), distances[0] = max_it->distance;
597
- }
598
- });
599
-
600
- // At the end report the latest numbers, because the reporter thread may be finished earlier
601
- progress_(processed.load(), tasks_count);
602
- }
603
-
604
- static py::tuple search_many_brute_force( //
605
- py::buffer dataset, py::buffer queries, //
606
- std::size_t wanted, std::size_t threads, //
607
- metric_kind_t metric_kind, //
608
- metric_signature_t metric_signature, //
609
- std::uintptr_t metric_uintptr, //
610
- progress_func_t const& progress) {
448
+ static py::tuple search_many_brute_force( //
449
+ py::buffer dataset, py::buffer queries, //
450
+ std::size_t wanted, std::size_t threads, //
451
+ metric_kind_t metric_kind, //
452
+ metric_punned_signature_t metric_signature, //
453
+ std::uintptr_t metric_uintptr, //
454
+ progress_func_t const& progress_func) {
611
455
 
612
456
  if (wanted == 0)
613
457
  return py::tuple(5);
@@ -617,10 +461,13 @@ static py::tuple search_many_brute_force( //
617
461
  if (dataset_info.ndim != 2 || queries_info.ndim != 2)
618
462
  throw std::invalid_argument("Expects a matrix of dataset to add!");
619
463
 
620
- Py_ssize_t dataset_count = dataset_info.shape[0];
621
- Py_ssize_t dataset_dimensions = dataset_info.shape[1];
622
- Py_ssize_t queries_count = queries_info.shape[0];
623
- Py_ssize_t queries_dimensions = queries_info.shape[1];
464
+ std::size_t dataset_count = static_cast<std::size_t>(dataset_info.shape[0]);
465
+ std::size_t dataset_dimensions = static_cast<std::size_t>(dataset_info.shape[1]);
466
+ std::size_t dataset_stride = static_cast<std::size_t>(dataset_info.strides[0]);
467
+ std::size_t queries_stride = static_cast<std::size_t>(queries_info.strides[0]);
468
+ std::size_t queries_count = static_cast<std::size_t>(queries_info.shape[0]);
469
+ std::size_t queries_dimensions = static_cast<std::size_t>(queries_info.shape[1]);
470
+
624
471
  if (dataset_dimensions != queries_dimensions)
625
472
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
626
473
  if (wanted > dataset_count)
@@ -631,26 +478,51 @@ static py::tuple search_many_brute_force( //
631
478
  if (dataset_kind != queries_kind)
632
479
  throw std::invalid_argument("The types of vectors don't match!");
633
480
 
634
- py::array_t<dense_key_t> keys_py({queries_count, static_cast<Py_ssize_t>(wanted)});
635
- py::array_t<distance_t> distances_py({queries_count, static_cast<Py_ssize_t>(wanted)});
636
- py::array_t<Py_ssize_t> counts_py(queries_count);
637
-
638
481
  std::size_t dimensions = static_cast<std::size_t>(queries_dimensions);
639
482
  metric_t metric = //
640
483
  metric_uintptr //
641
- ? wrap_user_defined_metric(metric_kind, metric_signature, metric_uintptr, queries_kind, dimensions)
484
+ ? metric_t(dimensions, metric_uintptr, metric_signature, metric_kind, queries_kind)
642
485
  : metric_t(dimensions, metric_kind, queries_kind);
643
486
 
644
- // clang-format off
645
- switch (dataset_kind) {
646
- case scalar_kind_t::b1x8_k: search_typed_brute_force<b1x8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
647
- case scalar_kind_t::i8_k: search_typed_brute_force<i8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
648
- case scalar_kind_t::f16_k: search_typed_brute_force<f16_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
649
- case scalar_kind_t::f32_k: search_typed_brute_force<f32_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
650
- case scalar_kind_t::f64_k: search_typed_brute_force<f64_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
651
- default: throw std::invalid_argument("Incompatible vector types: " + dataset_info.format);
487
+ py::array_t<dense_key_t> keys_py({static_cast<Py_ssize_t>(queries_count), static_cast<Py_ssize_t>(wanted)});
488
+ py::array_t<distance_t> distances_py({static_cast<Py_ssize_t>(queries_count), static_cast<Py_ssize_t>(wanted)});
489
+ py::array_t<Py_ssize_t> counts_py(static_cast<Py_ssize_t>(queries_count));
490
+
491
+ auto keys_py2d = keys_py.template mutable_unchecked<2>();
492
+ auto distances_py2d = distances_py.template mutable_unchecked<2>();
493
+ auto counts_py1d = counts_py.template mutable_unchecked<1>();
494
+
495
+ byte_t const* dataset_data = reinterpret_cast<byte_t const*>(dataset_info.ptr);
496
+ byte_t const* queries_data = reinterpret_cast<byte_t const*>(queries_info.ptr);
497
+ for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx)
498
+ counts_py1d(query_idx) = wanted;
499
+
500
+ if (!threads)
501
+ threads = std::thread::hardware_concurrency();
502
+
503
+ // Dispatch brute-force search
504
+ progress_t progress{progress_func};
505
+ executor_default_t executor{threads};
506
+ exact_search_t search;
507
+
508
+ exact_search_results_t offsets_and_distances = search( //
509
+ dataset_data, dataset_count, dataset_stride, //
510
+ queries_data, queries_count, queries_stride, //
511
+ wanted, metric, executor,
512
+ [&](std::size_t passed, std::size_t total) { return PyErr_CheckSignals() == 0 && progress(passed, total); });
513
+
514
+ if (!offsets_and_distances)
515
+ throw std::bad_alloc();
516
+
517
+ // Export the results
518
+ for (std::size_t query_idx = 0; query_idx != queries_count; ++query_idx) {
519
+ dense_key_t* query_keys = &keys_py2d(query_idx, 0);
520
+ distance_t* query_distances = &distances_py2d(query_idx, 0);
521
+ auto query_result = offsets_and_distances.at(query_idx);
522
+ for (std::size_t i = 0; i != wanted; ++i)
523
+ query_keys[i] = static_cast<dense_key_t>(query_result[i].offset),
524
+ query_distances[i] = query_result[i].distance;
652
525
  }
653
- // clang-format on
654
526
 
655
527
  py::tuple results(5);
656
528
  results[0] = keys_py;
@@ -980,10 +852,9 @@ PYBIND11_MODULE(compiled, m) {
980
852
  m.attr("USES_SIMSIMD") = py::int_(USEARCH_USE_SIMSIMD);
981
853
  m.attr("USES_NATIVE_F16") = py::int_(USEARCH_USE_NATIVE_F16);
982
854
 
983
- py::enum_<metric_signature_t>(m, "MetricSignature")
984
- .value("ArrayArray", metric_signature_t::array_array_k)
985
- .value("ArrayArraySize", metric_signature_t::array_array_size_k)
986
- .value("ArraySizeArraySize", metric_signature_t::array_size_array_size_k);
855
+ py::enum_<metric_punned_signature_t>(m, "MetricSignature")
856
+ .value("ArrayArray", metric_punned_signature_t::array_array_k)
857
+ .value("ArrayArraySize", metric_punned_signature_t::array_array_size_k);
987
858
 
988
859
  py::enum_<metric_kind_t>(m, "MetricKind")
989
860
  .value("Unknown", metric_kind_t::unknown_k)
@@ -1032,16 +903,16 @@ PYBIND11_MODULE(compiled, m) {
1032
903
  return index_metadata(meta);
1033
904
  });
1034
905
 
1035
- m.def("exact_search", &search_many_brute_force, //
1036
- py::arg("dataset"), //
1037
- py::arg("queries"), //
1038
- py::arg("count") = 10, //
1039
- py::kw_only(), //
1040
- py::arg("threads") = 0, //
1041
- py::arg("metric_kind") = metric_kind_t::cos_k, //
1042
- py::arg("metric_signature") = metric_signature_t::array_array_k, //
1043
- py::arg("metric_pointer") = 0, //
1044
- py::arg("progress") = nullptr //
906
+ m.def("exact_search", &search_many_brute_force, //
907
+ py::arg("dataset"), //
908
+ py::arg("queries"), //
909
+ py::arg("count") = 10, //
910
+ py::kw_only(), //
911
+ py::arg("threads") = 0, //
912
+ py::arg("metric_kind") = metric_kind_t::cos_k, //
913
+ py::arg("metric_signature") = metric_punned_signature_t::array_array_k, //
914
+ py::arg("metric_pointer") = 0, //
915
+ py::arg("progress") = nullptr //
1045
916
  );
1046
917
 
1047
918
  m.def(
@@ -1057,18 +928,18 @@ PYBIND11_MODULE(compiled, m) {
1057
928
 
1058
929
  auto i = py::class_<dense_index_py_t, std::shared_ptr<dense_index_py_t>>(m, "Index");
1059
930
 
1060
- i.def(py::init(&make_index), //
1061
- py::kw_only(), //
1062
- py::arg("ndim") = 0, //
1063
- py::arg("dtype") = scalar_kind_t::f32_k, //
1064
- py::arg("connectivity") = default_connectivity(), //
1065
- py::arg("expansion_add") = default_expansion_add(), //
1066
- py::arg("expansion_search") = default_expansion_search(), //
1067
- py::arg("metric_kind") = metric_kind_t::cos_k, //
1068
- py::arg("metric_signature") = metric_signature_t::array_array_k, //
1069
- py::arg("metric_pointer") = 0, //
1070
- py::arg("multi") = false, //
1071
- py::arg("enable_key_lookups") = true //
931
+ i.def(py::init(&make_index), //
932
+ py::kw_only(), //
933
+ py::arg("ndim") = 0, //
934
+ py::arg("dtype") = scalar_kind_t::f32_k, //
935
+ py::arg("connectivity") = default_connectivity(), //
936
+ py::arg("expansion_add") = default_expansion_add(), //
937
+ py::arg("expansion_search") = default_expansion_search(), //
938
+ py::arg("metric_kind") = metric_kind_t::cos_k, //
939
+ py::arg("metric_signature") = metric_punned_signature_t::array_array_k, //
940
+ py::arg("metric_pointer") = 0, //
941
+ py::arg("multi") = false, //
942
+ py::arg("enable_key_lookups") = true //
1072
943
  );
1073
944
 
1074
945
  i.def( //
@@ -1201,19 +1072,19 @@ PYBIND11_MODULE(compiled, m) {
1201
1072
 
1202
1073
  i.def(
1203
1074
  "change_metric",
1204
- [](dense_index_py_t& index, metric_kind_t metric_kind, metric_signature_t metric_signature,
1075
+ [](dense_index_py_t& index, metric_kind_t metric_kind, metric_punned_signature_t metric_signature,
1205
1076
  std::uintptr_t metric_uintptr) {
1206
1077
  scalar_kind_t scalar_kind = index.scalar_kind();
1207
1078
  std::size_t dimensions = index.dimensions();
1208
1079
  metric_t metric = //
1209
1080
  metric_uintptr //
1210
- ? wrap_user_defined_metric(metric_kind, metric_signature, metric_uintptr, scalar_kind, dimensions)
1081
+ ? metric_t(dimensions, metric_uintptr, metric_signature, metric_kind, scalar_kind)
1211
1082
  : metric_t(dimensions, metric_kind, scalar_kind);
1212
1083
  index.change_metric(std::move(metric));
1213
1084
  },
1214
- py::arg("metric_kind") = metric_kind_t::cos_k, //
1215
- py::arg("metric_signature") = metric_signature_t::array_array_k, //
1216
- py::arg("metric_pointer") = 0 //
1085
+ py::arg("metric_kind") = metric_kind_t::cos_k, //
1086
+ py::arg("metric_signature") = metric_punned_signature_t::array_array_k, //
1087
+ py::arg("metric_pointer") = 0 //
1217
1088
  );
1218
1089
 
1219
1090
  i.def_property_readonly("hardware_acceleration", [](dense_index_py_t const& index) -> py::str {
@@ -2,7 +2,7 @@ from time import time
2
2
 
3
3
  from usearch.index import search, MetricKind, ScalarKind
4
4
  from usearch.compiled import hardware_acceleration
5
- from faiss import IndexFlatL2, knn
5
+ from faiss import knn, METRIC_L2
6
6
  import numpy as np
7
7
  import fire
8
8
 
@@ -36,7 +36,7 @@ def run(
36
36
  print("USearch: ", time() - start)
37
37
 
38
38
  start = time()
39
- _ = knn(x, x[:q], k)[1]
39
+ _ = knn(x[:q], x, k, metric=METRIC_L2)[1]
40
40
  print("FAISS: ", time() - start)
41
41
 
42
42
 
@@ -58,24 +58,13 @@ def test_index_numba(ndim: int, batch_size: int):
58
58
  c += a_array[i] * b_array[i]
59
59
  return 1 - c
60
60
 
61
- @cfunc(signature_four_args)
62
- def python_inner_product_four_args(a, a_ndim, b, b_ndim):
63
- a_array = carray(a, a_ndim)
64
- b_array = carray(b, b_ndim)
65
- c = 0.0
66
- for i in range(a_ndim):
67
- c += a_array[i] * b_array[i]
68
- return 1 - c
69
-
70
61
  functions = [
71
62
  python_inner_product_two_args,
72
63
  python_inner_product_three_args,
73
- python_inner_product_four_args,
74
64
  ]
75
65
  signatures = [
76
66
  MetricSignature.ArrayArray,
77
67
  MetricSignature.ArrayArraySize,
78
- MetricSignature.ArraySizeArraySize,
79
68
  ]
80
69
  for function, signature in zip(functions, signatures):
81
70
  metric = CompiledMetric(
@@ -133,12 +122,12 @@ def test_index_cppyy(ndim: int, batch_size: int):
133
122
  functions = [
134
123
  cppyy.gbl.inner_product_two_args,
135
124
  cppyy.gbl.inner_product_three_args,
136
- cppyy.gbl.inner_product_four_args,
125
+ # cppyy.gbl.inner_product_four_args,
137
126
  ]
138
127
  signatures = [
139
128
  MetricSignature.ArrayArray,
140
129
  MetricSignature.ArrayArraySize,
141
- MetricSignature.ArraySizeArraySize,
130
+ # MetricSignature.ArraySizeArraySize,
142
131
  ]
143
132
  for function, signature in zip(functions, signatures):
144
133
  metric = CompiledMetric(
@@ -8,7 +8,7 @@ from usearch.index import (
8
8
  )
9
9
 
10
10
 
11
- @pytest.mark.parametrize("bits", [3, 97, 256, 4097])
11
+ @pytest.mark.parametrize("bits", [7, 97, 256, 4097])
12
12
  @pytest.mark.parametrize("metric", [MetricKind.Tanimoto])
13
13
  @pytest.mark.parametrize("connectivity", [3, 13, 50])
14
14
  @pytest.mark.parametrize("batch_size", [3, 77])
@@ -48,7 +48,9 @@ def test_serializing_ibin_matrix(rows: int, cols: int):
48
48
 
49
49
  @pytest.mark.parametrize("rows", batch_sizes)
50
50
  @pytest.mark.parametrize("cols", dimensions)
51
- def test_exact_search(rows: int, cols: int):
51
+ @pytest.mark.parametrize("k", [1, 5])
52
+ @pytest.mark.parametrize("reordered", [False, True])
53
+ def test_exact_search(rows: int, cols: int, k: int, reordered: bool):
52
54
  """
53
55
  Test exact search.
54
56
 
@@ -57,13 +59,21 @@ def test_exact_search(rows: int, cols: int):
57
59
  """
58
60
  original = np.random.rand(rows, cols)
59
61
  keys = np.arange(rows)
60
- matches: BatchMatches = search(original, original, min(10, rows), exact=True)
62
+ k = min(k, rows)
63
+
64
+ if reordered:
65
+ reordered_keys = np.arange(rows)
66
+ np.random.shuffle(reordered_keys)
67
+ else:
68
+ reordered_keys = keys
69
+
70
+ matches: BatchMatches = search(original, original[reordered_keys], k, exact=True)
61
71
  top_matches = (
62
- [int(m.keys[0]) for m in matches] if rows > 1 else int(matches.keys[0])
72
+ [int(m.keys[0]) for m in matches] if rows > 1 else [int(matches.keys[0])]
63
73
  )
64
- assert np.all(top_matches == keys)
74
+ assert top_matches == list(reordered_keys)
65
75
 
66
- matches: Matches = search(original, original[-1], min(10, rows), exact=True)
76
+ matches: Matches = search(original, original[-1], k, exact=True)
67
77
  top_match = int(matches.keys[0])
68
78
  assert top_match == keys[-1]
69
79
 
package/rust/lib.cpp CHANGED
@@ -36,10 +36,10 @@ Matches NativeIndex::search_f16(rust::Slice<uint16_t const> vec, size_t count) c
36
36
  Matches NativeIndex::search_f32(rust::Slice<float const> vec, size_t count) const { return search_(*index_, vec.data(), count); }
37
37
  Matches NativeIndex::search_f64(rust::Slice<double const> vec, size_t count) const { return search_(*index_, vec.data(), count); }
38
38
 
39
- size_t NativeIndex::get_i8(key_t key, rust::Slice<int8_t> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
40
- size_t NativeIndex::get_f16(key_t key, rust::Slice<uint16_t> vec) const { return index_->get(key, (f16_t*)vec.data(), vec.size() / dimensions()); }
41
- size_t NativeIndex::get_f32(key_t key, rust::Slice<float> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
42
- size_t NativeIndex::get_f64(key_t key, rust::Slice<double> vec) const { return index_->get(key, vec.data(), vec.size() / dimensions()); }
39
+ size_t NativeIndex::get_i8(key_t key, rust::Slice<int8_t> vec) const { if (vec.size() % dimensions()) throw std::invalid_argument("Vector length must be a multiple of index dimensionality"); return index_->get(key, vec.data(), vec.size() / dimensions()); }
40
+ size_t NativeIndex::get_f16(key_t key, rust::Slice<uint16_t> vec) const { if (vec.size() % dimensions()) throw std::invalid_argument("Vector length must be a multiple of index dimensionality"); return index_->get(key, (f16_t*)vec.data(), vec.size() / dimensions()); }
41
+ size_t NativeIndex::get_f32(key_t key, rust::Slice<float> vec) const { if (vec.size() % dimensions()) throw std::invalid_argument("Vector length must be a multiple of index dimensionality"); return index_->get(key, vec.data(), vec.size() / dimensions()); }
42
+ size_t NativeIndex::get_f64(key_t key, rust::Slice<double> vec) const { if (vec.size() % dimensions()) throw std::invalid_argument("Vector length must be a multiple of index dimensionality"); return index_->get(key, vec.data(), vec.size() / dimensions()); }
43
43
  // clang-format on
44
44
 
45
45
  size_t NativeIndex::remove(key_t key) const {
@@ -71,6 +71,9 @@ void NativeIndex::view(rust::Str path) const {
71
71
  index_->view(memory_mapped_file_t(std::string(path).c_str())).error.raise();
72
72
  }
73
73
 
74
+ void NativeIndex::reset() const { index_->reset(); }
75
+ size_t NativeIndex::memory_usage() const { return index_->memory_usage(); }
76
+
74
77
  void NativeIndex::save_to_buffer(rust::Slice<uint8_t> buffer) const {
75
78
  index_->save(memory_mapped_file_t((byte_t*)buffer.data(), buffer.size())).error.raise();
76
79
  }
package/rust/lib.hpp CHANGED
@@ -50,6 +50,8 @@ class NativeIndex {
50
50
  void save(rust::Str path) const;
51
51
  void load(rust::Str path) const;
52
52
  void view(rust::Str path) const;
53
+ void reset() const;
54
+ size_t memory_usage() const;
53
55
 
54
56
  void save_to_buffer(rust::Slice<uint8_t> buffer) const;
55
57
  void load_from_buffer(rust::Slice<uint8_t const> buffer) const;