usearch 0.2.0 → 0.2.2

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.
@@ -6,7 +6,30 @@
6
6
  *
7
7
  * @copyright Copyright (c) 2023
8
8
  */
9
- #pragma once
9
+ #ifndef UNUM_USEARCH_H
10
+ #define UNUM_USEARCH_H
11
+
12
+ #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
13
+ #define _USE_MATH_DEFINES
14
+
15
+ #include <Windows.h>
16
+
17
+ #define usearch_pack_m
18
+ #define usearch_align_m __declspec(align(64))
19
+ #define WINDOWS
20
+
21
+ #else
22
+ #include <fcntl.h> // `fallocate`
23
+ #include <stdlib.h> // `posix_memalign`
24
+ #include <sys/mman.h> // `mmap`
25
+ #include <unistd.h> // `open`, `close`
26
+
27
+ #define usearch_pack_m __attribute__((packed))
28
+ #define usearch_align_m __attribute__((aligned(64)))
29
+ #endif
30
+
31
+ #include <sys/stat.h> // `fstat` for file size
32
+
10
33
  #include <algorithm> // `std::sort_heap`
11
34
  #include <atomic> // `std::atomic`
12
35
  #include <bitset> // `std::bitset`
@@ -15,18 +38,10 @@
15
38
  #include <cstring> // `std::memset`
16
39
  #include <mutex> // `std::unique_lock` - replacement candidate
17
40
  #include <random> // `std::default_random_engine` - replacement candidate
18
- #include <unistd.h> // `open`, `close`
41
+ #include <stdexcept> // `std::runtime_exception`
19
42
  #include <utility> // `std::exchange`
20
43
  #include <vector> // `std::vector`
21
44
 
22
- #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
23
- #else
24
- #include <fcntl.h> // `fallocate`
25
- #include <stdlib.h> // `posix_memalign`
26
- #include <sys/mman.h> // `mmap`
27
- #include <sys/stat.h> // `fstat` for file size
28
- #endif
29
-
30
45
  #if defined(__GNUC__)
31
46
  // https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
32
47
  // Zero means we are only going to read from that memory.
@@ -39,7 +54,7 @@
39
54
  #define prefetch_m(ptr)
40
55
  #endif
41
56
 
42
- #ifdef NDEBUG
57
+ #if defined(NDEBUG)
43
58
  #define assert_m(must_be_true, message)
44
59
  #else
45
60
  #define assert_m(must_be_true, message) \
@@ -48,21 +63,12 @@
48
63
  }
49
64
  #endif
50
65
 
51
- #define usearch_align_m __attribute__((aligned(64)))
52
- #define usearch_pack_m __attribute__((packed))
53
-
54
66
  namespace unum {
55
67
  namespace usearch {
56
68
 
57
69
  using f64_t = double;
58
70
  using f32_t = float;
59
71
 
60
- /**
61
- * GCC supports half-precision types.
62
- * https://gcc.gnu.org/onlinedocs/gcc/Half-Precision.html
63
- */
64
- enum class f16_t : std::uint16_t {};
65
-
66
72
  template <typename at> at angle_to_radians(at angle) noexcept { return angle * M_PI / 180.f; }
67
73
 
68
74
  template <typename at> at square(at value) noexcept { return value * value; }
@@ -79,11 +85,12 @@ inline std::size_t ceil2(std::size_t v) noexcept {
79
85
  return v;
80
86
  }
81
87
 
82
- template <typename scalar_at> struct ip_gt {
88
+ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
83
89
  using scalar_t = scalar_at;
84
- using result_type = scalar_t;
90
+ using result_t = result_at;
91
+ using result_type = result_t;
85
92
 
86
- inline scalar_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
93
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
87
94
  result_type ab{};
88
95
  #if defined(__GNUC__)
89
96
  #pragma GCC ivdep
@@ -93,17 +100,18 @@ template <typename scalar_at> struct ip_gt {
93
100
  #pragma omp simd reduction(+ : ab)
94
101
  #endif
95
102
  for (std::size_t i = 0; i != dim; ++i)
96
- ab += a[i] * b[i];
103
+ ab += result_t(a[i]) * result_t(b[i]);
97
104
  return 1 - ab;
98
105
  }
99
106
  };
100
107
 
101
- template <typename scalar_at> struct cos_gt {
108
+ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
102
109
  using scalar_t = scalar_at;
103
- using result_type = scalar_t;
110
+ using result_t = result_at;
111
+ using result_type = result_t;
104
112
 
105
- inline scalar_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
106
- result_type ab{}, a2{}, b2{};
113
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
114
+ result_t ab{}, a2{}, b2{};
107
115
  #if defined(__GNUC__)
108
116
  #pragma GCC ivdep
109
117
  #elif defined(__clang__)
@@ -112,17 +120,20 @@ template <typename scalar_at> struct cos_gt {
112
120
  #pragma omp simd reduction(+ : ab, a2, b2)
113
121
  #endif
114
122
  for (std::size_t i = 0; i != dim; ++i)
115
- ab += a[i] * b[i], a2 += a[i] * a[i], b2 += b[i] * b[i];
123
+ ab += result_t(a[i]) * result_t(b[i]), //
124
+ a2 += square<result_t>(a[i]), //
125
+ b2 += square<result_t>(b[i]);
116
126
  return ab / (std::sqrt(a2) * std::sqrt(b2));
117
127
  }
118
128
  };
119
129
 
120
- template <typename scalar_at> struct l2_squared_gt {
130
+ template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
121
131
  using scalar_t = scalar_at;
122
- using result_type = scalar_t;
132
+ using result_t = result_at;
133
+ using result_type = result_t;
123
134
 
124
- inline scalar_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
125
- result_type ab_deltas_sq{};
135
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
136
+ result_t ab_deltas_sq{};
126
137
  #if defined(__GNUC__)
127
138
  #pragma GCC ivdep
128
139
  #elif defined(__clang__)
@@ -131,7 +142,7 @@ template <typename scalar_at> struct l2_squared_gt {
131
142
  #pragma omp simd reduction(+ : ab_deltas_sq)
132
143
  #endif
133
144
  for (std::size_t i = 0; i != dim; ++i)
134
- ab_deltas_sq += (a[i] - b[i]) * (a[i] - b[i]);
145
+ ab_deltas_sq += square(result_t(a[i]) - result_t(b[i]));
135
146
  return ab_deltas_sq;
136
147
  }
137
148
  };
@@ -140,13 +151,14 @@ template <typename scalar_at> struct l2_squared_gt {
140
151
  * @brief Hamming distance computes the number of differing elements in
141
152
  * two arrays. An example would be a chess board.
142
153
  */
143
- template <typename scalar_at> struct hamming_gt {
154
+ template <typename scalar_at, typename result_at = std::size_t> struct hamming_gt {
144
155
  using scalar_t = scalar_at;
145
- using result_type = std::size_t;
156
+ using result_t = result_at;
157
+ using result_type = result_t;
146
158
 
147
- inline std::size_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
148
- std::size_t = 0) const noexcept {
149
- std::size_t matches{};
159
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
160
+ std::size_t = 0) const noexcept {
161
+ result_t matches{};
150
162
  #if defined(__GNUC__)
151
163
  #pragma GCC ivdep
152
164
  #elif defined(__clang__)
@@ -165,15 +177,16 @@ template <typename scalar_at> struct hamming_gt {
165
177
  * two arrays of integers. An example would be a textual document,
166
178
  * tokenized and hashed into a fixed-capacity bitset.
167
179
  */
168
- template <typename scalar_at> struct bit_hamming_gt {
180
+ template <typename scalar_at, typename result_at = std::size_t> struct bit_hamming_gt {
169
181
  using scalar_t = scalar_at;
170
- using result_type = std::size_t;
182
+ using result_t = result_at;
183
+ using result_type = result_t;
171
184
  static_assert(std::is_unsigned<scalar_t>::value, "Hamming distance requires unsigned integral words");
172
185
  static constexpr std::size_t bits_per_word_k = sizeof(scalar_t) * CHAR_BIT;
173
186
 
174
- inline std::size_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
175
- std::size_t = 0) const noexcept {
176
- std::size_t matches{};
187
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
188
+ std::size_t = 0) const noexcept {
189
+ result_t matches{};
177
190
  #if defined(__GNUC__)
178
191
  #pragma GCC ivdep
179
192
  #elif defined(__clang__)
@@ -192,14 +205,15 @@ template <typename scalar_at> struct bit_hamming_gt {
192
205
  * Can be used to compute the similarity between two textual documents
193
206
  * using the IDs of tokens present in them.
194
207
  */
195
- template <typename scalar_at> struct jaccard_gt {
208
+ template <typename scalar_at, typename result_at = scalar_at> struct jaccard_gt {
196
209
  using scalar_t = scalar_at;
197
- using result_type = f32_t;
210
+ using result_t = result_at;
211
+ using result_type = result_t;
198
212
  static_assert(!std::is_floating_point<scalar_t>::value, "Jaccard distance requires integral scalars");
199
213
 
200
- inline result_type operator()( //
214
+ inline result_t operator()( //
201
215
  scalar_t const* a, scalar_t const* b, std::size_t a_length, std::size_t b_length) const noexcept {
202
- std::size_t intersection{};
216
+ result_t intersection{};
203
217
  std::size_t i{};
204
218
  std::size_t j{};
205
219
  while (i != a_length && j != b_length) {
@@ -216,14 +230,15 @@ template <typename scalar_at> struct jaccard_gt {
216
230
  * Can be used to compute the similarity between two textual documents
217
231
  * using the IDs of tokens present in them.
218
232
  */
219
- template <typename scalar_at> struct pearson_correlation_gt {
233
+ template <typename scalar_at, typename result_at = scalar_at> struct pearson_correlation_gt {
220
234
  using scalar_t = scalar_at;
221
- using result_type = scalar_t;
235
+ using result_t = result_at;
236
+ using result_type = result_t;
222
237
 
223
- inline result_type operator()( //
238
+ inline result_t operator()( //
224
239
  scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
225
- scalar_t a_sum{}, b_sum{}, ab_sum{};
226
- scalar_t a_sq_sum{}, b_sq_sum{};
240
+ result_t a_sum{}, b_sum{}, ab_sum{};
241
+ result_t a_sq_sum{}, b_sq_sum{};
227
242
  #if defined(__GNUC__)
228
243
  #pragma GCC ivdep
229
244
  #elif defined(__clang__)
@@ -238,8 +253,8 @@ template <typename scalar_at> struct pearson_correlation_gt {
238
253
  a_sq_sum += a[i] * a[i];
239
254
  b_sq_sum += b[i] * b[i];
240
255
  }
241
- result_type denom = std::sqrt((dim * a_sq_sum - a_sum * a_sum) * (dim * b_sq_sum - b_sum * b_sum));
242
- result_type corr = (dim * ab_sum - a_sum * b_sum) / denom;
256
+ result_t denom = std::sqrt((dim * a_sq_sum - a_sum * a_sum) * (dim * b_sq_sum - b_sum * b_sum));
257
+ result_t corr = (dim * ab_sum - a_sum * b_sum) / denom;
243
258
  return -corr;
244
259
  }
245
260
  };
@@ -248,23 +263,25 @@ template <typename scalar_at> struct pearson_correlation_gt {
248
263
  * @brief Haversine distance for the shortest distance between two nodes on
249
264
  * the surface of a 3D sphere, defined with latitude and longitude.
250
265
  */
251
- template <typename scalar_at> struct haversine_gt {
266
+ template <typename scalar_at, typename result_at = scalar_at> struct haversine_gt {
252
267
  using scalar_t = scalar_at;
253
- using result_type = scalar_t;
268
+ using result_t = result_at;
269
+ using result_type = result_t;
254
270
  static_assert(!std::is_integral<scalar_t>::value, "Latitude and longitude must be floating-node");
255
271
 
256
- inline scalar_t operator()(scalar_t const* a, scalar_t const* b, std::size_t = 2, std::size_t = 2) const noexcept {
257
- scalar_t lat_a = a[0], lon_a = a[1];
258
- scalar_t lat_b = b[0], lon_b = b[1];
272
+ inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t = 2, std::size_t = 2) const noexcept {
273
+ result_t lat_a = a[0], lon_a = a[1];
274
+ result_t lat_b = b[0], lon_b = b[1];
259
275
 
260
- scalar_t lat_delta = angle_to_radians<scalar_t>(lat_b - lat_a);
261
- scalar_t lon_delta = angle_to_radians<scalar_t>(lon_b - lon_a);
276
+ result_t lat_delta = angle_to_radians<result_t>(lat_b - lat_a);
277
+ result_t lon_delta = angle_to_radians<result_t>(lon_b - lon_a);
262
278
 
263
- scalar_t converted_lat_a = angle_to_radians<scalar_t>(lat_a);
264
- scalar_t converted_lat_b = angle_to_radians<scalar_t>(lat_b);
279
+ result_t converted_lat_a = angle_to_radians<result_t>(lat_a);
280
+ result_t converted_lat_b = angle_to_radians<result_t>(lat_b);
265
281
 
266
- scalar_t x = square(std::sin(lat_delta / 2.f)) +
267
- std::cos(converted_lat_a) * std::cos(converted_lat_b) * square(std::sin(lon_delta / 2.f));
282
+ result_t x = //
283
+ square(std::sin(lat_delta / 2.f)) +
284
+ std::cos(converted_lat_a) * std::cos(converted_lat_b) * square(std::sin(lon_delta / 2.f));
268
285
 
269
286
  return std::atan2(std::sqrt(x), std::sqrt(1.f - x));
270
287
  }
@@ -336,6 +353,23 @@ class max_heap_gt {
336
353
  max_heap_gt(std::size_t max_capacity = 0) noexcept
337
354
  : elements_(nullptr), size_(0), capacity_(0), max_capacity_(max_capacity) {}
338
355
 
356
+ max_heap_gt(max_heap_gt&& other) noexcept {
357
+ std::swap(elements_, other.elements_);
358
+ std::swap(size_, other.size_);
359
+ std::swap(capacity_, other.capacity_);
360
+ std::swap(max_capacity_, other.max_capacity_);
361
+ }
362
+ max_heap_gt& operator=(max_heap_gt&& other) noexcept {
363
+ std::swap(elements_, other.elements_);
364
+ std::swap(size_, other.size_);
365
+ std::swap(capacity_, other.capacity_);
366
+ std::swap(max_capacity_, other.max_capacity_);
367
+ return *this;
368
+ }
369
+
370
+ max_heap_gt(max_heap_gt const&) = delete;
371
+ max_heap_gt& operator=(max_heap_gt const&) = delete;
372
+
339
373
  bool empty() const noexcept { return !size_; }
340
374
  std::size_t size() const noexcept { return size_; }
341
375
  std::size_t capacity() const noexcept { return capacity_; }
@@ -356,9 +390,9 @@ class max_heap_gt {
356
390
  if (max_capacity_ && capacity_ == max_capacity_)
357
391
  return false;
358
392
 
359
- auto new_capacity = std::max<std::size_t>(capacity_ * 2u, 16u);
393
+ auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
360
394
  if (max_capacity_)
361
- new_capacity = std::min(new_capacity, max_capacity_);
395
+ new_capacity = (std::min)(new_capacity, max_capacity_);
362
396
 
363
397
  auto allocator = allocator_t{};
364
398
  auto new_elements = allocator.allocate(new_capacity);
@@ -437,7 +471,12 @@ class max_heap_gt {
437
471
  *
438
472
  */
439
473
  class mutex_t {
474
+ #if defined(WINDOWS)
475
+ using slot_t = volatile LONG;
476
+ #else
440
477
  using slot_t = std::int32_t;
478
+ #endif // WINDOWS
479
+
441
480
  slot_t flag_;
442
481
 
443
482
  public:
@@ -446,18 +485,32 @@ class mutex_t {
446
485
 
447
486
  inline bool try_lock() noexcept {
448
487
  slot_t raw = 0;
488
+ #if defined(WINDOWS)
489
+ return InterlockedCompareExchange(&flag_, 1, raw);
490
+ #else
449
491
  return __atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED);
492
+ #endif // WINDOWS
450
493
  }
451
494
 
452
495
  inline void lock() noexcept {
453
- slot_t raw;
496
+ slot_t raw = 0;
497
+ #if defined(WINDOWS)
498
+ InterlockedCompareExchange(&flag_, 1, raw);
499
+ #else
454
500
  lock_again:
455
501
  raw = 0;
456
502
  if (!__atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
457
503
  goto lock_again;
504
+ #endif // WINDOWS
458
505
  }
459
506
 
460
- inline void unlock() noexcept { __atomic_store_n(&flag_, 0, __ATOMIC_RELEASE); }
507
+ inline void unlock() noexcept {
508
+ #if defined(WINDOWS)
509
+ InterlockedExchange(&flag_, 0);
510
+ #else
511
+ __atomic_store_n(&flag_, 0, __ATOMIC_RELEASE);
512
+ #endif
513
+ }
461
514
  };
462
515
 
463
516
  static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than expected");
@@ -467,6 +520,10 @@ using lock_t = std::unique_lock<mutex_t>;
467
520
  /**
468
521
  * @brief Five-byte integer type to address node clouds with over 4B entries.
469
522
  */
523
+ #if defined(WINDOWS)
524
+ #pragma pack(push, 1) // Pack struct members on 1-byte alignment
525
+ #endif
526
+
470
527
  class usearch_pack_m uint40_t {
471
528
  unsigned char octets[5];
472
529
 
@@ -504,6 +561,9 @@ class usearch_pack_m uint40_t {
504
561
  return old;
505
562
  }
506
563
  };
564
+ #if defined(WINDOWS)
565
+ #pragma pack(pop) // Reset alignment to default
566
+ #endif
507
567
 
508
568
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
509
569
 
@@ -548,8 +608,8 @@ struct config_t {
548
608
 
549
609
  ///
550
610
  std::size_t max_elements = 0;
551
- std::size_t max_threads_add = 64;
552
- std::size_t max_threads_search = 64;
611
+ std::size_t max_threads_add = 0;
612
+ std::size_t max_threads_search = 0;
553
613
  };
554
614
 
555
615
  /**
@@ -624,13 +684,23 @@ class index_gt {
624
684
  using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
625
685
 
626
686
  struct neighbors_ref_t {
627
- neighbors_count_t& count;
628
- id_t* neighbors{};
687
+ neighbors_count_t& count_;
688
+ id_t* neighbors_{};
629
689
 
630
690
  inline neighbors_ref_t(byte_t* tape) noexcept
631
- : count(*(neighbors_count_t*)tape), neighbors((neighbors_count_t*)tape + 1) {}
691
+ : count_(*(neighbors_count_t*)tape), neighbors_((neighbors_count_t*)tape + 1) {}
692
+ inline id_t* begin() noexcept { return neighbors_; }
693
+ inline id_t* end() noexcept { return neighbors_ + count_; }
694
+ inline id_t const* begin() const noexcept { return neighbors_; }
695
+ inline id_t const* end() const noexcept { return neighbors_ + count_; }
696
+ inline id_t& operator[](std::size_t i) noexcept { return neighbors_[i]; }
697
+ inline id_t operator[](std::size_t i) const noexcept { return neighbors_[i]; }
698
+ inline std::size_t size() const noexcept { return count_; }
632
699
  };
633
700
 
701
+ #if defined(WINDOWS)
702
+ #pragma pack(push, 1) // Pack struct members on 1-byte alignment
703
+ #endif
634
704
  struct usearch_pack_m node_head_t {
635
705
  label_t label;
636
706
  dim_t dim;
@@ -639,11 +709,21 @@ class index_gt {
639
709
  // Each starts with a `neighbors_count_t` and is followed by such number of `id_t`s.
640
710
  byte_t neighbors[1];
641
711
  };
712
+ #if defined(WINDOWS)
713
+ #pragma pack(pop) // Reset alignment to default
714
+ #endif
715
+
642
716
  static constexpr std::size_t head_bytes_k = sizeof(label_t) + sizeof(dim_t) + sizeof(level_t);
643
717
 
644
718
  struct node_t {
645
- byte_t* tape_;
646
- scalar_t* vector_;
719
+ byte_t* tape_{};
720
+ scalar_t* vector_{};
721
+
722
+ explicit node_t(byte_t* tape, scalar_t* vector) noexcept : tape_(tape), vector_(vector) {}
723
+
724
+ node_t() = default;
725
+ node_t(node_t const&) = default;
726
+ node_t& operator=(node_t const&) = default;
647
727
  };
648
728
 
649
729
  class node_ref_t {
@@ -655,20 +735,21 @@ class index_gt {
655
735
 
656
736
  inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
657
737
  inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
658
- inline operator node_t() const noexcept { return {mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
738
+ inline operator node_t() const noexcept { return node_t{mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
659
739
  };
660
740
 
661
741
  struct usearch_align_m thread_context_t {
662
742
  distances_and_ids_t top_candidates;
663
743
  distances_and_ids_t candidates_set;
664
- distances_and_ids_t candidates_filter;
665
744
  visits_bitset_t visits;
666
745
  std::default_random_engine level_generator;
667
746
  metric_t metric;
668
747
  };
669
748
 
670
749
  config_t config_{};
671
- precomputed_constants_t pre_;
750
+ metric_t metric_{};
751
+ allocator_t allocator_{};
752
+ precomputed_constants_t pre_{};
672
753
  int viewed_file_descriptor_{};
673
754
 
674
755
  usearch_align_m mutable std::atomic<std::size_t> capacity_{};
@@ -682,7 +763,7 @@ class index_gt {
682
763
  std::vector<node_t, node_allocator_t> nodes_{};
683
764
 
684
765
  using thread_context_allocator_t = typename allocator_traits_t::template rebind_alloc<thread_context_t>;
685
- mutable std::vector<thread_context_t, thread_context_allocator_t> thread_contexts_;
766
+ mutable std::vector<thread_context_t, thread_context_allocator_t> thread_contexts_{};
686
767
 
687
768
  public:
688
769
  std::size_t connectivity() const noexcept { return config_.connectivity; }
@@ -692,10 +773,11 @@ class index_gt {
692
773
  bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
693
774
  bool synchronize() const noexcept { return config_.max_threads_add > 1; }
694
775
 
695
- index_gt(config_t config = {}, metric_t metric = {}, allocator_t = {}) : config_(config) {
776
+ index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
777
+ : config_(config), metric_(metric), allocator_(allocator) {
696
778
 
697
779
  // Externally defined hyper-parameters:
698
- config_.expansion_add = std::max(config_.expansion_add, config_.connectivity);
780
+ config_.expansion_add = (std::max)(config_.expansion_add, config_.connectivity);
699
781
  pre_ = precompute(config);
700
782
 
701
783
  // Configure initial empty state:
@@ -705,14 +787,23 @@ class index_gt {
705
787
  viewed_file_descriptor_ = 0;
706
788
 
707
789
  // Dynamic memory:
708
- thread_contexts_.resize(std::max(config.max_threads_search, config.max_threads_add));
790
+ thread_contexts_.resize((std::max)(config.max_threads_search, config.max_threads_add));
709
791
  for (thread_context_t& context : thread_contexts_)
710
792
  context.metric = metric;
711
793
  reserve(config.max_elements);
712
794
  }
713
795
 
796
+ index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
797
+
714
798
  ~index_gt() noexcept { clear(); }
715
799
 
800
+ index_gt(index_gt&& other) noexcept { swap(other); }
801
+
802
+ index_gt& operator=(index_gt&& other) noexcept {
803
+ swap(other);
804
+ return *this;
805
+ }
806
+
716
807
  #pragma region Adjusting Configuration
717
808
 
718
809
  void clear() noexcept {
@@ -724,6 +815,26 @@ class index_gt {
724
815
  entry_id_ = 0u;
725
816
  }
726
817
 
818
+ void swap(index_gt& other) noexcept {
819
+ std::swap(config_, other.config_);
820
+ std::swap(metric_, other.metric_);
821
+ std::swap(allocator_, other.allocator_);
822
+ std::swap(pre_, other.pre_);
823
+ std::swap(viewed_file_descriptor_, other.viewed_file_descriptor_);
824
+ std::swap(max_level_, other.max_level_);
825
+ std::swap(entry_id_, other.entry_id_);
826
+ std::swap(nodes_, other.nodes_);
827
+ std::swap(thread_contexts_, other.thread_contexts_);
828
+
829
+ // Non-atomic parts.
830
+ std::size_t capacity = capacity_;
831
+ std::size_t size = size_;
832
+ capacity_ = other.capacity_.load();
833
+ size_ = other.size_.load();
834
+ other.capacity_ = capacity;
835
+ other.size_ = size;
836
+ }
837
+
727
838
  void reserve(std::size_t new_capacity) noexcept(false) {
728
839
 
729
840
  assert_m(new_capacity >= size_, "Can't drop existing values");
@@ -777,31 +888,10 @@ class index_gt {
777
888
  }
778
889
 
779
890
  // Go down the level, tracking only the closest match
780
- id_t closest_id = entry_id_;
781
- distance_t closest_dist =
782
- context.metric(new_vector, node(closest_id).vector, new_dim, node(closest_id).head.dim);
783
- for (level_t level = max_level; level > new_target_level; level--) {
784
- bool changed;
785
- do {
786
- changed = false;
787
- node_ref_t closest_node = node(closest_id);
788
- lock_t closest_lock = closest_node.lock();
789
- neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
790
- iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
791
- node_ref_t candidate_node = node(candidate_id);
792
- distance_t candidate_dist =
793
- context.metric(new_vector, candidate_node.vector, new_dim, candidate_node.head.dim);
794
- if (candidate_dist < closest_dist) {
795
- closest_dist = candidate_dist;
796
- closest_id = candidate_id;
797
- changed = true;
798
- }
799
- });
800
- } while (changed);
801
- }
891
+ id_t closest_id = search_for_one(entry_id_, new_vector, new_dim, max_level, new_target_level, context);
802
892
 
803
893
  // From `new_target_level` down perform proper extensive search.
804
- for (level_t level = std::min(new_target_level, max_level); level >= 0; level--) {
894
+ for (level_t level = (std::min)(new_target_level, max_level); level >= 0; level--) {
805
895
  search_to_insert(closest_id, new_vector, new_dim, level, context);
806
896
  closest_id = connect_new_element(new_id, level, context);
807
897
  }
@@ -827,32 +917,11 @@ class index_gt {
827
917
 
828
918
  // Go down the level, tracking only the closest match
829
919
  thread_context_t& context = thread_contexts_[thread_idx];
830
- id_t closest_id = entry_id_;
831
- distance_t closest_dist =
832
- context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
833
- for (level_t level = max_level_; level > 0; level--) {
834
- bool changed;
835
- do {
836
- changed = false;
837
- node_ref_t closest_node = node(closest_id);
838
- neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
839
- iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
840
- node_ref_t candidate_node = node(candidate_id);
841
- distance_t candidate_dist = context.metric( //
842
- query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
843
- if (candidate_dist < closest_dist) {
844
- closest_dist = candidate_dist;
845
- closest_id = candidate_id;
846
- changed = true;
847
- }
848
- });
849
-
850
- } while (changed);
851
- }
920
+ id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
852
921
 
853
922
  // For bottom layer we need a more optimized procedure
854
923
  search_to_find_in_base( //
855
- closest_id, query_vec, query_dim, std::max(config_.expansion_search, wanted), context);
924
+ closest_id, query_vec, query_dim, (std::max)(config_.expansion_search, wanted), context);
856
925
  while (context.top_candidates.size() > wanted)
857
926
  context.top_candidates.pop();
858
927
 
@@ -904,6 +973,10 @@ class index_gt {
904
973
  void save(char const* file_path) const noexcept(false) {
905
974
 
906
975
  state_t state;
976
+ // Check compatibility
977
+ state.bytes_per_label = sizeof(label_t);
978
+ state.bytes_per_id = sizeof(id_t);
979
+ // Describe state
907
980
  state.connectivity = config_.connectivity;
908
981
  state.size = size_;
909
982
  state.entry_id = entry_id_;
@@ -926,12 +999,15 @@ class index_gt {
926
999
  for (std::size_t i = 0; i != state.size; ++i) {
927
1000
  node_ref_t node_ref = node(static_cast<id_t>(i));
928
1001
  std::size_t bytes_to_dump = node_dump_size(node_ref.head.dim, node_ref.head.level);
929
- std::size_t written = std::fwrite(&node_ref.head, bytes_to_dump - node_ref.head.dim, 1, file);
1002
+ std::size_t bytes_in_vec = node_ref.head.dim * sizeof(scalar_t);
1003
+ // Dump just neighbors, as vectors may be in a disjoint location
1004
+ std::size_t written = std::fwrite(&node_ref.head, bytes_to_dump - bytes_in_vec, 1, file);
930
1005
  if (!written) {
931
1006
  std::fclose(file);
932
1007
  throw std::runtime_error(std::strerror(errno));
933
1008
  }
934
- written = std::fwrite(node_ref.vector, node_ref.head.dim, 1, file);
1009
+ // Dump the vector
1010
+ written = std::fwrite(node_ref.vector, bytes_in_vec, 1, file);
935
1011
  if (!written) {
936
1012
  std::fclose(file);
937
1013
  throw std::runtime_error(std::strerror(errno));
@@ -957,13 +1033,22 @@ class index_gt {
957
1033
  std::fclose(file);
958
1034
  throw std::runtime_error(std::strerror(errno));
959
1035
  }
1036
+ if (state.bytes_per_label != sizeof(label_t)) {
1037
+ std::fclose(file);
1038
+ throw std::runtime_error("Incompatible label type!");
1039
+ }
1040
+ if (state.bytes_per_id != sizeof(id_t)) {
1041
+ std::fclose(file);
1042
+ throw std::runtime_error("Incompatible ID type!");
1043
+ }
1044
+
960
1045
  config_.connectivity = state.connectivity;
961
1046
  config_.max_elements = state.size;
962
1047
  pre_ = precompute(config_);
963
1048
  reserve(state.size);
964
1049
  size_ = state.size;
965
- max_level_ = state.max_level;
966
- entry_id_ = state.entry_id;
1050
+ max_level_ = static_cast<level_t>(state.max_level);
1051
+ entry_id_ = static_cast<id_t>(state.entry_id);
967
1052
  }
968
1053
 
969
1054
  // Load nodes one by one
@@ -1022,14 +1107,23 @@ class index_gt {
1022
1107
  // Read the header
1023
1108
  {
1024
1109
  std::memcpy(&state, file, sizeof(state));
1110
+ if (state.bytes_per_label != sizeof(label_t)) {
1111
+ close(descriptor);
1112
+ throw std::runtime_error("Incompatible label type!");
1113
+ }
1114
+ if (state.bytes_per_id != sizeof(id_t)) {
1115
+ close(descriptor);
1116
+ throw std::runtime_error("Incompatible ID type!");
1117
+ }
1118
+
1025
1119
  config_.connectivity = state.connectivity;
1026
1120
  config_.max_elements = state.size;
1027
1121
  config_.max_threads_add = 0;
1028
1122
  pre_ = precompute(config_);
1029
1123
  reserve(state.size);
1030
1124
  size_ = state.size;
1031
- max_level_ = state.max_level;
1032
- entry_id_ = state.entry_id;
1125
+ max_level_ = static_cast<level_t>(state.max_level);
1126
+ entry_id_ = static_cast<id_t>(state.entry_id);
1033
1127
  }
1034
1128
 
1035
1129
  // Locate every node packed into file
@@ -1037,14 +1131,15 @@ class index_gt {
1037
1131
  for (std::size_t i = 0; i != state.size; ++i) {
1038
1132
  node_head_t const& head = *(node_head_t const*)(file + progress);
1039
1133
  std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
1134
+ std::size_t bytes_in_vec = head.dim * sizeof(scalar_t);
1040
1135
  nodes_[i].tape_ = (byte_t*)(file + progress);
1041
- nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump - head.dim);
1136
+ nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump - bytes_in_vec);
1042
1137
  progress += bytes_to_dump;
1043
- max_level_ = std::max(max_level_, head.level);
1138
+ max_level_ = (std::max)(max_level_, head.level);
1044
1139
  }
1045
1140
 
1046
1141
  viewed_file_descriptor_ = descriptor;
1047
- #endif
1142
+ #endif // POSIX
1048
1143
  }
1049
1144
 
1050
1145
  #pragma endregion
@@ -1066,7 +1161,7 @@ class index_gt {
1066
1161
 
1067
1162
  void node_free(std::size_t id) noexcept {
1068
1163
 
1069
- if (viewed_file_descriptor_)
1164
+ if (viewed_file_descriptor_ != 0)
1070
1165
  return;
1071
1166
 
1072
1167
  // This function is rarely called and can be as expensive as needed for higher space-efficiency.
@@ -1085,7 +1180,7 @@ class index_gt {
1085
1180
  ;
1086
1181
 
1087
1182
  allocator_t{}.deallocate(node.tape_, node_bytes);
1088
- node = {};
1183
+ node = node_t{};
1089
1184
  }
1090
1185
 
1091
1186
  node_ref_t node_malloc( //
@@ -1119,9 +1214,9 @@ class index_gt {
1119
1214
  return {*mutex, head, scalars};
1120
1215
  }
1121
1216
 
1122
- inline node_ref_t node(id_t id) const noexcept {
1217
+ inline node_ref_t node(id_t id) const noexcept { return node(nodes_[id]); }
1123
1218
 
1124
- node_t node = nodes_[id];
1219
+ inline node_ref_t node(node_t node) const noexcept {
1125
1220
  byte_t* data = node.tape_;
1126
1221
  mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
1127
1222
  node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
@@ -1144,74 +1239,64 @@ class index_gt {
1144
1239
 
1145
1240
  node_ref_t new_node = node(new_id);
1146
1241
  distances_and_ids_t& top_candidates = context.top_candidates;
1147
- std::size_t connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
1148
- filter_top_candidates_with_heuristic(top_candidates, context.candidates_set, config_.connectivity, context);
1242
+ std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
1243
+ span_gt<distance_and_id_t const> top = filter_heuristic(top_candidates, config_.connectivity, context.metric);
1149
1244
 
1150
- distance_and_id_t const* const top_unordered = top_candidates.data();
1151
- std::size_t const top_count = top_candidates.size();
1152
- id_t next_closest_entry_id = top_unordered[0].second;
1153
- distance_t next_closest_distance = top_unordered[0].first;
1245
+ distance_and_id_t const* const top_ordered = top.data();
1246
+ std::size_t const top_count = top.size();
1247
+ id_t const next_closest_entry_id = top_ordered[0].second;
1154
1248
 
1155
1249
  // Outgoing links from `new_id`:
1156
1250
  {
1157
1251
  neighbors_ref_t new_neighbors = neighbors(new_node, level);
1158
- assert_m(!new_neighbors.count, "The newly inserted element should have blank link list");
1252
+ assert_m(!new_neighbors.count_, "The newly inserted element should have blank link list");
1159
1253
 
1160
- new_neighbors.count = static_cast<neighbors_count_t>(top_count);
1254
+ new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
1161
1255
  for (std::size_t idx = 0; idx < top_count; idx++) {
1162
- assert_m(!new_neighbors.neighbors[idx], "Possible memory corruption");
1163
- assert_m(level <= node(top_unordered[idx].second).head.level, "Linking to missing level");
1164
-
1165
- new_neighbors.neighbors[idx] = top_unordered[idx].second;
1166
- if (top_unordered[idx].first < next_closest_distance) {
1167
- next_closest_entry_id = top_unordered[idx].second;
1168
- next_closest_distance = top_unordered[idx].first;
1169
- }
1256
+ assert_m(!new_neighbors[idx], "Possible memory corruption");
1257
+ assert_m(level <= node(top_ordered[idx].second).head.level, "Linking to missing level");
1258
+ new_neighbors[idx] = top_ordered[idx].second;
1170
1259
  }
1171
1260
  }
1172
1261
 
1173
1262
  // Reverse links from the neighbors:
1174
1263
  for (std::size_t idx = 0; idx < top_count; idx++) {
1175
- id_t close_id = top_unordered[idx].second;
1264
+ id_t close_id = top_ordered[idx].second;
1176
1265
  node_ref_t close_node = node(close_id);
1177
1266
  lock_t close_lock = close_node.lock();
1178
1267
 
1179
1268
  neighbors_ref_t close_header = neighbors(close_node, level);
1180
- assert_m(close_header.count <= connectivity_max, "Possible corruption");
1269
+ assert_m(close_header.count_ <= connectivity_max, "Possible corruption");
1181
1270
  assert_m(close_id != new_id, "Self-loops are impossible");
1182
1271
  assert_m(level <= close_node.head.level, "Linking to missing level");
1183
1272
 
1184
1273
  // If `new_id` is already present in the neighboring connections of `close_id`
1185
1274
  // then no need to modify any connections or run the heuristics.
1186
- if (close_header.count < connectivity_max) {
1187
- close_header.neighbors[close_header.count] = new_id;
1188
- close_header.count++;
1275
+ if (close_header.count_ < connectivity_max) {
1276
+ close_header[close_header.count_] = new_id;
1277
+ close_header.count_++;
1189
1278
  continue;
1190
1279
  }
1191
1280
 
1192
1281
  // To fit a new connection we need to drop an existing one.
1193
- distances_and_ids_t& candidates = context.candidates_filter;
1282
+ distances_and_ids_t& candidates = context.candidates_set;
1194
1283
  candidates.clear();
1195
1284
  candidates.emplace( //
1196
1285
  context.metric( //
1197
1286
  new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
1198
1287
  new_id);
1199
- iterate_through_neighbors(close_header, [&](id_t successor_id) noexcept {
1288
+ for (id_t successor_id : close_header) {
1200
1289
  node_ref_t successor_node = node(successor_id);
1201
1290
  candidates.emplace( //
1202
1291
  context.metric( //
1203
1292
  successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
1204
1293
  successor_id);
1205
- });
1206
- filter_top_candidates_with_heuristic(candidates, context.candidates_set, connectivity_max, context);
1294
+ }
1295
+ span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
1207
1296
 
1208
1297
  // Export the results:
1209
- close_header.count = 0u;
1210
- while (candidates.size()) {
1211
- close_header.neighbors[close_header.count] = candidates.top().second;
1212
- close_header.count++;
1213
- candidates.pop();
1214
- }
1298
+ for (close_header.count_ = 0u; close_header.count_ != top.size(); ++close_header.count_)
1299
+ close_header[close_header.count_] = top[close_header.count_].second;
1215
1300
  }
1216
1301
 
1217
1302
  return next_closest_entry_id;
@@ -1223,13 +1308,42 @@ class index_gt {
1223
1308
  return (level_t)r;
1224
1309
  }
1225
1310
 
1226
- void search_to_insert( //
1227
- id_t start_id, scalar_t const* query_vec, dim_t query_dim, //
1311
+ id_t search_for_one( //
1312
+ id_t entry_id, scalar_t const* query_vec, std::size_t query_dim, //
1313
+ level_t begin_level, level_t end_level, thread_context_t& context) const noexcept {
1314
+
1315
+ id_t closest_id = entry_id;
1316
+ distance_t closest_dist =
1317
+ context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
1318
+ for (level_t level = begin_level; level > end_level; level--) {
1319
+ bool changed;
1320
+ do {
1321
+ changed = false;
1322
+ node_ref_t closest_node = node(closest_id);
1323
+ lock_t closest_lock = closest_node.lock();
1324
+ neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
1325
+ for (id_t candidate_id : closest_header) {
1326
+ node_ref_t candidate_node = node(candidate_id);
1327
+ distance_t candidate_dist =
1328
+ context.metric(query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
1329
+ if (candidate_dist < closest_dist) {
1330
+ closest_dist = candidate_dist;
1331
+ closest_id = candidate_id;
1332
+ changed = true;
1333
+ }
1334
+ }
1335
+ } while (changed);
1336
+ }
1337
+ return closest_id;
1338
+ }
1339
+
1340
+ void search_to_insert( //
1341
+ id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1228
1342
  level_t level, thread_context_t& context) noexcept(false) {
1229
1343
 
1230
1344
  visits_bitset_t& visits = context.visits;
1231
- distances_and_ids_t& top_candidates = context.top_candidates;
1232
- distances_and_ids_t& candidates_set = context.candidates_set;
1345
+ distances_and_ids_t& top_candidates = context.top_candidates; // pop max, push
1346
+ distances_and_ids_t& candidates_set = context.candidates_set; // pop min, push
1233
1347
 
1234
1348
  top_candidates.clear();
1235
1349
  candidates_set.clear();
@@ -1252,9 +1366,10 @@ class index_gt {
1252
1366
  lock_t candidate_lock = candidate_node.lock();
1253
1367
  neighbors_ref_t candidate_header = neighbors(candidate_node, level);
1254
1368
 
1255
- iterate_through_neighbors(candidate_header, [&](id_t successor_id) noexcept {
1369
+ prefetch_neighbors(candidate_header, visits);
1370
+ for (id_t successor_id : candidate_header) {
1256
1371
  if (visits.test(successor_id))
1257
- return;
1372
+ continue;
1258
1373
 
1259
1374
  visits.set(successor_id);
1260
1375
  node_ref_t successor_node = node(successor_id);
@@ -1269,17 +1384,17 @@ class index_gt {
1269
1384
  if (!top_candidates.empty())
1270
1385
  closest_dist = top_candidates.top().first;
1271
1386
  }
1272
- });
1387
+ }
1273
1388
  }
1274
1389
  }
1275
1390
 
1276
- void search_to_find_in_base( //
1277
- id_t start_id, scalar_t const* query_vec, dim_t query_dim, //
1391
+ void search_to_find_in_base( //
1392
+ id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1278
1393
  std::size_t expansion, thread_context_t& context) const noexcept(false) {
1279
1394
 
1280
1395
  visits_bitset_t& visits = context.visits;
1281
- distances_and_ids_t& top_candidates = context.top_candidates;
1282
- distances_and_ids_t& candidates_set = context.candidates_set;
1396
+ distances_and_ids_t& top_candidates = context.top_candidates; // pop max, push
1397
+ distances_and_ids_t& candidates_set = context.candidates_set; // pop min, push
1283
1398
 
1284
1399
  visits.clear();
1285
1400
  top_candidates.clear();
@@ -1301,9 +1416,10 @@ class index_gt {
1301
1416
  id_t candidate_id = current_node_node.second;
1302
1417
  neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
1303
1418
 
1304
- iterate_through_neighbors(candidate_header, [&](id_t successor_id) noexcept {
1419
+ prefetch_neighbors(candidate_header, visits);
1420
+ for (id_t successor_id : candidate_header) {
1305
1421
  if (visits.test(successor_id))
1306
- return;
1422
+ continue;
1307
1423
 
1308
1424
  visits.set(successor_id);
1309
1425
  node_ref_t successor_node = node(successor_id);
@@ -1319,60 +1435,52 @@ class index_gt {
1319
1435
  if (!top_candidates.empty())
1320
1436
  closest_dist = top_candidates.top().first;
1321
1437
  }
1322
- });
1438
+ }
1323
1439
  }
1324
1440
  }
1325
1441
 
1326
- /**
1327
- * @brief A simple `for`-loop that prefetches vectors of neighbors.
1328
- */
1329
- template <typename neighbor_id_callback_at>
1330
- inline void iterate_through_neighbors(neighbors_ref_t head, neighbor_id_callback_at&& callback) const noexcept {
1331
- std::size_t n = head.count;
1332
- for (std::size_t j = 0; j != n; j++)
1333
- callback(head.neighbors[j]);
1334
- }
1335
-
1336
- void filter_top_candidates_with_heuristic( //
1337
- distances_and_ids_t& top_candidates, distances_and_ids_t& temporary, std::size_t needed,
1338
- thread_context_t& context) const noexcept(false) {
1339
-
1340
- if (top_candidates.size() < needed)
1341
- return;
1342
-
1343
- // TODO: Sort ascending, then run an inplace triangular reduction.
1344
- temporary.clear();
1345
- while (top_candidates.size()) {
1346
- temporary.emplace(-top_candidates.top().first, top_candidates.top().second);
1347
- top_candidates.pop();
1348
- }
1442
+ void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
1349
1443
 
1350
- while (temporary.size() && top_candidates.size() < needed) {
1444
+ span_gt<distance_and_id_t const> filter_heuristic( //
1445
+ distances_and_ids_t& top_candidates, std::size_t needed, metric_t const& metric) const noexcept {
1351
1446
 
1352
- distance_and_id_t best = temporary.top();
1353
- distance_t dist_to_query = -best.first;
1354
- temporary.pop();
1447
+ top_candidates.sort_ascending();
1448
+ distance_and_id_t* top_ordered = top_candidates.data();
1449
+ std::size_t const top_count = top_candidates.size();
1450
+ if (top_count < needed)
1451
+ return {top_ordered, top_count};
1452
+
1453
+ std::size_t submitted_count = 1;
1454
+ std::size_t consumed_count = 1; /// Always equal or greater than `submitted_count`.
1455
+ while (submitted_count < needed && consumed_count < top_count) {
1456
+ distance_and_id_t candidate = top_ordered[consumed_count];
1457
+ node_ref_t candidate_node = node(candidate.second);
1458
+ distance_t candidate_dist = candidate.first;
1355
1459
  bool good = true;
1356
-
1357
- distance_and_id_t const* const top_unordered = top_candidates.data();
1358
- std::size_t const top_count = top_candidates.size();
1359
- for (std::size_t idx = 0; idx < top_count; idx++) {
1360
- distance_and_id_t other = top_unordered[idx];
1361
- node_ref_t other_node = node(other.second);
1362
- node_ref_t best_node = node(best.second);
1363
- distance_t inter_result_dist =
1364
- context.metric(other_node.vector, best_node.vector, other_node.head.dim, best_node.head.dim);
1365
- if (inter_result_dist < dist_to_query) {
1460
+ for (std::size_t idx = 0; idx < submitted_count; idx++) {
1461
+ distance_and_id_t submitted = top_ordered[idx];
1462
+ node_ref_t submitted_node = node(submitted.second);
1463
+ distance_t inter_result_dist = metric( //
1464
+ submitted_node.vector, candidate_node.vector, //
1465
+ submitted_node.head.dim, candidate_node.head.dim);
1466
+ if (inter_result_dist < candidate_dist) {
1366
1467
  good = false;
1367
1468
  break;
1368
1469
  }
1369
1470
  }
1370
1471
 
1371
- if (good)
1372
- top_candidates.emplace(-best.first, best.second);
1472
+ if (good) {
1473
+ top_ordered[submitted_count] = top_ordered[consumed_count];
1474
+ submitted_count++;
1475
+ }
1476
+ consumed_count++;
1373
1477
  }
1478
+
1479
+ return {top_ordered, submitted_count};
1374
1480
  }
1375
1481
  };
1376
1482
 
1377
1483
  } // namespace usearch
1378
1484
  } // namespace unum
1485
+
1486
+ #endif