usearch 0.1.10 → 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,11 +353,29 @@ 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_; }
376
+ /// @brief Selects the largest element in the heap.
377
+ /// @return Reference to the stored element.
342
378
  element_t const& top() const noexcept { return elements_[0]; }
343
- element_t const& max() const noexcept { return elements_[0]; }
344
379
 
345
380
  void clear() noexcept {
346
381
  while (size_) {
@@ -355,9 +390,9 @@ class max_heap_gt {
355
390
  if (max_capacity_ && capacity_ == max_capacity_)
356
391
  return false;
357
392
 
358
- auto new_capacity = std::max<std::size_t>(capacity_ * 2u, 16u);
393
+ auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
359
394
  if (max_capacity_)
360
- new_capacity = std::min(new_capacity, max_capacity_);
395
+ new_capacity = (std::min)(new_capacity, max_capacity_);
361
396
 
362
397
  auto allocator = allocator_t{};
363
398
  auto new_elements = allocator.allocate(new_capacity);
@@ -436,7 +471,12 @@ class max_heap_gt {
436
471
  *
437
472
  */
438
473
  class mutex_t {
474
+ #if defined(WINDOWS)
475
+ using slot_t = volatile LONG;
476
+ #else
439
477
  using slot_t = std::int32_t;
478
+ #endif // WINDOWS
479
+
440
480
  slot_t flag_;
441
481
 
442
482
  public:
@@ -445,18 +485,32 @@ class mutex_t {
445
485
 
446
486
  inline bool try_lock() noexcept {
447
487
  slot_t raw = 0;
488
+ #if defined(WINDOWS)
489
+ return InterlockedCompareExchange(&flag_, 1, raw);
490
+ #else
448
491
  return __atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED);
492
+ #endif // WINDOWS
449
493
  }
450
494
 
451
495
  inline void lock() noexcept {
452
- slot_t raw;
496
+ slot_t raw = 0;
497
+ #if defined(WINDOWS)
498
+ InterlockedCompareExchange(&flag_, 1, raw);
499
+ #else
453
500
  lock_again:
454
501
  raw = 0;
455
502
  if (!__atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
456
503
  goto lock_again;
504
+ #endif // WINDOWS
457
505
  }
458
506
 
459
- 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
+ }
460
514
  };
461
515
 
462
516
  static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than expected");
@@ -466,6 +520,10 @@ using lock_t = std::unique_lock<mutex_t>;
466
520
  /**
467
521
  * @brief Five-byte integer type to address node clouds with over 4B entries.
468
522
  */
523
+ #if defined(WINDOWS)
524
+ #pragma pack(push, 1) // Pack struct members on 1-byte alignment
525
+ #endif
526
+
469
527
  class usearch_pack_m uint40_t {
470
528
  unsigned char octets[5];
471
529
 
@@ -503,6 +561,9 @@ class usearch_pack_m uint40_t {
503
561
  return old;
504
562
  }
505
563
  };
564
+ #if defined(WINDOWS)
565
+ #pragma pack(pop) // Reset alignment to default
566
+ #endif
506
567
 
507
568
  static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
508
569
 
@@ -547,8 +608,8 @@ struct config_t {
547
608
 
548
609
  ///
549
610
  std::size_t max_elements = 0;
550
- std::size_t max_threads_add = 64;
551
- std::size_t max_threads_search = 64;
611
+ std::size_t max_threads_add = 0;
612
+ std::size_t max_threads_search = 0;
552
613
  };
553
614
 
554
615
  /**
@@ -623,13 +684,23 @@ class index_gt {
623
684
  using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
624
685
 
625
686
  struct neighbors_ref_t {
626
- neighbors_count_t& count;
627
- id_t* neighbors{};
687
+ neighbors_count_t& count_;
688
+ id_t* neighbors_{};
628
689
 
629
690
  inline neighbors_ref_t(byte_t* tape) noexcept
630
- : 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_; }
631
699
  };
632
700
 
701
+ #if defined(WINDOWS)
702
+ #pragma pack(push, 1) // Pack struct members on 1-byte alignment
703
+ #endif
633
704
  struct usearch_pack_m node_head_t {
634
705
  label_t label;
635
706
  dim_t dim;
@@ -638,11 +709,21 @@ class index_gt {
638
709
  // Each starts with a `neighbors_count_t` and is followed by such number of `id_t`s.
639
710
  byte_t neighbors[1];
640
711
  };
712
+ #if defined(WINDOWS)
713
+ #pragma pack(pop) // Reset alignment to default
714
+ #endif
715
+
641
716
  static constexpr std::size_t head_bytes_k = sizeof(label_t) + sizeof(dim_t) + sizeof(level_t);
642
717
 
643
718
  struct node_t {
644
- byte_t* tape_;
645
- 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;
646
727
  };
647
728
 
648
729
  class node_ref_t {
@@ -654,20 +735,21 @@ class index_gt {
654
735
 
655
736
  inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
656
737
  inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
657
- 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}; }
658
739
  };
659
740
 
660
741
  struct usearch_align_m thread_context_t {
661
742
  distances_and_ids_t top_candidates;
662
743
  distances_and_ids_t candidates_set;
663
- distances_and_ids_t candidates_filter;
664
744
  visits_bitset_t visits;
665
745
  std::default_random_engine level_generator;
666
746
  metric_t metric;
667
747
  };
668
748
 
669
749
  config_t config_{};
670
- precomputed_constants_t pre_;
750
+ metric_t metric_{};
751
+ allocator_t allocator_{};
752
+ precomputed_constants_t pre_{};
671
753
  int viewed_file_descriptor_{};
672
754
 
673
755
  usearch_align_m mutable std::atomic<std::size_t> capacity_{};
@@ -681,7 +763,7 @@ class index_gt {
681
763
  std::vector<node_t, node_allocator_t> nodes_{};
682
764
 
683
765
  using thread_context_allocator_t = typename allocator_traits_t::template rebind_alloc<thread_context_t>;
684
- 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_{};
685
767
 
686
768
  public:
687
769
  std::size_t connectivity() const noexcept { return config_.connectivity; }
@@ -691,10 +773,11 @@ class index_gt {
691
773
  bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
692
774
  bool synchronize() const noexcept { return config_.max_threads_add > 1; }
693
775
 
694
- 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) {
695
778
 
696
779
  // Externally defined hyper-parameters:
697
- config_.expansion_add = std::max(config_.expansion_add, config_.connectivity);
780
+ config_.expansion_add = (std::max)(config_.expansion_add, config_.connectivity);
698
781
  pre_ = precompute(config);
699
782
 
700
783
  // Configure initial empty state:
@@ -704,14 +787,23 @@ class index_gt {
704
787
  viewed_file_descriptor_ = 0;
705
788
 
706
789
  // Dynamic memory:
707
- 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));
708
791
  for (thread_context_t& context : thread_contexts_)
709
792
  context.metric = metric;
710
793
  reserve(config.max_elements);
711
794
  }
712
795
 
796
+ index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
797
+
713
798
  ~index_gt() noexcept { clear(); }
714
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
+
715
807
  #pragma region Adjusting Configuration
716
808
 
717
809
  void clear() noexcept {
@@ -723,6 +815,26 @@ class index_gt {
723
815
  entry_id_ = 0u;
724
816
  }
725
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
+
726
838
  void reserve(std::size_t new_capacity) noexcept(false) {
727
839
 
728
840
  assert_m(new_capacity >= size_, "Can't drop existing values");
@@ -776,31 +888,10 @@ class index_gt {
776
888
  }
777
889
 
778
890
  // Go down the level, tracking only the closest match
779
- id_t closest_id = entry_id_;
780
- distance_t closest_dist =
781
- context.metric(new_vector, node(closest_id).vector, new_dim, node(closest_id).head.dim);
782
- for (level_t level = max_level; level > new_target_level; level--) {
783
- bool changed;
784
- do {
785
- changed = false;
786
- node_ref_t closest_node = node(closest_id);
787
- lock_t closest_lock = closest_node.lock();
788
- neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
789
- iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
790
- node_ref_t candidate_node = node(candidate_id);
791
- distance_t candidate_dist =
792
- context.metric(new_vector, candidate_node.vector, new_dim, candidate_node.head.dim);
793
- if (candidate_dist < closest_dist) {
794
- closest_dist = candidate_dist;
795
- closest_id = candidate_id;
796
- changed = true;
797
- }
798
- });
799
- } while (changed);
800
- }
891
+ id_t closest_id = search_for_one(entry_id_, new_vector, new_dim, max_level, new_target_level, context);
801
892
 
802
893
  // From `new_target_level` down perform proper extensive search.
803
- 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--) {
804
895
  search_to_insert(closest_id, new_vector, new_dim, level, context);
805
896
  closest_id = connect_new_element(new_id, level, context);
806
897
  }
@@ -826,32 +917,11 @@ class index_gt {
826
917
 
827
918
  // Go down the level, tracking only the closest match
828
919
  thread_context_t& context = thread_contexts_[thread_idx];
829
- id_t closest_id = entry_id_;
830
- distance_t closest_dist =
831
- context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
832
- for (level_t level = max_level_; level > 0; level--) {
833
- bool changed;
834
- do {
835
- changed = false;
836
- node_ref_t closest_node = node(closest_id);
837
- neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
838
- iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
839
- node_ref_t candidate_node = node(candidate_id);
840
- distance_t candidate_dist = context.metric( //
841
- query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
842
- if (candidate_dist < closest_dist) {
843
- closest_dist = candidate_dist;
844
- closest_id = candidate_id;
845
- changed = true;
846
- }
847
- });
848
-
849
- } while (changed);
850
- }
920
+ id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
851
921
 
852
922
  // For bottom layer we need a more optimized procedure
853
923
  search_to_find_in_base( //
854
- 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);
855
925
  while (context.top_candidates.size() > wanted)
856
926
  context.top_candidates.pop();
857
927
 
@@ -903,6 +973,10 @@ class index_gt {
903
973
  void save(char const* file_path) const noexcept(false) {
904
974
 
905
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
906
980
  state.connectivity = config_.connectivity;
907
981
  state.size = size_;
908
982
  state.entry_id = entry_id_;
@@ -925,12 +999,15 @@ class index_gt {
925
999
  for (std::size_t i = 0; i != state.size; ++i) {
926
1000
  node_ref_t node_ref = node(static_cast<id_t>(i));
927
1001
  std::size_t bytes_to_dump = node_dump_size(node_ref.head.dim, node_ref.head.level);
928
- 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);
929
1005
  if (!written) {
930
1006
  std::fclose(file);
931
1007
  throw std::runtime_error(std::strerror(errno));
932
1008
  }
933
- 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);
934
1011
  if (!written) {
935
1012
  std::fclose(file);
936
1013
  throw std::runtime_error(std::strerror(errno));
@@ -956,13 +1033,22 @@ class index_gt {
956
1033
  std::fclose(file);
957
1034
  throw std::runtime_error(std::strerror(errno));
958
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
+
959
1045
  config_.connectivity = state.connectivity;
960
1046
  config_.max_elements = state.size;
961
1047
  pre_ = precompute(config_);
962
1048
  reserve(state.size);
963
1049
  size_ = state.size;
964
- max_level_ = state.max_level;
965
- 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);
966
1052
  }
967
1053
 
968
1054
  // Load nodes one by one
@@ -1021,14 +1107,23 @@ class index_gt {
1021
1107
  // Read the header
1022
1108
  {
1023
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
+
1024
1119
  config_.connectivity = state.connectivity;
1025
1120
  config_.max_elements = state.size;
1026
1121
  config_.max_threads_add = 0;
1027
1122
  pre_ = precompute(config_);
1028
1123
  reserve(state.size);
1029
1124
  size_ = state.size;
1030
- max_level_ = state.max_level;
1031
- 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);
1032
1127
  }
1033
1128
 
1034
1129
  // Locate every node packed into file
@@ -1036,14 +1131,15 @@ class index_gt {
1036
1131
  for (std::size_t i = 0; i != state.size; ++i) {
1037
1132
  node_head_t const& head = *(node_head_t const*)(file + progress);
1038
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);
1039
1135
  nodes_[i].tape_ = (byte_t*)(file + progress);
1040
- 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);
1041
1137
  progress += bytes_to_dump;
1042
- max_level_ = std::max(max_level_, head.level);
1138
+ max_level_ = (std::max)(max_level_, head.level);
1043
1139
  }
1044
1140
 
1045
1141
  viewed_file_descriptor_ = descriptor;
1046
- #endif
1142
+ #endif // POSIX
1047
1143
  }
1048
1144
 
1049
1145
  #pragma endregion
@@ -1065,7 +1161,7 @@ class index_gt {
1065
1161
 
1066
1162
  void node_free(std::size_t id) noexcept {
1067
1163
 
1068
- if (viewed_file_descriptor_)
1164
+ if (viewed_file_descriptor_ != 0)
1069
1165
  return;
1070
1166
 
1071
1167
  // This function is rarely called and can be as expensive as needed for higher space-efficiency.
@@ -1084,7 +1180,7 @@ class index_gt {
1084
1180
  ;
1085
1181
 
1086
1182
  allocator_t{}.deallocate(node.tape_, node_bytes);
1087
- node = {};
1183
+ node = node_t{};
1088
1184
  }
1089
1185
 
1090
1186
  node_ref_t node_malloc( //
@@ -1118,9 +1214,9 @@ class index_gt {
1118
1214
  return {*mutex, head, scalars};
1119
1215
  }
1120
1216
 
1121
- 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]); }
1122
1218
 
1123
- node_t node = nodes_[id];
1219
+ inline node_ref_t node(node_t node) const noexcept {
1124
1220
  byte_t* data = node.tape_;
1125
1221
  mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
1126
1222
  node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
@@ -1143,74 +1239,64 @@ class index_gt {
1143
1239
 
1144
1240
  node_ref_t new_node = node(new_id);
1145
1241
  distances_and_ids_t& top_candidates = context.top_candidates;
1146
- std::size_t connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
1147
- 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);
1148
1244
 
1149
- distance_and_id_t const* const top_unordered = top_candidates.data();
1150
- std::size_t const top_count = top_candidates.size();
1151
- id_t next_closest_entry_id = top_unordered[0].second;
1152
- 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;
1153
1248
 
1154
1249
  // Outgoing links from `new_id`:
1155
1250
  {
1156
1251
  neighbors_ref_t new_neighbors = neighbors(new_node, level);
1157
- 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");
1158
1253
 
1159
- new_neighbors.count = static_cast<neighbors_count_t>(top_count);
1254
+ new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
1160
1255
  for (std::size_t idx = 0; idx < top_count; idx++) {
1161
- assert_m(!new_neighbors.neighbors[idx], "Possible memory corruption");
1162
- assert_m(level <= node(top_unordered[idx].second).head.level, "Linking to missing level");
1163
-
1164
- new_neighbors.neighbors[idx] = top_unordered[idx].second;
1165
- if (top_unordered[idx].first < next_closest_distance) {
1166
- next_closest_entry_id = top_unordered[idx].second;
1167
- next_closest_distance = top_unordered[idx].first;
1168
- }
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;
1169
1259
  }
1170
1260
  }
1171
1261
 
1172
1262
  // Reverse links from the neighbors:
1173
1263
  for (std::size_t idx = 0; idx < top_count; idx++) {
1174
- id_t close_id = top_unordered[idx].second;
1264
+ id_t close_id = top_ordered[idx].second;
1175
1265
  node_ref_t close_node = node(close_id);
1176
1266
  lock_t close_lock = close_node.lock();
1177
1267
 
1178
1268
  neighbors_ref_t close_header = neighbors(close_node, level);
1179
- assert_m(close_header.count <= connectivity_max, "Possible corruption");
1269
+ assert_m(close_header.count_ <= connectivity_max, "Possible corruption");
1180
1270
  assert_m(close_id != new_id, "Self-loops are impossible");
1181
1271
  assert_m(level <= close_node.head.level, "Linking to missing level");
1182
1272
 
1183
1273
  // If `new_id` is already present in the neighboring connections of `close_id`
1184
1274
  // then no need to modify any connections or run the heuristics.
1185
- if (close_header.count < connectivity_max) {
1186
- close_header.neighbors[close_header.count] = new_id;
1187
- close_header.count++;
1275
+ if (close_header.count_ < connectivity_max) {
1276
+ close_header[close_header.count_] = new_id;
1277
+ close_header.count_++;
1188
1278
  continue;
1189
1279
  }
1190
1280
 
1191
1281
  // To fit a new connection we need to drop an existing one.
1192
- distances_and_ids_t& candidates = context.candidates_filter;
1282
+ distances_and_ids_t& candidates = context.candidates_set;
1193
1283
  candidates.clear();
1194
1284
  candidates.emplace( //
1195
1285
  context.metric( //
1196
1286
  new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
1197
1287
  new_id);
1198
- iterate_through_neighbors(close_header, [&](id_t successor_id) noexcept {
1288
+ for (id_t successor_id : close_header) {
1199
1289
  node_ref_t successor_node = node(successor_id);
1200
1290
  candidates.emplace( //
1201
1291
  context.metric( //
1202
1292
  successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
1203
1293
  successor_id);
1204
- });
1205
- 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);
1206
1296
 
1207
1297
  // Export the results:
1208
- close_header.count = 0u;
1209
- while (candidates.size()) {
1210
- close_header.neighbors[close_header.count] = candidates.top().second;
1211
- close_header.count++;
1212
- candidates.pop();
1213
- }
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;
1214
1300
  }
1215
1301
 
1216
1302
  return next_closest_entry_id;
@@ -1222,13 +1308,42 @@ class index_gt {
1222
1308
  return (level_t)r;
1223
1309
  }
1224
1310
 
1225
- void search_to_insert( //
1226
- 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, //
1227
1342
  level_t level, thread_context_t& context) noexcept(false) {
1228
1343
 
1229
1344
  visits_bitset_t& visits = context.visits;
1230
- distances_and_ids_t& top_candidates = context.top_candidates;
1231
- 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
1232
1347
 
1233
1348
  top_candidates.clear();
1234
1349
  candidates_set.clear();
@@ -1251,9 +1366,10 @@ class index_gt {
1251
1366
  lock_t candidate_lock = candidate_node.lock();
1252
1367
  neighbors_ref_t candidate_header = neighbors(candidate_node, level);
1253
1368
 
1254
- iterate_through_neighbors(candidate_header, [&](id_t successor_id) noexcept {
1369
+ prefetch_neighbors(candidate_header, visits);
1370
+ for (id_t successor_id : candidate_header) {
1255
1371
  if (visits.test(successor_id))
1256
- return;
1372
+ continue;
1257
1373
 
1258
1374
  visits.set(successor_id);
1259
1375
  node_ref_t successor_node = node(successor_id);
@@ -1268,17 +1384,17 @@ class index_gt {
1268
1384
  if (!top_candidates.empty())
1269
1385
  closest_dist = top_candidates.top().first;
1270
1386
  }
1271
- });
1387
+ }
1272
1388
  }
1273
1389
  }
1274
1390
 
1275
- void search_to_find_in_base( //
1276
- 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, //
1277
1393
  std::size_t expansion, thread_context_t& context) const noexcept(false) {
1278
1394
 
1279
1395
  visits_bitset_t& visits = context.visits;
1280
- distances_and_ids_t& top_candidates = context.top_candidates;
1281
- 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
1282
1398
 
1283
1399
  visits.clear();
1284
1400
  top_candidates.clear();
@@ -1300,9 +1416,10 @@ class index_gt {
1300
1416
  id_t candidate_id = current_node_node.second;
1301
1417
  neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
1302
1418
 
1303
- iterate_through_neighbors(candidate_header, [&](id_t successor_id) noexcept {
1419
+ prefetch_neighbors(candidate_header, visits);
1420
+ for (id_t successor_id : candidate_header) {
1304
1421
  if (visits.test(successor_id))
1305
- return;
1422
+ continue;
1306
1423
 
1307
1424
  visits.set(successor_id);
1308
1425
  node_ref_t successor_node = node(successor_id);
@@ -1318,60 +1435,52 @@ class index_gt {
1318
1435
  if (!top_candidates.empty())
1319
1436
  closest_dist = top_candidates.top().first;
1320
1437
  }
1321
- });
1438
+ }
1322
1439
  }
1323
1440
  }
1324
1441
 
1325
- /**
1326
- * @brief A simple `for`-loop that prefetches vectors of neighbors.
1327
- */
1328
- template <typename neighbor_id_callback_at>
1329
- inline void iterate_through_neighbors(neighbors_ref_t head, neighbor_id_callback_at&& callback) const noexcept {
1330
- std::size_t n = head.count;
1331
- for (std::size_t j = 0; j != n; j++)
1332
- callback(head.neighbors[j]);
1333
- }
1334
-
1335
- void filter_top_candidates_with_heuristic( //
1336
- distances_and_ids_t& top_candidates, distances_and_ids_t& temporary, std::size_t needed,
1337
- thread_context_t& context) const noexcept(false) {
1338
-
1339
- if (top_candidates.size() < needed)
1340
- return;
1341
-
1342
- // TODO: Sort ascending, then run an inplace triangular reduction.
1343
- temporary.clear();
1344
- while (top_candidates.size()) {
1345
- temporary.emplace(-top_candidates.top().first, top_candidates.top().second);
1346
- top_candidates.pop();
1347
- }
1442
+ void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
1348
1443
 
1349
- 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 {
1350
1446
 
1351
- distance_and_id_t best = temporary.top();
1352
- distance_t dist_to_query = -best.first;
1353
- 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;
1354
1459
  bool good = true;
1355
-
1356
- distance_and_id_t const* const top_unordered = top_candidates.data();
1357
- std::size_t const top_count = top_candidates.size();
1358
- for (std::size_t idx = 0; idx < top_count; idx++) {
1359
- distance_and_id_t other = top_unordered[idx];
1360
- node_ref_t other_node = node(other.second);
1361
- node_ref_t best_node = node(best.second);
1362
- distance_t inter_result_dist =
1363
- context.metric(other_node.vector, best_node.vector, other_node.head.dim, best_node.head.dim);
1364
- 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) {
1365
1467
  good = false;
1366
1468
  break;
1367
1469
  }
1368
1470
  }
1369
1471
 
1370
- if (good)
1371
- 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++;
1372
1477
  }
1478
+
1479
+ return {top_ordered, submitted_count};
1373
1480
  }
1374
1481
  };
1375
1482
 
1376
1483
  } // namespace usearch
1377
1484
  } // namespace unum
1485
+
1486
+ #endif