usearch 0.2.3 → 0.4.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.
@@ -85,6 +85,9 @@ inline std::size_t ceil2(std::size_t v) noexcept {
85
85
  return v;
86
86
  }
87
87
 
88
+ /**
89
+ * @brief Inner (Dot) Product distance.
90
+ */
88
91
  template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
89
92
  using scalar_t = scalar_at;
90
93
  using result_t = result_at;
@@ -92,10 +95,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
92
95
 
93
96
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
94
97
  result_type ab{};
95
- #if defined(__GNUC__)
96
- #pragma GCC ivdep
97
- #elif defined(__clang__)
98
+ #if defined(__clang__)
98
99
  #pragma clang loop vectorize(enable)
100
+ #elif defined(__GNUC__)
101
+ #pragma GCC ivdep
99
102
  #elif defined(_OPENMP)
100
103
  #pragma omp simd reduction(+ : ab)
101
104
  #endif
@@ -105,6 +108,12 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
105
108
  }
106
109
  };
107
110
 
111
+ /**
112
+ * @brief Cosine (Angular) distance.
113
+ * Identical to the Inner Product of normalized vectors.
114
+ * Unless you are running on an tiny embedded platform, this metric
115
+ * is recommended over `::ip_gt` for low-precision scalars.
116
+ */
108
117
  template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
109
118
  using scalar_t = scalar_at;
110
119
  using result_t = result_at;
@@ -112,10 +121,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
112
121
 
113
122
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
114
123
  result_t ab{}, a2{}, b2{};
115
- #if defined(__GNUC__)
116
- #pragma GCC ivdep
117
- #elif defined(__clang__)
124
+ #if defined(__clang__)
118
125
  #pragma clang loop vectorize(enable)
126
+ #elif defined(__GNUC__)
127
+ #pragma GCC ivdep
119
128
  #elif defined(_OPENMP)
120
129
  #pragma omp simd reduction(+ : ab, a2, b2)
121
130
  #endif
@@ -123,10 +132,14 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
123
132
  ab += result_t(a[i]) * result_t(b[i]), //
124
133
  a2 += square<result_t>(a[i]), //
125
134
  b2 += square<result_t>(b[i]);
126
- return ab / (std::sqrt(a2) * std::sqrt(b2));
135
+ return 1 - ab / (std::sqrt(a2) * std::sqrt(b2));
127
136
  }
128
137
  };
129
138
 
139
+ /**
140
+ * @brief Squared Euclidean (L2) distance.
141
+ * Square root is avoided at the end, as it won't affect the ordering.
142
+ */
130
143
  template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
131
144
  using scalar_t = scalar_at;
132
145
  using result_t = result_at;
@@ -134,10 +147,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
134
147
 
135
148
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
136
149
  result_t ab_deltas_sq{};
137
- #if defined(__GNUC__)
138
- #pragma GCC ivdep
139
- #elif defined(__clang__)
150
+ #if defined(__clang__)
140
151
  #pragma clang loop vectorize(enable)
152
+ #elif defined(__GNUC__)
153
+ #pragma GCC ivdep
141
154
  #elif defined(_OPENMP)
142
155
  #pragma omp simd reduction(+ : ab_deltas_sq)
143
156
  #endif
@@ -159,10 +172,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct hamming_g
159
172
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
160
173
  std::size_t = 0) const noexcept {
161
174
  result_t matches{};
162
- #if defined(__GNUC__)
163
- #pragma GCC ivdep
164
- #elif defined(__clang__)
175
+ #if defined(__clang__)
165
176
  #pragma clang loop vectorize(enable)
177
+ #elif defined(__GNUC__)
178
+ #pragma GCC ivdep
166
179
  #elif defined(_OPENMP)
167
180
  #pragma omp simd reduction(+ : matches)
168
181
  #endif
@@ -187,10 +200,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct bit_hammi
187
200
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
188
201
  std::size_t = 0) const noexcept {
189
202
  result_t matches{};
190
- #if defined(__GNUC__)
191
- #pragma GCC ivdep
192
- #elif defined(__clang__)
203
+ #if defined(__clang__)
193
204
  #pragma clang loop vectorize(enable)
205
+ #elif defined(__GNUC__)
206
+ #pragma GCC ivdep
194
207
  #elif defined(_OPENMP)
195
208
  #pragma omp simd reduction(+ : matches)
196
209
  #endif
@@ -239,10 +252,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct pearson_cor
239
252
  scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
240
253
  result_t a_sum{}, b_sum{}, ab_sum{};
241
254
  result_t a_sq_sum{}, b_sq_sum{};
242
- #if defined(__GNUC__)
243
- #pragma GCC ivdep
244
- #elif defined(__clang__)
255
+ #if defined(__clang__)
245
256
  #pragma clang loop vectorize(enable)
257
+ #elif defined(__GNUC__)
258
+ #pragma GCC ivdep
246
259
  #elif defined(_OPENMP)
247
260
  #pragma omp simd reduction(+ : a_sum, b_sum, ab_sum, a_sq_sum, b_sq_sum)
248
261
  #endif
@@ -291,6 +304,9 @@ template <std::size_t multiple_ak> inline std::size_t divide_round_up(std::size_
291
304
  return (num + multiple_ak - 1) / multiple_ak;
292
305
  }
293
306
 
307
+ /**
308
+ * @brief Light-weight bitset implementation to track visited nodes during graph traversal.
309
+ */
294
310
  template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
295
311
  using allocator_t = allocator_at;
296
312
  using byte_t = typename allocator_t::value_type;
@@ -330,7 +346,8 @@ using visits_bitset_t = visits_bitset_gt<>;
330
346
 
331
347
  /**
332
348
  * @brief Similar to `std::priority_queue`, but allows raw access to underlying
333
- * memory, in case you want to shuffle it or sort.
349
+ * memory, in case you want to shuffle it or sort. Good for collections
350
+ * from 100s to 10'000s elements.
334
351
  */
335
352
  template <typename element_at, //
336
353
  typename comparator_at = std::less<void>, // <void> is needed before C++14.
@@ -347,23 +364,19 @@ class max_heap_gt {
347
364
  element_t* elements_;
348
365
  std::size_t size_;
349
366
  std::size_t capacity_;
350
- std::size_t max_capacity_;
351
367
 
352
368
  public:
353
- max_heap_gt(std::size_t max_capacity = 0) noexcept
354
- : elements_(nullptr), size_(0), capacity_(0), max_capacity_(max_capacity) {}
369
+ max_heap_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
355
370
 
356
371
  max_heap_gt(max_heap_gt&& other) noexcept {
357
372
  std::swap(elements_, other.elements_);
358
373
  std::swap(size_, other.size_);
359
374
  std::swap(capacity_, other.capacity_);
360
- std::swap(max_capacity_, other.max_capacity_);
361
375
  }
362
376
  max_heap_gt& operator=(max_heap_gt&& other) noexcept {
363
377
  std::swap(elements_, other.elements_);
364
378
  std::swap(size_, other.size_);
365
379
  std::swap(capacity_, other.capacity_);
366
- std::swap(max_capacity_, other.max_capacity_);
367
380
  return *this;
368
381
  }
369
382
 
@@ -384,16 +397,12 @@ class max_heap_gt {
384
397
  }
385
398
  }
386
399
 
387
- bool reserve(std::size_t n) noexcept {
388
- if (n < capacity_)
400
+ bool reserve(std::size_t new_capacity) noexcept {
401
+ if (new_capacity < capacity_)
389
402
  return true;
390
- if (max_capacity_ && capacity_ == max_capacity_)
391
- return false;
392
-
393
- auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
394
- if (max_capacity_)
395
- new_capacity = (std::min)(new_capacity, max_capacity_);
396
403
 
404
+ new_capacity = ceil2(new_capacity);
405
+ new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
397
406
  auto allocator = allocator_t{};
398
407
  auto new_elements = allocator.allocate(new_capacity);
399
408
  if (!new_elements)
@@ -413,10 +422,15 @@ class max_heap_gt {
413
422
  if (!reserve(size_ + 1))
414
423
  return false;
415
424
 
425
+ emplace_reserved(std::forward<args_at>(args)...);
426
+ return true;
427
+ }
428
+
429
+ template <typename... args_at> //
430
+ void emplace_reserved(args_at&&... args) noexcept {
416
431
  new (&elements_[size_]) element_t({std::forward<args_at>(args)...});
417
432
  size_++;
418
433
  shift_up(size_ - 1);
419
- return true;
420
434
  }
421
435
 
422
436
  element_t pop() noexcept {
@@ -430,10 +444,7 @@ class max_heap_gt {
430
444
 
431
445
  /** @brief Invalidates the "max-heap" property, transforming into ascending range. */
432
446
  void sort_ascending() noexcept { std::sort_heap(elements_, elements_ + size_, &less); }
433
- /** @brief Invalidates the "max-heap" property, transforming into descending range. */
434
- void sort_descending() noexcept { sort_ascending(), std::reverse(elements_, elements_ + size_); }
435
- void sort_heap() noexcept { std::make_heap(elements_, elements_ + size_, &less); }
436
- void shrink(std::size_t n) noexcept { size_ = n; }
447
+ void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
437
448
 
438
449
  element_t* data() noexcept { return elements_; }
439
450
  element_t const* data() const noexcept { return elements_; }
@@ -468,7 +479,109 @@ class max_heap_gt {
468
479
  };
469
480
 
470
481
  /**
471
- *
482
+ * @brief Similar to `std::priority_queue`, but allows raw access to underlying
483
+ * memory and always keeps the data sorted. Ideal for small collections
484
+ * under 128 elements.
485
+ */
486
+ template <typename element_at, //
487
+ typename comparator_at = std::less<void>, // <void> is needed before C++14.
488
+ typename allocator_at = std::allocator<element_at>> //
489
+ class sorted_buffer_gt {
490
+ public:
491
+ using element_t = element_at;
492
+ using comparator_t = comparator_at;
493
+ using allocator_t = allocator_at;
494
+
495
+ using value_type = element_t;
496
+
497
+ private:
498
+ element_t* elements_;
499
+ std::size_t size_;
500
+ std::size_t capacity_;
501
+
502
+ public:
503
+ sorted_buffer_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
504
+
505
+ sorted_buffer_gt(sorted_buffer_gt&& other) noexcept {
506
+ std::swap(elements_, other.elements_);
507
+ std::swap(size_, other.size_);
508
+ std::swap(capacity_, other.capacity_);
509
+ }
510
+ sorted_buffer_gt& operator=(sorted_buffer_gt&& other) noexcept {
511
+ std::swap(elements_, other.elements_);
512
+ std::swap(size_, other.size_);
513
+ std::swap(capacity_, other.capacity_);
514
+ return *this;
515
+ }
516
+
517
+ sorted_buffer_gt(sorted_buffer_gt const&) = delete;
518
+ sorted_buffer_gt& operator=(sorted_buffer_gt const&) = delete;
519
+
520
+ ~sorted_buffer_gt() {
521
+ clear();
522
+ allocator_t{}.deallocate(elements_, capacity_);
523
+ }
524
+
525
+ bool empty() const noexcept { return !size_; }
526
+ std::size_t size() const noexcept { return size_; }
527
+ std::size_t capacity() const noexcept { return capacity_; }
528
+ element_t const& top() const noexcept { return elements_[size_ - 1]; }
529
+
530
+ void clear() noexcept {
531
+ while (size_)
532
+ size_--, elements_[size_].~element_t();
533
+ }
534
+
535
+ bool reserve(std::size_t new_capacity) noexcept {
536
+ if (new_capacity < capacity_)
537
+ return true;
538
+
539
+ new_capacity = ceil2(new_capacity);
540
+ new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
541
+ auto allocator = allocator_t{};
542
+ auto new_elements = allocator.allocate(new_capacity);
543
+ if (!new_elements)
544
+ return false;
545
+
546
+ if (size_) {
547
+ std::uninitialized_copy_n(elements_, size_, new_elements);
548
+ allocator.deallocate(elements_, capacity_);
549
+ }
550
+ elements_ = new_elements;
551
+ capacity_ = new_capacity;
552
+ return true;
553
+ }
554
+
555
+ template <typename... args_at> void emplace_reserved(args_at&&... args) {
556
+ element_t element({std::forward<args_at>(args)...});
557
+ std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
558
+ std::size_t to_move = size_ - slot;
559
+ element_t* source = elements_ + size_ - 1;
560
+ for (; to_move; --to_move, --source)
561
+ source[1] = source[0];
562
+ elements_[slot] = element;
563
+ size_++;
564
+ }
565
+
566
+ element_t pop() noexcept {
567
+ size_--;
568
+ element_t result = elements_[size_];
569
+ elements_[size_].~element_t();
570
+ return result;
571
+ }
572
+
573
+ void sort_ascending() noexcept {}
574
+ void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
575
+
576
+ element_t* data() noexcept { return elements_; }
577
+ element_t const* data() const noexcept { return elements_; }
578
+
579
+ private:
580
+ static bool less(element_t const& a, element_t const& b) noexcept { return comparator_t{}(a, b); }
581
+ };
582
+
583
+ /**
584
+ * @brief Tiny userspace mutex, designed to fit in a single integer.
472
585
  */
473
586
  class mutex_t {
474
587
  #if defined(WINDOWS)
@@ -517,13 +630,13 @@ static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than exp
517
630
 
518
631
  using lock_t = std::unique_lock<mutex_t>;
519
632
 
520
- /**
521
- * @brief Five-byte integer type to address node clouds with over 4B entries.
522
- */
523
633
  #if defined(WINDOWS)
524
634
  #pragma pack(push, 1) // Pack struct members on 1-byte alignment
525
635
  #endif
526
636
 
637
+ /**
638
+ * @brief Five-byte integer type to address node clouds with over 4B entries.
639
+ */
527
640
  class usearch_pack_m uint40_t {
528
641
  unsigned char octets[5];
529
642
 
@@ -561,6 +674,7 @@ class usearch_pack_m uint40_t {
561
674
  return old;
562
675
  }
563
676
  };
677
+
564
678
  #if defined(WINDOWS)
565
679
  #pragma pack(pop) // Reset alignment to default
566
680
  #endif
@@ -580,10 +694,9 @@ template <typename scalar_at> class span_gt {
580
694
  };
581
695
 
582
696
  /**
583
- * @brief
584
- * Configuration settings for the index construction.
585
- * Includes the main `::connectivity` parameter (`M` in the paper)
586
- * and two expansion factors - for construction and search.
697
+ * @brief Configuration settings for the index construction.
698
+ * Includes the main `::connectivity` parameter (`M` in the paper)
699
+ * and two expansion factors - for construction and search.
587
700
  */
588
701
  struct config_t {
589
702
 
@@ -612,10 +725,35 @@ struct config_t {
612
725
  std::size_t max_threads_search = 0;
613
726
  };
614
727
 
728
+ struct add_config_t {
729
+ /// @brief Optional thread identifier for multi-threaded construction.
730
+ std::size_t thread{};
731
+ /// @brief Don't copy the ::vector, if it's persisted elsewhere.
732
+ bool store_vector{true};
733
+ };
734
+
735
+ struct add_result_t {
736
+ std::size_t new_size{};
737
+ std::size_t cycles{};
738
+ std::size_t measurements{};
739
+ };
740
+
741
+ struct search_config_t {
742
+ std::size_t thread{};
743
+ bool exact{};
744
+ };
745
+
746
+ struct search_result_t {
747
+ std::size_t count{};
748
+ std::size_t cycles{};
749
+ std::size_t measurements{};
750
+
751
+ inline operator std::size_t() const noexcept { return count; }
752
+ };
753
+
615
754
  /**
616
- * @brief
617
- * Approximate Nearest Neighbors Search index using the
618
- * Hierarchical Navigable Small World graph algorithm.
755
+ * @brief Approximate Nearest Neighbors Search index using the
756
+ * Hierarchical Navigable Small World graph algorithm.
619
757
  *
620
758
  * @section Features
621
759
  * - Search for vectors of different dimensionality.
@@ -650,8 +788,15 @@ class index_gt {
650
788
  using id_t = id_at;
651
789
  using allocator_t = allocator_at;
652
790
 
791
+ using vector_view_t = span_gt<scalar_t const>;
792
+
793
+ #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) // C++17 ann newer
794
+ using distance_t =
795
+ typename std::invoke_result<metric_t, scalar_t const*, scalar_t const*, std::size_t, std::size_t>::type;
796
+ #else
653
797
  using distance_t =
654
798
  typename std::result_of<metric_t(scalar_t const*, scalar_t const*, std::size_t, std::size_t)>::type;
799
+ #endif
655
800
 
656
801
  private:
657
802
  using neighbors_count_t = id_t;
@@ -672,16 +817,18 @@ class index_gt {
672
817
  std::size_t neighbors_base_bytes{};
673
818
  std::size_t mutex_bytes{};
674
819
  };
675
- struct distance_and_id_t {
820
+ struct candidate_t {
676
821
  distance_t first;
677
822
  id_t second;
678
823
  };
679
824
  struct compare_by_distance_t {
680
- inline bool operator()(distance_and_id_t a, distance_and_id_t b) const noexcept { return a.first < b.first; }
825
+ inline bool operator()(candidate_t a, candidate_t b) const noexcept { return a.first < b.first; }
681
826
  };
682
827
 
683
- using distances_and_ids_allocator_t = typename allocator_traits_t::template rebind_alloc<distance_and_id_t>;
684
- using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
828
+ using candidates_view_t = span_gt<candidate_t const>;
829
+ using candidates_allocator_t = typename allocator_traits_t::template rebind_alloc<candidate_t>;
830
+ using top_candidates_t = sorted_buffer_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
831
+ using next_candidates_t = max_heap_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
685
832
 
686
833
  struct neighbors_ref_t {
687
834
  neighbors_count_t& count_;
@@ -696,6 +843,8 @@ class index_gt {
696
843
  inline id_t& operator[](std::size_t i) noexcept { return neighbors_[i]; }
697
844
  inline id_t operator[](std::size_t i) const noexcept { return neighbors_[i]; }
698
845
  inline std::size_t size() const noexcept { return count_; }
846
+ inline void clear() noexcept { count_ = 0u; }
847
+ inline void push_back(id_t id) noexcept { neighbors_[count_] = id, count_++; }
699
848
  };
700
849
 
701
850
  #if defined(WINDOWS)
@@ -734,16 +883,25 @@ class index_gt {
734
883
  scalar_t* vector{};
735
884
 
736
885
  inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
886
+ /// @brief Locks the list of neighbors for concurrent updates.
737
887
  inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
738
888
  inline operator node_t() const noexcept { return node_t{mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
889
+ inline operator vector_view_t() const noexcept { return {vector, head.dim}; }
739
890
  };
740
891
 
741
892
  struct usearch_align_m thread_context_t {
742
- distances_and_ids_t top_candidates;
743
- distances_and_ids_t candidates_set;
744
- visits_bitset_t visits;
745
- std::default_random_engine level_generator;
746
- metric_t metric;
893
+ top_candidates_t top_candidates{};
894
+ next_candidates_t next_candidates{};
895
+ visits_bitset_t visits{};
896
+ std::default_random_engine level_generator{};
897
+ metric_t metric{};
898
+ std::size_t iteration_cycles{};
899
+ std::size_t measurements_count{};
900
+
901
+ inline distance_t measure(vector_view_t a, vector_view_t b) noexcept {
902
+ measurements_count++;
903
+ return metric(a.data(), b.data(), a.size(), b.size());
904
+ }
747
905
  };
748
906
 
749
907
  config_t config_{};
@@ -793,6 +951,9 @@ class index_gt {
793
951
  reserve(config.max_elements);
794
952
  }
795
953
 
954
+ /**
955
+ * @brief Clones the structure with the same hyper-parameters, but without contents.
956
+ */
796
957
  index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
797
958
 
798
959
  ~index_gt() noexcept { clear(); }
@@ -806,6 +967,10 @@ class index_gt {
806
967
 
807
968
  #pragma region Adjusting Configuration
808
969
 
970
+ /**
971
+ * @brief Erases all the vectors from the index.
972
+ * Will change `size()` to zero, but will keep the same `capacity()`.
973
+ */
809
974
  void clear() noexcept {
810
975
  std::size_t n = size_;
811
976
  for (std::size_t i = 0; i != n; ++i)
@@ -815,6 +980,9 @@ class index_gt {
815
980
  entry_id_ = 0u;
816
981
  }
817
982
 
983
+ /**
984
+ * @brief Swaps the underlying memory buffers and thread contexts.
985
+ */
818
986
  void swap(index_gt& other) noexcept {
819
987
  std::swap(config_, other.config_);
820
988
  std::swap(metric_, other.metric_);
@@ -835,6 +1003,9 @@ class index_gt {
835
1003
  other.size_ = size;
836
1004
  }
837
1005
 
1006
+ /**
1007
+ * @brief Increases the `capacity()` of the index to allow adding more vectors.
1008
+ */
838
1009
  void reserve(std::size_t new_capacity) noexcept(false) {
839
1010
 
840
1011
  assert_m(new_capacity >= size_, "Can't drop existing values");
@@ -845,6 +1016,10 @@ class index_gt {
845
1016
  capacity_ = new_capacity;
846
1017
  }
847
1018
 
1019
+ /**
1020
+ * @brief Optimizes configuration options to fit the maximum number
1021
+ * of neighbors in CPU-cache-aligned buffers.
1022
+ */
848
1023
  static config_t optimize(config_t const& config) noexcept {
849
1024
  precomputed_constants_t pre = precompute(config);
850
1025
  std::size_t bytes_per_node_base = head_bytes_k + pre.neighbors_base_bytes + pre.mutex_bytes;
@@ -859,103 +1034,140 @@ class index_gt {
859
1034
 
860
1035
  #pragma region Construction and Search
861
1036
 
862
- id_t add( //
863
- label_t new_label, span_gt<scalar_t const> new_span, //
864
- std::size_t thread_idx = 0, bool store_vector = true) {
1037
+ /**
1038
+ * @brief Inserts a new vector into the index. Thread-safe.
1039
+ *
1040
+ * @param[in] label External identifier/name/descriptor for the vector.
1041
+ * @param[in] vector Contiguous range of scalars forming a vector view.
1042
+ * @param[in] config Configuration options for this specific operation.
1043
+ */
1044
+ add_result_t add(label_t label, vector_view_t vector, add_config_t config = {}) noexcept(false) {
865
1045
 
866
1046
  assert_m(!is_immutable(), "Can't add to an immutable index");
867
- id_t new_id = static_cast<id_t>(size_.fetch_add(1));
868
- scalar_t const* new_vector = new_span.data();
869
- std::size_t new_dim = new_span.size();
1047
+ add_result_t result;
1048
+ result.new_size = size_.fetch_add(1);
1049
+ id_t id = static_cast<id_t>(result.new_size);
870
1050
 
871
- // Determining how much memory to allocate depends on the target level.
1051
+ // Determining how much memory to allocate depends on the target level
872
1052
  lock_t new_level_lock(global_mutex_);
873
1053
  level_t max_level = max_level_;
874
- thread_context_t& context = thread_contexts_[thread_idx];
875
- level_t new_target_level = choose_random_level(context.level_generator);
876
- if (new_target_level <= max_level)
1054
+ thread_context_t& context = thread_contexts_[config.thread];
1055
+ level_t target_level = choose_random_level(context.level_generator);
1056
+ if (target_level <= max_level)
877
1057
  new_level_lock.unlock();
878
1058
 
879
1059
  // Allocate the neighbors
880
- node_ref_t new_node = node_malloc(new_label, new_vector, new_dim, new_target_level, store_vector);
1060
+ node_ref_t new_node = node_malloc(label, vector, target_level, config.store_vector);
881
1061
  lock_t new_lock = new_node.lock();
882
- nodes_[new_id] = new_node;
1062
+ nodes_[id] = new_node;
883
1063
 
884
1064
  // Do nothing for the first element
885
- if (!new_id) {
886
- max_level_ = new_target_level;
887
- return new_id;
1065
+ if (!id) {
1066
+ max_level_ = target_level;
1067
+ return result;
888
1068
  }
889
1069
 
1070
+ // Pull stats
1071
+ result.measurements = context.measurements_count;
1072
+ result.cycles = context.iteration_cycles;
1073
+
890
1074
  // Go down the level, tracking only the closest match
891
- id_t closest_id = search_for_one(entry_id_, new_vector, new_dim, max_level, new_target_level, context);
1075
+ id_t closest_id = search_for_one(entry_id_, vector, max_level, target_level, context);
892
1076
 
893
- // From `new_target_level` down perform proper extensive search.
894
- for (level_t level = (std::min)(new_target_level, max_level); level >= 0; level--) {
895
- search_to_insert(closest_id, new_vector, new_dim, level, context);
896
- closest_id = connect_new_element(new_id, level, context);
1077
+ // From `target_level` down perform proper extensive search
1078
+ for (level_t level = (std::min)(target_level, max_level); level >= 0; level--) {
1079
+ search_to_insert(closest_id, vector, level, context);
1080
+ closest_id = connect_new_element(id, level, context);
897
1081
  }
898
1082
 
899
- // Releasing lock for the maximum level
900
- if (new_target_level > max_level) {
901
- entry_id_ = new_id;
902
- max_level_ = new_target_level;
1083
+ // Normalize stats
1084
+ result.measurements = context.measurements_count - result.measurements;
1085
+ result.cycles = context.iteration_cycles - result.cycles;
1086
+
1087
+ // Updating the entry point if needed
1088
+ if (target_level > max_level) {
1089
+ entry_id_ = id;
1090
+ max_level_ = target_level;
903
1091
  }
904
- return new_id;
1092
+ return result;
905
1093
  }
906
1094
 
907
- template <typename label_and_distance_callback_at>
908
- void search( //
909
- span_gt<scalar_t const> query_span, std::size_t wanted, //
910
- label_and_distance_callback_at&& callback, std::size_t thread_idx = 0) const {
1095
+ /**
1096
+ * @brief Searches for the closest members to the given ::query. Thread-safe.
1097
+ *
1098
+ * @param[in] query Contiguous range of scalars forming a vector view.
1099
+ * @param[in] wanted The upper bound for the number of results to return.
1100
+ * @param[in] callback Function receiving the labels and distances to matches in ascending order.
1101
+ * @param[in] config Configuration options for this specific operation.
1102
+ */
1103
+ template <typename callback_at>
1104
+ search_result_t search( //
1105
+ vector_view_t query, std::size_t wanted, //
1106
+ callback_at&& callback, search_config_t config = {}) const noexcept(false) {
911
1107
 
1108
+ search_result_t result;
912
1109
  if (!size_)
913
- return;
914
-
915
- scalar_t const* query_vec = query_span.data();
916
- std::size_t query_dim = query_span.size();
1110
+ return result;
917
1111
 
918
1112
  // Go down the level, tracking only the closest match
919
- thread_context_t& context = thread_contexts_[thread_idx];
920
- id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
921
-
922
- // For bottom layer we need a more optimized procedure
923
- search_to_find_in_base( //
924
- closest_id, query_vec, query_dim, (std::max)(config_.expansion_search, wanted), context);
925
- while (context.top_candidates.size() > wanted)
926
- context.top_candidates.pop();
927
-
928
- while (context.top_candidates.size()) {
929
- distance_and_id_t top = context.top_candidates.top();
930
- callback(node(top.second).head.label, top.first);
931
- context.top_candidates.pop();
1113
+ thread_context_t& context = thread_contexts_[config.thread];
1114
+ result.measurements = context.measurements_count;
1115
+ result.cycles = context.iteration_cycles;
1116
+
1117
+ if (config.exact) {
1118
+ search_exact(query, wanted, context);
1119
+ } else {
1120
+ id_t closest_id = search_for_one(entry_id_, query, max_level_, 0, context);
1121
+ std::size_t expansion = (std::max)(config_.expansion_search, wanted);
1122
+ // For bottom layer we need a more optimized procedure
1123
+ search_to_find_in_base(closest_id, query, expansion, context);
932
1124
  }
1125
+
1126
+ top_candidates_t& top = context.top_candidates;
1127
+ top.sort_ascending();
1128
+ top.shrink(wanted);
1129
+
1130
+ candidate_t const* top_ordered = top.data();
1131
+ for (std::size_t i = 0; i < top.size(); ++i)
1132
+ callback(node(top_ordered[i].second).head.label, top_ordered[i].first);
1133
+
1134
+ // Normalize stats
1135
+ result.measurements = context.measurements_count - result.measurements;
1136
+ result.cycles = context.iteration_cycles - result.cycles;
1137
+ result.count = top.size();
1138
+ return result;
933
1139
  }
934
1140
 
935
- std::size_t search( //
936
- span_gt<scalar_t const> query_span, std::size_t wanted, //
937
- label_t* matches, distance_t* distances, //
938
- std::size_t thread_idx = 0) const {
1141
+ /**
1142
+ * @brief Searches for the closest members to the given ::query. Thread-safe.
1143
+ * @note Convenience method for callback-based `search()`.
1144
+ *
1145
+ * @param[in] query Contiguous range of scalars forming a vector view.
1146
+ * @param[in] wanted The upper bound for the number of results to return.
1147
+ * @param[out] matches Contiguous output buffer for the labels of closest members.
1148
+ * @param[out] distances Contiguous output buffer for the distances to the closest members.
1149
+ * @param[in] config Configuration options for this specific operation.
1150
+ * @return The number of found approximate matches. Always equal or less then ::wanted.
1151
+ */
1152
+ search_result_t search( //
1153
+ vector_view_t query, std::size_t wanted, //
1154
+ label_t* matches, distance_t* distances, search_config_t config = {}) const noexcept(false) {
939
1155
 
940
- std::size_t found = 0;
941
1156
  auto callback = [&](label_t label, distance_t distance) noexcept {
942
1157
  if (matches)
943
- matches[found] = label;
1158
+ *(matches++) = label;
944
1159
  if (distances)
945
- distances[found] = distance;
946
- ++found;
1160
+ *(distances++) = distance;
947
1161
  };
948
- search(query_span, wanted, callback, thread_idx);
949
- std::reverse(matches, matches + found * (matches != nullptr));
950
- std::reverse(distances, distances + found * (distances != nullptr));
951
- return found;
1162
+ return search(query, wanted, callback, config);
952
1163
  }
953
1164
 
954
1165
  #pragma endregion
955
1166
 
956
1167
  #pragma region Serialization
957
1168
 
958
- struct state_t {
1169
+ private:
1170
+ struct serialized_state_t {
959
1171
  // Check compatibility
960
1172
  std::uint64_t bytes_per_label{};
961
1173
  std::uint64_t bytes_per_id{};
@@ -967,12 +1179,15 @@ class index_gt {
967
1179
  std::uint64_t max_level{};
968
1180
  };
969
1181
 
1182
+ public:
970
1183
  /**
971
- * Compatibility: Linux, MacOS, Windows.
1184
+ * @brief Saves serialized binary index representation to disk,
1185
+ * co-locating vectors and neighbors lists.
1186
+ * Available on Linux, MacOS, Windows.
972
1187
  */
973
1188
  void save(char const* file_path) const noexcept(false) {
974
1189
 
975
- state_t state;
1190
+ serialized_state_t state;
976
1191
  // Check compatibility
977
1192
  state.bytes_per_label = sizeof(label_t);
978
1193
  state.bytes_per_id = sizeof(id_t);
@@ -1018,10 +1233,12 @@ class index_gt {
1018
1233
  }
1019
1234
 
1020
1235
  /**
1021
- * Compatibility: Linux, MacOS, Windows.
1236
+ * @brief Loads the serialized binary index representation from disk,
1237
+ * copying both vectors and neighbors lists into RAM.
1238
+ * Available on Linux, MacOS, Windows.
1022
1239
  */
1023
1240
  void load(char const* file_path) noexcept(false) {
1024
- state_t state;
1241
+ serialized_state_t state;
1025
1242
  std::FILE* file = std::fopen(file_path, "r");
1026
1243
  if (!file)
1027
1244
  throw std::runtime_error(std::strerror(errno));
@@ -1061,7 +1278,7 @@ class index_gt {
1061
1278
  }
1062
1279
 
1063
1280
  std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
1064
- node_ref_t node_ref = node_malloc(head.label, nullptr, head.dim, head.level, true);
1281
+ node_ref_t node_ref = node_malloc(head.label, {nullptr, head.dim}, head.level, true);
1065
1282
  read = std::fread((byte_t*)&node_ref.head + head_bytes_k, bytes_to_dump - head_bytes_k, 1, file);
1066
1283
  if (!read) {
1067
1284
  std::fclose(file);
@@ -1076,13 +1293,15 @@ class index_gt {
1076
1293
  }
1077
1294
 
1078
1295
  /**
1079
- * Compatibility: Linux, MacOS.
1296
+ * @brief Memory-maps the serialized binary index representation from disk,
1297
+ * @b without copying the vectors and neighbors lists into RAM.
1298
+ * Available on Linux, MacOS, but @b not on Windows.
1080
1299
  */
1081
1300
  void view(char const* file_path) noexcept(false) {
1082
1301
  #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
1083
1302
  throw std::logic_error("Memory-mapping is not yet available for Windows");
1084
1303
  #else
1085
- state_t state;
1304
+ serialized_state_t state;
1086
1305
  int open_flags = O_RDONLY;
1087
1306
  #if __linux__
1088
1307
  open_flags |= O_NOATIME;
@@ -1183,11 +1402,10 @@ class index_gt {
1183
1402
  node = node_t{};
1184
1403
  }
1185
1404
 
1186
- node_ref_t node_malloc( //
1187
- label_t label, scalar_t const* vector, std::size_t dim, //
1188
- level_t level, bool store_vector = true) noexcept(false) {
1405
+ node_ref_t node_malloc(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept(false) {
1189
1406
 
1190
1407
  // This function is rarely called and can be as expensive as needed for higher space-efficiency.
1408
+ std::size_t dim = vector.size();
1191
1409
  std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * level;
1192
1410
  std::size_t node_bytes = //
1193
1411
  pre_.mutex_bytes + // Optional concurrency-control
@@ -1201,10 +1419,10 @@ class index_gt {
1201
1419
  mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
1202
1420
  scalar_t* scalars = store_vector //
1203
1421
  ? (scalar_t*)(data + pre_.mutex_bytes + head_bytes_k + levels_bytes)
1204
- : (scalar_t*)(vector);
1422
+ : (scalar_t*)(vector.data());
1205
1423
 
1206
1424
  std::memset(data, 0, node_bytes);
1207
- std::memcpy(scalars, vector, sizeof(scalar_t) * dim * (store_vector && vector));
1425
+ std::memcpy(scalars, vector.data(), sizeof(scalar_t) * dim * (store_vector && vector.data()));
1208
1426
 
1209
1427
  node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
1210
1428
  head.label = label;
@@ -1238,68 +1456,54 @@ class index_gt {
1238
1456
  id_t connect_new_element(id_t new_id, level_t level, thread_context_t& context) noexcept(false) {
1239
1457
 
1240
1458
  node_ref_t new_node = node(new_id);
1241
- distances_and_ids_t& top_candidates = context.top_candidates;
1459
+ top_candidates_t& top = context.top_candidates;
1242
1460
  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);
1244
-
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;
1248
1461
 
1249
1462
  // Outgoing links from `new_id`:
1463
+ neighbors_ref_t new_neighbors = neighbors(new_node, level);
1250
1464
  {
1251
- neighbors_ref_t new_neighbors = neighbors(new_node, level);
1252
- assert_m(!new_neighbors.count_, "The newly inserted element should have blank link list");
1465
+ assert_m(!new_neighbors.size(), "The newly inserted element should have blank link list");
1466
+ candidates_view_t top_view = refine(top, config_.connectivity, context);
1253
1467
 
1254
- new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
1255
- for (std::size_t idx = 0; idx < top_count; idx++) {
1468
+ for (std::size_t idx = 0; idx != top_view.size(); idx++) {
1256
1469
  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;
1470
+ assert_m(level <= node(top_view[idx].second).head.level, "Linking to missing level");
1471
+ new_neighbors.push_back(top_view[idx].second);
1259
1472
  }
1260
1473
  }
1261
1474
 
1262
1475
  // Reverse links from the neighbors:
1263
- for (std::size_t idx = 0; idx < top_count; idx++) {
1264
- id_t close_id = top_ordered[idx].second;
1476
+ for (id_t close_id : new_neighbors) {
1265
1477
  node_ref_t close_node = node(close_id);
1266
1478
  lock_t close_lock = close_node.lock();
1267
1479
 
1268
1480
  neighbors_ref_t close_header = neighbors(close_node, level);
1269
- assert_m(close_header.count_ <= connectivity_max, "Possible corruption");
1481
+ assert_m(close_header.size() <= connectivity_max, "Possible corruption");
1270
1482
  assert_m(close_id != new_id, "Self-loops are impossible");
1271
1483
  assert_m(level <= close_node.head.level, "Linking to missing level");
1272
1484
 
1273
1485
  // If `new_id` is already present in the neighboring connections of `close_id`
1274
1486
  // then no need to modify any connections or run the heuristics.
1275
- if (close_header.count_ < connectivity_max) {
1276
- close_header[close_header.count_] = new_id;
1277
- close_header.count_++;
1487
+ if (close_header.size() < connectivity_max) {
1488
+ close_header.push_back(new_id);
1278
1489
  continue;
1279
1490
  }
1280
1491
 
1281
1492
  // To fit a new connection we need to drop an existing one.
1282
- distances_and_ids_t& candidates = context.candidates_set;
1283
- candidates.clear();
1284
- candidates.emplace( //
1285
- context.metric( //
1286
- new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
1287
- new_id);
1288
- for (id_t successor_id : close_header) {
1289
- node_ref_t successor_node = node(successor_id);
1290
- candidates.emplace( //
1291
- context.metric( //
1292
- successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
1293
- successor_id);
1294
- }
1295
- span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
1493
+ top.clear();
1494
+ top.reserve(close_header.size() + 1);
1495
+ top.emplace_reserved(context.measure(new_node, close_node), new_id);
1496
+ for (id_t successor_id : close_header)
1497
+ top.emplace_reserved(context.measure(node(successor_id), close_node), successor_id);
1296
1498
 
1297
1499
  // Export the results:
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;
1500
+ close_header.clear();
1501
+ candidates_view_t top_view = refine(top, connectivity_max, context);
1502
+ for (std::size_t idx = 0; idx != top_view.size(); idx++)
1503
+ close_header.push_back(top_view[idx].second);
1300
1504
  }
1301
1505
 
1302
- return next_closest_entry_id;
1506
+ return new_neighbors[0];
1303
1507
  }
1304
1508
 
1305
1509
  level_t choose_random_level(std::default_random_engine& level_generator) const noexcept {
@@ -1308,13 +1512,12 @@ class index_gt {
1308
1512
  return (level_t)r;
1309
1513
  }
1310
1514
 
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 {
1515
+ id_t search_for_one( //
1516
+ id_t closest_id, vector_view_t query, //
1517
+ level_t begin_level, level_t end_level, //
1518
+ thread_context_t& context) const noexcept {
1314
1519
 
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);
1520
+ distance_t closest_dist = context.measure(query, node(closest_id));
1318
1521
  for (level_t level = begin_level; level > end_level; level--) {
1319
1522
  bool changed;
1320
1523
  do {
@@ -1323,44 +1526,47 @@ class index_gt {
1323
1526
  lock_t closest_lock = closest_node.lock();
1324
1527
  neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
1325
1528
  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);
1529
+ distance_t candidate_dist = context.measure(query, node(candidate_id));
1329
1530
  if (candidate_dist < closest_dist) {
1330
1531
  closest_dist = candidate_dist;
1331
1532
  closest_id = candidate_id;
1332
1533
  changed = true;
1333
1534
  }
1334
1535
  }
1536
+ context.iteration_cycles++;
1335
1537
  } while (changed);
1336
1538
  }
1337
1539
  return closest_id;
1338
1540
  }
1339
1541
 
1340
- void search_to_insert( //
1341
- id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1342
- level_t level, thread_context_t& context) noexcept(false) {
1542
+ void search_to_insert( //
1543
+ id_t start_id, vector_view_t query, level_t level, //
1544
+ thread_context_t& context) noexcept(false) {
1343
1545
 
1344
1546
  visits_bitset_t& visits = context.visits;
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
1547
+ next_candidates_t& candidates = context.next_candidates; // pop min, push
1548
+ top_candidates_t& top = context.top_candidates; // pop max, push
1549
+ std::size_t const top_limit = config_.expansion_add;
1347
1550
 
1348
- top_candidates.clear();
1349
- candidates_set.clear();
1350
1551
  visits.clear();
1552
+ candidates.clear();
1553
+ top.clear();
1554
+ top.reserve(top_limit + 1);
1351
1555
 
1352
- distance_t closest_dist = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1353
- top_candidates.emplace(closest_dist, start_id);
1354
- candidates_set.emplace(-closest_dist, start_id);
1556
+ distance_t radius = context.measure(query, node(start_id));
1557
+ candidates.emplace(-radius, start_id);
1558
+ top.emplace_reserved(radius, start_id);
1355
1559
  visits.set(start_id);
1356
1560
 
1357
- while (!candidates_set.empty()) {
1561
+ while (!candidates.empty()) {
1358
1562
 
1359
- distance_and_id_t candidacy = candidates_set.top();
1360
- if ((-candidacy.first) > closest_dist && top_candidates.size() == config_.expansion_add)
1563
+ candidate_t candidacy = candidates.top();
1564
+ if ((-candidacy.first) > radius && top.size() == top_limit)
1361
1565
  break;
1362
1566
 
1363
- candidates_set.pop();
1567
+ candidates.pop();
1568
+ context.iteration_cycles++;
1569
+
1364
1570
  id_t candidate_id = candidacy.second;
1365
1571
  node_ref_t candidate_node = node(candidate_id);
1366
1572
  lock_t candidate_lock = candidate_node.lock();
@@ -1372,48 +1578,48 @@ class index_gt {
1372
1578
  continue;
1373
1579
 
1374
1580
  visits.set(successor_id);
1375
- node_ref_t successor_node = node(successor_id);
1376
- distance_t successor_dist =
1377
- context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
1378
- if (top_candidates.size() < config_.expansion_add || closest_dist > successor_dist) {
1379
- candidates_set.emplace(-successor_dist, successor_id);
1380
-
1381
- top_candidates.emplace(successor_dist, successor_id);
1382
- if (top_candidates.size() > config_.expansion_add)
1383
- top_candidates.pop();
1384
- if (!top_candidates.empty())
1385
- closest_dist = top_candidates.top().first;
1581
+ distance_t successor_dist = context.measure(query, node(successor_id));
1582
+
1583
+ if (top.size() < top_limit || successor_dist < radius) {
1584
+ candidates.emplace(-successor_dist, successor_id);
1585
+ top.emplace_reserved(successor_dist, successor_id);
1586
+ if (top.size() > top_limit)
1587
+ top.pop();
1588
+ radius = top.top().first;
1386
1589
  }
1387
1590
  }
1388
1591
  }
1389
1592
  }
1390
1593
 
1391
- void search_to_find_in_base( //
1392
- id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1393
- std::size_t expansion, thread_context_t& context) const noexcept(false) {
1594
+ void search_to_find_in_base( //
1595
+ id_t start_id, vector_view_t query, std::size_t expansion, //
1596
+ thread_context_t& context) const noexcept(false) {
1394
1597
 
1395
1598
  visits_bitset_t& visits = context.visits;
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
1599
+ next_candidates_t& candidates = context.next_candidates; // pop min, push
1600
+ top_candidates_t& top = context.top_candidates; // pop max, push
1601
+ std::size_t const top_limit = expansion;
1398
1602
 
1399
1603
  visits.clear();
1400
- top_candidates.clear();
1401
- candidates_set.clear();
1604
+ candidates.clear();
1605
+ top.clear();
1606
+ top.reserve(top_limit + 1);
1402
1607
 
1403
- distance_t closest_dist = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1404
- top_candidates.emplace(closest_dist, start_id);
1405
- candidates_set.emplace(-closest_dist, start_id);
1608
+ distance_t radius = context.measure(query, node(start_id));
1609
+ candidates.emplace(-radius, start_id);
1610
+ top.emplace_reserved(radius, start_id);
1406
1611
  visits.set(start_id);
1407
1612
 
1408
- while (!candidates_set.empty()) {
1613
+ while (!candidates.empty()) {
1409
1614
 
1410
- distance_and_id_t current_node_node = candidates_set.top();
1411
- if ((-current_node_node.first) > closest_dist)
1615
+ candidate_t candidate = candidates.top();
1616
+ if ((-candidate.first) > radius)
1412
1617
  break;
1413
1618
 
1414
- candidates_set.pop();
1619
+ candidates.pop();
1620
+ context.iteration_cycles++;
1415
1621
 
1416
- id_t candidate_id = current_node_node.second;
1622
+ id_t candidate_id = candidate.second;
1417
1623
  neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
1418
1624
 
1419
1625
  prefetch_neighbors(candidate_header, visits);
@@ -1422,47 +1628,56 @@ class index_gt {
1422
1628
  continue;
1423
1629
 
1424
1630
  visits.set(successor_id);
1425
- node_ref_t successor_node = node(successor_id);
1426
- distance_t successor_dist =
1427
- context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
1428
-
1429
- if (top_candidates.size() < expansion || closest_dist > successor_dist) {
1430
- candidates_set.emplace(-successor_dist, successor_id);
1431
-
1432
- top_candidates.emplace(successor_dist, successor_id);
1433
- if (top_candidates.size() > expansion)
1434
- top_candidates.pop();
1435
- if (!top_candidates.empty())
1436
- closest_dist = top_candidates.top().first;
1631
+ distance_t successor_dist = context.measure(query, node(successor_id));
1632
+
1633
+ if (top.size() < top_limit || successor_dist < radius) {
1634
+ candidates.emplace(-successor_dist, successor_id);
1635
+ top.emplace_reserved(successor_dist, successor_id);
1636
+ if (top.size() > top_limit)
1637
+ top.pop();
1638
+ radius = top.top().first;
1437
1639
  }
1438
1640
  }
1439
1641
  }
1440
1642
  }
1441
1643
 
1644
+ void search_exact(vector_view_t query, std::size_t count, thread_context_t& context) const noexcept(false) {
1645
+ top_candidates_t& top = context.top_candidates;
1646
+ top.clear();
1647
+ top.reserve(count);
1648
+ for (std::size_t i = 0; i != nodes_.size(); ++i)
1649
+ top.emplace_reserved(context.measure(query, node(nodes_[i])), static_cast<id_t>(i));
1650
+ }
1651
+
1442
1652
  void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
1443
1653
 
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 {
1654
+ /**
1655
+ * @brief This algorithm from the original paper implements a heuristic,
1656
+ * that massively reduces the number of connections a point has,
1657
+ * to keep only the neighbors, that are from each other.
1658
+ */
1446
1659
 
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();
1660
+ candidates_view_t refine( //
1661
+ top_candidates_t& top, std::size_t needed, //
1662
+ thread_context_t& context) const noexcept {
1663
+
1664
+ top.sort_ascending();
1665
+ candidate_t* top_data = top.data();
1666
+ std::size_t const top_count = top.size();
1450
1667
  if (top_count < needed)
1451
- return {top_ordered, top_count};
1668
+ return {top_data, top_count};
1452
1669
 
1453
1670
  std::size_t submitted_count = 1;
1454
1671
  std::size_t consumed_count = 1; /// Always equal or greater than `submitted_count`.
1455
1672
  while (submitted_count < needed && consumed_count < top_count) {
1456
- distance_and_id_t candidate = top_ordered[consumed_count];
1673
+ candidate_t candidate = top_data[consumed_count];
1457
1674
  node_ref_t candidate_node = node(candidate.second);
1458
1675
  distance_t candidate_dist = candidate.first;
1459
1676
  bool good = true;
1460
1677
  for (std::size_t idx = 0; idx < submitted_count; idx++) {
1461
- distance_and_id_t submitted = top_ordered[idx];
1678
+ candidate_t submitted = top_data[idx];
1462
1679
  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);
1680
+ distance_t inter_result_dist = context.measure(submitted_node, candidate_node);
1466
1681
  if (inter_result_dist < candidate_dist) {
1467
1682
  good = false;
1468
1683
  break;
@@ -1470,13 +1685,14 @@ class index_gt {
1470
1685
  }
1471
1686
 
1472
1687
  if (good) {
1473
- top_ordered[submitted_count] = top_ordered[consumed_count];
1688
+ top_data[submitted_count] = top_data[consumed_count];
1474
1689
  submitted_count++;
1475
1690
  }
1476
1691
  consumed_count++;
1477
1692
  }
1478
1693
 
1479
- return {top_ordered, submitted_count};
1694
+ top.shrink(submitted_count);
1695
+ return {top_data, submitted_count};
1480
1696
  }
1481
1697
  };
1482
1698