usearch 0.20.0 → 0.22.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.
@@ -41,6 +41,13 @@
41
41
  #define USEARCH_DEFINED_ARM
42
42
  #endif
43
43
 
44
+ // Inferring hardware bitness: 32 vs 64
45
+ #if __WORDSIZE == 64
46
+ #define USEARCH_64BIT_ENV
47
+ #else
48
+ #define USEARCH_32BIT_ENV
49
+ #endif
50
+
44
51
  #if !defined(USEARCH_USE_OPENMP)
45
52
  #define USEARCH_USE_OPENMP 0
46
53
  #endif
@@ -173,19 +180,21 @@ inline std::size_t ceil2(std::size_t v) noexcept {
173
180
  v |= v >> 4;
174
181
  v |= v >> 8;
175
182
  v |= v >> 16;
183
+ #ifdef USEARCH_64BIT_ENV
176
184
  v |= v >> 32;
185
+ #endif
177
186
  v++;
178
187
  return v;
179
188
  }
180
189
 
181
190
  template <typename at> void misaligned_store(void* ptr, at v) noexcept {
182
- static_assert(!std::is_reference<at>::value);
191
+ static_assert(!std::is_reference<at>::value, "Can't store a reference");
183
192
  std::memcpy(ptr, &v, sizeof(at));
184
193
  }
185
194
 
186
195
  /// @brief Simply dereferencing misaligned pointers can be dangerous.
187
196
  template <typename at> at misaligned_load(void* ptr) noexcept {
188
- static_assert(!std::is_reference<at>::value);
197
+ static_assert(!std::is_reference<at>::value, "Can't load a reference");
189
198
  at v;
190
199
  std::memcpy(&v, ptr, sizeof(at));
191
200
  return v;
@@ -258,10 +267,13 @@ template <typename scalar_at> class span_gt {
258
267
  std::size_t size_;
259
268
 
260
269
  public:
261
- span_gt(scalar_at* begin, scalar_at const* end) noexcept : data_(begin), size_(end - begin) {}
270
+ span_gt() noexcept : data_(nullptr), size_(0u) {}
271
+ span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
262
272
  span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
263
273
  scalar_at* data() const noexcept { return data_; }
264
274
  std::size_t size() const noexcept { return size_; }
275
+ scalar_at* begin() const noexcept { return data_; }
276
+ scalar_at* end() const noexcept { return data_ + size_; }
265
277
  operator scalar_at*() const noexcept { return data(); }
266
278
  };
267
279
 
@@ -617,8 +629,8 @@ template <typename result_at> struct expected_gt {
617
629
  }
618
630
  result_at const& operator*() const noexcept { return result; }
619
631
  explicit operator bool() const noexcept { return !error; }
620
- expected_gt failed(char const* message) noexcept {
621
- error = message;
632
+ expected_gt failed(error_t message) noexcept {
633
+ error = std::move(message);
622
634
  return std::move(*this);
623
635
  }
624
636
  };
@@ -981,12 +993,19 @@ class sorted_buffer_gt {
981
993
 
982
994
  /**
983
995
  * @brief Five-byte integer type to address node clouds with over 4B entries.
996
+ *
997
+ * @note Avoid usage in 32bit environment
984
998
  */
985
999
  class usearch_pack_m uint40_t {
986
1000
  unsigned char octets[5];
987
1001
 
1002
+ inline uint40_t& broadcast(unsigned char c) {
1003
+ std::memset(octets, c, 5);
1004
+ return *this;
1005
+ }
1006
+
988
1007
  public:
989
- inline uint40_t() noexcept { std::memset(octets, 0, 5); }
1008
+ inline uint40_t() noexcept { broadcast(0); }
990
1009
  inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets, (char*)&n, 4); }
991
1010
  inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
992
1011
  #if defined(USEARCH_DEFINED_CLANG) && defined(USEARCH_DEFINED_APPLE)
@@ -1007,7 +1026,7 @@ class usearch_pack_m uint40_t {
1007
1026
 
1008
1027
  inline uint40_t& operator+=(std::size_t n) noexcept {
1009
1028
  unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
1010
- std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(&n);
1029
+ std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
1011
1030
  std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1012
1031
  octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
1013
1032
  tail += n_tail;
@@ -1023,7 +1042,11 @@ class usearch_pack_m uint40_t {
1023
1042
 
1024
1043
  inline operator std::size_t() const noexcept {
1025
1044
  std::size_t result = 0;
1026
- std::memcpy((char*)&result, octets, 5);
1045
+ #ifdef USEARCH_64BIT_ENV
1046
+ std::memcpy((char*)&result + 3, octets, 5);
1047
+ #else
1048
+ std::memcpy((char*)&result, octets + 1, 4);
1049
+ #endif
1027
1050
  return result;
1028
1051
  }
1029
1052
 
@@ -1034,6 +1057,9 @@ class usearch_pack_m uint40_t {
1034
1057
  *this += 1u;
1035
1058
  return old;
1036
1059
  }
1060
+
1061
+ inline static uint40_t max() noexcept { return uint40_t{}.broadcast(0xFF); }
1062
+ inline static uint40_t min() noexcept { return uint40_t{}.broadcast(0); }
1037
1063
  };
1038
1064
 
1039
1065
  #if defined(USEARCH_DEFINED_WINDOWS)
@@ -1042,6 +1068,123 @@ class usearch_pack_m uint40_t {
1042
1068
 
1043
1069
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
1044
1070
 
1071
+ template <typename element_at, typename allocator_at = std::allocator<element_at>> //
1072
+ class ring_gt {
1073
+ public:
1074
+ using element_t = element_at;
1075
+ using allocator_t = allocator_at;
1076
+
1077
+ static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
1078
+ static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
1079
+
1080
+ using value_type = element_t;
1081
+
1082
+ private:
1083
+ element_t* elements_{};
1084
+ std::size_t capacity_{};
1085
+ std::size_t head_{};
1086
+ std::size_t tail_{};
1087
+ bool empty_{true};
1088
+ allocator_t allocator_{};
1089
+
1090
+ public:
1091
+ explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept : allocator_(alloc) {}
1092
+
1093
+ ring_gt(ring_gt const&) = delete;
1094
+ ring_gt& operator=(ring_gt const&) = delete;
1095
+
1096
+ ring_gt(ring_gt&& other) noexcept { swap(other); }
1097
+ ring_gt& operator=(ring_gt&& other) noexcept {
1098
+ swap(other);
1099
+ return *this;
1100
+ }
1101
+
1102
+ void swap(ring_gt& other) noexcept {
1103
+ std::swap(elements_, other.elements_);
1104
+ std::swap(capacity_, other.capacity_);
1105
+ std::swap(head_, other.head_);
1106
+ std::swap(tail_, other.tail_);
1107
+ std::swap(empty_, other.empty_);
1108
+ std::swap(allocator_, other.allocator_);
1109
+ }
1110
+
1111
+ ~ring_gt() noexcept { reset(); }
1112
+
1113
+ bool empty() const noexcept { return empty_; }
1114
+ size_t capacity() const noexcept { return capacity_; }
1115
+ size_t size() const noexcept {
1116
+ if (empty_)
1117
+ return 0;
1118
+ else if (head_ >= tail_)
1119
+ return head_ - tail_;
1120
+ else
1121
+ return capacity_ - (tail_ - head_);
1122
+ }
1123
+
1124
+ void clear() noexcept {
1125
+ head_ = 0;
1126
+ tail_ = 0;
1127
+ empty_ = true;
1128
+ }
1129
+
1130
+ void reset() noexcept {
1131
+ if (elements_)
1132
+ allocator_.deallocate(elements_, capacity_);
1133
+ elements_ = nullptr;
1134
+ capacity_ = 0;
1135
+ head_ = 0;
1136
+ tail_ = 0;
1137
+ empty_ = true;
1138
+ }
1139
+
1140
+ bool reserve(std::size_t n) noexcept {
1141
+ if (n < size())
1142
+ return false; // prevent data loss
1143
+ n = (std::max<std::size_t>)(n, 64u);
1144
+ element_t* elements = allocator_.allocate(n);
1145
+ if (!elements)
1146
+ return false;
1147
+
1148
+ std::size_t i = 0;
1149
+ while (try_pop(elements[i]))
1150
+ i++;
1151
+
1152
+ reset();
1153
+ elements_ = elements;
1154
+ capacity_ = n;
1155
+ head_ = i;
1156
+ tail_ = 0;
1157
+ empty_ = (i == 0);
1158
+ return true;
1159
+ }
1160
+
1161
+ void push(element_t const& value) noexcept {
1162
+ elements_[head_] = value;
1163
+ head_ = (head_ + 1) % capacity_;
1164
+ empty_ = false;
1165
+ }
1166
+
1167
+ bool try_push(element_t const& value) noexcept {
1168
+ if (head_ == tail_ && !empty_)
1169
+ return false; // elements_ is full
1170
+
1171
+ return push(value);
1172
+ return true;
1173
+ }
1174
+
1175
+ bool try_pop(element_t& value) noexcept {
1176
+ if (empty_)
1177
+ return false;
1178
+
1179
+ value = std::move(elements_[tail_]);
1180
+ tail_ = (tail_ + 1) % capacity_;
1181
+ empty_ = head_ == tail_;
1182
+ return true;
1183
+ }
1184
+
1185
+ element_t const& operator[](std::size_t i) const noexcept { return elements_[(tail_ + i) % capacity_]; }
1186
+ };
1187
+
1045
1188
  /// @brief Number of neighbors per graph node.
1046
1189
  /// Defaults to 32 in FAISS and 16 in hnswlib.
1047
1190
  /// > It is called `M` in the paper.
@@ -1087,11 +1230,11 @@ struct index_config_t {
1087
1230
  };
1088
1231
 
1089
1232
  struct index_limits_t {
1090
- std::size_t elements = 0;
1233
+ std::size_t members = 0;
1091
1234
  std::size_t threads_add = std::thread::hardware_concurrency();
1092
1235
  std::size_t threads_search = std::thread::hardware_concurrency();
1093
1236
 
1094
- index_limits_t(std::size_t n, std::size_t t) noexcept : elements(n), threads_add(t), threads_search(t) {}
1237
+ index_limits_t(std::size_t n, std::size_t t) noexcept : members(n), threads_add(t), threads_search(t) {}
1095
1238
  index_limits_t(std::size_t n = 0) noexcept : index_limits_t(n, std::thread::hardware_concurrency()) {}
1096
1239
  std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
1097
1240
  std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
@@ -1123,6 +1266,21 @@ struct search_config_t {
1123
1266
  bool exact = false;
1124
1267
  };
1125
1268
 
1269
+ struct copy_config_t {
1270
+ bool copy_vectors = true;
1271
+ };
1272
+
1273
+ struct join_config_t {
1274
+ /// @brief Controls maximum number of proposals per man during stable marriage.
1275
+ std::size_t max_proposals = 0;
1276
+ /// @brief Hyper-parameter controlling the quality of search.
1277
+ /// Defaults to 16 in FAISS and 10 in hnswlib.
1278
+ /// > It is called `ef` in the paper.
1279
+ std::size_t expansion = default_expansion_search();
1280
+ /// @brief Brute-forces exhaustive search over all entries in the index.
1281
+ bool exact = false;
1282
+ };
1283
+
1126
1284
  using file_header_t = byte_t[64];
1127
1285
 
1128
1286
  /**
@@ -1227,8 +1385,8 @@ struct file_head_result_t {
1227
1385
  error_t error;
1228
1386
 
1229
1387
  explicit operator bool() const noexcept { return !error; }
1230
- file_head_result_t failed(char const* message) noexcept {
1231
- error = message;
1388
+ file_head_result_t failed(error_t message) noexcept {
1389
+ error = std::move(message);
1232
1390
  return std::move(*this);
1233
1391
  }
1234
1392
  };
@@ -1262,6 +1420,37 @@ struct viewed_file_t {
1262
1420
  };
1263
1421
  #endif
1264
1422
 
1423
+ struct dummy_predicate_t {
1424
+ template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
1425
+ };
1426
+
1427
+ struct dummy_progress_t {
1428
+ inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
1429
+ };
1430
+
1431
+ struct dummy_executor_t {
1432
+ dummy_executor_t() noexcept {}
1433
+ std::size_t size() const noexcept { return 1; }
1434
+
1435
+ template <typename thread_aware_function_at>
1436
+ void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
1437
+ for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
1438
+ thread_aware_function(0, task_idx);
1439
+ }
1440
+
1441
+ template <typename thread_aware_function_at>
1442
+ void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
1443
+ thread_aware_function(0);
1444
+ }
1445
+ };
1446
+
1447
+ struct dummy_label_to_label_mapping_t {
1448
+ struct member_ref_t {
1449
+ template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
1450
+ };
1451
+ template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
1452
+ };
1453
+
1265
1454
  /**
1266
1455
  * @brief Approximate Nearest Neighbors Search index using the
1267
1456
  * Hierarchical Navigable Small World graph algorithm.
@@ -1325,7 +1514,7 @@ struct viewed_file_t {
1325
1514
  *
1326
1515
  */
1327
1516
  template <typename metric_at = ip_gt<float>, //
1328
- typename label_at = std::size_t, //
1517
+ typename label_at = std::int64_t, //
1329
1518
  typename id_at = std::uint32_t, //
1330
1519
  typename allocator_at = std::allocator<char>, //
1331
1520
  typename point_allocator_at = allocator_at> //
@@ -1342,16 +1531,18 @@ class index_gt {
1342
1531
  using vector_view_t = span_gt<scalar_t const>;
1343
1532
  using distance_t = return_type_gt<metric_t, vector_view_t, vector_view_t>;
1344
1533
 
1345
- struct member_ref_t {
1346
- misaligned_ref_gt<label_t> label;
1534
+ struct member_cref_t {
1535
+ label_t label;
1347
1536
  vector_view_t vector;
1348
1537
  id_t id;
1349
1538
  };
1350
1539
 
1351
- struct member_cref_t {
1352
- label_t label;
1540
+ struct member_ref_t {
1541
+ misaligned_ref_gt<label_t> label;
1353
1542
  vector_view_t vector;
1354
1543
  id_t id;
1544
+
1545
+ inline operator member_cref_t() const noexcept { return {label, vector, id}; }
1355
1546
  };
1356
1547
 
1357
1548
  template <typename ref_at, typename index_at> class member_iterator_gt {
@@ -1552,7 +1743,7 @@ class index_gt {
1552
1743
  index_config_t config_{};
1553
1744
  index_limits_t limits_{};
1554
1745
  metric_t metric_{};
1555
- allocator_t allocator_{};
1746
+ mutable allocator_t allocator_{};
1556
1747
  point_allocator_t point_allocator_{};
1557
1748
  precomputed_constants_t pre_{};
1558
1749
  viewed_file_t viewed_file_{};
@@ -1606,6 +1797,35 @@ class index_gt {
1606
1797
  return *this;
1607
1798
  }
1608
1799
 
1800
+ struct copy_result_t {
1801
+ error_t error;
1802
+ index_gt index;
1803
+
1804
+ explicit operator bool() const noexcept { return !error; }
1805
+ copy_result_t failed(error_t message) noexcept {
1806
+ error = std::move(message);
1807
+ return std::move(*this);
1808
+ }
1809
+ };
1810
+
1811
+ copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
1812
+ copy_result_t result;
1813
+ index_gt& other = result.index;
1814
+ other = index_gt(config_, metric_, allocator_, point_allocator_);
1815
+ if (!other.reserve(limits_))
1816
+ return result.failed("Failed to reserve the contexts");
1817
+
1818
+ // Now all is left - is to allocate new `node_t` instances and populate
1819
+ // the `other.nodes_` array into it.
1820
+ for (std::size_t i = 0; i != size_; ++i)
1821
+ other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
1822
+
1823
+ other.size_ = size_.load();
1824
+ other.max_level_ = max_level_;
1825
+ other.entry_id_ = entry_id_;
1826
+ return result;
1827
+ }
1828
+
1609
1829
  member_citerator_t cbegin() const noexcept { return {this, 0}; }
1610
1830
  member_citerator_t cend() const noexcept { return {this, size()}; }
1611
1831
  member_citerator_t begin() const noexcept { return {this, 0}; }
@@ -1613,6 +1833,9 @@ class index_gt {
1613
1833
  member_iterator_t begin() noexcept { return {this, 0}; }
1614
1834
  member_iterator_t end() noexcept { return {this, size()}; }
1615
1835
 
1836
+ member_cref_t at(std::size_t i) const noexcept { return member_cref_t(node_with_id_(i)); }
1837
+ member_ref_t at(std::size_t i) noexcept { return member_ref_t(node_with_id_(i)); }
1838
+
1616
1839
  #pragma region Adjusting Configuration
1617
1840
 
1618
1841
  /**
@@ -1638,7 +1861,7 @@ class index_gt {
1638
1861
  clear();
1639
1862
 
1640
1863
  if (nodes_)
1641
- node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.elements);
1864
+ node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.members);
1642
1865
  if (contexts_) {
1643
1866
  for (std::size_t i = 0; i != limits_.threads(); ++i)
1644
1867
  contexts_[i].~context_t();
@@ -1657,11 +1880,13 @@ class index_gt {
1657
1880
  std::swap(limits_, other.limits_);
1658
1881
  std::swap(metric_, other.metric_);
1659
1882
  std::swap(allocator_, other.allocator_);
1883
+ std::swap(point_allocator_, other.point_allocator_);
1660
1884
  std::swap(pre_, other.pre_);
1661
1885
  std::swap(viewed_file_, other.viewed_file_);
1662
1886
  std::swap(max_level_, other.max_level_);
1663
1887
  std::swap(entry_id_, other.entry_id_);
1664
1888
  std::swap(nodes_, other.nodes_);
1889
+ std::swap(nodes_mutexes_, other.nodes_mutexes_);
1665
1890
  std::swap(contexts_, other.contexts_);
1666
1891
 
1667
1892
  // Non-atomic parts.
@@ -1679,13 +1904,16 @@ class index_gt {
1679
1904
  */
1680
1905
  bool reserve(index_limits_t limits) usearch_noexcept_m {
1681
1906
 
1682
- usearch_assert_m(limits.elements >= size_, "Can't drop existing values");
1907
+ if (limits.threads_add <= limits_.threads_add //
1908
+ && limits.threads_search <= limits_.threads_search //
1909
+ && limits.members <= limits_.members)
1910
+ return true;
1683
1911
 
1684
- if (!nodes_mutexes_.resize(limits.elements))
1912
+ if (!nodes_mutexes_.resize(limits.members))
1685
1913
  return false;
1686
1914
 
1687
1915
  node_allocator_t node_allocator;
1688
- node_t* new_nodes = node_allocator.allocate(limits.elements);
1916
+ node_t* new_nodes = node_allocator.allocate(limits.members);
1689
1917
  if (!new_nodes)
1690
1918
  return false;
1691
1919
 
@@ -1693,17 +1921,17 @@ class index_gt {
1693
1921
  context_allocator_t context_allocator;
1694
1922
  context_t* new_contexts = context_allocator.allocate(limits_threads);
1695
1923
  if (!new_contexts) {
1696
- node_allocator.deallocate(new_nodes, limits.elements);
1924
+ node_allocator.deallocate(new_nodes, limits.members);
1697
1925
  return false;
1698
1926
  }
1699
1927
  for (std::size_t i = 0; i != limits_threads; ++i) {
1700
1928
  context_t& context = new_contexts[i];
1701
1929
  new (&context) context_t();
1702
1930
  context.metric = metric_;
1703
- if (!context.visits.resize(limits.elements)) {
1931
+ if (!context.visits.resize(limits.members)) {
1704
1932
  for (std::size_t j = 0; j != i; ++j)
1705
1933
  context.visits.reset();
1706
- node_allocator.deallocate(new_nodes, limits.elements);
1934
+ node_allocator.deallocate(new_nodes, limits.members);
1707
1935
  context_allocator.deallocate(new_contexts, limits_threads);
1708
1936
  return false;
1709
1937
  }
@@ -1723,13 +1951,12 @@ class index_gt {
1723
1951
 
1724
1952
  // Move the nodes info, and deallocate previous buffers.
1725
1953
  if (nodes_)
1726
- std::memcpy(new_nodes, nodes_, sizeof(node_t) * size()),
1727
- node_allocator.deallocate(nodes_, limits_.elements);
1954
+ std::memcpy(new_nodes, nodes_, sizeof(node_t) * size()), node_allocator.deallocate(nodes_, limits_.members);
1728
1955
  if (contexts_)
1729
1956
  context_allocator.deallocate(contexts_, limits_.threads());
1730
1957
 
1731
1958
  limits_ = limits;
1732
- capacity_ = limits.elements;
1959
+ capacity_ = limits.members;
1733
1960
  nodes_ = new_nodes;
1734
1961
  contexts_ = new_contexts;
1735
1962
  return true;
@@ -1760,8 +1987,8 @@ class index_gt {
1760
1987
  id_t id{};
1761
1988
 
1762
1989
  explicit operator bool() const noexcept { return !error; }
1763
- add_result_t failed(char const* message) noexcept {
1764
- error = message;
1990
+ add_result_t failed(error_t message) noexcept {
1991
+ error = std::move(message);
1765
1992
  return std::move(*this);
1766
1993
  }
1767
1994
  };
@@ -1793,14 +2020,17 @@ class index_gt {
1793
2020
  inline search_result_t& operator=(search_result_t&&) = default;
1794
2021
 
1795
2022
  explicit operator bool() const noexcept { return !error; }
1796
- search_result_t failed(char const* message) noexcept {
1797
- error = message;
2023
+ search_result_t failed(error_t message) noexcept {
2024
+ error = std::move(message);
1798
2025
  return std::move(*this);
1799
2026
  }
1800
2027
 
1801
2028
  inline operator std::size_t() const noexcept { return count; }
1802
2029
  inline std::size_t size() const noexcept { return count; }
2030
+ inline bool empty() const noexcept { return !count; }
1803
2031
  inline match_t operator[](std::size_t i) const noexcept { return at(i); }
2032
+ inline match_t front() const noexcept { return at(0); }
2033
+ inline match_t back() const noexcept { return at(count - 1); }
1804
2034
  inline bool contains(label_t label) const noexcept {
1805
2035
  for (std::size_t i = 0; i != count; ++i)
1806
2036
  if (at(i).member.label == label)
@@ -1866,7 +2096,7 @@ class index_gt {
1866
2096
  new_level_lock.unlock();
1867
2097
 
1868
2098
  // Allocate the neighbors
1869
- node_t node = node_malloc_(label, vector, target_level, config.store_vector);
2099
+ node_t node = node_make_(label, vector, target_level, config.store_vector);
1870
2100
  if (!node)
1871
2101
  return result.failed("Out of memory!");
1872
2102
  std::size_t old_size = size_.fetch_add(1);
@@ -1887,16 +2117,7 @@ class index_gt {
1887
2117
  result.measurements = context.measurements_count;
1888
2118
  result.cycles = context.iteration_cycles;
1889
2119
 
1890
- // Go down the level, tracking only the closest match
1891
- id_t closest_id = search_for_one_(entry_id_copy, vector, max_level_copy, target_level, context);
1892
-
1893
- // From `target_level` down perform proper extensive search
1894
- for (level_t level = (std::min)(target_level, max_level_copy); level >= 0; --level) {
1895
- // TODO: Handle out of memory conditions
1896
- search_to_insert_(closest_id, vector, level, config.expansion, context);
1897
- closest_id = connect_new_node_(new_id, level, context);
1898
- reconnect_neighbor_nodes_(new_id, level, context);
1899
- }
2120
+ connect_node_across_levels_(new_id, vector, entry_id_copy, max_level_copy, target_level, config, context);
1900
2121
 
1901
2122
  // Normalize stats
1902
2123
  result.measurements = context.measurements_count - result.measurements;
@@ -1910,9 +2131,50 @@ class index_gt {
1910
2131
  return result;
1911
2132
  }
1912
2133
 
1913
- struct dummy_predicate_t {
1914
- constexpr bool operator()(match_t const&) const noexcept { return true; }
1915
- };
2134
+ /**
2135
+ * @brief Update an existing entry, replacing a vector and a label. Thread-safe.
2136
+ *
2137
+ * @param[in] old_id Existing internal identifier for a node to be replaced.
2138
+ * @param[in] label External identifier/name/descriptor for the vector.
2139
+ * @param[in] vector Contiguous range of scalars forming a vector view.
2140
+ * @param[in] config Configuration options for this specific operation.
2141
+ */
2142
+ add_result_t update(id_t old_id, label_t label, vector_view_t vector, add_config_t config = {}) usearch_noexcept_m {
2143
+
2144
+ usearch_assert_m(!is_immutable(), "Can't add to an immutable index");
2145
+ add_result_t result;
2146
+
2147
+ // Make sure we have enough local memory to perform this request
2148
+ context_t& context = contexts_[config.thread];
2149
+ top_candidates_t& top = context.top_candidates;
2150
+ next_candidates_t& next = context.next_candidates;
2151
+ top.clear();
2152
+ next.clear();
2153
+
2154
+ // The top list needs one more slot than the connectivity of the base level
2155
+ // for the heuristic, that tries to squeeze one more element into saturated list.
2156
+ std::size_t top_limit = (std::max)(base_level_multiple_() * config_.connectivity + 1, config.expansion);
2157
+ if (!top.reserve(top_limit))
2158
+ return result.failed("Out of memory!");
2159
+ if (!next.reserve(config.expansion))
2160
+ return result.failed("Out of memory!");
2161
+
2162
+ node_lock_t new_lock = node_lock_(old_id);
2163
+ node_t node = node_with_id_(old_id);
2164
+
2165
+ // Pull stats
2166
+ result.measurements = context.measurements_count;
2167
+ result.cycles = context.iteration_cycles;
2168
+
2169
+ connect_node_across_levels_(old_id, vector, entry_id_, max_level_, node.level(), config, context);
2170
+ node.label(label);
2171
+
2172
+ // Normalize stats
2173
+ result.measurements = context.measurements_count - result.measurements;
2174
+ result.cycles = context.iteration_cycles - result.cycles;
2175
+
2176
+ return result;
2177
+ }
1916
2178
 
1917
2179
  /**
1918
2180
  * @brief Searches for the closest elements to the given ::query. Thread-safe.
@@ -1966,40 +2228,6 @@ class index_gt {
1966
2228
  return result;
1967
2229
  }
1968
2230
 
1969
- template <typename predicate_at = dummy_predicate_t>
1970
- search_result_t search_around( //
1971
- id_t hint, vector_view_t query, std::size_t wanted, //
1972
- search_config_t config = {}, predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
1973
-
1974
- context_t& context = contexts_[config.thread];
1975
- top_candidates_t& top = context.top_candidates;
1976
- next_candidates_t& next = context.next_candidates;
1977
- search_result_t result{*this, top};
1978
-
1979
- if (!size_)
1980
- return result;
1981
-
1982
- std::size_t expansion = (std::max)(config.expansion, wanted);
1983
- if (!next.reserve(expansion))
1984
- return result.failed("Out of memory!");
1985
- if (!top.reserve(expansion))
1986
- return result.failed("Out of memory!");
1987
-
1988
- // Go down the level, tracking only the closest match
1989
- result.measurements = context.measurements_count;
1990
- result.cycles = context.iteration_cycles;
1991
-
1992
- search_to_find_in_base_(hint, query, expansion, context, std::forward<predicate_at>(predicate));
1993
- top.sort_ascending();
1994
- top.shrink(wanted);
1995
-
1996
- // Normalize stats
1997
- result.measurements = context.measurements_count - result.measurements;
1998
- result.cycles = context.iteration_cycles - result.cycles;
1999
- result.count = top.size();
2000
- return result;
2001
- }
2002
-
2003
2231
  #pragma endregion
2004
2232
 
2005
2233
  #pragma region Metadata
@@ -2060,7 +2288,7 @@ class index_gt {
2060
2288
  }
2061
2289
 
2062
2290
  // Temporary data-structures, proportional to the number of nodes:
2063
- total += limits_.elements * sizeof(node_t) + allocator_entry_bytes;
2291
+ total += limits_.members * sizeof(node_t) + allocator_entry_bytes;
2064
2292
 
2065
2293
  // Temporary data-structures, proportional to the number of threads:
2066
2294
  total += limits_.threads() * sizeof(context_t) + allocator_entry_bytes * 3;
@@ -2083,8 +2311,8 @@ class index_gt {
2083
2311
  error_t error;
2084
2312
 
2085
2313
  explicit operator bool() const noexcept { return !error; }
2086
- serialization_result_t failed(char const* message) noexcept {
2087
- error = message;
2314
+ serialization_result_t failed(error_t message) noexcept {
2315
+ error = std::move(message);
2088
2316
  return std::move(*this);
2089
2317
  }
2090
2318
  };
@@ -2094,7 +2322,8 @@ class index_gt {
2094
2322
  * co-locating vectors and neighbors lists.
2095
2323
  * Available on Linux, MacOS, Windows.
2096
2324
  */
2097
- serialization_result_t save(char const* file_path) const noexcept {
2325
+ template <typename progress_at = dummy_progress_t>
2326
+ serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
2098
2327
 
2099
2328
  // Make sure we have right to write to that file
2100
2329
  serialization_result_t result;
@@ -2164,6 +2393,7 @@ class index_gt {
2164
2393
  write_chunk(node.vector(), node_vector_bytes);
2165
2394
  if (result.error)
2166
2395
  return result;
2396
+ progress(i, state.size);
2167
2397
  }
2168
2398
 
2169
2399
  std::fclose(file);
@@ -2175,7 +2405,8 @@ class index_gt {
2175
2405
  * copying both vectors and neighbors lists into RAM.
2176
2406
  * Available on Linux, MacOS, Windows.
2177
2407
  */
2178
- serialization_result_t load(char const* file_path) noexcept {
2408
+ template <typename progress_at = dummy_progress_t>
2409
+ serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
2179
2410
 
2180
2411
  serialization_result_t result;
2181
2412
  file_header_t state_buffer{};
@@ -2212,7 +2443,7 @@ class index_gt {
2212
2443
  pre_ = precompute_(config_);
2213
2444
 
2214
2445
  index_limits_t limits;
2215
- limits.elements = state.size;
2446
+ limits.members = state.size;
2216
2447
  if (!reserve(limits)) {
2217
2448
  std::fclose(file);
2218
2449
  return result.failed("Out of memory");
@@ -2223,7 +2454,8 @@ class index_gt {
2223
2454
  }
2224
2455
 
2225
2456
  // Load nodes one by one
2226
- for (std::size_t i = 0; i != size_; ++i) {
2457
+ std::size_t const size = size_;
2458
+ for (std::size_t i = 0; i != size; ++i) {
2227
2459
  label_t label;
2228
2460
  dim_t dim;
2229
2461
  level_t level;
@@ -2238,11 +2470,15 @@ class index_gt {
2238
2470
  return result;
2239
2471
 
2240
2472
  std::size_t node_bytes = node_bytes_(dim, level);
2241
- node_t node = node_malloc_(label, {nullptr, dim}, level, true);
2473
+ node_t node = node_malloc_(dim, level);
2474
+ node.label(label);
2475
+ node.dim(dim);
2476
+ node.level(level);
2242
2477
  read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
2243
2478
  if (result.error)
2244
2479
  return result;
2245
2480
  nodes_[i] = node;
2481
+ progress(i, size);
2246
2482
  }
2247
2483
 
2248
2484
  std::fclose(file);
@@ -2255,7 +2491,8 @@ class index_gt {
2255
2491
  * @b without copying the vectors and neighbors lists into RAM.
2256
2492
  * Available on Linux, MacOS, but @b not on Windows.
2257
2493
  */
2258
- serialization_result_t view(char const* file_path) noexcept {
2494
+ template <typename progress_at = dummy_progress_t>
2495
+ serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
2259
2496
  serialization_result_t result;
2260
2497
 
2261
2498
  #if defined(USEARCH_DEFINED_WINDOWS)
@@ -2326,7 +2563,7 @@ class index_gt {
2326
2563
  pre_ = precompute_(config_);
2327
2564
 
2328
2565
  index_limits_t limits;
2329
- limits.elements = state.size;
2566
+ limits.members = state.size;
2330
2567
  limits.threads_add = 0;
2331
2568
  if (!reserve(limits))
2332
2569
  return result.failed("Out of memory!");
@@ -2337,16 +2574,18 @@ class index_gt {
2337
2574
  }
2338
2575
 
2339
2576
  // Locate every node packed into file
2340
- std::size_t progress = sizeof(file_header_t);
2341
- for (std::size_t i = 0; i != size_; ++i) {
2342
- byte_t* tape = (byte_t*)(file + progress);
2577
+ std::size_t progress_bytes = sizeof(file_header_t);
2578
+ std::size_t const size = size_;
2579
+ for (std::size_t i = 0; i != size; ++i) {
2580
+ byte_t* tape = (byte_t*)(file + progress_bytes);
2343
2581
  dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
2344
2582
  level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
2345
2583
 
2346
2584
  std::size_t node_bytes = node_bytes_(dim, level);
2347
2585
  std::size_t node_vector_bytes = dim * sizeof(scalar_t);
2348
2586
  nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
2349
- progress += node_bytes;
2587
+ progress_bytes += node_bytes;
2588
+ progress(i, size);
2350
2589
  }
2351
2590
 
2352
2591
  return {};
@@ -2354,7 +2593,267 @@ class index_gt {
2354
2593
 
2355
2594
  #pragma endregion
2356
2595
 
2596
+ struct join_result_t {
2597
+ error_t error{};
2598
+ std::size_t intersection_size{};
2599
+ std::size_t engagements{};
2600
+ std::size_t cycles{};
2601
+ std::size_t measurements{};
2602
+
2603
+ explicit operator bool() const noexcept { return !error; }
2604
+ join_result_t failed(error_t message) noexcept {
2605
+ error = std::move(message);
2606
+ return std::move(*this);
2607
+ }
2608
+ };
2609
+
2610
+ /**
2611
+ * @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
2612
+ * to perform fast one-to-one matching between two large collections
2613
+ * of vectors, using approximate nearest neighbors search.
2614
+ *
2615
+ * @param[inout] first_to_second Container to map ::first labels to ::second.
2616
+ * @param[inout] second_to_first Container to map ::second labels to ::first.
2617
+ * @param[in] executor Thread-pool to execute the job in parallel.
2618
+ * @param[in] progress Callback to report the execution progress.
2619
+ */
2620
+ template < //
2621
+ typename first_to_second_at = dummy_label_to_label_mapping_t, //
2622
+ typename second_to_first_at = dummy_label_to_label_mapping_t, //
2623
+ typename executor_at = dummy_executor_t, //
2624
+ typename progress_at = dummy_progress_t //
2625
+ >
2626
+ static join_result_t join( //
2627
+ index_gt const& first, index_gt const& second, //
2628
+ join_config_t config = {}, //
2629
+ first_to_second_at&& first_to_second = first_to_second_at{}, //
2630
+ second_to_first_at&& second_to_first = second_to_first_at{}, //
2631
+ executor_at&& executor = executor_at{}, //
2632
+ progress_at&& progress = progress_at{}) noexcept {
2633
+
2634
+ if (second.size() < first.size())
2635
+ return join_small_and_big_( //
2636
+ second, first, //
2637
+ config, //
2638
+ std::forward<second_to_first_at>(second_to_first), //
2639
+ std::forward<first_to_second_at>(first_to_second), //
2640
+ std::forward<executor_at>(executor), //
2641
+ std::forward<progress_at>(progress));
2642
+ else
2643
+ return join_small_and_big_( //
2644
+ first, second, //
2645
+ config, //
2646
+ std::forward<first_to_second_at>(first_to_second), //
2647
+ std::forward<second_to_first_at>(second_to_first), //
2648
+ std::forward<executor_at>(executor), //
2649
+ std::forward<progress_at>(progress));
2650
+ }
2651
+
2652
+ /**
2653
+ * @brief Scans the whole collection, removing the links leading towards
2654
+ * banned entries. This essentially isolates some nodes from the rest
2655
+ * of the graph, while keeping their outgoing links, in case the node
2656
+ * is structurally relevant and has a crucial role in the index.
2657
+ * It won't reclaim the memory.
2658
+ *
2659
+ * @param[in] allow_member Predicate to mark nodes for isolation.
2660
+ * @param[in] executor Thread-pool to execute the job in parallel.
2661
+ * @param[in] progress Callback to report the execution progress.
2662
+ */
2663
+ template < //
2664
+ typename allow_member_at = dummy_predicate_t, //
2665
+ typename executor_at = dummy_executor_t, //
2666
+ typename progress_at = dummy_progress_t //
2667
+ >
2668
+ void isolate( //
2669
+ allow_member_at&& allow_member, //
2670
+ executor_at&& executor = executor_at{}, //
2671
+ progress_at&& progress = progress_at{}) noexcept {
2672
+
2673
+ // Erase all the incoming links
2674
+ executor.execute_bulk(size(), [&](std::size_t node_idx, std::size_t) {
2675
+ node_t node = node_with_id_(node_idx);
2676
+ for (level_t level = 0; level <= node.level(); ++level) {
2677
+ neighbors_ref_t neighbors = neighbors_(node, level);
2678
+ std::size_t old_size = neighbors.size();
2679
+ neighbors.clear();
2680
+ for (std::size_t i = 0; i != old_size; ++i) {
2681
+ id_t neighbor_id = neighbors[i];
2682
+ node_t neighbor = node_with_id_(neighbor_id);
2683
+ if (allow_member(member_cref_t{neighbor.label(), neighbor.vector_view(), neighbor_id}))
2684
+ neighbors.push_back(neighbor_id);
2685
+ }
2686
+ }
2687
+ });
2688
+ }
2689
+
2357
2690
  private:
2691
+ template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
2692
+ static join_result_t join_small_and_big_( //
2693
+ index_gt const& men, index_gt const& women, //
2694
+ join_config_t config, //
2695
+ first_to_second_at&& man_to_woman, //
2696
+ second_to_first_at&& woman_to_man, //
2697
+ executor_at&& executor, //
2698
+ progress_at&& progress) noexcept {
2699
+
2700
+ join_result_t result;
2701
+
2702
+ // Sanity checks and argument validation:
2703
+ if (&men == &women)
2704
+ return result.failed("Can't join with itself, consider copying");
2705
+ if (config.max_proposals == 0)
2706
+ config.max_proposals = std::log(men.size()) + executor.size();
2707
+ config.max_proposals = (std::min)(men.size(), config.max_proposals);
2708
+
2709
+ using proposals_count_t = std::uint16_t;
2710
+ using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
2711
+ allocator_t& alloc = men.allocator_;
2712
+
2713
+ // Create an atomic queue, as a ring structure, from/to which
2714
+ // free men will be added/pulled.
2715
+ std::mutex free_men_mutex{};
2716
+ ring_gt<id_t, ids_allocator_t> free_men;
2717
+ free_men.reserve(men.size());
2718
+ for (std::size_t i = 0; i != men.size(); ++i)
2719
+ free_men.push(static_cast<id_t>(i));
2720
+
2721
+ // We are gonna need some temporary memory.
2722
+ proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
2723
+ id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
2724
+ id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
2725
+ if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
2726
+ return result.failed("Can't temporary mappings");
2727
+
2728
+ id_t missing_id;
2729
+ std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
2730
+ std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
2731
+ std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
2732
+ std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
2733
+
2734
+ // Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
2735
+ visits_bitset_t men_locks, women_locks;
2736
+ if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
2737
+ return result.failed("Can't allocate locks");
2738
+
2739
+ // Accumulate statistics from all the contexts,
2740
+ // to have a baseline to compare with, by the time the `join` is finished.
2741
+ std::size_t old_measurements{};
2742
+ std::size_t old_cycles{};
2743
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2744
+ old_measurements += women.contexts_[thread_idx].measurements_count;
2745
+ old_cycles += women.contexts_[thread_idx].iteration_cycles;
2746
+ }
2747
+ std::atomic<std::size_t> rounds{0};
2748
+ std::atomic<std::size_t> engagements{0};
2749
+
2750
+ // Concurrently process all the men
2751
+ executor.execute_bulk([&](std::size_t thread_idx) {
2752
+ context_t& context = women.contexts_[thread_idx];
2753
+ search_config_t search_config;
2754
+ search_config.expansion = config.expansion;
2755
+ search_config.exact = config.exact;
2756
+ search_config.thread = thread_idx;
2757
+ id_t free_man_id;
2758
+
2759
+ // While there exist a free man who still has a woman to propose to.
2760
+ while (true) {
2761
+ std::size_t passed_rounds = 0;
2762
+ std::size_t total_rounds = 0;
2763
+ {
2764
+ std::unique_lock<std::mutex> pop_lock(free_men_mutex);
2765
+ if (!free_men.try_pop(free_man_id))
2766
+ // Primary exit path, we have exhausted the list of candidates
2767
+ break;
2768
+ passed_rounds = ++rounds;
2769
+ total_rounds = passed_rounds + free_men.size();
2770
+ }
2771
+ progress(passed_rounds, total_rounds);
2772
+ while (men_locks.atomic_set(free_man_id))
2773
+ ;
2774
+
2775
+ node_t free_man = men.node_with_id_(free_man_id);
2776
+ proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
2777
+ if (free_man_proposals >= config.max_proposals)
2778
+ continue;
2779
+
2780
+ // Find the closest woman, to whom this man hasn't proposed yet.
2781
+ free_man_proposals++;
2782
+ search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
2783
+ if (!candidates) {
2784
+ // TODO:
2785
+ }
2786
+
2787
+ match_t match = candidates.back();
2788
+ member_cref_t woman = match.member;
2789
+ while (women_locks.atomic_set(woman.id))
2790
+ ;
2791
+
2792
+ id_t husband_id = woman_to_man_ids[woman.id];
2793
+ bool woman_is_free = husband_id == missing_id;
2794
+ if (woman_is_free) {
2795
+ // Engagement
2796
+ man_to_woman_ids[free_man_id] = woman.id;
2797
+ woman_to_man_ids[woman.id] = free_man_id;
2798
+ engagements++;
2799
+ } else {
2800
+ distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
2801
+ distance_t distance_from_candidate = match.distance;
2802
+ if (distance_from_husband > distance_from_candidate) {
2803
+ // Break-up
2804
+ while (men_locks.atomic_set(husband_id))
2805
+ ;
2806
+ man_to_woman_ids[husband_id] = missing_id;
2807
+ men_locks.atomic_reset(husband_id);
2808
+
2809
+ // New Engagement
2810
+ man_to_woman_ids[free_man_id] = woman.id;
2811
+ woman_to_man_ids[woman.id] = free_man_id;
2812
+ engagements++;
2813
+
2814
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2815
+ free_men.push(husband_id);
2816
+ } else {
2817
+ std::unique_lock<std::mutex> push_lock(free_men_mutex);
2818
+ free_men.push(free_man_id);
2819
+ }
2820
+ }
2821
+
2822
+ men_locks.atomic_reset(free_man_id);
2823
+ women_locks.atomic_reset(woman.id);
2824
+ }
2825
+ });
2826
+
2827
+ // Export the IDs into labels:
2828
+ std::size_t intersection_size = 0;
2829
+ for (std::size_t i = 0; i != men.size(); ++i) {
2830
+ id_t woman_id = man_to_woman_ids[i];
2831
+ if (woman_id != missing_id) {
2832
+ label_t man = men.node_with_id_(i).label();
2833
+ label_t woman = women.node_with_id_(woman_id).label();
2834
+ man_to_woman[man] = woman;
2835
+ woman_to_man[woman] = man;
2836
+ intersection_size++;
2837
+ }
2838
+ }
2839
+
2840
+ // Deallocate memory
2841
+ alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
2842
+ alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
2843
+ alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
2844
+
2845
+ // Export stats
2846
+ result.engagements = engagements;
2847
+ result.intersection_size = intersection_size;
2848
+ for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
2849
+ result.measurements += women.contexts_[thread_idx].measurements_count;
2850
+ result.cycles += women.contexts_[thread_idx].iteration_cycles;
2851
+ }
2852
+ result.measurements -= old_measurements;
2853
+ result.cycles -= old_cycles;
2854
+ return result;
2855
+ }
2856
+
2358
2857
  void reset_view_() noexcept {
2359
2858
  if (!viewed_file_)
2360
2859
  return;
@@ -2384,51 +2883,80 @@ class index_gt {
2384
2883
  return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
2385
2884
  }
2386
2885
 
2387
- inline bool node_stored_(node_t node) const noexcept {
2886
+ using span_bytes_t = span_gt<byte_t>;
2887
+ struct node_bytes_split_t {
2888
+ span_bytes_t tape{};
2889
+ span_bytes_t vector{};
2890
+
2891
+ node_bytes_split_t() {}
2892
+ node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
2893
+
2894
+ std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
2895
+ bool colocated() const noexcept { return tape.end() == vector.begin(); }
2896
+ operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
2897
+ };
2898
+
2899
+ inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
2388
2900
  std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
2389
- return (node.tape() + node_head_bytes_() + levels_bytes) == (byte_t*)node.vector();
2901
+ std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
2902
+ return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
2390
2903
  }
2391
2904
 
2392
2905
  inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
2393
2906
  inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
2394
2907
 
2395
- void node_free_(std::size_t id) noexcept {
2396
-
2397
- if (viewed_file_)
2398
- return;
2399
-
2400
- node_t& node = nodes_[id];
2401
- std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
2402
- point_allocator_.deallocate(node.tape(), node_bytes);
2403
- node = node_t{};
2404
- }
2405
-
2406
- node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2908
+ node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
2407
2909
 
2408
- std::size_t dim = vector.size();
2409
- std::size_t stored_vector_bytes = node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(store_vector);
2410
- std::size_t node_bytes = node_bytes_(static_cast<dim_t>(dim), level) -
2411
- node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
2910
+ std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
2911
+ std::size_t node_bytes = node_bytes_(dims_to_store, level);
2912
+ std::size_t non_vector_bytes = node_bytes - vector_bytes;
2412
2913
 
2413
2914
  byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
2414
2915
  if (!data)
2415
- return {};
2416
-
2417
- std::memset(data, 0, node_bytes);
2418
- if (vector.data())
2419
- std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
2420
-
2421
- scalar_t* scalars = store_vector //
2422
- ? (scalar_t*)(data + node_bytes - stored_vector_bytes)
2423
- : (scalar_t*)(vector.data());
2916
+ return node_bytes_split_t{};
2917
+ return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
2918
+ }
2424
2919
 
2425
- node_t node{data, scalars};
2920
+ node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
2921
+ node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
2922
+ if (store_vector) {
2923
+ std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
2924
+ std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
2925
+ } else {
2926
+ std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
2927
+ }
2928
+ node_t node = node_bytes;
2426
2929
  node.label(label);
2427
- node.dim(static_cast<dim_t>(dim));
2930
+ node.dim(static_cast<dim_t>(vector.size()));
2428
2931
  node.level(level);
2429
2932
  return node;
2430
2933
  }
2431
2934
 
2935
+ node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
2936
+ if (old_bytes.colocated()) {
2937
+ byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
2938
+ std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
2939
+ return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
2940
+ } else {
2941
+ node_t old_node = old_bytes;
2942
+ node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
2943
+ std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
2944
+ std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
2945
+ return node_bytes;
2946
+ }
2947
+ }
2948
+
2949
+ void node_free_(std::size_t id) noexcept {
2950
+
2951
+ if (viewed_file_)
2952
+ return;
2953
+
2954
+ node_t& node = nodes_[id];
2955
+ std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
2956
+ point_allocator_.deallocate(node.tape(), node_bytes);
2957
+ node = node_t{};
2958
+ }
2959
+
2432
2960
  inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
2433
2961
  inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
2434
2962
 
@@ -2453,6 +2981,23 @@ class index_gt {
2453
2981
  return {nodes_mutexes_, idx};
2454
2982
  }
2455
2983
 
2984
+ void connect_node_across_levels_( //
2985
+ id_t node_id, vector_view_t vector, //
2986
+ id_t entry_id, level_t max_level, level_t target_level, //
2987
+ add_config_t const& config, context_t& context) usearch_noexcept_m {
2988
+
2989
+ // Go down the level, tracking only the closest match
2990
+ id_t closest_id = search_for_one_(entry_id, vector, max_level, target_level, context);
2991
+
2992
+ // From `target_level` down perform proper extensive search
2993
+ for (level_t level = (std::min)(target_level, max_level); level >= 0; --level) {
2994
+ // TODO: Handle out of memory conditions
2995
+ search_to_insert_(closest_id, vector, level, config.expansion, context);
2996
+ closest_id = connect_new_node_(node_id, level, context);
2997
+ reconnect_neighbor_nodes_(node_id, level, context);
2998
+ }
2999
+ }
3000
+
2456
3001
  id_t connect_new_node_(id_t new_id, level_t level, context_t& context) usearch_noexcept_m {
2457
3002
 
2458
3003
  node_t new_node = node_with_id_(new_id);