usearch 0.19.3 → 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.
@@ -162,6 +162,10 @@ template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num)
162
162
  return (num + multiple_ak - 1) / multiple_ak;
163
163
  }
164
164
 
165
+ inline std::size_t divide_round_up(std::size_t num, std::size_t denominator) noexcept {
166
+ return (num + denominator - 1) / denominator;
167
+ }
168
+
165
169
  inline std::size_t ceil2(std::size_t v) noexcept {
166
170
  v--;
167
171
  v |= v >> 1;
@@ -174,12 +178,16 @@ inline std::size_t ceil2(std::size_t v) noexcept {
174
178
  return v;
175
179
  }
176
180
 
177
- template <typename at> void misaligned_store(void* ptr, at v) noexcept { std::memcpy(ptr, &v, sizeof(v)); }
181
+ template <typename at> void misaligned_store(void* ptr, at v) noexcept {
182
+ static_assert(!std::is_reference<at>::value);
183
+ std::memcpy(ptr, &v, sizeof(at));
184
+ }
178
185
 
179
186
  /// @brief Simply dereferencing misaligned pointers can be dangerous.
180
187
  template <typename at> at misaligned_load(void* ptr) noexcept {
188
+ static_assert(!std::is_reference<at>::value);
181
189
  at v;
182
- std::memcpy(&v, ptr, sizeof(v));
190
+ std::memcpy(&v, ptr, sizeof(at));
183
191
  return v;
184
192
  }
185
193
 
@@ -250,10 +258,13 @@ template <typename scalar_at> class span_gt {
250
258
  std::size_t size_;
251
259
 
252
260
  public:
253
- span_gt(scalar_at* begin, scalar_at const* end) noexcept : data_(begin), size_(end - begin) {}
261
+ span_gt() noexcept : data_(nullptr), size_(0u) {}
262
+ span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
254
263
  span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
255
264
  scalar_at* data() const noexcept { return data_; }
256
265
  std::size_t size() const noexcept { return size_; }
266
+ scalar_at* begin() const noexcept { return data_; }
267
+ scalar_at* end() const noexcept { return data_ + size_; }
257
268
  operator scalar_at*() const noexcept { return data(); }
258
269
  };
259
270
 
@@ -609,8 +620,8 @@ template <typename result_at> struct expected_gt {
609
620
  }
610
621
  result_at const& operator*() const noexcept { return result; }
611
622
  explicit operator bool() const noexcept { return !error; }
612
- expected_gt failed(char const* message) noexcept {
613
- error = message;
623
+ expected_gt failed(error_t message) noexcept {
624
+ error = std::move(message);
614
625
  return std::move(*this);
615
626
  }
616
627
  };
@@ -640,7 +651,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
640
651
 
641
652
  void reset() noexcept {
642
653
  if (slots_)
643
- allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t)), slots_ = nullptr, count_ = 0;
654
+ allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t));
655
+ slots_ = nullptr;
656
+ count_ = 0;
644
657
  }
645
658
 
646
659
  bool resize(std::size_t capacity) noexcept {
@@ -661,10 +674,8 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
661
674
  }
662
675
 
663
676
  visits_bitset_gt(visits_bitset_gt&& other) noexcept {
664
- slots_ = other.slots_;
665
- count_ = other.count_;
666
- other.slots_ = nullptr;
667
- other.count_ = 0;
677
+ slots_ = exchange(other.slots_, nullptr);
678
+ count_ = exchange(other.count_, 0);
668
679
  }
669
680
 
670
681
  visits_bitset_gt& operator=(visits_bitset_gt&& other) noexcept {
@@ -673,6 +684,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
673
684
  return *this;
674
685
  }
675
686
 
687
+ visits_bitset_gt(visits_bitset_gt const&) = delete;
688
+ visits_bitset_gt& operator=(visits_bitset_gt const&) = delete;
689
+
676
690
  inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
677
691
  inline void set(std::size_t i) noexcept { slots_[i / bits_per_slot()] |= (1ul << (i & bits_mask())); }
678
692
 
@@ -911,17 +925,18 @@ class sorted_buffer_gt {
911
925
  if (!new_elements)
912
926
  return false;
913
927
 
914
- if (size_) {
928
+ if (size_)
915
929
  std::memcpy(new_elements, elements_, size_ * sizeof(element_t));
930
+ if (elements_)
916
931
  allocator.deallocate(elements_, capacity_);
917
- }
932
+
918
933
  elements_ = new_elements;
919
934
  capacity_ = new_capacity;
920
935
  return true;
921
936
  }
922
937
 
923
938
  inline void insert_reserved(element_t&& element) noexcept {
924
- std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
939
+ std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
925
940
  std::size_t to_move = size_ - slot;
926
941
  element_t* source = elements_ + size_ - 1;
927
942
  for (; to_move; --to_move, --source)
@@ -934,7 +949,7 @@ class sorted_buffer_gt {
934
949
  * @return `true` if the entry was added, `false` if it wasn't relevant enough.
935
950
  */
936
951
  inline bool insert(element_t&& element, std::size_t limit) noexcept {
937
- std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
952
+ std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
938
953
  if (slot == limit)
939
954
  return false;
940
955
  std::size_t to_move = size_ - slot - (size_ == limit);
@@ -975,10 +990,10 @@ class usearch_pack_m uint40_t {
975
990
 
976
991
  public:
977
992
  inline uint40_t() noexcept { std::memset(octets, 0, 5); }
978
- inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets + 1, (char*)&n, 4), octets[0] = 0; }
979
- inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n + 3, 5); }
993
+ inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets, (char*)&n, 4); }
994
+ inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
980
995
  #if defined(USEARCH_DEFINED_CLANG) && defined(USEARCH_DEFINED_APPLE)
981
- inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n + 3, 5); }
996
+ inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
982
997
  #endif
983
998
 
984
999
  uint40_t(uint40_t&&) = default;
@@ -986,24 +1001,40 @@ class usearch_pack_m uint40_t {
986
1001
  uint40_t& operator=(uint40_t&&) = default;
987
1002
  uint40_t& operator=(uint40_t const&) = default;
988
1003
 
989
- inline uint40_t& operator+=(std::uint32_t i) noexcept {
990
- std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets + 1);
991
- octets[0] += static_cast<unsigned char>((tail + i) < tail);
992
- tail += i;
1004
+ inline uint40_t& operator+=(std::uint32_t n) noexcept {
1005
+ std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1006
+ octets[4] += static_cast<unsigned char>((tail + n) < tail);
1007
+ tail += n;
1008
+ return *this;
1009
+ }
1010
+
1011
+ inline uint40_t& operator+=(std::size_t n) noexcept {
1012
+ unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
1013
+ std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
1014
+ std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1015
+ octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
1016
+ tail += n_tail;
1017
+ octets[4] += n_octets[4];
993
1018
  return *this;
994
1019
  }
995
1020
 
1021
+ inline uint40_t operator+(std::size_t n) noexcept {
1022
+ uint40_t other(*this);
1023
+ other += n;
1024
+ return other;
1025
+ }
1026
+
996
1027
  inline operator std::size_t() const noexcept {
997
1028
  std::size_t result = 0;
998
- std::memcpy((char*)&result + 3, octets, 5);
1029
+ std::memcpy((char*)&result, octets, 5);
999
1030
  return result;
1000
1031
  }
1001
1032
 
1002
- inline uint40_t& operator++() noexcept { return *this += 1; }
1033
+ inline uint40_t& operator++() noexcept { return *this += 1u; }
1003
1034
 
1004
1035
  inline uint40_t operator++(int) noexcept {
1005
1036
  uint40_t old = *this;
1006
- *this += 1;
1037
+ *this += 1u;
1007
1038
  return old;
1008
1039
  }
1009
1040
  };
@@ -1014,6 +1045,89 @@ class usearch_pack_m uint40_t {
1014
1045
 
1015
1046
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
1016
1047
 
1048
+ template <typename element_at, typename allocator_at = std::allocator<element_at>> //
1049
+ class ring_gt {
1050
+ public:
1051
+ using element_t = element_at;
1052
+ using allocator_t = allocator_at;
1053
+
1054
+ static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
1055
+ static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
1056
+
1057
+ using value_type = element_t;
1058
+
1059
+ private:
1060
+ element_t* elements_;
1061
+ std::size_t capacity_;
1062
+ std::size_t head_;
1063
+ std::size_t tail_;
1064
+ bool empty_;
1065
+ allocator_t allocator_;
1066
+
1067
+ public:
1068
+ explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept
1069
+ : elements_(nullptr), capacity_(0), head_(0), tail_(0), empty_(true), allocator_(alloc) {}
1070
+
1071
+ ring_gt(ring_gt const&) = delete;
1072
+ ring_gt& operator=(ring_gt const&) = delete;
1073
+
1074
+ ~ring_gt() noexcept { reset(); }
1075
+
1076
+ bool empty() const noexcept { return empty_; }
1077
+ size_t size() const noexcept {
1078
+ if (empty_)
1079
+ return 0;
1080
+ else if (head_ >= tail_)
1081
+ return head_ - tail_;
1082
+ else
1083
+ return capacity_ - (tail_ - head_);
1084
+ }
1085
+
1086
+ void reset() noexcept {
1087
+ if (elements_)
1088
+ allocator_.deallocate(elements_, capacity_);
1089
+ elements_ = nullptr;
1090
+ capacity_ = 0;
1091
+ head_ = 0;
1092
+ tail_ = 0;
1093
+ empty_ = true;
1094
+ }
1095
+
1096
+ bool resize(std::size_t n) noexcept {
1097
+ element_t* elements = allocator_.allocate(n);
1098
+ if (!elements)
1099
+ return false;
1100
+ reset();
1101
+ elements_ = elements;
1102
+ capacity_ = n;
1103
+ return true;
1104
+ }
1105
+
1106
+ void push(element_t const& value) noexcept {
1107
+ elements_[head_] = value;
1108
+ head_ = (head_ + 1) % capacity_;
1109
+ empty_ = false;
1110
+ }
1111
+
1112
+ bool try_push(element_t const& value) noexcept {
1113
+ if (head_ == tail_ && !empty_)
1114
+ return false; // elements_ is full
1115
+
1116
+ return push(value);
1117
+ return true;
1118
+ }
1119
+
1120
+ bool try_pop(element_t& value) noexcept {
1121
+ if (empty_)
1122
+ return false;
1123
+
1124
+ value = std::move(elements_[tail_]);
1125
+ tail_ = (tail_ + 1) % capacity_;
1126
+ empty_ = head_ == tail_;
1127
+ return true;
1128
+ }
1129
+ };
1130
+
1017
1131
  /// @brief Number of neighbors per graph node.
1018
1132
  /// Defaults to 32 in FAISS and 16 in hnswlib.
1019
1133
  /// > It is called `M` in the paper.
@@ -1095,6 +1209,21 @@ struct search_config_t {
1095
1209
  bool exact = false;
1096
1210
  };
1097
1211
 
1212
+ struct copy_config_t {
1213
+ bool copy_vectors = true;
1214
+ };
1215
+
1216
+ struct join_config_t {
1217
+ /// @brief Controls maximum number of proposals per man during stable marriage.
1218
+ std::size_t max_proposals = 0;
1219
+ /// @brief Hyper-parameter controlling the quality of search.
1220
+ /// Defaults to 16 in FAISS and 10 in hnswlib.
1221
+ /// > It is called `ef` in the paper.
1222
+ std::size_t expansion = default_expansion_search();
1223
+ /// @brief Brute-forces exhaustive search over all entries in the index.
1224
+ bool exact = false;
1225
+ };
1226
+
1098
1227
  using file_header_t = byte_t[64];
1099
1228
 
1100
1229
  /**
@@ -1199,8 +1328,8 @@ struct file_head_result_t {
1199
1328
  error_t error;
1200
1329
 
1201
1330
  explicit operator bool() const noexcept { return !error; }
1202
- file_head_result_t failed(char const* message) noexcept {
1203
- error = message;
1331
+ file_head_result_t failed(error_t message) noexcept {
1332
+ error = std::move(message);
1204
1333
  return std::move(*this);
1205
1334
  }
1206
1335
  };
@@ -1234,6 +1363,37 @@ struct viewed_file_t {
1234
1363
  };
1235
1364
  #endif
1236
1365
 
1366
+ struct dummy_predicate_t {
1367
+ template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
1368
+ };
1369
+
1370
+ struct dummy_progress_t {
1371
+ inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
1372
+ };
1373
+
1374
+ struct dummy_executor_t {
1375
+ dummy_executor_t() noexcept {}
1376
+ std::size_t size() const noexcept { return 1; }
1377
+
1378
+ template <typename thread_aware_function_at>
1379
+ void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1380
+ for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
1381
+ thread_aware_function(0, task_idx);
1382
+ }
1383
+
1384
+ template <typename thread_aware_function_at>
1385
+ void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
1386
+ thread_aware_function(0);
1387
+ }
1388
+ };
1389
+
1390
+ struct dummy_label_to_label_mapping_t {
1391
+ struct member_ref_t {
1392
+ template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
1393
+ };
1394
+ template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
1395
+ };
1396
+
1237
1397
  /**
1238
1398
  * @brief Approximate Nearest Neighbors Search index using the
1239
1399
  * Hierarchical Navigable Small World graph algorithm.
@@ -1317,11 +1477,13 @@ class index_gt {
1317
1477
  struct member_ref_t {
1318
1478
  misaligned_ref_gt<label_t> label;
1319
1479
  vector_view_t vector;
1480
+ id_t id;
1320
1481
  };
1321
1482
 
1322
1483
  struct member_cref_t {
1323
1484
  label_t label;
1324
1485
  vector_view_t vector;
1486
+ id_t id;
1325
1487
  };
1326
1488
 
1327
1489
  template <typename ref_at, typename index_at> class member_iterator_gt {
@@ -1341,7 +1503,11 @@ class index_gt {
1341
1503
  using pointer = void;
1342
1504
  using reference = ref_t;
1343
1505
 
1344
- reference operator*() const noexcept { return ref_t(index_->node_with_id_(offset_)); }
1506
+ reference operator*() const noexcept {
1507
+ node_t node = index_->node_with_id_(offset_);
1508
+ return {node.label(), node.vector_view(), static_cast<id_t>(offset_)};
1509
+ }
1510
+
1345
1511
  member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
1346
1512
  member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
1347
1513
  member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
@@ -1453,9 +1619,9 @@ class index_gt {
1453
1619
  node_t(node_t const&) = default;
1454
1620
  node_t& operator=(node_t const&) = default;
1455
1621
 
1456
- label_t label() const noexcept { return misaligned_load<label_t>(tape_); }
1457
- dim_t dim() const noexcept { return misaligned_load<dim_t>(tape_ + sizeof(label_t)); }
1458
- level_t level() const noexcept { return misaligned_load<level_t>(tape_ + sizeof(label_t) + sizeof(dim_t)); }
1622
+ misaligned_ref_gt<label_t> label() const noexcept { return {tape_}; }
1623
+ misaligned_ref_gt<dim_t> dim() const noexcept { return {tape_ + sizeof(label_t)}; }
1624
+ misaligned_ref_gt<level_t> level() const noexcept { return {tape_ + sizeof(label_t) + sizeof(dim_t)}; }
1459
1625
 
1460
1626
  void label(label_t v) noexcept { return misaligned_store<label_t>(tape_, v); }
1461
1627
  void dim(dim_t v) noexcept { return misaligned_store<dim_t>(tape_ + sizeof(label_t), v); }
@@ -1518,7 +1684,7 @@ class index_gt {
1518
1684
  index_config_t config_{};
1519
1685
  index_limits_t limits_{};
1520
1686
  metric_t metric_{};
1521
- allocator_t allocator_{};
1687
+ mutable allocator_t allocator_{};
1522
1688
  point_allocator_t point_allocator_{};
1523
1689
  precomputed_constants_t pre_{};
1524
1690
  viewed_file_t viewed_file_{};
@@ -1572,6 +1738,35 @@ class index_gt {
1572
1738
  return *this;
1573
1739
  }
1574
1740
 
1741
+ struct copy_result_t {
1742
+ error_t error;
1743
+ index_gt index;
1744
+
1745
+ explicit operator bool() const noexcept { return !error; }
1746
+ copy_result_t failed(error_t message) noexcept {
1747
+ error = std::move(message);
1748
+ return std::move(*this);
1749
+ }
1750
+ };
1751
+
1752
+ copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
1753
+ copy_result_t result;
1754
+ index_gt& other = result.index;
1755
+ other = index_gt(config_, metric_, allocator_, point_allocator_);
1756
+ if (!other.reserve(limits_))
1757
+ return result.failed("Failed to reserve the contexts");
1758
+
1759
+ // Now all is left - is to allocate new `node_t` instances and populate
1760
+ // the `other.nodes_` array into it.
1761
+ for (std::size_t i = 0; i != size_; ++i)
1762
+ other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
1763
+
1764
+ other.size_ = size_.load();
1765
+ other.max_level_ = max_level_;
1766
+ other.entry_id_ = entry_id_;
1767
+ return result;
1768
+ }
1769
+
1575
1770
  member_citerator_t cbegin() const noexcept { return {this, 0}; }
1576
1771
  member_citerator_t cend() const noexcept { return {this, size()}; }
1577
1772
  member_citerator_t begin() const noexcept { return {this, 0}; }
@@ -1605,8 +1800,11 @@ class index_gt {
1605
1800
 
1606
1801
  if (nodes_)
1607
1802
  node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.elements);
1608
- if (contexts_)
1803
+ if (contexts_) {
1804
+ for (std::size_t i = 0; i != limits_.threads(); ++i)
1805
+ contexts_[i].~context_t();
1609
1806
  context_allocator_t{}.deallocate(exchange(contexts_, nullptr), limits_.threads());
1807
+ }
1610
1808
  limits_ = index_limits_t{0, 0};
1611
1809
  capacity_ = 0;
1612
1810
  reset_view_();
@@ -1620,11 +1818,13 @@ class index_gt {
1620
1818
  std::swap(limits_, other.limits_);
1621
1819
  std::swap(metric_, other.metric_);
1622
1820
  std::swap(allocator_, other.allocator_);
1821
+ std::swap(point_allocator_, other.point_allocator_);
1623
1822
  std::swap(pre_, other.pre_);
1624
1823
  std::swap(viewed_file_, other.viewed_file_);
1625
1824
  std::swap(max_level_, other.max_level_);
1626
1825
  std::swap(entry_id_, other.entry_id_);
1627
1826
  std::swap(nodes_, other.nodes_);
1827
+ std::swap(nodes_mutexes_, other.nodes_mutexes_);
1628
1828
  std::swap(contexts_, other.contexts_);
1629
1829
 
1630
1830
  // Non-atomic parts.
@@ -1723,14 +1923,14 @@ class index_gt {
1723
1923
  id_t id{};
1724
1924
 
1725
1925
  explicit operator bool() const noexcept { return !error; }
1726
- add_result_t failed(char const* message) noexcept {
1727
- error = message;
1926
+ add_result_t failed(error_t message) noexcept {
1927
+ error = std::move(message);
1728
1928
  return std::move(*this);
1729
1929
  }
1730
1930
  };
1731
1931
 
1732
1932
  struct match_t {
1733
- member_cref_t element;
1933
+ member_cref_t member;
1734
1934
  distance_t distance;
1735
1935
  };
1736
1936
 
@@ -1756,21 +1956,33 @@ class index_gt {
1756
1956
  inline search_result_t& operator=(search_result_t&&) = default;
1757
1957
 
1758
1958
  explicit operator bool() const noexcept { return !error; }
1759
- search_result_t failed(char const* message) noexcept {
1760
- error = message;
1959
+ search_result_t failed(error_t message) noexcept {
1960
+ error = std::move(message);
1761
1961
  return std::move(*this);
1762
1962
  }
1763
1963
 
1764
1964
  inline operator std::size_t() const noexcept { return count; }
1765
1965
  inline std::size_t size() const noexcept { return count; }
1766
- inline match_t operator[](std::size_t i) const noexcept {
1966
+ inline bool empty() const noexcept { return !count; }
1967
+ inline match_t operator[](std::size_t i) const noexcept { return at(i); }
1968
+ inline match_t front() const noexcept { return at(0); }
1969
+ inline match_t back() const noexcept { return at(count - 1); }
1970
+ inline bool contains(label_t label) const noexcept {
1971
+ for (std::size_t i = 0; i != count; ++i)
1972
+ if (at(i).member.label == label)
1973
+ return true;
1974
+ return false;
1975
+ }
1976
+ inline match_t at(std::size_t i) const noexcept {
1767
1977
  candidate_t const* top_ordered = top_.data();
1768
- return {member_cref_t(index_.node_with_id_(top_ordered[i].id)), top_ordered[i].distance};
1978
+ candidate_t candidate = top_ordered[i];
1979
+ node_t node = index_.node_with_id_(candidate.id);
1980
+ return {member_cref_t{node.label(), node.vector_view(), candidate.id}, candidate.distance};
1769
1981
  }
1770
1982
  inline std::size_t dump_to(label_t* labels, distance_t* distances) const noexcept {
1771
1983
  for (std::size_t i = 0; i != count; ++i) {
1772
1984
  match_t result = operator[](i);
1773
- labels[i] = result.element.label;
1985
+ labels[i] = result.member.label;
1774
1986
  distances[i] = result.distance;
1775
1987
  }
1776
1988
  return count;
@@ -1778,7 +1990,7 @@ class index_gt {
1778
1990
  inline std::size_t dump_to(label_t* labels) const noexcept {
1779
1991
  for (std::size_t i = 0; i != count; ++i) {
1780
1992
  match_t result = operator[](i);
1781
- labels[i] = result.element.label;
1993
+ labels[i] = result.member.label;
1782
1994
  }
1783
1995
  return count;
1784
1996
  }
@@ -1820,7 +2032,7 @@ class index_gt {
1820
2032
  new_level_lock.unlock();
1821
2033
 
1822
2034
  // Allocate the neighbors
1823
- node_t node = node_malloc_(label, vector, target_level, config.store_vector);
2035
+ node_t node = node_make_(label, vector, target_level, config.store_vector);
1824
2036
  if (!node)
1825
2037
  return result.failed("Out of memory!");
1826
2038
  std::size_t old_size = size_.fetch_add(1);
@@ -1849,6 +2061,7 @@ class index_gt {
1849
2061
  // TODO: Handle out of memory conditions
1850
2062
  search_to_insert_(closest_id, vector, level, config.expansion, context);
1851
2063
  closest_id = connect_new_node_(new_id, level, context);
2064
+ reconnect_neighbor_nodes_(new_id, level, context);
1852
2065
  }
1853
2066
 
1854
2067
  // Normalize stats
@@ -1869,9 +2082,13 @@ class index_gt {
1869
2082
  * @param[in] query Contiguous range of scalars forming a vector view.
1870
2083
  * @param[in] wanted The upper bound for the number of results to return.
1871
2084
  * @param[in] config Configuration options for this specific operation.
2085
+ * @param[in] predicate Optional filtering predicate for `member_cref_t`.
1872
2086
  * @return Smart object referencing temporary memory. Valid until next `search()` or `add()`.
1873
2087
  */
1874
- search_result_t search(vector_view_t query, std::size_t wanted, search_config_t config = {}) const noexcept {
2088
+ template <typename predicate_at = dummy_predicate_t>
2089
+ search_result_t search( //
2090
+ vector_view_t query, std::size_t wanted, search_config_t config = {},
2091
+ predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
1875
2092
 
1876
2093
  context_t& context = contexts_[config.thread];
1877
2094
  top_candidates_t& top = context.top_candidates;
@@ -1886,7 +2103,7 @@ class index_gt {
1886
2103
  if (config.exact) {
1887
2104
  if (!top.reserve(wanted))
1888
2105
  return result.failed("Out of memory!");
1889
- search_exact_(query, wanted, context);
2106
+ search_exact_(query, wanted, context, std::forward<predicate_at>(predicate));
1890
2107
  } else {
1891
2108
  next_candidates_t& next = context.next_candidates;
1892
2109
  std::size_t expansion = (std::max)(config.expansion, wanted);
@@ -1897,7 +2114,7 @@ class index_gt {
1897
2114
 
1898
2115
  id_t closest_id = search_for_one_(entry_id_, query, max_level_, 0, context);
1899
2116
  // For bottom layer we need a more optimized procedure
1900
- if (!search_to_find_in_base_(closest_id, query, expansion, context))
2117
+ if (!search_to_find_in_base_(closest_id, query, expansion, context, std::forward<predicate_at>(predicate)))
1901
2118
  return result.failed("Out of memory!");
1902
2119
  }
1903
2120
 
@@ -1911,9 +2128,10 @@ class index_gt {
1911
2128
  return result;
1912
2129
  }
1913
2130
 
2131
+ template <typename predicate_at = dummy_predicate_t>
1914
2132
  search_result_t search_around( //
1915
2133
  id_t hint, vector_view_t query, std::size_t wanted, //
1916
- search_config_t config = {}) const noexcept {
2134
+ search_config_t config = {}, predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
1917
2135
 
1918
2136
  context_t& context = contexts_[config.thread];
1919
2137
  top_candidates_t& top = context.top_candidates;
@@ -1933,7 +2151,7 @@ class index_gt {
1933
2151
  result.measurements = context.measurements_count;
1934
2152
  result.cycles = context.iteration_cycles;
1935
2153
 
1936
- search_to_find_in_base_(hint, query, expansion, context);
2154
+ search_to_find_in_base_(hint, query, expansion, context, std::forward<predicate_at>(predicate));
1937
2155
  top.sort_ascending();
1938
2156
  top.shrink(wanted);
1939
2157
 
@@ -1944,6 +2162,10 @@ class index_gt {
1944
2162
  return result;
1945
2163
  }
1946
2164
 
2165
+ #pragma endregion
2166
+
2167
+ #pragma region Metadata
2168
+
1947
2169
  struct stats_t {
1948
2170
  std::size_t nodes;
1949
2171
  std::size_t edges;
@@ -1958,7 +2180,7 @@ class index_gt {
1958
2180
  node_t node = node_with_id_(i);
1959
2181
  std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
1960
2182
  std::size_t edges = 0;
1961
- for (level_t level = 0; level != node.level(); ++level)
2183
+ for (level_t level = 0; level <= node.level(); ++level)
1962
2184
  edges += neighbors_(node, level).size();
1963
2185
 
1964
2186
  result.allocated_bytes += node_bytes_(node);
@@ -2007,6 +2229,8 @@ class index_gt {
2007
2229
  return total;
2008
2230
  }
2009
2231
 
2232
+ std::size_t memory_usage_per_node(dim_t dim, level_t level) const noexcept { return node_bytes_(dim, level); }
2233
+
2010
2234
  void change_metric(metric_t const& m) noexcept {
2011
2235
  metric_ = m;
2012
2236
  for (std::size_t i = 0; i != limits_.threads(); ++i)
@@ -2021,8 +2245,8 @@ class index_gt {
2021
2245
  error_t error;
2022
2246
 
2023
2247
  explicit operator bool() const noexcept { return !error; }
2024
- serialization_result_t failed(char const* message) noexcept {
2025
- error = message;
2248
+ serialization_result_t failed(error_t message) noexcept {
2249
+ error = std::move(message);
2026
2250
  return std::move(*this);
2027
2251
  }
2028
2252
  };
@@ -2032,7 +2256,8 @@ class index_gt {
2032
2256
  * co-locating vectors and neighbors lists.
2033
2257
  * Available on Linux, MacOS, Windows.
2034
2258
  */
2035
- serialization_result_t save(char const* file_path) const noexcept {
2259
+ template <typename progress_at = dummy_progress_t>
2260
+ serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
2036
2261
 
2037
2262
  // Make sure we have right to write to that file
2038
2263
  serialization_result_t result;
@@ -2102,6 +2327,7 @@ class index_gt {
2102
2327
  write_chunk(node.vector(), node_vector_bytes);
2103
2328
  if (result.error)
2104
2329
  return result;
2330
+ progress(i, state.size);
2105
2331
  }
2106
2332
 
2107
2333
  std::fclose(file);
@@ -2113,7 +2339,8 @@ class index_gt {
2113
2339
  * copying both vectors and neighbors lists into RAM.
2114
2340
  * Available on Linux, MacOS, Windows.
2115
2341
  */
2116
- serialization_result_t load(char const* file_path) noexcept {
2342
+ template <typename progress_at = dummy_progress_t>
2343
+ serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
2117
2344
 
2118
2345
  serialization_result_t result;
2119
2346
  file_header_t state_buffer{};
@@ -2161,7 +2388,8 @@ class index_gt {
2161
2388
  }
2162
2389
 
2163
2390
  // Load nodes one by one
2164
- for (std::size_t i = 0; i != size_; ++i) {
2391
+ std::size_t const size = size_;
2392
+ for (std::size_t i = 0; i != size; ++i) {
2165
2393
  label_t label;
2166
2394
  dim_t dim;
2167
2395
  level_t level;
@@ -2176,11 +2404,15 @@ class index_gt {
2176
2404
  return result;
2177
2405
 
2178
2406
  std::size_t node_bytes = node_bytes_(dim, level);
2179
- node_t node = node_malloc_(label, {nullptr, dim}, level, true);
2407
+ node_t node = node_malloc_(dim, level);
2408
+ node.label(label);
2409
+ node.dim(dim);
2410
+ node.level(level);
2180
2411
  read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
2181
2412
  if (result.error)
2182
2413
  return result;
2183
2414
  nodes_[i] = node;
2415
+ progress(i, size);
2184
2416
  }
2185
2417
 
2186
2418
  std::fclose(file);
@@ -2193,7 +2425,8 @@ class index_gt {
2193
2425
  * @b without copying the vectors and neighbors lists into RAM.
2194
2426
  * Available on Linux, MacOS, but @b not on Windows.
2195
2427
  */
2196
- serialization_result_t view(char const* file_path) noexcept {
2428
+ template <typename progress_at = dummy_progress_t>
2429
+ serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
2197
2430
  serialization_result_t result;
2198
2431
 
2199
2432
  #if defined(USEARCH_DEFINED_WINDOWS)
@@ -2275,17 +2508,18 @@ class index_gt {
2275
2508
  }
2276
2509
 
2277
2510
  // Locate every node packed into file
2278
- std::size_t progress = sizeof(file_header_t);
2279
- for (std::size_t i = 0; i != size_; ++i) {
2280
- byte_t* tape = (byte_t*)(file + progress);
2511
+ std::size_t progress_bytes = sizeof(file_header_t);
2512
+ std::size_t const size = size_;
2513
+ for (std::size_t i = 0; i != size; ++i) {
2514
+ byte_t* tape = (byte_t*)(file + progress_bytes);
2281
2515
  dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
2282
2516
  level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
2283
2517
 
2284
2518
  std::size_t node_bytes = node_bytes_(dim, level);
2285
2519
  std::size_t node_vector_bytes = dim * sizeof(scalar_t);
2286
2520
  nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
2287
- progress += node_bytes;
2288
- max_level_ = (std::max)(max_level_, level);
2521
+ progress_bytes += node_bytes;
2522
+ progress(i, size);
2289
2523
  }
2290
2524
 
2291
2525
  return {};
@@ -2293,7 +2527,224 @@ class index_gt {
2293
2527
 
2294
2528
  #pragma endregion
2295
2529
 
2530
+ struct join_result_t {
2531
+ error_t error{};
2532
+ std::size_t intersection_size{};
2533
+ std::size_t engagements{};
2534
+ std::size_t cycles{};
2535
+ std::size_t measurements{};
2536
+
2537
+ explicit operator bool() const noexcept { return !error; }
2538
+ join_result_t failed(error_t message) noexcept {
2539
+ error = std::move(message);
2540
+ return std::move(*this);
2541
+ }
2542
+ };
2543
+
2544
+ /**
2545
+ * @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
2546
+ * to perform fast one-to-one matching between two large collections
2547
+ * of vectors, using approximate nearest neighbors search.
2548
+ */
2549
+ template < //
2550
+ typename first_to_second_at = dummy_label_to_label_mapping_t, //
2551
+ typename second_to_first_at = dummy_label_to_label_mapping_t, //
2552
+ typename executor_at = dummy_executor_t, //
2553
+ typename progress_at = dummy_progress_t //
2554
+ >
2555
+ static join_result_t join( //
2556
+ index_gt const& first, index_gt const& second, //
2557
+ join_config_t config = {}, //
2558
+ first_to_second_at&& first_to_second = first_to_second_at{}, //
2559
+ second_to_first_at&& second_to_first = second_to_first_at{}, //
2560
+ executor_at&& executor = executor_at{}, //
2561
+ progress_at&& progress = progress_at{}) noexcept {
2562
+
2563
+ if (second.size() < first.size())
2564
+ return join_small_and_big_( //
2565
+ second, first, //
2566
+ config, //
2567
+ std::forward<second_to_first_at>(second_to_first), //
2568
+ std::forward<first_to_second_at>(first_to_second), //
2569
+ std::forward<executor_at>(executor), //
2570
+ std::forward<progress_at>(progress));
2571
+ else
2572
+ return join_small_and_big_( //
2573
+ first, second, //
2574
+ config, //
2575
+ std::forward<first_to_second_at>(first_to_second), //
2576
+ std::forward<second_to_first_at>(second_to_first), //
2577
+ std::forward<executor_at>(executor), //
2578
+ std::forward<progress_at>(progress));
2579
+ }
2580
+
2296
2581
  private:
2582
+ template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
2583
+ static join_result_t join_small_and_big_( //
2584
+ index_gt const& men, index_gt const& women, //
2585
+ join_config_t config, //
2586
+ first_to_second_at&& man_to_woman, //
2587
+ second_to_first_at&& woman_to_man, //
2588
+ executor_at&& executor, //
2589
+ progress_at&& progress) noexcept {
2590
+
2591
+ join_result_t result;
2592
+
2593
+ // Sanity checks and argument validation:
2594
+ if (&men == &women)
2595
+ return result.failed("Can't join with itself, consider copying");
2596
+ if (config.max_proposals == 0)
2597
+ config.max_proposals = std::log(men.size()) + executor.size();
2598
+ config.max_proposals = (std::min)(men.size(), config.max_proposals);
2599
+
2600
+ using proposals_count_t = std::uint16_t;
2601
+ using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
2602
+ allocator_t& alloc = men.allocator_;
2603
+
2604
+ // Create an atomic queue, as a ring structure, from/to which
2605
+ // free men will be added/pulled.
2606
+ std::mutex free_men_mutex{};
2607
+ ring_gt<id_t, ids_allocator_t> free_men;
2608
+ free_men.resize(men.size());
2609
+ for (std::size_t i = 0; i != men.size(); ++i)
2610
+ free_men.push(static_cast<id_t>(i));
2611
+
2612
+ // We are gonna need some temporary memory.
2613
+ proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
2614
+ id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
2615
+ id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
2616
+ if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
2617
+ return result.failed("Can't temporary mappings");
2618
+
2619
+ id_t missing_id;
2620
+ std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
2621
+ std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
2622
+ std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
2623
+ std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
2624
+
2625
+ // Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
2626
+ visits_bitset_t men_locks, women_locks;
2627
+ if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
2628
+ return result.failed("Can't allocate locks");
2629
+
2630
+ // Accumulate statistics from all the contexts,
2631
+ // to have a baseline to compare with, by the time the `join` is finished.
2632
+ std::size_t old_measurements{};
2633
+ std::size_t old_cycles{};
2634
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2635
+ old_measurements += women.contexts_[thread_idx].measurements_count;
2636
+ old_cycles += women.contexts_[thread_idx].iteration_cycles;
2637
+ }
2638
+ std::atomic<std::size_t> rounds{0};
2639
+ std::atomic<std::size_t> engagements{0};
2640
+
2641
+ // Concurrently process all the men
2642
+ executor.execute_bulk([&](std::size_t thread_idx) {
2643
+ context_t& context = women.contexts_[thread_idx];
2644
+ search_config_t search_config;
2645
+ search_config.expansion = config.expansion;
2646
+ search_config.exact = config.exact;
2647
+ search_config.thread = thread_idx;
2648
+ id_t free_man_id;
2649
+
2650
+ // While there exist a free man who still has a woman to propose to.
2651
+ while (true) {
2652
+ std::size_t passed_rounds = 0;
2653
+ std::size_t total_rounds = 0;
2654
+ {
2655
+ std::unique_lock<std::mutex> pop_lock(free_men_mutex);
2656
+ if (!free_men.try_pop(free_man_id))
2657
+ // Primary exit path, we have exhausted the list of candidates
2658
+ break;
2659
+ passed_rounds = ++rounds;
2660
+ total_rounds = passed_rounds + free_men.size();
2661
+ }
2662
+ progress(passed_rounds, total_rounds);
2663
+ while (men_locks.atomic_set(free_man_id))
2664
+ ;
2665
+
2666
+ node_t free_man = men.node_with_id_(free_man_id);
2667
+ proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
2668
+ if (free_man_proposals >= config.max_proposals)
2669
+ continue;
2670
+
2671
+ // Find the closest woman, to whom this man hasn't proposed yet.
2672
+ free_man_proposals++;
2673
+ search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
2674
+ if (!candidates) {
2675
+ // TODO:
2676
+ }
2677
+
2678
+ match_t match = candidates.back();
2679
+ member_cref_t woman = match.member;
2680
+ while (women_locks.atomic_set(woman.id))
2681
+ ;
2682
+
2683
+ id_t husband_id = woman_to_man_ids[woman.id];
2684
+ bool woman_is_free = husband_id == missing_id;
2685
+ if (woman_is_free) {
2686
+ // Engagement
2687
+ man_to_woman_ids[free_man_id] = woman.id;
2688
+ woman_to_man_ids[woman.id] = free_man_id;
2689
+ engagements++;
2690
+ } else {
2691
+ distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
2692
+ distance_t distance_from_candidate = match.distance;
2693
+ if (distance_from_husband > distance_from_candidate) {
2694
+ // Break-up
2695
+ while (men_locks.atomic_set(husband_id))
2696
+ ;
2697
+ man_to_woman_ids[husband_id] = missing_id;
2698
+ men_locks.atomic_reset(husband_id);
2699
+
2700
+ // New Engagement
2701
+ man_to_woman_ids[free_man_id] = woman.id;
2702
+ woman_to_man_ids[woman.id] = free_man_id;
2703
+ engagements++;
2704
+
2705
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2706
+ free_men.push(husband_id);
2707
+ } else {
2708
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2709
+ free_men.push(free_man_id);
2710
+ }
2711
+ }
2712
+
2713
+ men_locks.atomic_reset(free_man_id);
2714
+ women_locks.atomic_reset(woman.id);
2715
+ }
2716
+ });
2717
+
2718
+ // Export the IDs into labels:
2719
+ std::size_t intersection_size = 0;
2720
+ for (std::size_t i = 0; i != men.size(); ++i) {
2721
+ id_t woman_id = man_to_woman_ids[i];
2722
+ if (woman_id != missing_id) {
2723
+ label_t man = men.node_with_id_(i).label();
2724
+ label_t woman = women.node_with_id_(woman_id).label();
2725
+ man_to_woman[man] = woman;
2726
+ woman_to_man[woman] = man;
2727
+ intersection_size++;
2728
+ }
2729
+ }
2730
+
2731
+ // Deallocate memory
2732
+ alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
2733
+ alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
2734
+ alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
2735
+
2736
+ // Export stats
2737
+ result.engagements = engagements;
2738
+ result.intersection_size = intersection_size;
2739
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2740
+ result.measurements += women.contexts_[thread_idx].measurements_count;
2741
+ result.cycles += women.contexts_[thread_idx].iteration_cycles;
2742
+ }
2743
+ result.measurements -= old_measurements;
2744
+ result.cycles -= old_cycles;
2745
+ return result;
2746
+ }
2747
+
2297
2748
  void reset_view_() noexcept {
2298
2749
  if (!viewed_file_)
2299
2750
  return;
@@ -2323,51 +2774,80 @@ class index_gt {
2323
2774
  return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
2324
2775
  }
2325
2776
 
2326
- inline bool node_stored_(node_t node) const noexcept {
2777
+ using span_bytes_t = span_gt<byte_t>;
2778
+ struct node_bytes_split_t {
2779
+ span_bytes_t tape{};
2780
+ span_bytes_t vector{};
2781
+
2782
+ node_bytes_split_t() {}
2783
+ node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
2784
+
2785
+ std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
2786
+ bool colocated() const noexcept { return tape.end() == vector.begin(); }
2787
+ operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
2788
+ };
2789
+
2790
+ inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
2327
2791
  std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
2328
- return (node.tape() + node_head_bytes_() + levels_bytes) == (byte_t*)node.vector();
2792
+ std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
2793
+ return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
2329
2794
  }
2330
2795
 
2331
2796
  inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
2332
2797
  inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
2333
2798
 
2334
- void node_free_(std::size_t id) noexcept {
2335
-
2336
- if (viewed_file_)
2337
- return;
2338
-
2339
- node_t& node = nodes_[id];
2340
- std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
2341
- point_allocator_.deallocate(node.tape(), node_bytes);
2342
- node = node_t{};
2343
- }
2344
-
2345
- node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2799
+ node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
2346
2800
 
2347
- std::size_t dim = vector.size();
2348
- std::size_t stored_vector_bytes = node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(store_vector);
2349
- std::size_t node_bytes = node_bytes_(static_cast<dim_t>(dim), level) -
2350
- node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
2801
+ std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
2802
+ std::size_t node_bytes = node_bytes_(dims_to_store, level);
2803
+ std::size_t non_vector_bytes = node_bytes - vector_bytes;
2351
2804
 
2352
2805
  byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
2353
2806
  if (!data)
2354
- return {};
2355
-
2356
- std::memset(data, 0, node_bytes);
2357
- if (vector.data())
2358
- std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
2359
-
2360
- scalar_t* scalars = store_vector //
2361
- ? (scalar_t*)(data + node_bytes - stored_vector_bytes)
2362
- : (scalar_t*)(vector.data());
2807
+ return node_bytes_split_t{};
2808
+ return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
2809
+ }
2363
2810
 
2364
- node_t node{data, scalars};
2811
+ node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2812
+ node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
2813
+ if (store_vector) {
2814
+ std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
2815
+ std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
2816
+ } else {
2817
+ std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
2818
+ }
2819
+ node_t node = node_bytes;
2365
2820
  node.label(label);
2366
- node.dim(static_cast<dim_t>(dim));
2821
+ node.dim(static_cast<dim_t>(vector.size()));
2367
2822
  node.level(level);
2368
2823
  return node;
2369
2824
  }
2370
2825
 
2826
+ node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
2827
+ if (old_bytes.colocated()) {
2828
+ byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
2829
+ std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
2830
+ return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
2831
+ } else {
2832
+ node_t old_node = old_bytes;
2833
+ node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
2834
+ std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
2835
+ std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
2836
+ return node_bytes;
2837
+ }
2838
+ }
2839
+
2840
+ void node_free_(std::size_t id) noexcept {
2841
+
2842
+ if (viewed_file_)
2843
+ return;
2844
+
2845
+ node_t& node = nodes_[id];
2846
+ std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
2847
+ point_allocator_.deallocate(node.tape(), node_bytes);
2848
+ node = node_t{};
2849
+ }
2850
+
2371
2851
  inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
2372
2852
  inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
2373
2853
 
@@ -2396,7 +2876,6 @@ class index_gt {
2396
2876
 
2397
2877
  node_t new_node = node_with_id_(new_id);
2398
2878
  top_candidates_t& top = context.top_candidates;
2399
- std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
2400
2879
 
2401
2880
  // Outgoing links from `new_id`:
2402
2881
  neighbors_ref_t new_neighbors = neighbors_(new_node, level);
@@ -2411,7 +2890,17 @@ class index_gt {
2411
2890
  }
2412
2891
  }
2413
2892
 
2893
+ return new_neighbors[0];
2894
+ }
2895
+
2896
+ void reconnect_neighbor_nodes_(id_t new_id, level_t level, context_t& context) usearch_noexcept_m {
2897
+
2898
+ node_t new_node = node_with_id_(new_id);
2899
+ top_candidates_t& top = context.top_candidates;
2900
+ neighbors_ref_t new_neighbors = neighbors_(new_node, level);
2901
+
2414
2902
  // Reverse links from the neighbors:
2903
+ std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
2415
2904
  for (id_t close_id : new_neighbors) {
2416
2905
  node_t close_node = node_with_id_(close_id);
2417
2906
  node_lock_t close_lock = node_lock_(close_id);
@@ -2441,8 +2930,6 @@ class index_gt {
2441
2930
  for (std::size_t idx = 0; idx != top_view.size(); idx++)
2442
2931
  close_header.push_back(top_view[idx].id);
2443
2932
  }
2444
-
2445
- return new_neighbors[0];
2446
2933
  }
2447
2934
 
2448
2935
  level_t choose_random_level_(std::default_random_engine& level_generator) const noexcept {
@@ -2538,8 +3025,10 @@ class index_gt {
2538
3025
  * Doesn't lock any nodes, assuming read-only simultaneous access.
2539
3026
  * @return `true` if procedure succeeded, `false` if run out of memory.
2540
3027
  */
3028
+ template <typename predicate_at>
2541
3029
  bool search_to_find_in_base_( //
2542
- id_t start_id, vector_view_t query, std::size_t expansion, context_t& context) const noexcept {
3030
+ id_t start_id, vector_view_t query, std::size_t expansion, context_t& context,
3031
+ predicate_at&& predicate) const noexcept {
2543
3032
 
2544
3033
  visits_bitset_t& visits = context.visits;
2545
3034
  next_candidates_t& next = context.next_candidates; // pop min, push
@@ -2573,14 +3062,19 @@ class index_gt {
2573
3062
  continue;
2574
3063
 
2575
3064
  visits.set(successor_id);
2576
- distance_t successor_dist = context.measure(query, node_with_id_(successor_id));
3065
+ node_t successor = node_with_id_(successor_id);
3066
+ distance_t successor_dist = context.measure(query, successor);
2577
3067
 
2578
3068
  if (top.size() < top_limit || successor_dist < radius) {
2579
3069
  // This can substantially grow our priority queue:
2580
3070
  next.insert({-successor_dist, successor_id});
2581
- // This will automatically evict poor matches:
2582
- top.insert({successor_dist, successor_id}, top_limit);
2583
- radius = top.top().distance;
3071
+ if (predicate( //
3072
+ match_t{member_cref_t{successor.label(), successor.vector_view(), successor_id},
3073
+ successor_dist})) {
3074
+ // This will automatically evict poor matches:
3075
+ top.insert({successor_dist, successor_id}, top_limit);
3076
+ radius = top.top().distance;
3077
+ }
2584
3078
  }
2585
3079
  }
2586
3080
  }
@@ -2591,12 +3085,21 @@ class index_gt {
2591
3085
  /**
2592
3086
  * @brief Iterates through all managed vectors, without actually touching the index.
2593
3087
  */
2594
- void search_exact_(vector_view_t query, std::size_t count, context_t& context) const noexcept {
3088
+ template <typename predicate_at>
3089
+ void search_exact_( //
3090
+ vector_view_t query, std::size_t count, context_t& context, //
3091
+ predicate_at&& predicate) const noexcept {
3092
+
2595
3093
  top_candidates_t& top = context.top_candidates;
2596
3094
  top.clear();
2597
3095
  top.reserve(count);
2598
- for (std::size_t i = 0; i != size(); ++i)
2599
- top.insert({context.measure(query, node_with_id_(i)), static_cast<id_t>(i)}, count);
3096
+ for (std::size_t i = 0; i != size(); ++i) {
3097
+ id_t id = static_cast<id_t>(i);
3098
+ node_t node = node_with_id_(i);
3099
+ distance_t distance = context.measure(query, node);
3100
+ if (predicate(match_t{member_cref_t{node.label(), node.vector_view(), id}, distance}))
3101
+ top.insert(candidate_t{distance, id}, count);
3102
+ }
2600
3103
  }
2601
3104
 
2602
3105
  void prefetch_neighbors_(neighbors_ref_t, visits_bitset_t const&) const noexcept {}