usearch 2.11.1 → 2.13.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 (50) hide show
  1. package/README.md +100 -30
  2. package/binding.gyp +11 -12
  3. package/c/README.md +3 -0
  4. package/c/lib.cpp +50 -13
  5. package/c/test.c +60 -55
  6. package/c/usearch.h +16 -0
  7. package/cpp/CMakeLists.txt +6 -0
  8. package/cpp/README.md +34 -13
  9. package/cpp/bench.cpp +99 -47
  10. package/cpp/test.cpp +947 -132
  11. package/include/usearch/index.hpp +895 -273
  12. package/include/usearch/index_dense.hpp +418 -156
  13. package/include/usearch/index_plugins.hpp +99 -49
  14. package/javascript/dist/cjs/usearch.d.ts +39 -1
  15. package/javascript/dist/cjs/usearch.js +11 -1
  16. package/javascript/dist/esm/usearch.d.ts +39 -1
  17. package/javascript/dist/esm/usearch.js +11 -1
  18. package/javascript/tsconfig-esm.json +3 -2
  19. package/javascript/usearch.ts +18 -7
  20. package/package.json +2 -2
  21. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  22. package/prebuilds/linux-arm64/usearch.node +0 -0
  23. package/prebuilds/linux-x64/usearch.node +0 -0
  24. package/simsimd/.cmake-format.py +19 -0
  25. package/simsimd/CONTRIBUTING.md +49 -8
  26. package/simsimd/Cargo.lock +1 -1
  27. package/simsimd/Cargo.toml +10 -5
  28. package/simsimd/Package.swift +16 -0
  29. package/simsimd/README.md +69 -15
  30. package/simsimd/binding.gyp +14 -19
  31. package/simsimd/build.rs +2 -0
  32. package/simsimd/c/lib.c +18 -0
  33. package/simsimd/include/module.modulemap +4 -0
  34. package/simsimd/include/simsimd/binary.h +2 -0
  35. package/simsimd/include/simsimd/dot.h +466 -32
  36. package/simsimd/include/simsimd/probability.h +20 -10
  37. package/simsimd/include/simsimd/simsimd.h +444 -98
  38. package/simsimd/include/simsimd/spatial.h +480 -44
  39. package/simsimd/include/simsimd/types.h +161 -39
  40. package/simsimd/javascript/simsimd.ts +26 -2
  41. package/simsimd/package.json +1 -1
  42. package/simsimd/rust/benches/cosine.rs +8 -2
  43. package/simsimd/rust/benches/native.rs +240 -2
  44. package/simsimd/rust/benches/sqeuclidean.rs +8 -2
  45. package/simsimd/rust/lib.rs +50 -13
  46. package/simsimd/swift/source/SimSIMD.swift +123 -0
  47. package/simsimd/swift/tests/SimSIMDTests.swift +92 -0
  48. package/prebuilds/win32-ia32/usearch.node +0 -0
  49. package/prebuilds/win32-x64/usearch.node +0 -0
  50. package/simsimd/.releaserc +0 -87
@@ -1,11 +1,13 @@
1
+ /**
2
+ * @file index_dense.hpp
3
+ * @author Ash Vardanian
4
+ * @brief Single-header Vector Search engine for equi-dimensional dense vectors.
5
+ * @date July 26, 2023
6
+ */
1
7
  #pragma once
8
+ #include "index.hpp"
2
9
  #include <stdlib.h> // `aligned_alloc`
3
10
 
4
- #include <functional> // `std::function`
5
- #include <numeric> // `std::iota`
6
- #include <thread> // `std::thread`
7
- #include <vector> // `std::vector`
8
-
9
11
  #include <usearch/index.hpp>
10
12
  #include <usearch/index_plugins.hpp>
11
13
 
@@ -89,10 +91,32 @@ struct index_dense_head_result_t {
89
91
  }
90
92
  };
91
93
 
94
+ /**
95
+ * @brief Configuration settings for the construction of dense
96
+ * equidimensional vector indexes.
97
+ *
98
+ * Unlike the underlying `index_gt` class, incroporates the
99
+ * `::expansion_add` and `::expansion_search` parameters passed
100
+ * separately for the lower-level engine.
101
+ */
92
102
  struct index_dense_config_t : public index_config_t {
93
103
  std::size_t expansion_add = default_expansion_add();
94
104
  std::size_t expansion_search = default_expansion_search();
105
+
106
+ /**
107
+ * @brief Excludes vectors from the serialized file.
108
+ * This is handy when you want to store the vectors in a separate file.
109
+ *
110
+ * ! For advanced users only.
111
+ */
95
112
  bool exclude_vectors = false;
113
+
114
+ /**
115
+ * @brief Allows you to store multiple vectors per key.
116
+ * This is handy when a large document is chunked into many parts.
117
+ *
118
+ * ! May degrade the performance of iterators.
119
+ */
96
120
  bool multi = false;
97
121
 
98
122
  /**
@@ -101,17 +125,37 @@ struct index_dense_config_t : public index_config_t {
101
125
  * the vectors-to-keys mappings.
102
126
  *
103
127
  * ! This configuration parameter doesn't affect the serialized file,
104
- * ! and is not preserved between runs. Makes sense for small vector
105
- * ! representations that fit ina single cache line.
128
+ * ! and is not preserved between runs. Makes sense for smaller vectors
129
+ * ! that fit in a couple of cache lines.
130
+ *
131
+ * The trade-off is that some methods won't be available, like `get`, `rename`,
132
+ * and `remove`. The basic functionality, like `add` and `search` will work as
133
+ * expected even with `enable_key_lookups = false`.
134
+ *
135
+ * If both `!multi && !enable_key_lookups`, the "duplicate entry" checks won't
136
+ * be performed and no errors will be raised.
106
137
  */
107
138
  bool enable_key_lookups = true;
108
139
 
109
- index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
140
+ inline index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
141
+
142
+ inline index_dense_config_t(std::size_t c = 0, std::size_t ea = 0, std::size_t es = 0) noexcept
143
+ : index_config_t(c), expansion_add(ea), expansion_search(es) {}
110
144
 
111
- index_dense_config_t(std::size_t c = default_connectivity(), std::size_t ea = default_expansion_add(),
112
- std::size_t es = default_expansion_search()) noexcept
113
- : index_config_t(c), expansion_add(ea ? ea : default_expansion_add()),
114
- expansion_search(es ? es : default_expansion_search()) {}
145
+ /**
146
+ * @brief Validates the configuration settings, updating them in-place.
147
+ * @return Error message, if any.
148
+ */
149
+ inline error_t validate() noexcept {
150
+ error_t error = index_config_t::validate();
151
+ if (error)
152
+ return error;
153
+ if (expansion_add == 0)
154
+ expansion_add = default_expansion_add();
155
+ if (expansion_search == 0)
156
+ expansion_search = default_expansion_search();
157
+ return {};
158
+ }
115
159
  };
116
160
 
117
161
  struct index_dense_clustering_config_t {
@@ -163,6 +207,40 @@ struct index_dense_metadata_result_t {
163
207
  }
164
208
  };
165
209
 
210
+ inline scalar_kind_t convert_pre_2_10_scalar_kind(scalar_kind_t scalar_kind) noexcept {
211
+ // The old enum `scalar_kind_t` is defined without explicit constants from 0
212
+ switch (static_cast<std::underlying_type<scalar_kind_t>::type>(scalar_kind)) {
213
+ case 0: return scalar_kind_t::unknown_k;
214
+ case 1: return scalar_kind_t::b1x8_k;
215
+ case 2: return scalar_kind_t::u40_k;
216
+ case 3: return scalar_kind_t::uuid_k;
217
+ case 4: return scalar_kind_t::f64_k;
218
+ case 5: return scalar_kind_t::f32_k;
219
+ case 6: return scalar_kind_t::f16_k;
220
+ case 7: return scalar_kind_t::f8_k;
221
+ case 8: return scalar_kind_t::u64_k;
222
+ case 9: return scalar_kind_t::u32_k;
223
+ case 10: return scalar_kind_t::u8_k;
224
+ case 11: return scalar_kind_t::i64_k;
225
+ case 12: return scalar_kind_t::i32_k;
226
+ case 13: return scalar_kind_t::i16_k;
227
+ case 14: return scalar_kind_t::i8_k;
228
+ default: return scalar_kind;
229
+ }
230
+ }
231
+
232
+ inline void fix_pre_2_10_metadata(index_dense_head_t& head) {
233
+ // deal with incompatibility with pre-2.10 versions: https://github.com/unum-cloud/usearch/issues/423
234
+ if (head.version_major == 2 && head.version_minor < 10) {
235
+ head.kind_scalar = convert_pre_2_10_scalar_kind(head.kind_scalar);
236
+ head.kind_key = convert_pre_2_10_scalar_kind(head.kind_key);
237
+ head.kind_compressed_slot = convert_pre_2_10_scalar_kind(head.kind_compressed_slot);
238
+ // update minor versions so that no repeated conversion occurs
239
+ head.version_minor = 10;
240
+ head.version_patch = 0;
241
+ }
242
+ }
243
+
166
244
  /**
167
245
  * @brief Extracts metadata from a pre-constructed index on disk,
168
246
  * without loading it or mapping the whole binary file.
@@ -180,8 +258,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
180
258
 
181
259
  // Check if the file immediately starts with the index, instead of vectors
182
260
  result.config.exclude_vectors = true;
183
- if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
261
+ if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
262
+ fix_pre_2_10_metadata(result.head);
184
263
  return result;
264
+ }
185
265
 
186
266
  if (std::fseek(file.get(), 0L, SEEK_END) != 0)
187
267
  return result.failed("Can't infer file size");
@@ -207,8 +287,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
207
287
 
208
288
  result.config.exclude_vectors = false;
209
289
  result.config.use_64_bit_dimensions = false;
210
- if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
290
+ if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
291
+ fix_pre_2_10_metadata(result.head);
211
292
  return result;
293
+ }
212
294
  }
213
295
 
214
296
  // Check if it starts with 64-bit
@@ -222,8 +304,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
222
304
  // Check if it starts with 64-bit
223
305
  result.config.exclude_vectors = false;
224
306
  result.config.use_64_bit_dimensions = true;
225
- if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
307
+ if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
308
+ fix_pre_2_10_metadata(result.head);
226
309
  return result;
310
+ }
227
311
  }
228
312
 
229
313
  return result.failed("Not a dense USearch index!");
@@ -318,7 +402,7 @@ class index_dense_gt {
318
402
 
319
403
  private:
320
404
  /// @brief Schema: input buffer, bytes in input buffer, output buffer.
321
- using cast_t = std::function<bool(byte_t const*, std::size_t, byte_t*)>;
405
+ using cast_t = bool (*)(byte_t const*, std::size_t, byte_t*);
322
406
  /// @brief Punned index.
323
407
  using index_t = index_gt< //
324
408
  distance_t, vector_key_t, compressed_slot_t, //
@@ -353,7 +437,10 @@ class index_dense_gt {
353
437
  index_dense_config_t config_;
354
438
  index_t* typed_ = nullptr;
355
439
 
356
- mutable std::vector<byte_t> cast_buffer_;
440
+ using cast_buffer_t = buffer_gt<byte_t, dynamic_allocator_t>;
441
+
442
+ /// @brief Temporary memory for every thread to store a casted vector.
443
+ mutable cast_buffer_t cast_buffer_;
357
444
  struct casts_t {
358
445
  cast_t from_b1x8;
359
446
  cast_t from_i8;
@@ -375,11 +462,17 @@ class index_dense_gt {
375
462
  /// @brief Allocator for the copied vectors, aligned to widest double-precision scalars.
376
463
  vectors_tape_allocator_t vectors_tape_allocator_;
377
464
 
465
+ using vectors_lookup_allocator_t = aligned_allocator_gt<byte_t*, 64>;
466
+ using vectors_lookup_t = buffer_gt<byte_t*, vectors_lookup_allocator_t>;
467
+
378
468
  /// @brief For every managed `compressed_slot_t` stores a pointer to the allocated vector copy.
379
- mutable std::vector<byte_t*> vectors_lookup_;
469
+ mutable vectors_lookup_t vectors_lookup_;
470
+
471
+ using available_threads_allocator_t = aligned_allocator_gt<std::size_t, 64>;
472
+ using available_threads_t = ring_gt<std::size_t, available_threads_allocator_t>;
380
473
 
381
- /// @brief Originally forms and array of integers [0, threads], marking all
382
- mutable std::vector<std::size_t> available_threads_;
474
+ /// @brief Originally forms and array of integers [0, threads], marking all as available.
475
+ mutable available_threads_t available_threads_;
383
476
 
384
477
  /// @brief Mutex, controlling concurrent access to `available_threads_`.
385
478
  mutable std::mutex available_threads_mutex_;
@@ -402,8 +495,8 @@ class index_dense_gt {
402
495
 
403
496
  struct lookup_key_hash_t {
404
497
  using is_transparent = void;
405
- std::size_t operator()(key_and_slot_t const& k) const noexcept { return std::hash<vector_key_t>{}(k.key); }
406
- std::size_t operator()(vector_key_t const& k) const noexcept { return std::hash<vector_key_t>{}(k); }
498
+ std::size_t operator()(key_and_slot_t const& k) const noexcept { return hash_gt<vector_key_t>{}(k.key); }
499
+ std::size_t operator()(vector_key_t const& k) const noexcept { return hash_gt<vector_key_t>{}(k); }
407
500
  };
408
501
 
409
502
  struct lookup_key_same_t {
@@ -428,13 +521,57 @@ class index_dense_gt {
428
521
  /// @brief A constant for the reserved key value, used to mark deleted entries.
429
522
  vector_key_t free_key_ = default_free_value<vector_key_t>();
430
523
 
524
+ /// @brief Locks the thread for the duration of the operation.
525
+ struct thread_lock_t {
526
+ index_dense_gt const& parent;
527
+ std::size_t thread_id = 0;
528
+ bool engaged = false;
529
+
530
+ ~thread_lock_t() usearch_noexcept_m {
531
+ if (engaged)
532
+ parent.thread_unlock_(thread_id);
533
+ }
534
+
535
+ thread_lock_t(thread_lock_t const&) = delete;
536
+ thread_lock_t& operator=(thread_lock_t const&) = delete;
537
+
538
+ thread_lock_t(index_dense_gt const& parent, std::size_t thread_id, bool engaged = true) noexcept
539
+ : parent(parent), thread_id(thread_id), engaged(engaged) {}
540
+ thread_lock_t(thread_lock_t&& other) noexcept
541
+ : parent(other.parent), thread_id(other.thread_id), engaged(other.engaged) {
542
+ other.engaged = false;
543
+ }
544
+ };
545
+
431
546
  public:
432
- using search_result_t = typename index_t::search_result_t;
433
547
  using cluster_result_t = typename index_t::cluster_result_t;
434
548
  using add_result_t = typename index_t::add_result_t;
435
549
  using stats_t = typename index_t::stats_t;
436
550
  using match_t = typename index_t::match_t;
437
551
 
552
+ /**
553
+ * @brief A search result, containing the found keys and distances.
554
+ *
555
+ * As the `index_dense_gt` manages the thread-pool on its own, the search result
556
+ * preserves the thread-lock to avoid undefined behaviors, when other threads
557
+ * start overwriting the results.
558
+ */
559
+ struct search_result_t : public index_t::search_result_t {
560
+ inline search_result_t(index_dense_gt const& parent) noexcept
561
+ : index_t::search_result_t(), lock_(parent, 0, false) {}
562
+ search_result_t failed(error_t message) noexcept {
563
+ this->error = std::move(message);
564
+ return std::move(*this);
565
+ }
566
+
567
+ private:
568
+ friend class index_dense_gt;
569
+ thread_lock_t lock_;
570
+
571
+ inline search_result_t(typename index_t::search_result_t result, thread_lock_t lock) noexcept
572
+ : index_t::search_result_t(std::move(result)), lock_(std::move(lock)) {}
573
+ };
574
+
438
575
  index_dense_gt() = default;
439
576
  index_dense_gt(index_dense_gt&& other)
440
577
  : config_(std::move(other.config_)),
@@ -485,51 +622,76 @@ class index_dense_gt {
485
622
  typed_ = nullptr;
486
623
  }
487
624
 
625
+ struct state_result_t {
626
+ index_dense_gt index;
627
+ error_t error;
628
+
629
+ explicit operator bool() const noexcept { return !error; }
630
+ state_result_t failed(error_t message) noexcept {
631
+ error = std::move(message);
632
+ return std::move(*this);
633
+ }
634
+ operator index_dense_gt&&() && {
635
+ if (error)
636
+ __usearch_raise_runtime_error(error.what());
637
+ return std::move(index);
638
+ }
639
+ };
640
+ using copy_result_t = state_result_t;
641
+
488
642
  /**
489
643
  * @brief Constructs an instance of ::index_dense_gt.
490
644
  * @param[in] metric One of the provided or an @b ad-hoc metric, type-punned.
491
645
  * @param[in] config The index configuration (optional).
492
646
  * @param[in] free_key The key used for freed vectors (optional).
493
- * @return An instance of ::index_dense_gt.
647
+ * @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
648
+ *
649
+ * ! If the `metric` isn't provided in this method, it has to be set with
650
+ * ! the `change_metric` method before the index can be used. Alternatively,
651
+ * ! if you are loading an existing index, the metric will be set automatically.
494
652
  */
495
- static index_dense_gt make( //
496
- metric_t metric, //
653
+ static state_result_t make( //
654
+ metric_t metric = {}, //
497
655
  index_dense_config_t config = {}, //
498
656
  vector_key_t free_key = default_free_value<vector_key_t>()) {
499
657
 
500
- scalar_kind_t scalar_kind = metric.scalar_kind();
501
- std::size_t hardware_threads = std::thread::hardware_concurrency();
502
-
503
- index_dense_gt result;
504
- result.config_ = config;
505
- result.cast_buffer_.resize(hardware_threads * metric.bytes_per_vector());
506
- result.casts_ = make_casts_(scalar_kind);
507
- result.metric_ = metric;
508
- result.free_key_ = free_key;
509
-
510
- // Fill the thread IDs.
511
- result.available_threads_.resize(hardware_threads);
512
- std::iota(result.available_threads_.begin(), result.available_threads_.end(), 0ul);
513
-
514
- // Available since C11, but only C++17, so we use the C version.
658
+ if (metric.missing())
659
+ return state_result_t{}.failed("Metric won't be initialized!");
660
+ error_t error = config.validate();
661
+ if (error)
662
+ return state_result_t{}.failed(std::move(error));
515
663
  index_t* raw = index_allocator_t{}.allocate(1);
664
+ if (!raw)
665
+ return state_result_t{}.failed("Failed to allocate memory for the index!");
666
+
667
+ state_result_t result;
668
+ index_dense_gt& index = result.index;
669
+ index.config_ = config;
670
+ index.free_key_ = free_key;
671
+
672
+ // In some cases the metric is not provided, and will be set later.
673
+ if (metric) {
674
+ scalar_kind_t scalar_kind = metric.scalar_kind();
675
+ index.casts_ = make_casts_(scalar_kind);
676
+ index.metric_ = metric;
677
+ }
678
+
516
679
  new (raw) index_t(config);
517
- result.typed_ = raw;
680
+ index.typed_ = raw;
518
681
  return result;
519
682
  }
520
683
 
521
- static index_dense_gt make(char const* path, bool view = false) {
522
- index_dense_metadata_result_t meta = index_dense_metadata_from_path(path);
523
- if (!meta)
524
- return {};
525
- metric_punned_t metric(meta.head.dimensions, meta.head.kind_metric, meta.head.kind_scalar);
526
- index_dense_gt result = make(metric);
527
- if (!result)
528
- return result;
529
- if (view)
530
- result.view(path);
531
- else
532
- result.load(path);
684
+ /**
685
+ * @brief Constructs an instance of ::index_dense_gt from a serialized binary file.
686
+ * @param[in] path The path to the binary file.
687
+ * @param[in] view Whether to map the file into memory or load it.
688
+ * @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
689
+ */
690
+ static state_result_t make(char const* path, bool view = false) {
691
+ state_result_t result;
692
+ serialization_result_t serialization_result = view ? result.index.view(path) : result.index.load(path);
693
+ if (!serialization_result)
694
+ return result.failed(std::move(serialization_result.error));
533
695
  return result;
534
696
  }
535
697
 
@@ -537,19 +699,23 @@ class index_dense_gt {
537
699
  std::size_t connectivity() const { return typed_->connectivity(); }
538
700
  std::size_t size() const { return typed_->size() - free_keys_.size(); }
539
701
  std::size_t capacity() const { return typed_->capacity(); }
540
- std::size_t max_level() const noexcept { return typed_->max_level(); }
702
+ std::size_t max_level() const { return typed_->max_level(); }
541
703
  index_dense_config_t const& config() const { return config_; }
542
704
  index_limits_t const& limits() const { return typed_->limits(); }
543
705
  bool multi() const { return config_.multi; }
706
+ std::size_t currently_available_threads() const {
707
+ std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
708
+ return available_threads_.size();
709
+ }
544
710
 
545
711
  // The metric and its properties
546
712
  metric_t const& metric() const { return metric_; }
547
713
  void change_metric(metric_t metric) { metric_ = std::move(metric); }
548
714
 
549
- scalar_kind_t scalar_kind() const noexcept { return metric_.scalar_kind(); }
550
- std::size_t bytes_per_vector() const noexcept { return metric_.bytes_per_vector(); }
551
- std::size_t scalar_words() const noexcept { return metric_.scalar_words(); }
552
- std::size_t dimensions() const noexcept { return metric_.dimensions(); }
715
+ scalar_kind_t scalar_kind() const { return metric_.scalar_kind(); }
716
+ std::size_t bytes_per_vector() const { return metric_.bytes_per_vector(); }
717
+ std::size_t scalar_words() const { return metric_.scalar_words(); }
718
+ std::size_t dimensions() const { return metric_.dimensions(); }
553
719
 
554
720
  // Fetching and changing search criteria
555
721
  std::size_t expansion_add() const { return config_.expansion_add; }
@@ -559,8 +725,6 @@ class index_dense_gt {
559
725
 
560
726
  member_citerator_t cbegin() const { return typed_->cbegin(); }
561
727
  member_citerator_t cend() const { return typed_->cend(); }
562
- member_citerator_t begin() const { return typed_->begin(); }
563
- member_citerator_t end() const { return typed_->end(); }
564
728
  member_iterator_t begin() { return typed_->begin(); }
565
729
  member_iterator_t end() { return typed_->end(); }
566
730
 
@@ -641,6 +805,7 @@ class index_dense_gt {
641
805
  * exporting the mean, maximum, and minimum values.
642
806
  */
643
807
  aggregated_distances_t distance_between(vector_key_t a, vector_key_t b, std::size_t = any_thread()) const {
808
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled!");
644
809
  shared_lock_t lock(slot_lookup_mutex_);
645
810
  aggregated_distances_t result;
646
811
  if (!multi()) {
@@ -714,9 +879,8 @@ class index_dense_gt {
714
879
  cluster_config.thread = lock.thread_id;
715
880
  cluster_config.expansion = config_.expansion_search;
716
881
  metric_proxy_t metric{*this};
717
- auto allow = [free_key_ = this->free_key_](member_cref_t const& member) noexcept {
718
- return member.key != free_key_;
719
- };
882
+ vector_key_t free_key_copy = free_key_;
883
+ auto allow = [free_key_copy](member_cref_t const& member) noexcept { return member.key != free_key_copy; };
720
884
 
721
885
  // Find the closest cluster for any vector under that key.
722
886
  while (key_range.first != key_range.second) {
@@ -736,21 +900,54 @@ class index_dense_gt {
736
900
  /**
737
901
  * @brief Reserves memory for the index and the keyed lookup.
738
902
  * @return `true` if the memory reservation was successful, `false` otherwise.
903
+ *
904
+ * ! No update or search operations should be running during this operation.
739
905
  */
740
- bool reserve(index_limits_t limits) {
741
- {
906
+ bool try_reserve(index_limits_t limits) {
907
+
908
+ // The slot lookup system will generally prefer power-of-two sizes.
909
+ if (config_.enable_key_lookups) {
742
910
  unique_lock_t lock(slot_lookup_mutex_);
743
- slot_lookup_.reserve(limits.members);
744
- vectors_lookup_.resize(limits.members);
911
+ if (!slot_lookup_.try_reserve(limits.members))
912
+ return false;
913
+ limits.members = slot_lookup_.capacity();
914
+ }
745
915
 
746
- // During reserve, no insertions may be happening, so we can safely overwrite the whole collection.
747
- std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
748
- available_threads_.resize(limits.threads());
749
- std::iota(available_threads_.begin(), available_threads_.end(), 0ul);
916
+ // Once the `slot_lookup_` grows, let's use its capacity as the new
917
+ // target for the `vectors_lookup_` to synchronize allocations and
918
+ // expensive index re-organizations.
919
+ if (limits.members != vectors_lookup_.size()) {
920
+ vectors_lookup_t new_vectors_lookup(limits.members);
921
+ if (!new_vectors_lookup)
922
+ return false;
923
+ if (vectors_lookup_.size() > 0)
924
+ std::memcpy(new_vectors_lookup.data(), vectors_lookup_.data(),
925
+ vectors_lookup_.size() * sizeof(byte_t*));
926
+ vectors_lookup_ = std::move(new_vectors_lookup);
750
927
  }
928
+
929
+ // During reserve, no insertions may be happening, so we can safely overwrite the whole collection.
930
+ std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
931
+ available_threads_.clear();
932
+ if (!available_threads_.reserve(limits.threads()))
933
+ return false;
934
+ for (std::size_t i = 0; i < limits.threads(); i++)
935
+ available_threads_.push(i);
936
+
937
+ // Allocate a buffer for the casted vectors.
938
+ cast_buffer_t cast_buffer(limits.threads() * metric_.bytes_per_vector());
939
+ if (!cast_buffer)
940
+ return false;
941
+ cast_buffer_ = std::move(cast_buffer);
942
+
751
943
  return typed_->reserve(limits);
752
944
  }
753
945
 
946
+ void reserve(index_limits_t limits) {
947
+ if (!try_reserve(limits))
948
+ throw std::bad_alloc();
949
+ }
950
+
754
951
  /**
755
952
  * @brief Erases all the vectors from the index.
756
953
  *
@@ -763,7 +960,7 @@ class index_dense_gt {
763
960
  std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
764
961
  typed_->clear();
765
962
  slot_lookup_.clear();
766
- vectors_lookup_.clear();
963
+ vectors_lookup_.reset();
767
964
  free_keys_.clear();
768
965
  vectors_tape_allocator_.reset();
769
966
  }
@@ -776,19 +973,18 @@ class index_dense_gt {
776
973
  * If the index is memory-mapped - releases the mapping and the descriptor.
777
974
  */
778
975
  void reset() {
779
- unique_lock_t lookup_lock(slot_lookup_mutex_);
780
976
 
977
+ unique_lock_t lookup_lock(slot_lookup_mutex_);
781
978
  std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
782
979
  std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
783
- typed_->reset();
980
+
981
+ if (typed_)
982
+ typed_->reset();
784
983
  slot_lookup_.clear();
785
- vectors_lookup_.clear();
984
+ vectors_lookup_.reset();
786
985
  free_keys_.clear();
787
986
  vectors_tape_allocator_.reset();
788
-
789
- // Reset the thread IDs.
790
- available_threads_.resize(std::thread::hardware_concurrency());
791
- std::iota(available_threads_.begin(), available_threads_.end(), 0ul);
987
+ available_threads_.reset();
792
988
  }
793
989
 
794
990
  /**
@@ -867,7 +1063,7 @@ class index_dense_gt {
867
1063
  /**
868
1064
  * @brief Estimate the binary length (in bytes) of the serialized index.
869
1065
  */
870
- std::size_t serialized_length(serialization_config_t config = {}) const noexcept {
1066
+ std::size_t serialized_length(serialization_config_t config = {}) const {
871
1067
  std::size_t dimensions_length = 0;
872
1068
  std::size_t matrix_length = 0;
873
1069
  if (!config.exclude_vectors) {
@@ -889,6 +1085,7 @@ class index_dense_gt {
889
1085
  progress_at&& progress = {}) {
890
1086
 
891
1087
  // Discard all previous memory allocations of `vectors_tape_allocator_`
1088
+ index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
892
1089
  reset();
893
1090
 
894
1091
  // Infer the new index size
@@ -913,7 +1110,9 @@ class index_dense_gt {
913
1110
  matrix_cols = dimensions[1];
914
1111
  }
915
1112
  // Load the vectors one after another
916
- vectors_lookup_.resize(matrix_rows);
1113
+ vectors_lookup_ = vectors_lookup_t(matrix_rows);
1114
+ if (!vectors_lookup_)
1115
+ return result.failed("Failed to allocate memory to address vectors");
917
1116
  for (std::uint64_t slot = 0; slot != matrix_rows; ++slot) {
918
1117
  byte_t* vector = vectors_tape_allocator_.allocate(matrix_cols);
919
1118
  if (!input(vector, matrix_cols))
@@ -932,6 +1131,9 @@ class index_dense_gt {
932
1131
  if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
933
1132
  return result.failed("Magic header mismatch - the file isn't an index");
934
1133
 
1134
+ // fix pre-2.10 headers
1135
+ fix_pre_2_10_metadata(head);
1136
+
935
1137
  // Validate the software version
936
1138
  if (head.version_major != USEARCH_VERSION_MAJOR)
937
1139
  return result.failed("File format may be different, please rebuild");
@@ -944,16 +1146,38 @@ class index_dense_gt {
944
1146
 
945
1147
  config_.multi = head.multi;
946
1148
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
947
- cast_buffer_.resize(available_threads_.size() * metric_.bytes_per_vector());
1149
+ cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
1150
+ if (!cast_buffer_)
1151
+ return result.failed("Failed to allocate memory for the casts");
948
1152
  casts_ = make_casts_(head.kind_scalar);
949
1153
  }
950
1154
 
951
1155
  // Pull the actual proximity graph
1156
+ if (!typed_) {
1157
+ index_t* raw = index_allocator_t{}.allocate(1);
1158
+ if (!raw)
1159
+ return result.failed("Failed to allocate memory for the index");
1160
+ new (raw) index_t(config_);
1161
+ typed_ = raw;
1162
+ }
952
1163
  result = typed_->load_from_stream(std::forward<input_callback_at>(input), std::forward<progress_at>(progress));
953
1164
  if (!result)
954
1165
  return result;
955
1166
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
956
1167
  return result.failed("Index size and the number of vectors doesn't match");
1168
+ old_limits.members = static_cast<std::size_t>(matrix_rows);
1169
+ if (!typed_->try_reserve(old_limits))
1170
+ return result.failed("Failed to reserve memory for the index");
1171
+
1172
+ // After the index is loaded, we may have to resize the `available_threads_` to
1173
+ // match the limits of the underlying engine.
1174
+ available_threads_t available_threads;
1175
+ std::size_t max_threads = old_limits.threads();
1176
+ if (!available_threads.reserve(max_threads))
1177
+ return result.failed("Failed to allocate memory for the available threads!");
1178
+ for (std::size_t i = 0; i < max_threads; i++)
1179
+ available_threads.push(i);
1180
+ available_threads_ = std::move(available_threads);
957
1181
 
958
1182
  reindex_keys_();
959
1183
  return result;
@@ -971,6 +1195,7 @@ class index_dense_gt {
971
1195
  progress_at&& progress = {}) {
972
1196
 
973
1197
  // Discard all previous memory allocations of `vectors_tape_allocator_`
1198
+ index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
974
1199
  reset();
975
1200
 
976
1201
  serialization_result_t result = file.open_if_not();
@@ -1018,6 +1243,9 @@ class index_dense_gt {
1018
1243
  if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
1019
1244
  return result.failed("Magic header mismatch - the file isn't an index");
1020
1245
 
1246
+ // fix pre-2.10 headers
1247
+ fix_pre_2_10_metadata(head);
1248
+
1021
1249
  // Validate the software version
1022
1250
  if (head.version_major != USEARCH_VERSION_MAJOR)
1023
1251
  return result.failed("File format may be different, please rebuild");
@@ -1030,24 +1258,48 @@ class index_dense_gt {
1030
1258
 
1031
1259
  config_.multi = head.multi;
1032
1260
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1033
- cast_buffer_.resize(available_threads_.size() * metric_.bytes_per_vector());
1261
+ cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
1262
+ if (!cast_buffer_)
1263
+ return result.failed("Failed to allocate memory for the casts");
1034
1264
  casts_ = make_casts_(head.kind_scalar);
1035
1265
  offset += sizeof(buffer);
1036
1266
  }
1037
1267
 
1038
1268
  // Pull the actual proximity graph
1269
+ if (!typed_) {
1270
+ index_t* raw = index_allocator_t{}.allocate(1);
1271
+ if (!raw)
1272
+ return result.failed("Failed to allocate memory for the index");
1273
+ new (raw) index_t(config_);
1274
+ typed_ = raw;
1275
+ }
1039
1276
  result = typed_->view(std::move(file), offset, std::forward<progress_at>(progress));
1040
1277
  if (!result)
1041
1278
  return result;
1042
1279
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
1043
1280
  return result.failed("Index size and the number of vectors doesn't match");
1281
+ old_limits.members = static_cast<std::size_t>(matrix_rows);
1282
+ if (!typed_->try_reserve(old_limits))
1283
+ return result.failed("Failed to reserve memory for the index");
1044
1284
 
1045
1285
  // Address the vectors
1046
- vectors_lookup_.resize(matrix_rows);
1286
+ vectors_lookup_ = vectors_lookup_t(matrix_rows);
1287
+ if (!vectors_lookup_)
1288
+ return result.failed("Failed to allocate memory to address vectors");
1047
1289
  if (!config.exclude_vectors)
1048
1290
  for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
1049
1291
  vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
1050
1292
 
1293
+ // After the index is loaded, we may have to resize the `available_threads_` to
1294
+ // match the limits of the underlying engine.
1295
+ available_threads_t available_threads;
1296
+ std::size_t max_threads = old_limits.threads();
1297
+ if (!available_threads.reserve(max_threads))
1298
+ return result.failed("Failed to allocate memory for the available threads!");
1299
+ for (std::size_t i = 0; i < max_threads; i++)
1300
+ available_threads.push(i);
1301
+ available_threads_ = std::move(available_threads);
1302
+
1051
1303
  reindex_keys_();
1052
1304
  return result;
1053
1305
  }
@@ -1180,6 +1432,7 @@ class index_dense_gt {
1180
1432
  * @return `true` if the key is present in the index, `false` otherwise.
1181
1433
  */
1182
1434
  bool contains(vector_key_t key) const {
1435
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
1183
1436
  shared_lock_t lock(slot_lookup_mutex_);
1184
1437
  return slot_lookup_.contains(key_and_slot_t::any_slot(key));
1185
1438
  }
@@ -1189,6 +1442,7 @@ class index_dense_gt {
1189
1442
  * @return Zero if nothing is found, a positive integer otherwise.
1190
1443
  */
1191
1444
  std::size_t count(vector_key_t key) const {
1445
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
1192
1446
  shared_lock_t lock(slot_lookup_mutex_);
1193
1447
  return slot_lookup_.count(key_and_slot_t::any_slot(key));
1194
1448
  }
@@ -1213,6 +1467,7 @@ class index_dense_gt {
1213
1467
  * If an error occurred during the removal operation, `result.error` will contain an error message.
1214
1468
  */
1215
1469
  labeling_result_t remove(vector_key_t key) {
1470
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
1216
1471
  labeling_result_t result;
1217
1472
 
1218
1473
  unique_lock_t lookup_lock(slot_lookup_mutex_);
@@ -1223,7 +1478,8 @@ class index_dense_gt {
1223
1478
  // Grow the removed entries ring, if needed
1224
1479
  std::size_t matching_count = std::distance(matching_slots.first, matching_slots.second);
1225
1480
  std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
1226
- if (!free_keys_.reserve(free_keys_.size() + matching_count))
1481
+ std::size_t free_count_old = free_keys_.size();
1482
+ if (!free_keys_.reserve(free_count_old + matching_count))
1227
1483
  return result.failed("Can't allocate memory for a free-list");
1228
1484
 
1229
1485
  // A removed entry would be:
@@ -1237,6 +1493,7 @@ class index_dense_gt {
1237
1493
  }
1238
1494
  slot_lookup_.erase(key);
1239
1495
  result.completed = matching_count;
1496
+ usearch_assert_m(free_keys_.size() == free_count_old + matching_count, "Free keys count mismatch");
1240
1497
 
1241
1498
  return result;
1242
1499
  }
@@ -1251,6 +1508,7 @@ class index_dense_gt {
1251
1508
  */
1252
1509
  template <typename keys_iterator_at>
1253
1510
  labeling_result_t remove(keys_iterator_at keys_begin, keys_iterator_at keys_end) {
1511
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
1254
1512
 
1255
1513
  labeling_result_t result;
1256
1514
  unique_lock_t lookup_lock(slot_lookup_mutex_);
@@ -1337,17 +1595,6 @@ class index_dense_gt {
1337
1595
  });
1338
1596
  }
1339
1597
 
1340
- struct copy_result_t {
1341
- index_dense_gt index;
1342
- error_t error;
1343
-
1344
- explicit operator bool() const noexcept { return !error; }
1345
- copy_result_t failed(error_t message) noexcept {
1346
- error = std::move(message);
1347
- return std::move(*this);
1348
- }
1349
- };
1350
-
1351
1598
  /**
1352
1599
  * @brief Copies the ::index_dense_gt @b with all the data in it.
1353
1600
  * @param config The copy configuration (optional).
@@ -1370,19 +1617,22 @@ class index_dense_gt {
1370
1617
  copy.free_keys_.push(free_keys_[i]);
1371
1618
 
1372
1619
  // Allocate buffers and move the vectors themselves
1373
- if (!config.force_vector_copy && copy.config_.exclude_vectors)
1374
- copy.vectors_lookup_ = vectors_lookup_;
1375
- else {
1376
- copy.vectors_lookup_.resize(vectors_lookup_.size());
1377
- for (std::size_t slot = 0; slot != vectors_lookup_.size(); ++slot)
1620
+ copy.vectors_lookup_ = vectors_lookup_t(vectors_lookup_.size());
1621
+ if (!copy.vectors_lookup_)
1622
+ return result.failed("Out of memory!");
1623
+ if (!config.force_vector_copy && copy.config_.exclude_vectors) {
1624
+ std::memcpy(copy.vectors_lookup_.data(), vectors_lookup_.data(), vectors_lookup_.size() * sizeof(byte_t*));
1625
+ } else {
1626
+ std::size_t slots_count = typed_result.index.size();
1627
+ for (std::size_t slot = 0; slot != slots_count; ++slot)
1378
1628
  copy.vectors_lookup_[slot] = copy.vectors_tape_allocator_.allocate(copy.metric_.bytes_per_vector());
1379
- if (std::count(copy.vectors_lookup_.begin(), copy.vectors_lookup_.end(), nullptr))
1629
+ if (std::count(copy.vectors_lookup_.begin(), copy.vectors_lookup_.begin() + slots_count, nullptr))
1380
1630
  return result.failed("Out of memory!");
1381
- for (std::size_t slot = 0; slot != vectors_lookup_.size(); ++slot)
1631
+ for (std::size_t slot = 0; slot != slots_count; ++slot)
1382
1632
  std::memcpy(copy.vectors_lookup_[slot], vectors_lookup_[slot], metric_.bytes_per_vector());
1383
1633
  }
1384
1634
 
1385
- copy.slot_lookup_ = slot_lookup_;
1635
+ copy.slot_lookup_ = slot_lookup_; // TODO: Handle out of memory
1386
1636
  *copy.typed_ = std::move(typed_result.index);
1387
1637
  return result;
1388
1638
  }
@@ -1392,22 +1642,34 @@ class index_dense_gt {
1392
1642
  * @return A similarly configured ::index_dense_gt instance.
1393
1643
  */
1394
1644
  copy_result_t fork() const {
1645
+
1646
+ cast_buffer_t cast_buffer(cast_buffer_.size());
1647
+ if (!cast_buffer)
1648
+ return state_result_t{}.failed("Failed to allocate memory for the casts!");
1649
+ available_threads_t available_threads;
1650
+ std::size_t max_threads = limits().threads();
1651
+ if (!available_threads.reserve(max_threads))
1652
+ return state_result_t{}.failed("Failed to allocate memory for the available threads!");
1653
+ for (std::size_t i = 0; i < max_threads; i++)
1654
+ available_threads.push(i);
1655
+ index_t* raw = index_allocator_t{}.allocate(1);
1656
+ if (!raw)
1657
+ return state_result_t{}.failed("Failed to allocate memory for the index!");
1658
+
1395
1659
  copy_result_t result;
1396
1660
  index_dense_gt& other = result.index;
1397
-
1661
+ index_limits_t other_limits = limits();
1662
+ other_limits.members = 0;
1398
1663
  other.config_ = config_;
1399
- other.cast_buffer_ = cast_buffer_;
1664
+ other.cast_buffer_ = std::move(cast_buffer);
1400
1665
  other.casts_ = casts_;
1401
1666
 
1402
1667
  other.metric_ = metric_;
1403
- other.available_threads_ = available_threads_;
1668
+ other.available_threads_ = std::move(available_threads);
1404
1669
  other.free_key_ = free_key_;
1405
1670
 
1406
- index_t* raw = index_allocator_t{}.allocate(1);
1407
- if (!raw)
1408
- return result.failed("Can't allocate the index");
1409
-
1410
1671
  new (raw) index_t(config());
1672
+ raw->try_reserve(other_limits);
1411
1673
  other.typed_ = raw;
1412
1674
  return result;
1413
1675
  }
@@ -1466,7 +1728,10 @@ class index_dense_gt {
1466
1728
  compaction_result_t compact(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
1467
1729
  compaction_result_t result;
1468
1730
 
1469
- std::vector<byte_t*> new_vectors_lookup(vectors_lookup_.size());
1731
+ vectors_lookup_t new_vectors_lookup(vectors_lookup_.size());
1732
+ if (!new_vectors_lookup)
1733
+ return result.failed("Out of memory!");
1734
+
1470
1735
  vectors_tape_allocator_t new_vectors_allocator;
1471
1736
 
1472
1737
  auto track_slot_change = [&](vector_key_t, compressed_slot_t old_slot, compressed_slot_t new_slot) {
@@ -1703,31 +1968,21 @@ class index_dense_gt {
1703
1968
  }
1704
1969
 
1705
1970
  private:
1706
- struct thread_lock_t {
1707
- index_dense_gt const& parent;
1708
- std::size_t thread_id;
1709
- bool engaged;
1710
-
1711
- ~thread_lock_t() {
1712
- if (engaged)
1713
- parent.thread_unlock_(thread_id);
1714
- }
1715
- };
1716
-
1717
- thread_lock_t thread_lock_(std::size_t thread_id) const {
1971
+ thread_lock_t thread_lock_(std::size_t thread_id) const usearch_noexcept_m {
1718
1972
  if (thread_id != any_thread())
1719
1973
  return {*this, thread_id, false};
1720
1974
 
1721
1975
  available_threads_mutex_.lock();
1722
- thread_id = available_threads_.back();
1723
- available_threads_.pop_back();
1976
+ usearch_assert_m(available_threads_.size(), "No available threads to lock");
1977
+ available_threads_.try_pop(thread_id);
1724
1978
  available_threads_mutex_.unlock();
1725
1979
  return {*this, thread_id, true};
1726
1980
  }
1727
1981
 
1728
- void thread_unlock_(std::size_t thread_id) const {
1982
+ void thread_unlock_(std::size_t thread_id) const usearch_noexcept_m {
1729
1983
  available_threads_mutex_.lock();
1730
- available_threads_.push_back(thread_id);
1984
+ usearch_assert_m(available_threads_.size() < available_threads_.capacity(), "Too many threads unlocked");
1985
+ available_threads_.push(thread_id);
1731
1986
  available_threads_mutex_.unlock();
1732
1987
  }
1733
1988
 
@@ -1736,7 +1991,7 @@ class index_dense_gt {
1736
1991
  vector_key_t key, scalar_at const* vector, //
1737
1992
  std::size_t thread, bool force_vector_copy, cast_t const& cast) {
1738
1993
 
1739
- if (!multi() && contains(key))
1994
+ if (!multi() && config().enable_key_lookups && contains(key))
1740
1995
  return add_result_t{}.failed("Duplicate keys not allowed in high-level wrappers");
1741
1996
 
1742
1997
  // Cast the vector, if needed for compatibility with `metric_`
@@ -1760,8 +2015,10 @@ class index_dense_gt {
1760
2015
  // Perform the insertion or the update
1761
2016
  bool reuse_node = free_slot != default_free_value<compressed_slot_t>();
1762
2017
  auto on_success = [&](member_ref_t member) {
1763
- unique_lock_t slot_lock(slot_lookup_mutex_);
1764
- slot_lookup_.try_emplace(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
2018
+ if (config_.enable_key_lookups) {
2019
+ unique_lock_t slot_lock(slot_lookup_mutex_);
2020
+ slot_lookup_.try_emplace(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
2021
+ }
1765
2022
  if (copy_vector) {
1766
2023
  if (!reuse_node)
1767
2024
  vectors_lookup_[member.slot] = vectors_tape_allocator_.allocate(metric_.bytes_per_vector());
@@ -1799,16 +2056,19 @@ class index_dense_gt {
1799
2056
  search_config.expansion = config_.expansion_search;
1800
2057
  search_config.exact = exact;
1801
2058
 
2059
+ vector_key_t free_key_copy = free_key_;
1802
2060
  if (std::is_same<typename std::decay<predicate_at>::type, dummy_predicate_t>::value) {
1803
- auto allow = [free_key_ = this->free_key_](member_cref_t const& member) noexcept {
1804
- return member.key != free_key_;
2061
+ auto allow = [free_key_copy](member_cref_t const& member) noexcept {
2062
+ return (vector_key_t)member.key != free_key_copy;
1805
2063
  };
1806
- return typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
2064
+ auto typed_result = typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
2065
+ return search_result_t{std::move(typed_result), std::move(lock)};
1807
2066
  } else {
1808
- auto allow = [free_key_ = this->free_key_, &predicate](member_cref_t const& member) noexcept {
1809
- return member.key != free_key_ && predicate(member.key);
2067
+ auto allow = [free_key_copy, &predicate](member_cref_t const& member) noexcept {
2068
+ return (vector_key_t)member.key != free_key_copy && predicate(member.key);
1810
2069
  };
1811
- return typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
2070
+ auto typed_result = typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
2071
+ return search_result_t{std::move(typed_result), std::move(lock)};
1812
2072
  }
1813
2073
  }
1814
2074
 
@@ -1831,9 +2091,8 @@ class index_dense_gt {
1831
2091
  cluster_config.thread = lock.thread_id;
1832
2092
  cluster_config.expansion = config_.expansion_search;
1833
2093
 
1834
- auto allow = [free_key_ = this->free_key_](member_cref_t const& member) noexcept {
1835
- return member.key != free_key_;
1836
- };
2094
+ vector_key_t free_key_copy = free_key_;
2095
+ auto allow = [free_key_copy](member_cref_t const& member) noexcept { return member.key != free_key_copy; };
1837
2096
  return typed_->cluster(vector_data, level, metric_proxy_t{*this}, cluster_config, allow);
1838
2097
  }
1839
2098
 
@@ -1853,6 +2112,7 @@ class index_dense_gt {
1853
2112
  }
1854
2113
 
1855
2114
  // Check if such `key` is even present.
2115
+ usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled!");
1856
2116
  shared_lock_t slots_lock(slot_lookup_mutex_);
1857
2117
  auto key_range = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
1858
2118
  aggregated_distances_t result;
@@ -1889,7 +2149,8 @@ class index_dense_gt {
1889
2149
  std::size_t count_total = typed_->size();
1890
2150
  std::size_t count_removed = 0;
1891
2151
  for (std::size_t i = 0; i != count_total; ++i) {
1892
- member_cref_t member = typed_->at(i);
2152
+ auto member_slot = static_cast<compressed_slot_t>(i);
2153
+ member_cref_t member = typed_->at(member_slot);
1893
2154
  count_removed += member.key == free_key_;
1894
2155
  }
1895
2156
 
@@ -1905,11 +2166,12 @@ class index_dense_gt {
1905
2166
  free_keys_.clear();
1906
2167
  free_keys_.reserve(count_removed);
1907
2168
  for (std::size_t i = 0; i != typed_->size(); ++i) {
1908
- member_cref_t member = typed_->at(i);
2169
+ auto member_slot = static_cast<compressed_slot_t>(i);
2170
+ member_cref_t member = typed_->at(member_slot);
1909
2171
  if (member.key == free_key_)
1910
- free_keys_.push(static_cast<compressed_slot_t>(i));
2172
+ free_keys_.push(member_slot);
1911
2173
  else if (config_.enable_key_lookups)
1912
- slot_lookup_.try_emplace(key_and_slot_t{vector_key_t(member.key), static_cast<compressed_slot_t>(i)});
2174
+ slot_lookup_.try_emplace(key_and_slot_t{vector_key_t(member.key), member_slot});
1913
2175
  }
1914
2176
  }
1915
2177
 
@@ -1953,17 +2215,17 @@ class index_dense_gt {
1953
2215
  template <typename to_scalar_at> static casts_t make_casts_() {
1954
2216
  casts_t result;
1955
2217
 
1956
- result.from_b1x8 = cast_gt<b1x8_t, to_scalar_at>{};
1957
- result.from_i8 = cast_gt<i8_t, to_scalar_at>{};
1958
- result.from_f16 = cast_gt<f16_t, to_scalar_at>{};
1959
- result.from_f32 = cast_gt<f32_t, to_scalar_at>{};
1960
- result.from_f64 = cast_gt<f64_t, to_scalar_at>{};
2218
+ result.from_b1x8 = &cast_gt<b1x8_t, to_scalar_at>::try_;
2219
+ result.from_i8 = &cast_gt<i8_t, to_scalar_at>::try_;
2220
+ result.from_f16 = &cast_gt<f16_t, to_scalar_at>::try_;
2221
+ result.from_f32 = &cast_gt<f32_t, to_scalar_at>::try_;
2222
+ result.from_f64 = &cast_gt<f64_t, to_scalar_at>::try_;
1961
2223
 
1962
- result.to_b1x8 = cast_gt<to_scalar_at, b1x8_t>{};
1963
- result.to_i8 = cast_gt<to_scalar_at, i8_t>{};
1964
- result.to_f16 = cast_gt<to_scalar_at, f16_t>{};
1965
- result.to_f32 = cast_gt<to_scalar_at, f32_t>{};
1966
- result.to_f64 = cast_gt<to_scalar_at, f64_t>{};
2224
+ result.to_b1x8 = &cast_gt<to_scalar_at, b1x8_t>::try_;
2225
+ result.to_i8 = &cast_gt<to_scalar_at, i8_t>::try_;
2226
+ result.to_f16 = &cast_gt<to_scalar_at, f16_t>::try_;
2227
+ result.to_f32 = &cast_gt<to_scalar_at, f32_t>::try_;
2228
+ result.to_f64 = &cast_gt<to_scalar_at, f64_t>::try_;
1967
2229
 
1968
2230
  return result;
1969
2231
  }
@@ -2013,7 +2275,7 @@ static join_result_t join( //
2013
2275
  man_to_woman_at&& man_to_woman = man_to_woman_at{}, //
2014
2276
  woman_to_man_at&& woman_to_man = woman_to_man_at{}, //
2015
2277
  executor_at&& executor = executor_at{}, //
2016
- progress_at&& progress = progress_at{}) noexcept {
2278
+ progress_at&& progress = progress_at{}) {
2017
2279
 
2018
2280
  return men.join( //
2019
2281
  women, config, //