usearch 2.25.1 → 2.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +2 -2
  2. package/c/lib.cpp +28 -13
  3. package/cpp/test.cpp +169 -0
  4. package/include/usearch/index.hpp +364 -58
  5. package/include/usearch/index_dense.hpp +155 -50
  6. package/include/usearch/index_plugins.hpp +156 -79
  7. package/javascript/lib.cpp +4 -2
  8. package/numkong/CITATION.cff +1 -1
  9. package/numkong/Cargo.toml +1 -1
  10. package/numkong/bench/bench_cross_amx.cpp +12 -0
  11. package/numkong/bench/bench_mesh.cpp +12 -0
  12. package/numkong/bench/bench_spatial.cpp +0 -3
  13. package/numkong/binding.gyp +18 -0
  14. package/numkong/build.rs +1 -0
  15. package/numkong/c/dispatch_e5m2.c +23 -3
  16. package/numkong/include/numkong/capabilities.h +1 -1
  17. package/numkong/include/numkong/cast/README.md +3 -0
  18. package/numkong/include/numkong/cast/haswell.h +28 -64
  19. package/numkong/include/numkong/cast/serial.h +17 -0
  20. package/numkong/include/numkong/cast/skylake.h +67 -52
  21. package/numkong/include/numkong/cast.h +1 -0
  22. package/numkong/include/numkong/dot/README.md +1 -0
  23. package/numkong/include/numkong/dot/haswell.h +92 -13
  24. package/numkong/include/numkong/dot/serial.h +15 -0
  25. package/numkong/include/numkong/dot/skylake.h +61 -14
  26. package/numkong/include/numkong/dots/README.md +2 -0
  27. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  28. package/numkong/include/numkong/dots/haswell.h +28 -28
  29. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  30. package/numkong/include/numkong/dots/serial.h +23 -8
  31. package/numkong/include/numkong/dots/skylake.h +28 -23
  32. package/numkong/include/numkong/dots.h +12 -0
  33. package/numkong/include/numkong/each/serial.h +18 -1
  34. package/numkong/include/numkong/geospatial/serial.h +14 -3
  35. package/numkong/include/numkong/maxsim/serial.h +15 -0
  36. package/numkong/include/numkong/mesh/README.md +50 -44
  37. package/numkong/include/numkong/mesh/genoa.h +462 -0
  38. package/numkong/include/numkong/mesh/haswell.h +806 -933
  39. package/numkong/include/numkong/mesh/neon.h +871 -943
  40. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  41. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  42. package/numkong/include/numkong/mesh/rvv.h +404 -319
  43. package/numkong/include/numkong/mesh/serial.h +204 -162
  44. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  45. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  46. package/numkong/include/numkong/mesh.h +38 -0
  47. package/numkong/include/numkong/reduce/serial.h +15 -1
  48. package/numkong/include/numkong/sparse/serial.h +17 -2
  49. package/numkong/include/numkong/spatial/genoa.h +0 -68
  50. package/numkong/include/numkong/spatial/haswell.h +98 -56
  51. package/numkong/include/numkong/spatial/serial.h +15 -0
  52. package/numkong/include/numkong/spatial/skylake.h +114 -54
  53. package/numkong/include/numkong/spatial.h +0 -12
  54. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  55. package/numkong/include/numkong/spatials/serial.h +18 -1
  56. package/numkong/include/numkong/spatials/skylake.h +2 -2
  57. package/numkong/include/numkong/spatials.h +17 -0
  58. package/numkong/include/numkong/tensor.hpp +107 -23
  59. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  60. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  61. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  62. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  63. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  65. package/numkong/javascript/numkong.c +3 -2
  66. package/numkong/package.json +7 -7
  67. package/numkong/test/test.cu +841 -0
  68. package/numkong/test/test_cross_amx.cpp +13 -0
  69. package/numkong/test/test_dlpack.py +492 -0
  70. package/numkong/test/test_mesh.cpp +12 -0
  71. package/numkong/test/test_spatial.cpp +0 -3
  72. package/numkong/test/test_tensor.cpp +87 -27
  73. package/package.json +2 -2
  74. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  75. package/prebuilds/linux-arm64/usearch.node +0 -0
  76. package/prebuilds/linux-x64/usearch.node +0 -0
  77. package/prebuilds/win32-x64/usearch.node +0 -0
@@ -276,15 +276,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
276
276
 
277
277
  std::uint32_t dimensions_u32[2]{0};
278
278
  std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
279
- std::size_t offset_if_u32 = std::size_t(dimensions_u32[0]) * dimensions_u32[1] + sizeof(dimensions_u32);
279
+ checked_size_result_t offset_if_u32 =
280
+ checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
280
281
 
281
282
  std::uint64_t dimensions_u64[2]{0};
282
283
  std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
283
- std::size_t offset_if_u64 = std::size_t(dimensions_u64[0]) * dimensions_u64[1] + sizeof(dimensions_u64);
284
+ checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
285
+ checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
286
+ checked_size_result_t offset_if_u64 =
287
+ rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
288
+ : checked_size_overflow();
284
289
 
285
290
  // Check if it starts with 32-bit
286
- if (offset_if_u32 + sizeof(index_dense_head_buffer_t) < file_size) {
287
- if (std::fseek(file.get(), static_cast<long>(offset_if_u32), SEEK_SET) != 0)
291
+ checked_size_result_t head_offset_if_u32 =
292
+ offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
293
+ if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
294
+ if (std::fseek(file.get(), static_cast<long>(offset_if_u32.value), SEEK_SET) != 0)
288
295
  return result.failed(std::strerror(errno));
289
296
  read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
290
297
  if (!read)
@@ -299,8 +306,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
299
306
  }
300
307
 
301
308
  // Check if it starts with 64-bit
302
- if (offset_if_u64 + sizeof(index_dense_head_buffer_t) < file_size) {
303
- if (std::fseek(file.get(), static_cast<long>(offset_if_u64), SEEK_SET) != 0)
309
+ checked_size_result_t head_offset_if_u64 =
310
+ offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
311
+ if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
312
+ if (std::fseek(file.get(), static_cast<long>(offset_if_u64.value), SEEK_SET) != 0)
304
313
  return result.failed(std::strerror(errno));
305
314
  read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
306
315
  if (!read)
@@ -341,15 +350,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
341
350
  // Check if it starts with 32-bit
342
351
  std::uint32_t dimensions_u32[2]{0};
343
352
  std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
344
- std::size_t offset_if_u32 = std::size_t(dimensions_u32[0]) * dimensions_u32[1] + sizeof(dimensions_u32);
353
+ checked_size_result_t offset_if_u32 =
354
+ checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
345
355
 
346
356
  std::uint64_t dimensions_u64[2]{0};
347
357
  std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
348
- std::size_t offset_if_u64 = std::size_t(dimensions_u64[0]) * dimensions_u64[1] + sizeof(dimensions_u64);
358
+ checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
359
+ checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
360
+ checked_size_result_t offset_if_u64 =
361
+ rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
362
+ : checked_size_overflow();
349
363
 
350
364
  // Check if it starts with 32-bit
351
- if (offset_if_u32 + sizeof(index_dense_head_buffer_t) < file_size) {
352
- std::memcpy(&result.head_buffer, file_data + offset_if_u32, sizeof(index_dense_head_buffer_t));
365
+ checked_size_result_t head_offset_if_u32 =
366
+ offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
367
+ if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
368
+ std::memcpy(&result.head_buffer, file_data + offset_if_u32.value, sizeof(index_dense_head_buffer_t));
353
369
  result.config.exclude_vectors = false;
354
370
  result.config.use_64_bit_dimensions = false;
355
371
  if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
@@ -357,8 +373,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
357
373
  }
358
374
 
359
375
  // Check if it starts with 64-bit
360
- if (offset_if_u64 + sizeof(index_dense_head_buffer_t) < file_size) {
361
- std::memcpy(&result.head_buffer, file_data + offset_if_u64, sizeof(index_dense_head_buffer_t));
376
+ checked_size_result_t head_offset_if_u64 =
377
+ offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
378
+ if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
379
+ std::memcpy(&result.head_buffer, file_data + offset_if_u64.value, sizeof(index_dense_head_buffer_t));
362
380
  result.config.exclude_vectors = false;
363
381
  result.config.use_64_bit_dimensions = true;
364
382
  if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
@@ -532,6 +550,9 @@ class index_dense_gt {
532
550
  : parent(other.parent), thread_id(other.thread_id), engaged(other.engaged) {
533
551
  other.engaged = false;
534
552
  }
553
+ explicit operator bool() const noexcept {
554
+ return parent.typed_ && thread_id != any_thread() && thread_id < parent.typed_->limits().threads();
555
+ }
535
556
  };
536
557
 
537
558
  public:
@@ -635,16 +656,14 @@ class index_dense_gt {
635
656
  * @param[in] metric One of the provided or an @b ad-hoc metric, type-punned.
636
657
  * @param[in] config The index configuration (optional).
637
658
  * @param[in] free_key The key used for freed vectors (optional).
659
+ * @param[in] limits Initial reservation. Default sizes the thread pool to
660
+ * @c hardware_concurrency(); pass @c {unreserved} to skip.
638
661
  * @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
639
- *
640
- * ! If the `metric` isn't provided in this method, it has to be set with
641
- * ! the `change_metric` method before the index can be used. Alternatively,
642
- * ! if you are loading an existing index, the metric will be set automatically.
643
662
  */
644
663
  static state_result_t make( //
645
664
  metric_t metric = {}, //
646
665
  index_dense_config_t config = {}, //
647
- vector_key_t free_key = default_free_value<vector_key_t>()) {
666
+ vector_key_t free_key = default_free_value<vector_key_t>(), index_limits_t limits = {}) {
648
667
 
649
668
  if (metric.missing())
650
669
  return state_result_t{}.failed("Metric won't be initialized!");
@@ -659,16 +678,15 @@ class index_dense_gt {
659
678
  index_dense_gt& index = result.index;
660
679
  index.config_ = config;
661
680
  index.free_key_ = free_key;
662
-
663
- // In some cases the metric is not provided, and will be set later.
664
- if (metric) {
665
- scalar_kind_t scalar_kind = metric.scalar_kind();
666
- index.casts_ = casts_punned_t::make(scalar_kind);
667
- index.metric_ = metric;
668
- }
681
+ index.casts_ = casts_punned_t::make(metric.scalar_kind());
682
+ index.metric_ = metric;
669
683
 
670
684
  new (raw) index_t(config);
671
685
  index.typed_ = raw;
686
+
687
+ if (!index.try_reserve(limits))
688
+ return state_result_t{}.failed("Failed to reserve memory for the index!");
689
+
672
690
  return result;
673
691
  }
674
692
 
@@ -704,7 +722,34 @@ class index_dense_gt {
704
722
 
705
723
  // The metric and its properties
706
724
  metric_t const& metric() const { return metric_; }
707
- void change_metric(metric_t metric) { metric_ = std::move(metric); }
725
+
726
+ /**
727
+ * @brief Replaces the active distance metric, resizing the per-thread cast
728
+ * buffer and rebuilding the cast dispatch table if the new metric
729
+ * changes @c bytes_per_vector() or @c scalar_kind().
730
+ * @return @c false if the cast buffer can't be re-allocated; the metric is
731
+ * left untouched in that case so the index stays consistent.
732
+ */
733
+ bool try_change_metric(metric_t metric) noexcept {
734
+ checked_size_result_t needed_bytes = checked_mul(limits().threads(), metric.bytes_per_vector());
735
+ if (!needed_bytes)
736
+ return false;
737
+ if (needed_bytes.value > cast_buffer_.size()) {
738
+ cast_buffer_t new_buffer(needed_bytes.value);
739
+ if (!new_buffer)
740
+ return false;
741
+ cast_buffer_ = std::move(new_buffer);
742
+ }
743
+ casts_ = casts_punned_t::make(metric.scalar_kind());
744
+ metric_ = std::move(metric);
745
+ return true;
746
+ }
747
+
748
+ /// @brief Throwing counterpart of @ref try_change_metric.
749
+ void change_metric(metric_t metric) {
750
+ if (!try_change_metric(std::move(metric)))
751
+ usearch_raise_runtime_error("failed to grow cast buffer for the new metric");
752
+ }
708
753
 
709
754
  scalar_kind_t scalar_kind() const { return metric_.scalar_kind(); }
710
755
  metric_kind_t metric_kind() const { return metric_.metric_kind(); }
@@ -729,6 +774,45 @@ class index_dense_gt {
729
774
  return typed_->stats(stats_per_level, max_level);
730
775
  }
731
776
 
777
+ using neighbors_view_t = typename index_t::neighbors_view_t;
778
+
779
+ /**
780
+ * @brief Returns a read-only range over the neighbors of @p key at the given
781
+ * graph @p level.
782
+ *
783
+ * For multi-key indexes, returns the neighbors of the first matching
784
+ * slot; iterate through `cbegin()`/`cend()` and call the underlying
785
+ * `index_gt::neighbors(member_citerator_t, std::size_t)` overload to
786
+ * inspect every copy of a duplicated key.
787
+ *
788
+ * @return An empty view if the key is not present, or if @p level exceeds
789
+ * the node's level.
790
+ * @warning The view aliases the node's adjacency tape. Hold no concurrent
791
+ * `add`/`update`/`remove` for the lifetime of the view.
792
+ */
793
+ neighbors_view_t neighbors(vector_key_t key, std::size_t level) const {
794
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
795
+ shared_lock_t lookup_lock(slot_lookup_mutex_);
796
+ auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
797
+ if (matching_slots.first == matching_slots.second)
798
+ return {};
799
+ compressed_slot_t slot = (*matching_slots.first).slot;
800
+ return typed_->neighbors(slot, level);
801
+ }
802
+
803
+ /**
804
+ * @brief Returns the top graph level at which @p key is present, or zero
805
+ * if the key is not in the index.
806
+ */
807
+ std::size_t level_of(vector_key_t key) const {
808
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
809
+ shared_lock_t lookup_lock(slot_lookup_mutex_);
810
+ auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
811
+ if (matching_slots.first == matching_slots.second)
812
+ return 0;
813
+ return typed_->level_of((*matching_slots.first).slot);
814
+ }
815
+
732
816
  dynamic_allocator_t const& allocator() const { return typed_->dynamic_allocator(); }
733
817
  vector_key_t const& free_key() const { return free_key_; }
734
818
 
@@ -935,6 +1019,8 @@ class index_dense_gt {
935
1019
 
936
1020
  index_cluster_config_t cluster_config;
937
1021
  thread_lock_t lock = thread_lock_(thread);
1022
+ if (!lock)
1023
+ return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
938
1024
  cluster_config.thread = lock.thread_id;
939
1025
  cluster_config.expansion = config_.expansion_search;
940
1026
  metric_proxy_t metric{*this};
@@ -994,7 +1080,10 @@ class index_dense_gt {
994
1080
  available_threads_.push(i);
995
1081
 
996
1082
  // Allocate a buffer for the casted vectors.
997
- cast_buffer_t cast_buffer(limits.threads() * metric_.bytes_per_vector());
1083
+ checked_size_result_t cast_buffer_bytes = checked_mul(limits.threads(), metric_.bytes_per_vector());
1084
+ if (!cast_buffer_bytes)
1085
+ return false;
1086
+ cast_buffer_t cast_buffer(cast_buffer_bytes.value);
998
1087
  if (!cast_buffer)
999
1088
  return false;
1000
1089
  cast_buffer_ = std::move(cast_buffer);
@@ -1019,7 +1108,8 @@ class index_dense_gt {
1019
1108
  std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
1020
1109
  typed_->clear();
1021
1110
  slot_lookup_.clear();
1022
- vectors_lookup_.reset();
1111
+ // Tape pointers are about to be invalidated by the reset below.
1112
+ std::fill(vectors_lookup_.begin(), vectors_lookup_.end(), nullptr);
1023
1113
  free_keys_.clear();
1024
1114
  vectors_tape_allocator_.reset();
1025
1115
  }
@@ -1144,8 +1234,9 @@ class index_dense_gt {
1144
1234
  serialization_config_t config = {}, //
1145
1235
  progress_at&& progress = {}) {
1146
1236
 
1147
- // Discard all previous memory allocations of `vectors_tape_allocator_`
1148
- index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
1237
+ // Preserve any explicit thread counts from the prior index state; fall
1238
+ // back to library defaults when they were never set (e.g. `unreserved`).
1239
+ index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
1149
1240
  reset();
1150
1241
 
1151
1242
  // Infer the new index size
@@ -1206,9 +1297,10 @@ class index_dense_gt {
1206
1297
 
1207
1298
  config_.multi = head.multi;
1208
1299
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1209
- // available_threads_.size() will be updated to old_limits.threads() later in this
1210
- // method, so use that as the number of threads to prepare for.
1211
- cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1300
+ checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
1301
+ if (!cast_buffer_bytes)
1302
+ return result.failed("Failed to allocate memory for the casts");
1303
+ cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
1212
1304
  if (!cast_buffer_)
1213
1305
  return result.failed("Failed to allocate memory for the casts");
1214
1306
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1227,14 +1319,13 @@ class index_dense_gt {
1227
1319
  return result;
1228
1320
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
1229
1321
  return result.failed("Index size and the number of vectors doesn't match");
1230
- old_limits.members = static_cast<std::size_t>(matrix_rows);
1231
- if (!typed_->try_reserve(old_limits))
1322
+ new_limits.members = static_cast<std::size_t>(matrix_rows);
1323
+ if (!typed_->try_reserve(new_limits))
1232
1324
  return result.failed("Failed to reserve memory for the index");
1233
1325
 
1234
- // After the index is loaded, we may have to resize the `available_threads_` to
1235
- // match the limits of the underlying engine.
1326
+ // After the index is loaded, resize `available_threads_` to match the new limits.
1236
1327
  available_threads_t available_threads;
1237
- std::size_t max_threads = old_limits.threads();
1328
+ std::size_t max_threads = new_limits.threads();
1238
1329
  if (!available_threads.reserve(max_threads))
1239
1330
  return result.failed("Failed to allocate memory for the available threads!");
1240
1331
  for (std::size_t i = 0; i < max_threads; i++)
@@ -1258,8 +1349,9 @@ class index_dense_gt {
1258
1349
  std::size_t offset = 0, serialization_config_t config = {}, //
1259
1350
  progress_at&& progress = {}) {
1260
1351
 
1261
- // Discard all previous memory allocations of `vectors_tape_allocator_`
1262
- index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
1352
+ // Preserve any explicit thread counts from the prior index state; fall
1353
+ // back to library defaults when they were never set (e.g. `unreserved`).
1354
+ index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
1263
1355
  reset();
1264
1356
 
1265
1357
  serialization_result_t result = file.open_if_not();
@@ -1323,8 +1415,10 @@ class index_dense_gt {
1323
1415
  config_.multi = head.multi;
1324
1416
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1325
1417
  // available_threads_.size() will be updated to old_limits.threads() later in this
1326
- // method, so use that as the number of threads to prepare for.
1327
- cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1418
+ checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
1419
+ if (!cast_buffer_bytes)
1420
+ return result.failed("Failed to allocate memory for the casts");
1421
+ cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
1328
1422
  if (!cast_buffer_)
1329
1423
  return result.failed("Failed to allocate memory for the casts");
1330
1424
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1344,8 +1438,8 @@ class index_dense_gt {
1344
1438
  return result;
1345
1439
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
1346
1440
  return result.failed("Index size and the number of vectors doesn't match");
1347
- old_limits.members = static_cast<std::size_t>(matrix_rows);
1348
- if (!typed_->try_reserve(old_limits))
1441
+ new_limits.members = static_cast<std::size_t>(matrix_rows);
1442
+ if (!typed_->try_reserve(new_limits))
1349
1443
  return result.failed("Failed to reserve memory for the index");
1350
1444
 
1351
1445
  // Address the vectors
@@ -1356,10 +1450,9 @@ class index_dense_gt {
1356
1450
  for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
1357
1451
  vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
1358
1452
 
1359
- // After the index is loaded, we may have to resize the `available_threads_` to
1360
- // match the limits of the underlying engine.
1453
+ // After the index is viewed, resize `available_threads_` to match the new limits.
1361
1454
  available_threads_t available_threads;
1362
- std::size_t max_threads = old_limits.threads();
1455
+ std::size_t max_threads = new_limits.threads();
1363
1456
  if (!available_threads.reserve(max_threads))
1364
1457
  return result.failed("Failed to allocate memory for the available threads!");
1365
1458
  for (std::size_t i = 0; i < max_threads; i++)
@@ -1624,6 +1717,11 @@ class index_dense_gt {
1624
1717
  */
1625
1718
  labeling_result_t rename(vector_key_t from, vector_key_t to) {
1626
1719
  labeling_result_t result;
1720
+ if (from == to) {
1721
+ result.completed = count(from);
1722
+ return result;
1723
+ }
1724
+
1627
1725
  unique_lock_t lookup_lock(slot_lookup_mutex_);
1628
1726
 
1629
1727
  if (!multi() && slot_lookup_.contains(key_and_slot_t::any_slot(to)))
@@ -2043,10 +2141,9 @@ class index_dense_gt {
2043
2141
  if (thread_id != any_thread())
2044
2142
  return {*this, thread_id, false};
2045
2143
 
2046
- available_threads_mutex_.lock();
2047
- usearch_assert_m(available_threads_.size(), "No available threads to lock");
2048
- available_threads_.try_pop(thread_id);
2049
- available_threads_mutex_.unlock();
2144
+ std::unique_lock<std::mutex> lock(available_threads_mutex_);
2145
+ if (!available_threads_.try_pop(thread_id))
2146
+ return {*this, any_thread(), false};
2050
2147
  return {*this, thread_id, true};
2051
2148
  }
2052
2149
 
@@ -2067,6 +2164,8 @@ class index_dense_gt {
2067
2164
 
2068
2165
  // Cast the vector, if needed for compatibility with `metric_`
2069
2166
  thread_lock_t lock = thread_lock_(thread);
2167
+ if (!lock)
2168
+ return add_result_t{}.failed("Reserve capacity ahead of insertions!");
2070
2169
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2071
2170
  {
2072
2171
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2120,6 +2219,8 @@ class index_dense_gt {
2120
2219
 
2121
2220
  // Cast the vector, if needed for compatibility with `metric_`
2122
2221
  thread_lock_t lock = thread_lock_(thread);
2222
+ if (!lock)
2223
+ return search_result_t{*this}.failed("Reserve capacity ahead of searches!");
2123
2224
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2124
2225
  {
2125
2226
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2156,6 +2257,8 @@ class index_dense_gt {
2156
2257
 
2157
2258
  // Cast the vector, if needed for compatibility with `metric_`
2158
2259
  thread_lock_t lock = thread_lock_(thread);
2260
+ if (!lock)
2261
+ return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
2159
2262
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2160
2263
  {
2161
2264
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2180,6 +2283,8 @@ class index_dense_gt {
2180
2283
 
2181
2284
  // Cast the vector, if needed for compatibility with `metric_`
2182
2285
  thread_lock_t lock = thread_lock_(thread);
2286
+ if (!lock)
2287
+ return {};
2183
2288
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
2184
2289
  {
2185
2290
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;