usearch 1.1.1 → 1.2.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.
Files changed (70) hide show
  1. package/.github/workflows/prerelease.yml +2 -21
  2. package/.github/workflows/release.yml +4 -36
  3. package/.github/workflows/update_version.sh +1 -1
  4. package/.gitmodules +1 -4
  5. package/Cargo.toml +1 -1
  6. package/Package.swift +2 -1
  7. package/README.md +86 -42
  8. package/binding.gyp +16 -12
  9. package/build.gradle +1 -1
  10. package/build.rs +0 -1
  11. package/c/CMakeLists.txt +11 -7
  12. package/c/lib.cpp +36 -24
  13. package/c/usearch.h +42 -12
  14. package/cluster.ipynb +167 -0
  15. package/conanfile.py +1 -1
  16. package/cpp/CMakeLists.txt +8 -3
  17. package/cpp/bench.cpp +2 -2
  18. package/cpp/test.cpp +42 -25
  19. package/docs/benchmarks.md +8 -0
  20. package/docs/compilation.md +7 -7
  21. package/docs/conf.py +27 -24
  22. package/docs/javascript/reference.rst +3 -0
  23. package/golang/README.md +31 -7
  24. package/golang/lib.go +1 -1
  25. package/include/usearch/index.hpp +348 -69
  26. package/include/usearch/index_dense.hpp +587 -179
  27. package/include/usearch/index_plugins.hpp +194 -16
  28. package/java/cloud/unum/usearch/Index.java +13 -0
  29. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +40 -9
  30. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.h +16 -0
  31. package/javascript/docs.js +111 -0
  32. package/javascript/lib.cpp +29 -9
  33. package/javascript/usearch.d.ts +9 -1
  34. package/objc/USearchObjective.mm +11 -11
  35. package/package.json +2 -2
  36. package/pyproject.toml +2 -2
  37. package/python/lib.cpp +488 -158
  38. package/python/scripts/index_faiss.py +3 -3
  39. package/python/scripts/join.py +6 -6
  40. package/python/scripts/test_index.py +219 -0
  41. package/python/scripts/test_sparse.py +21 -21
  42. package/python/scripts/test_tooling.py +111 -0
  43. package/python/usearch/eval.py +9 -5
  44. package/python/usearch/index.py +491 -216
  45. package/rust/lib.cpp +6 -24
  46. package/rust/lib.hpp +1 -3
  47. package/rust/lib.rs +89 -0
  48. package/setup.py +5 -6
  49. package/wasm/CMakeLists.txt +0 -1
  50. package/wasmer.toml +1 -1
  51. package/python/scripts/test.py +0 -364
  52. package/robin-map/.codecov.yml +0 -5
  53. package/robin-map/.github/workflows/ci.yml +0 -136
  54. package/robin-map/CMakeLists.txt +0 -86
  55. package/robin-map/LICENSE +0 -21
  56. package/robin-map/README.md +0 -483
  57. package/robin-map/cmake/tsl-robin-mapConfig.cmake.in +0 -9
  58. package/robin-map/doxygen.conf +0 -2483
  59. package/robin-map/include/tsl/robin_growth_policy.h +0 -406
  60. package/robin-map/include/tsl/robin_hash.h +0 -1639
  61. package/robin-map/include/tsl/robin_map.h +0 -807
  62. package/robin-map/include/tsl/robin_set.h +0 -660
  63. package/robin-map/tests/CMakeLists.txt +0 -26
  64. package/robin-map/tests/custom_allocator_tests.cpp +0 -138
  65. package/robin-map/tests/main.cpp +0 -26
  66. package/robin-map/tests/policy_tests.cpp +0 -97
  67. package/robin-map/tests/robin_map_tests.cpp +0 -1451
  68. package/robin-map/tests/robin_set_tests.cpp +0 -164
  69. package/robin-map/tests/utils.h +0 -443
  70. package/robin-map/tsl-robin-map.natvis +0 -78
@@ -19,7 +19,7 @@
19
19
  #define USEARCH_DEFINED_CPP17
20
20
  #endif
21
21
 
22
- // Inferring target OS
22
+ // Inferring target OS: Windows, MacOS, or Linux
23
23
  #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
24
24
  #define USEARCH_DEFINED_WINDOWS
25
25
  #elif defined(__APPLE__) && defined(__MACH__)
@@ -28,7 +28,7 @@
28
28
  #define USEARCH_DEFINED_LINUX
29
29
  #endif
30
30
 
31
- // Inferring the compiler
31
+ // Inferring the compiler: Clang vs GCC
32
32
  #if defined(__clang__)
33
33
  #define USEARCH_DEFINED_CLANG
34
34
  #elif defined(__GNUC__)
@@ -324,6 +324,9 @@ class error_t {
324
324
  message_ = message;
325
325
  return *this;
326
326
  }
327
+
328
+ error_t(error_t const&) = delete;
329
+ error_t& operator=(error_t const&) = delete;
327
330
  error_t(error_t&& other) noexcept : message_(exchange(other.message_, nullptr)) {}
328
331
  error_t& operator=(error_t&& other) noexcept {
329
332
  std::swap(message_, other.message_);
@@ -331,6 +334,7 @@ class error_t {
331
334
  }
332
335
  explicit operator bool() const noexcept { return message_ != nullptr; }
333
336
  char const* what() const noexcept { return message_; }
337
+ char const* release() noexcept { return exchange(message_, nullptr); }
334
338
 
335
339
  #if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
336
340
  ~error_t() noexcept(false) {
@@ -382,10 +386,10 @@ template <typename result_at> struct expected_gt {
382
386
  };
383
387
 
384
388
  /**
385
- * @brief Light-weight bitset implementation to track visited nodes during graph traversal.
389
+ * @brief Light-weight bitset implementation to sync nodes updates during graph mutations.
386
390
  * Extends basic functionality with @b atomic operations.
387
391
  */
388
- template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt {
392
+ template <typename allocator_at = std::allocator<byte_t>> class bitset_gt {
389
393
  using allocator_t = allocator_at;
390
394
  using byte_t = typename allocator_t::value_type;
391
395
  static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
@@ -401,8 +405,8 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
401
405
  std::size_t count_{};
402
406
 
403
407
  public:
404
- visits_bitset_gt() noexcept {}
405
- ~visits_bitset_gt() noexcept { reset(); }
408
+ bitset_gt() noexcept {}
409
+ ~bitset_gt() noexcept { reset(); }
406
410
 
407
411
  explicit operator bool() const noexcept { return slots_; }
408
412
  void clear() noexcept { std::memset(slots_, 0, count_ * sizeof(compressed_slot_t)); }
@@ -414,25 +418,25 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
414
418
  count_ = 0;
415
419
  }
416
420
 
417
- visits_bitset_gt(std::size_t capacity) noexcept
421
+ bitset_gt(std::size_t capacity) noexcept
418
422
  : slots_((compressed_slot_t*)allocator_t{}.allocate(slots(capacity) * sizeof(compressed_slot_t))),
419
423
  count_(slots_ ? slots(capacity) : 0u) {
420
424
  clear();
421
425
  }
422
426
 
423
- visits_bitset_gt(visits_bitset_gt&& other) noexcept {
427
+ bitset_gt(bitset_gt&& other) noexcept {
424
428
  slots_ = exchange(other.slots_, nullptr);
425
429
  count_ = exchange(other.count_, 0);
426
430
  }
427
431
 
428
- visits_bitset_gt& operator=(visits_bitset_gt&& other) noexcept {
432
+ bitset_gt& operator=(bitset_gt&& other) noexcept {
429
433
  std::swap(slots_, other.slots_);
430
434
  std::swap(count_, other.count_);
431
435
  return *this;
432
436
  }
433
437
 
434
- visits_bitset_gt(visits_bitset_gt const&) = delete;
435
- visits_bitset_gt& operator=(visits_bitset_gt const&) = delete;
438
+ bitset_gt(bitset_gt const&) = delete;
439
+ bitset_gt& operator=(bitset_gt const&) = delete;
436
440
 
437
441
  inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
438
442
  inline bool set(std::size_t i) noexcept {
@@ -470,13 +474,12 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
470
474
  #endif
471
475
 
472
476
  class lock_t {
473
- visits_bitset_gt& bitset_;
477
+ bitset_gt& bitset_;
474
478
  std::size_t bit_offset_;
475
479
 
476
480
  public:
477
481
  inline ~lock_t() noexcept { bitset_.atomic_reset(bit_offset_); }
478
- inline lock_t(visits_bitset_gt& bitset, std::size_t bit_offset) noexcept
479
- : bitset_(bitset), bit_offset_(bit_offset) {
482
+ inline lock_t(bitset_gt& bitset, std::size_t bit_offset) noexcept : bitset_(bitset), bit_offset_(bit_offset) {
480
483
  while (bitset_.atomic_set(bit_offset_))
481
484
  ;
482
485
  }
@@ -485,7 +488,7 @@ template <typename allocator_at = std::allocator<byte_t>> class visits_bitset_gt
485
488
  inline lock_t lock(std::size_t i) noexcept { return {*this, i}; }
486
489
  };
487
490
 
488
- using visits_bitset_t = visits_bitset_gt<>;
491
+ using bitset_t = bitset_gt<>;
489
492
 
490
493
  /**
491
494
  * @brief Similar to `std::priority_queue`, but allows raw access to underlying
@@ -806,6 +809,149 @@ class usearch_pack_m uint40_t {
806
809
 
807
810
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
808
811
 
812
+ // clang-format off
813
+ template <typename key_at, typename std::enable_if<std::is_integral<key_at>::value>::type* = nullptr> key_at default_free_value() { return std::numeric_limits<key_at>::max(); }
814
+ template <typename key_at, typename std::enable_if<std::is_same<key_at, uint40_t>::value>::type* = nullptr> uint40_t default_free_value() { return uint40_t::max(); }
815
+ template <typename key_at, typename std::enable_if<!std::is_integral<key_at>::value && !std::is_same<key_at, uint40_t>::value>::type* = nullptr> key_at default_free_value() { return key_at(); }
816
+ // clang-format on
817
+
818
+ template <typename element_at> struct hash_gt {
819
+ std::size_t operator()(element_at const& element) const noexcept { return std::hash<element_at>{}(element); }
820
+ };
821
+
822
+ template <> struct hash_gt<uint40_t> {
823
+ std::size_t operator()(uint40_t const& element) const noexcept { return std::hash<std::size_t>{}(element); }
824
+ };
825
+
826
+ /**
827
+ * @brief Minimalistic hash-set implementation to track visited nodes during graph traversal.
828
+ *
829
+ * It doesn't support deletion of separate objects, but supports `clear`-ing all at once.
830
+ * It expects `reserve` to be called ahead of all insertions, so no resizes are needed.
831
+ * It also assumes `0xFF...FF` slots to be unused, to simplify the design.
832
+ * It uses linear probing, the number of slots is always a power of two, and it uses linear-probing
833
+ * in case of bucket collisions.
834
+ */
835
+ template <typename element_at, typename hasher_at = hash_gt<element_at>, typename allocator_at = std::allocator<byte_t>>
836
+ class growing_hash_set_gt {
837
+
838
+ using element_t = element_at;
839
+ using hasher_t = hasher_at;
840
+
841
+ using allocator_t = allocator_at;
842
+ using byte_t = typename allocator_t::value_type;
843
+ static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
844
+
845
+ element_t* slots_{};
846
+ /// @brief Number of slots.
847
+ std::size_t capacity_{};
848
+ /// @brief Number of populated.
849
+ std::size_t count_{};
850
+ hasher_t hasher_{};
851
+
852
+ public:
853
+ growing_hash_set_gt() noexcept {}
854
+ ~growing_hash_set_gt() noexcept { reset(); }
855
+
856
+ explicit operator bool() const noexcept { return slots_; }
857
+ std::size_t size() const noexcept { return count_; }
858
+
859
+ void clear() noexcept {
860
+ std::memset(slots_, 0xFF, capacity_ * sizeof(element_t));
861
+ count_ = 0;
862
+ }
863
+
864
+ void reset() noexcept {
865
+ if (slots_)
866
+ allocator_t{}.deallocate((byte_t*)slots_, capacity_ * sizeof(element_t));
867
+ slots_ = nullptr;
868
+ capacity_ = 0;
869
+ count_ = 0;
870
+ }
871
+
872
+ growing_hash_set_gt(std::size_t capacity) noexcept
873
+ : slots_((element_t*)allocator_t{}.allocate(ceil2(capacity) * sizeof(element_t))),
874
+ capacity_(slots_ ? ceil2(capacity) : 0u), count_(0u) {
875
+ clear();
876
+ }
877
+
878
+ growing_hash_set_gt(growing_hash_set_gt&& other) noexcept {
879
+ slots_ = exchange(other.slots_, nullptr);
880
+ capacity_ = exchange(other.capacity_, 0);
881
+ count_ = exchange(other.count_, 0);
882
+ }
883
+
884
+ growing_hash_set_gt& operator=(growing_hash_set_gt&& other) noexcept {
885
+ std::swap(slots_, other.slots_);
886
+ std::swap(capacity_, other.capacity_);
887
+ std::swap(count_, other.count_);
888
+ return *this;
889
+ }
890
+
891
+ growing_hash_set_gt(growing_hash_set_gt const&) = delete;
892
+ growing_hash_set_gt& operator=(growing_hash_set_gt const&) = delete;
893
+
894
+ inline bool test(element_t const& elem) const noexcept {
895
+ std::size_t index = hasher_(elem) & (capacity_ - 1);
896
+ while (slots_[index] != default_free_value<element_t>()) {
897
+ if (slots_[index] == elem)
898
+ return true;
899
+
900
+ index = (index + 1) & (capacity_ - 1);
901
+ }
902
+ return false;
903
+ }
904
+
905
+ /**
906
+ *
907
+ * @return Similar to `bitset_gt`, returns the previous value.
908
+ */
909
+ inline bool set(element_t const& elem) noexcept {
910
+ std::size_t index = hasher_(elem) & (capacity_ - 1);
911
+ while (slots_[index] != default_free_value<element_t>()) {
912
+ // Already exists
913
+ if (slots_[index] == elem)
914
+ return true;
915
+
916
+ index = (index + 1) & (capacity_ - 1);
917
+ }
918
+ slots_[index] = elem;
919
+ ++count_;
920
+ return false;
921
+ }
922
+
923
+ bool reserve(std::size_t new_capacity) noexcept {
924
+ new_capacity = (new_capacity * 5u) / 3u;
925
+ if (new_capacity <= capacity_)
926
+ return true;
927
+
928
+ new_capacity = ceil2(new_capacity);
929
+ element_t* new_slots = (element_t*)allocator_t{}.allocate(new_capacity * sizeof(element_t));
930
+ if (!new_slots)
931
+ return false;
932
+
933
+ std::memset(new_slots, 0xFF, new_capacity * sizeof(element_t));
934
+ std::size_t new_count = count_;
935
+ if (count_) {
936
+ for (std::size_t old_index = 0; old_index != capacity_; ++old_index) {
937
+ if (slots_[old_index] == default_free_value<element_t>())
938
+ continue;
939
+
940
+ std::size_t new_index = hasher_(slots_[old_index]) & (new_capacity - 1);
941
+ while (new_slots[new_index] != default_free_value<element_t>())
942
+ new_index = (new_index + 1) & (new_capacity - 1);
943
+ new_slots[new_index] = slots_[old_index];
944
+ }
945
+ }
946
+
947
+ reset();
948
+ slots_ = new_slots;
949
+ capacity_ = new_capacity;
950
+ count_ = new_count;
951
+ return true;
952
+ }
953
+ };
954
+
809
955
  /**
810
956
  * @brief Basic single-threaded @b ring class, used for all kinds of task queues.
811
957
  */
@@ -881,7 +1027,9 @@ class ring_gt {
881
1027
  bool reserve(std::size_t n) noexcept {
882
1028
  if (n < size())
883
1029
  return false; // prevent data loss
884
- n = (std::max<std::size_t>)(n, 64u);
1030
+ if (n <= capacity())
1031
+ return true;
1032
+ n = (std::max<std::size_t>)(ceil2(n), 64u);
885
1033
  element_t* elements = allocator_.allocate(n);
886
1034
  if (!elements)
887
1035
  return false;
@@ -1000,6 +1148,16 @@ struct index_search_config_t {
1000
1148
  bool exact = false;
1001
1149
  };
1002
1150
 
1151
+ struct index_cluster_config_t {
1152
+ /// @brief Hyper-parameter controlling the quality of search.
1153
+ /// Defaults to 16 in FAISS and 10 in hnswlib.
1154
+ /// > It is called `ef` in the paper.
1155
+ std::size_t expansion = default_expansion_search();
1156
+
1157
+ /// @brief Optional thread identifier for multi-threaded construction.
1158
+ std::size_t thread = 0;
1159
+ };
1160
+
1003
1161
  struct index_copy_config_t {};
1004
1162
 
1005
1163
  struct index_join_config_t {
@@ -1028,7 +1186,7 @@ using return_type_gt =
1028
1186
  * @brief An example of what a USearch-compatible ad-hoc filter would look like.
1029
1187
  *
1030
1188
  * A similar function object can be passed to search queries to further filter entries
1031
- * on their auxiliary properties, such as some categorical labels stored in an external DBMS.
1189
+ * on their auxiliary properties, such as some categorical keys stored in an external DBMS.
1032
1190
  */
1033
1191
  struct dummy_predicate_t {
1034
1192
  template <typename member_at> constexpr bool operator()(member_at&&) const noexcept { return true; }
@@ -1052,7 +1210,7 @@ struct dummy_callback_t {
1052
1210
  * saving, and loading from disk, or index-level joins.
1053
1211
  */
1054
1212
  struct dummy_progress_t {
1055
- inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
1213
+ inline void operator()(std::size_t, std::size_t) const noexcept {}
1056
1214
  };
1057
1215
 
1058
1216
  /**
@@ -1078,9 +1236,9 @@ struct dummy_prefetch_t {
1078
1236
  /**
1079
1237
  * @brief An example of what a USearch-compatible executor (thread-pool) should look like.
1080
1238
  *
1081
- * It's expected to have `execute_bulk(callback)` API to schedule one task per thread;
1082
- * an identical `execute_bulk(count, callback)` overload that also accepts the number
1083
- * of tasks, and somehow schedules them between threads; as well as `size()` to
1239
+ * It's expected to have `parallel(callback)` API to schedule one task per thread;
1240
+ * an identical `fixed(count, callback)` and `dynamic(count, callback)` overloads that also accepts
1241
+ * the number of tasks, and somehow schedules them between threads; as well as `size()` to
1084
1242
  * determine the number of available threads.
1085
1243
  */
1086
1244
  struct dummy_executor_t {
@@ -1088,25 +1246,32 @@ struct dummy_executor_t {
1088
1246
  std::size_t size() const noexcept { return 1; }
1089
1247
 
1090
1248
  template <typename thread_aware_function_at>
1091
- void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1249
+ void fixed(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1092
1250
  for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
1093
1251
  thread_aware_function(0, task_idx);
1094
1252
  }
1095
1253
 
1096
1254
  template <typename thread_aware_function_at>
1097
- void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
1255
+ void dynamic(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1256
+ for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
1257
+ if (!thread_aware_function(0, task_idx))
1258
+ break;
1259
+ }
1260
+
1261
+ template <typename thread_aware_function_at>
1262
+ void parallel(thread_aware_function_at&& thread_aware_function) noexcept {
1098
1263
  thread_aware_function(0);
1099
1264
  }
1100
1265
  };
1101
1266
 
1102
1267
  /**
1103
- * @brief An example of what a USearch-compatible label-to-label mapping should look like.
1268
+ * @brief An example of what a USearch-compatible key-to-key mapping should look like.
1104
1269
  *
1105
1270
  * This is particularly helpful for "Semantic Joins", where we map entries of one collection
1106
1271
  * to entries of another. In assymetric setups, where A -> B is needed, but B -> A is not,
1107
1272
  * this can be passed to minimize memory usage.
1108
1273
  */
1109
- struct dummy_label_to_label_mapping_t {
1274
+ struct dummy_key_to_key_mapping_t {
1110
1275
  struct member_ref_t {
1111
1276
  template <typename key_at> member_ref_t& operator=(key_at&&) noexcept { return *this; }
1112
1277
  };
@@ -1124,7 +1289,7 @@ template <typename object_at> static constexpr bool is_dummy() {
1124
1289
  std::is_same<object_t, dummy_progress_t>::value || //
1125
1290
  std::is_same<object_t, dummy_prefetch_t>::value || //
1126
1291
  std::is_same<object_t, dummy_executor_t>::value || //
1127
- std::is_same<object_t, dummy_label_to_label_mapping_t>::value;
1292
+ std::is_same<object_t, dummy_key_to_key_mapping_t>::value;
1128
1293
  }
1129
1294
 
1130
1295
  template <typename, typename at> struct has_reset_gt {
@@ -1603,7 +1768,9 @@ class index_gt {
1603
1768
  */
1604
1769
  static constexpr std::size_t node_head_bytes_() { return sizeof(key_t) + sizeof(level_t); }
1605
1770
 
1606
- using visits_bitset_t = visits_bitset_gt<dynamic_allocator_t>;
1771
+ using nodes_mutexes_t = bitset_gt<dynamic_allocator_t>;
1772
+
1773
+ using visits_hash_set_t = growing_hash_set_gt<compressed_slot_t, hash_gt<compressed_slot_t>, dynamic_allocator_t>;
1607
1774
 
1608
1775
  struct precomputed_constants_t {
1609
1776
  double inverse_log_connectivity{};
@@ -1697,7 +1864,7 @@ class index_gt {
1697
1864
  struct usearch_align_m context_t {
1698
1865
  top_candidates_t top_candidates{};
1699
1866
  next_candidates_t next_candidates{};
1700
- visits_bitset_t visits{};
1867
+ visits_hash_set_t visits{};
1701
1868
  std::default_random_engine level_generator{};
1702
1869
  std::size_t iteration_cycles{};
1703
1870
  std::size_t computed_distances_count{};
@@ -1754,7 +1921,7 @@ class index_gt {
1754
1921
  buffer_gt<node_t, nodes_allocator_t> nodes_{};
1755
1922
 
1756
1923
  /// @brief Mutex, that limits concurrent access to `nodes_`.
1757
- mutable visits_bitset_t nodes_mutexes_{};
1924
+ mutable nodes_mutexes_t nodes_mutexes_{};
1758
1925
 
1759
1926
  using contexts_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<context_t>;
1760
1927
 
@@ -1806,7 +1973,7 @@ class index_gt {
1806
1973
  }
1807
1974
  };
1808
1975
 
1809
- copy_result_t copy(index_copy_config_t /*config*/ = {}) const noexcept {
1976
+ copy_result_t copy(index_copy_config_t config = {}) const noexcept {
1810
1977
  copy_result_t result;
1811
1978
  index_gt& other = result.index;
1812
1979
  other = index_gt(config_, dynamic_allocator_, tape_allocator_);
@@ -1821,6 +1988,9 @@ class index_gt {
1821
1988
  other.nodes_count_ = nodes_count_.load();
1822
1989
  other.max_level_ = max_level_;
1823
1990
  other.entry_slot_ = entry_slot_;
1991
+
1992
+ // This controls nothing for now :)
1993
+ (void)config;
1824
1994
  return result;
1825
1995
  }
1826
1996
 
@@ -1914,18 +2084,12 @@ class index_gt {
1914
2084
  && limits.members <= limits_.members)
1915
2085
  return true;
1916
2086
 
1917
- visits_bitset_t new_mutexes(limits.members);
2087
+ nodes_mutexes_t new_mutexes(limits.members);
1918
2088
  buffer_gt<node_t, nodes_allocator_t> new_nodes(limits.members);
1919
2089
  buffer_gt<context_t, contexts_allocator_t> new_contexts(limits.threads());
1920
2090
  if (!new_nodes || !new_contexts || !new_mutexes)
1921
2091
  return false;
1922
2092
 
1923
- for (context_t& context : new_contexts) {
1924
- context.visits = visits_bitset_t(limits.members);
1925
- if (!context.visits)
1926
- return false;
1927
- }
1928
-
1929
2093
  // Move the nodes info, and deallocate previous buffers.
1930
2094
  if (nodes_)
1931
2095
  std::memcpy(new_nodes.data(), nodes_.data(), sizeof(node_t) * size());
@@ -1961,6 +2125,30 @@ class index_gt {
1961
2125
  struct match_t {
1962
2126
  member_cref_t member;
1963
2127
  distance_t distance;
2128
+
2129
+ inline match_t() noexcept : member({nullptr, 0}), distance(std::numeric_limits<distance_t>::max()) {}
2130
+
2131
+ inline match_t(member_cref_t member, distance_t distance) noexcept : member(member), distance(distance) {}
2132
+
2133
+ inline match_t(match_t&& other) noexcept
2134
+ : member({other.member.key.ptr(), other.member.slot}), distance(other.distance) {}
2135
+
2136
+ inline match_t(match_t const& other) noexcept
2137
+ : member({other.member.key.ptr(), other.member.slot}), distance(other.distance) {}
2138
+
2139
+ inline match_t& operator=(match_t const& other) noexcept {
2140
+ member.key.reset(other.member.key.ptr());
2141
+ member.slot = other.member.slot;
2142
+ distance = other.distance;
2143
+ return *this;
2144
+ }
2145
+
2146
+ inline match_t& operator=(match_t&& other) noexcept {
2147
+ member.key.reset(other.member.key.ptr());
2148
+ member.slot = other.member.slot;
2149
+ distance = other.distance;
2150
+ return *this;
2151
+ }
1964
2152
  };
1965
2153
 
1966
2154
  class search_result_t {
@@ -2046,6 +2234,19 @@ class index_gt {
2046
2234
  }
2047
2235
  };
2048
2236
 
2237
+ struct cluster_result_t {
2238
+ error_t error{};
2239
+ std::size_t visited_members{};
2240
+ std::size_t computed_distances{};
2241
+ match_t cluster{};
2242
+
2243
+ explicit operator bool() const noexcept { return !error; }
2244
+ cluster_result_t failed(error_t message) noexcept {
2245
+ error = std::move(message);
2246
+ return std::move(*this);
2247
+ }
2248
+ };
2249
+
2049
2250
  /**
2050
2251
  * @brief Inserts a new entry into the index. Thread-safe. Supports @b heterogeneous lookups.
2051
2252
  * Expects needed capacity to be reserved ahead of time: `size() < capacity()`.
@@ -2240,7 +2441,7 @@ class index_gt {
2240
2441
  * @param[in] wanted The upper bound for the number of results to return.
2241
2442
  * @param[in] config Configuration options for this specific operation.
2242
2443
  * @param[in] predicate Optional filtering predicate for `member_cref_t`.
2243
- * @return Smart object referencing temporary memory. Valid until next `search()` or `add()`.
2444
+ * @return Smart object referencing temporary memory. Valid until next `search()`, `add()`, or `cluster()`.
2244
2445
  */
2245
2446
  template < //
2246
2447
  typename value_at, //
@@ -2295,27 +2496,75 @@ class index_gt {
2295
2496
  return result;
2296
2497
  }
2297
2498
 
2499
+ /**
2500
+ * @brief Identifies the closest cluster to the gived ::query. Thread-safe.
2501
+ *
2502
+ * @param[in] query Content that will be compared against other entries in the index.
2503
+ * @param[in] level The index level to target. Higher means lower resolution.
2504
+ * @param[in] config Configuration options for this specific operation.
2505
+ * @param[in] predicate Optional filtering predicate for `member_cref_t`.
2506
+ * @return Smart object referencing temporary memory. Valid until next `search()`, `add()`, or `cluster()`.
2507
+ */
2508
+ template < //
2509
+ typename value_at, //
2510
+ typename metric_at, //
2511
+ typename predicate_at = dummy_predicate_t, //
2512
+ typename prefetch_at = dummy_prefetch_t //
2513
+ >
2514
+ cluster_result_t cluster( //
2515
+ value_at&& query, //
2516
+ std::size_t level, //
2517
+ metric_at&& metric, //
2518
+ index_cluster_config_t config = {}, //
2519
+ predicate_at&& predicate = predicate_at{}, //
2520
+ prefetch_at&& prefetch = prefetch_at{}) const noexcept {
2521
+
2522
+ context_t& context = contexts_[config.thread];
2523
+ cluster_result_t result;
2524
+ if (!nodes_count_)
2525
+ return result.failed("No clusters to identify");
2526
+
2527
+ // Go down the level, tracking only the closest match
2528
+ result.computed_distances = context.computed_distances_count;
2529
+ result.visited_members = context.iteration_cycles;
2530
+
2531
+ next_candidates_t& next = context.next_candidates;
2532
+ std::size_t expansion = config.expansion;
2533
+ if (!next.reserve(expansion))
2534
+ return result.failed("Out of memory!");
2535
+
2536
+ result.cluster.member =
2537
+ at(search_for_one_(query, metric, prefetch, entry_slot_, max_level_, level - 1, context));
2538
+ result.cluster.distance = context.measure(query, result.cluster.member, metric);
2539
+
2540
+ // Normalize stats
2541
+ result.computed_distances = context.computed_distances_count - result.computed_distances;
2542
+ result.visited_members = context.iteration_cycles - result.visited_members;
2543
+ return result;
2544
+ }
2545
+
2298
2546
  #pragma endregion
2299
2547
 
2300
2548
  #pragma region Metadata
2301
2549
 
2302
2550
  struct stats_t {
2303
- std::size_t nodes;
2304
- std::size_t edges;
2305
- std::size_t max_edges;
2306
- std::size_t allocated_bytes;
2551
+ std::size_t nodes{};
2552
+ std::size_t edges{};
2553
+ std::size_t max_edges{};
2554
+ std::size_t allocated_bytes{};
2307
2555
  };
2308
2556
 
2309
2557
  stats_t stats() const noexcept {
2310
2558
  stats_t result{};
2311
- result.nodes = size();
2312
- for (std::size_t i = 0; i != result.nodes; ++i) {
2559
+
2560
+ for (std::size_t i = 0; i != size(); ++i) {
2313
2561
  node_t node = node_at_(i);
2314
2562
  std::size_t max_edges = node.level() * config_.connectivity + config_.connectivity_base;
2315
2563
  std::size_t edges = 0;
2316
2564
  for (level_t level = 0; level <= node.level(); ++level)
2317
2565
  edges += neighbors_(node, level).size();
2318
2566
 
2567
+ ++result.nodes;
2319
2568
  result.allocated_bytes += node_bytes_(node).size();
2320
2569
  result.edges += edges;
2321
2570
  result.max_edges += max_edges;
@@ -2325,14 +2574,14 @@ class index_gt {
2325
2574
 
2326
2575
  stats_t stats(std::size_t level) const noexcept {
2327
2576
  stats_t result{};
2328
- result.nodes = size();
2329
2577
 
2330
2578
  std::size_t neighbors_bytes = !level ? pre_.neighbors_base_bytes : pre_.neighbors_bytes;
2331
- for (std::size_t i = 0; i != result.nodes; ++i) {
2579
+ for (std::size_t i = 0; i != size(); ++i) {
2332
2580
  node_t node = node_at_(i);
2333
2581
  if (static_cast<std::size_t>(node.level()) < level)
2334
2582
  continue;
2335
2583
 
2584
+ ++result.nodes;
2336
2585
  result.edges += neighbors_(node, level).size();
2337
2586
  result.allocated_bytes += node_head_bytes_() + neighbors_bytes;
2338
2587
  }
@@ -2432,7 +2681,7 @@ class index_gt {
2432
2681
  std::size_t stream_length() const noexcept {
2433
2682
  std::size_t neighbors_length = 0;
2434
2683
  for (std::size_t i = 0; i != size(); ++i)
2435
- neighbors_length += node_bytes_(node_at_(i).level());
2684
+ neighbors_length += node_bytes_(node_at_(i).level()) + sizeof(level_t);
2436
2685
  return sizeof(index_serialized_header_t) + neighbors_length;
2437
2686
  }
2438
2687
 
@@ -2457,6 +2706,11 @@ class index_gt {
2457
2706
  if (!result)
2458
2707
  return result;
2459
2708
 
2709
+ if (!header.size) {
2710
+ reset();
2711
+ return result;
2712
+ }
2713
+
2460
2714
  // Allocate some dynamic memory to read all the levels
2461
2715
  using levels_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<level_t>;
2462
2716
  buffer_gt<level_t, levels_allocator_t> levels(header.size);
@@ -2515,6 +2769,11 @@ class index_gt {
2515
2769
  return result.failed("File is corrupted and lacks a header");
2516
2770
  std::memcpy(&header, file.data() + offset, sizeof(header));
2517
2771
 
2772
+ if (!header.size) {
2773
+ reset();
2774
+ return result;
2775
+ }
2776
+
2518
2777
  // Precompute offsets of every node, but before that we need to update the configs
2519
2778
  // This could have been done with `std::exclusive_scan`, but it's only available from C++17.
2520
2779
  using offsets_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<std::size_t>;
@@ -2574,10 +2833,10 @@ class index_gt {
2574
2833
  * @param[in] executor Thread-pool to execute the job in parallel.
2575
2834
  * @param[in] progress Callback to report the execution progress.
2576
2835
  */
2577
- template <typename values_at, typename metric_at, //
2578
- typename slot_transition_at = dummy_label_to_label_mapping_t, //
2579
- typename executor_at = dummy_executor_t, //
2580
- typename progress_at = dummy_progress_t, //
2836
+ template <typename values_at, typename metric_at, //
2837
+ typename slot_transition_at = dummy_key_to_key_mapping_t, //
2838
+ typename executor_at = dummy_executor_t, //
2839
+ typename progress_at = dummy_progress_t, //
2581
2840
  typename prefetch_at = dummy_prefetch_t>
2582
2841
  void compact( //
2583
2842
  values_at&& values, //
@@ -2601,7 +2860,7 @@ class index_gt {
2601
2860
  buffer_gt<slot_level_t, slot_level_allocator_t> slots_and_levels(size());
2602
2861
 
2603
2862
  // For every bottom level node, determine its parent cluster
2604
- executor.execute_bulk(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
2863
+ executor.fixed(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
2605
2864
  context_t& context = contexts_[thread_idx];
2606
2865
  std::size_t cluster = search_for_one_( //
2607
2866
  values[citerator_at(old_slot)], //
@@ -2680,7 +2939,7 @@ class index_gt {
2680
2939
 
2681
2940
  // Erase all the incoming links
2682
2941
  std::size_t nodes_count = size();
2683
- executor.execute_bulk(nodes_count, [&](std::size_t, std::size_t node_idx) {
2942
+ executor.fixed(nodes_count, [&](std::size_t, std::size_t node_idx) {
2684
2943
  node_t node = node_at_(node_idx);
2685
2944
  for (level_t level = 0; level <= node.level(); ++level) {
2686
2945
  neighbors_ref_t neighbors = neighbors_(node, level);
@@ -2764,9 +3023,9 @@ class index_gt {
2764
3023
  }
2765
3024
 
2766
3025
  struct node_lock_t {
2767
- visits_bitset_t& bitset;
3026
+ nodes_mutexes_t& mutexes;
2768
3027
  std::size_t slot;
2769
- inline ~node_lock_t() noexcept { bitset.atomic_reset(slot); }
3028
+ inline ~node_lock_t() noexcept { mutexes.atomic_reset(slot); }
2770
3029
  };
2771
3030
 
2772
3031
  inline node_lock_t node_lock_(std::size_t slot) const noexcept {
@@ -2876,10 +3135,12 @@ class index_gt {
2876
3135
 
2877
3136
  index_gt const& index_;
2878
3137
  neighbors_ref_t neighbors_;
2879
- visits_bitset_t& visits_;
3138
+ visits_hash_set_t& visits_;
2880
3139
  std::size_t current_;
2881
3140
 
2882
3141
  candidates_iterator_t& skip_missing() noexcept {
3142
+ if (!visits_.size())
3143
+ return *this;
2883
3144
  while (current_ != neighbors_.size()) {
2884
3145
  compressed_slot_t neighbor_slot = neighbors_[current_];
2885
3146
  if (visits_.test(neighbor_slot))
@@ -2899,7 +3160,7 @@ class index_gt {
2899
3160
  using reference = misaligned_ref_gt<element_t>;
2900
3161
 
2901
3162
  reference operator*() const noexcept { return slot(); }
2902
- candidates_iterator_t(index_gt const& index, neighbors_ref_t neighbors, visits_bitset_t& visits,
3163
+ candidates_iterator_t(index_gt const& index, neighbors_ref_t neighbors, visits_hash_set_t& visits,
2903
3164
  std::size_t progress) noexcept
2904
3165
  : index_(index), neighbors_(neighbors), visits_(visits), current_(progress) {}
2905
3166
  candidates_iterator_t operator++(int) noexcept {
@@ -2922,7 +3183,7 @@ class index_gt {
2922
3183
  struct candidates_range_t {
2923
3184
  index_gt const& index;
2924
3185
  neighbors_ref_t neighbors;
2925
- visits_bitset_t& visits;
3186
+ visits_hash_set_t& visits;
2926
3187
 
2927
3188
  candidates_iterator_t begin() const noexcept {
2928
3189
  return candidates_iterator_t{index, neighbors, visits, 0}.skip_missing();
@@ -2935,7 +3196,7 @@ class index_gt {
2935
3196
  value_at&& query, metric_at&& metric, prefetch_at&& prefetch, //
2936
3197
  std::size_t closest_slot, level_t begin_level, level_t end_level, context_t& context) const noexcept {
2937
3198
 
2938
- visits_bitset_t& visits = context.visits;
3199
+ visits_hash_set_t& visits = context.visits;
2939
3200
  visits.clear();
2940
3201
 
2941
3202
  // Optional prefetching
@@ -2982,13 +3243,15 @@ class index_gt {
2982
3243
  std::size_t start_slot, std::size_t new_slot, level_t level, std::size_t top_limit,
2983
3244
  context_t& context) noexcept {
2984
3245
 
2985
- visits_bitset_t& visits = context.visits;
3246
+ visits_hash_set_t& visits = context.visits;
2986
3247
  next_candidates_t& next = context.next_candidates; // pop min, push
2987
3248
  top_candidates_t& top = context.top_candidates; // pop max, push
2988
3249
 
2989
3250
  visits.clear();
2990
3251
  next.clear();
2991
3252
  top.clear();
3253
+ if (!visits.reserve(config_.connectivity_base + 1u))
3254
+ return false;
2992
3255
 
2993
3256
  // Optional prefetching
2994
3257
  if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
@@ -3021,6 +3284,10 @@ class index_gt {
3021
3284
  prefetch(missing_candidates.begin(), missing_candidates.end());
3022
3285
  }
3023
3286
 
3287
+ // Assume the worst-case when reserving memory
3288
+ if (!visits.reserve(visits.size() + candidate_neighbors.size()))
3289
+ return false;
3290
+
3024
3291
  for (compressed_slot_t successor_slot : candidate_neighbors) {
3025
3292
  if (visits.set(successor_slot))
3026
3293
  continue;
@@ -3049,7 +3316,7 @@ class index_gt {
3049
3316
  value_at&& query, metric_at&& metric, predicate_at&& predicate, prefetch_at&& prefetch, //
3050
3317
  std::size_t start_slot, std::size_t expansion, context_t& context) const noexcept {
3051
3318
 
3052
- visits_bitset_t& visits = context.visits;
3319
+ visits_hash_set_t& visits = context.visits;
3053
3320
  next_candidates_t& next = context.next_candidates; // pop min, push
3054
3321
  top_candidates_t& top = context.top_candidates; // pop max, push
3055
3322
  std::size_t const top_limit = expansion;
@@ -3057,6 +3324,8 @@ class index_gt {
3057
3324
  visits.clear();
3058
3325
  next.clear();
3059
3326
  top.clear();
3327
+ if (!visits.reserve(config_.connectivity_base + 1u))
3328
+ return false;
3060
3329
 
3061
3330
  // Optional prefetching
3062
3331
  if (!std::is_same<prefetch_at, dummy_prefetch_t>::value)
@@ -3084,6 +3353,10 @@ class index_gt {
3084
3353
  prefetch(missing_candidates.begin(), missing_candidates.end());
3085
3354
  }
3086
3355
 
3356
+ // Assume the worst-case when reserving memory
3357
+ if (!visits.reserve(visits.size() + candidate_neighbors.size()))
3358
+ return false;
3359
+
3087
3360
  for (compressed_slot_t successor_slot : candidate_neighbors) {
3088
3361
  if (visits.set(successor_slot))
3089
3362
  continue;
@@ -3205,10 +3478,10 @@ template < //
3205
3478
  typename men_metric_at, //
3206
3479
  typename women_metric_at, //
3207
3480
 
3208
- typename man_to_woman_at = dummy_label_to_label_mapping_t, //
3209
- typename woman_to_man_at = dummy_label_to_label_mapping_t, //
3210
- typename executor_at = dummy_executor_t, //
3211
- typename progress_at = dummy_progress_t //
3481
+ typename man_to_woman_at = dummy_key_to_key_mapping_t, //
3482
+ typename woman_to_man_at = dummy_key_to_key_mapping_t, //
3483
+ typename executor_at = dummy_executor_t, //
3484
+ typename progress_at = dummy_progress_t //
3212
3485
  >
3213
3486
  static join_result_t join( //
3214
3487
  men_at const& men, //
@@ -3249,6 +3522,7 @@ static join_result_t join( //
3249
3522
  config.max_proposals = (std::min)(men.size(), config.max_proposals);
3250
3523
 
3251
3524
  using distance_t = typename men_at::distance_t;
3525
+ using dynamic_allocator_t = typename men_at::dynamic_allocator_t;
3252
3526
  using dynamic_allocator_traits_t = typename men_at::dynamic_allocator_traits_t;
3253
3527
  using man_key_t = typename men_at::key_t;
3254
3528
  using woman_key_t = typename women_at::key_t;
@@ -3280,7 +3554,7 @@ static join_result_t join( //
3280
3554
  std::memset(proposal_counts.data(), 0, sizeof(proposals_count_t) * men.size());
3281
3555
 
3282
3556
  // Define locks, to limit concurrent accesses to `man_to_woman_slots` and `woman_to_man_slots`.
3283
- visits_bitset_t men_locks(men.size()), women_locks(women.size());
3557
+ bitset_t men_locks(men.size()), women_locks(women.size());
3284
3558
  if (!men_locks || !women_locks)
3285
3559
  return result.failed("Can't allocate locks");
3286
3560
 
@@ -3288,9 +3562,10 @@ static join_result_t join( //
3288
3562
  std::atomic<std::size_t> engagements{0};
3289
3563
  std::atomic<std::size_t> computed_distances{0};
3290
3564
  std::atomic<std::size_t> visited_members{0};
3565
+ std::atomic<char const*> atomic_error{nullptr};
3291
3566
 
3292
3567
  // Concurrently process all the men
3293
- executor.execute_bulk([&](std::size_t thread_idx) {
3568
+ executor.parallel([&](std::size_t thread_idx) {
3294
3569
  index_search_config_t search_config;
3295
3570
  search_config.expansion = config.expansion;
3296
3571
  search_config.exact = config.exact;
@@ -3298,7 +3573,7 @@ static join_result_t join( //
3298
3573
  compressed_slot_t free_man_slot;
3299
3574
 
3300
3575
  // While there exist a free man who still has a woman to propose to.
3301
- while (true) {
3576
+ while (!atomic_error.load(std::memory_order_relaxed)) {
3302
3577
  std::size_t passed_rounds = 0;
3303
3578
  std::size_t total_rounds = 0;
3304
3579
  {
@@ -3323,7 +3598,8 @@ static join_result_t join( //
3323
3598
  visited_members += candidates.visited_members;
3324
3599
  computed_distances += candidates.computed_distances;
3325
3600
  if (!candidates) {
3326
- // TODO:
3601
+ atomic_error = candidates.error.release();
3602
+ break;
3327
3603
  }
3328
3604
 
3329
3605
  auto match = candidates.back();
@@ -3366,6 +3642,9 @@ static join_result_t join( //
3366
3642
  }
3367
3643
  });
3368
3644
 
3645
+ if (atomic_error)
3646
+ return result.failed(atomic_error.load());
3647
+
3369
3648
  // Export the "slots" into keys:
3370
3649
  std::size_t intersection_size = 0;
3371
3650
  for (std::size_t man_slot = 0; man_slot != men.size(); ++man_slot) {