usearch 0.2.4 → 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
@@ -127,6 +136,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
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;
@@ -565,7 +581,7 @@ class sorted_buffer_gt {
565
581
  };
566
582
 
567
583
  /**
568
- *
584
+ * @brief Tiny userspace mutex, designed to fit in a single integer.
569
585
  */
570
586
  class mutex_t {
571
587
  #if defined(WINDOWS)
@@ -614,13 +630,13 @@ static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than exp
614
630
 
615
631
  using lock_t = std::unique_lock<mutex_t>;
616
632
 
617
- /**
618
- * @brief Five-byte integer type to address node clouds with over 4B entries.
619
- */
620
633
  #if defined(WINDOWS)
621
634
  #pragma pack(push, 1) // Pack struct members on 1-byte alignment
622
635
  #endif
623
636
 
637
+ /**
638
+ * @brief Five-byte integer type to address node clouds with over 4B entries.
639
+ */
624
640
  class usearch_pack_m uint40_t {
625
641
  unsigned char octets[5];
626
642
 
@@ -658,6 +674,7 @@ class usearch_pack_m uint40_t {
658
674
  return old;
659
675
  }
660
676
  };
677
+
661
678
  #if defined(WINDOWS)
662
679
  #pragma pack(pop) // Reset alignment to default
663
680
  #endif
@@ -677,10 +694,9 @@ template <typename scalar_at> class span_gt {
677
694
  };
678
695
 
679
696
  /**
680
- * @brief
681
- * Configuration settings for the index construction.
682
- * Includes the main `::connectivity` parameter (`M` in the paper)
683
- * 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.
684
700
  */
685
701
  struct config_t {
686
702
 
@@ -709,10 +725,35 @@ struct config_t {
709
725
  std::size_t max_threads_search = 0;
710
726
  };
711
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
+
712
754
  /**
713
- * @brief
714
- * Approximate Nearest Neighbors Search index using the
715
- * Hierarchical Navigable Small World graph algorithm.
755
+ * @brief Approximate Nearest Neighbors Search index using the
756
+ * Hierarchical Navigable Small World graph algorithm.
716
757
  *
717
758
  * @section Features
718
759
  * - Search for vectors of different dimensionality.
@@ -747,8 +788,15 @@ class index_gt {
747
788
  using id_t = id_at;
748
789
  using allocator_t = allocator_at;
749
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
750
797
  using distance_t =
751
798
  typename std::result_of<metric_t(scalar_t const*, scalar_t const*, std::size_t, std::size_t)>::type;
799
+ #endif
752
800
 
753
801
  private:
754
802
  using neighbors_count_t = id_t;
@@ -835,16 +883,25 @@ class index_gt {
835
883
  scalar_t* vector{};
836
884
 
837
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.
838
887
  inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
839
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}; }
840
890
  };
841
891
 
842
892
  struct usearch_align_m thread_context_t {
843
- top_candidates_t top_candidates;
844
- next_candidates_t next_candidates;
845
- visits_bitset_t visits;
846
- std::default_random_engine level_generator;
847
- 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
+ }
848
905
  };
849
906
 
850
907
  config_t config_{};
@@ -894,6 +951,9 @@ class index_gt {
894
951
  reserve(config.max_elements);
895
952
  }
896
953
 
954
+ /**
955
+ * @brief Clones the structure with the same hyper-parameters, but without contents.
956
+ */
897
957
  index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
898
958
 
899
959
  ~index_gt() noexcept { clear(); }
@@ -907,6 +967,10 @@ class index_gt {
907
967
 
908
968
  #pragma region Adjusting Configuration
909
969
 
970
+ /**
971
+ * @brief Erases all the vectors from the index.
972
+ * Will change `size()` to zero, but will keep the same `capacity()`.
973
+ */
910
974
  void clear() noexcept {
911
975
  std::size_t n = size_;
912
976
  for (std::size_t i = 0; i != n; ++i)
@@ -916,6 +980,9 @@ class index_gt {
916
980
  entry_id_ = 0u;
917
981
  }
918
982
 
983
+ /**
984
+ * @brief Swaps the underlying memory buffers and thread contexts.
985
+ */
919
986
  void swap(index_gt& other) noexcept {
920
987
  std::swap(config_, other.config_);
921
988
  std::swap(metric_, other.metric_);
@@ -936,6 +1003,9 @@ class index_gt {
936
1003
  other.size_ = size;
937
1004
  }
938
1005
 
1006
+ /**
1007
+ * @brief Increases the `capacity()` of the index to allow adding more vectors.
1008
+ */
939
1009
  void reserve(std::size_t new_capacity) noexcept(false) {
940
1010
 
941
1011
  assert_m(new_capacity >= size_, "Can't drop existing values");
@@ -946,6 +1016,10 @@ class index_gt {
946
1016
  capacity_ = new_capacity;
947
1017
  }
948
1018
 
1019
+ /**
1020
+ * @brief Optimizes configuration options to fit the maximum number
1021
+ * of neighbors in CPU-cache-aligned buffers.
1022
+ */
949
1023
  static config_t optimize(config_t const& config) noexcept {
950
1024
  precomputed_constants_t pre = precompute(config);
951
1025
  std::size_t bytes_per_node_base = head_bytes_k + pre.neighbors_base_bytes + pre.mutex_bytes;
@@ -960,99 +1034,140 @@ class index_gt {
960
1034
 
961
1035
  #pragma region Construction and Search
962
1036
 
963
- id_t add( //
964
- label_t new_label, span_gt<scalar_t const> new_span, //
965
- 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) {
966
1045
 
967
1046
  assert_m(!is_immutable(), "Can't add to an immutable index");
968
- id_t new_id = static_cast<id_t>(size_.fetch_add(1));
969
- scalar_t const* new_vector = new_span.data();
970
- 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);
971
1050
 
972
- // Determining how much memory to allocate depends on the target level.
1051
+ // Determining how much memory to allocate depends on the target level
973
1052
  lock_t new_level_lock(global_mutex_);
974
1053
  level_t max_level = max_level_;
975
- thread_context_t& context = thread_contexts_[thread_idx];
976
- level_t new_target_level = choose_random_level(context.level_generator);
977
- 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)
978
1057
  new_level_lock.unlock();
979
1058
 
980
1059
  // Allocate the neighbors
981
- 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);
982
1061
  lock_t new_lock = new_node.lock();
983
- nodes_[new_id] = new_node;
1062
+ nodes_[id] = new_node;
984
1063
 
985
1064
  // Do nothing for the first element
986
- if (!new_id) {
987
- max_level_ = new_target_level;
988
- return new_id;
1065
+ if (!id) {
1066
+ max_level_ = target_level;
1067
+ return result;
989
1068
  }
990
1069
 
1070
+ // Pull stats
1071
+ result.measurements = context.measurements_count;
1072
+ result.cycles = context.iteration_cycles;
1073
+
991
1074
  // Go down the level, tracking only the closest match
992
- 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);
993
1076
 
994
- // From `new_target_level` down perform proper extensive search.
995
- for (level_t level = (std::min)(new_target_level, max_level); level >= 0; level--) {
996
- search_to_insert(closest_id, new_vector, new_dim, level, context);
997
- 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);
998
1081
  }
999
1082
 
1000
- // Releasing lock for the maximum level
1001
- if (new_target_level > max_level) {
1002
- entry_id_ = new_id;
1003
- 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;
1004
1091
  }
1005
- return new_id;
1092
+ return result;
1006
1093
  }
1007
1094
 
1008
- template <typename label_and_distance_callback_at>
1009
- void search( //
1010
- span_gt<scalar_t const> query_span, std::size_t wanted, //
1011
- 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) {
1012
1107
 
1108
+ search_result_t result;
1013
1109
  if (!size_)
1014
- return;
1015
-
1016
- scalar_t const* query_vec = query_span.data();
1017
- std::size_t query_dim = query_span.size();
1110
+ return result;
1018
1111
 
1019
1112
  // Go down the level, tracking only the closest match
1020
- thread_context_t& context = thread_contexts_[thread_idx];
1021
- id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
1022
-
1023
- // For bottom layer we need a more optimized procedure
1024
- std::size_t expansion = (std::max)(config_.expansion_search, wanted);
1025
- search_to_find_in_base(closest_id, query_vec, query_dim, expansion, context);
1026
- context.top_candidates.sort_ascending();
1027
- context.top_candidates.shrink(wanted);
1028
-
1029
- candidate_t const* top = context.top_candidates.data();
1030
- for (std::size_t i = 0; i < context.top_candidates.size(); ++i)
1031
- callback(node(top[i].second).head.label, top[i].first);
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);
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;
1032
1139
  }
1033
1140
 
1034
- std::size_t search( //
1035
- span_gt<scalar_t const> query_span, std::size_t wanted, //
1036
- label_t* matches, distance_t* distances, //
1037
- 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) {
1038
1155
 
1039
- std::size_t found = 0;
1040
1156
  auto callback = [&](label_t label, distance_t distance) noexcept {
1041
1157
  if (matches)
1042
- matches[found] = label;
1158
+ *(matches++) = label;
1043
1159
  if (distances)
1044
- distances[found] = distance;
1045
- ++found;
1160
+ *(distances++) = distance;
1046
1161
  };
1047
- search(query_span, wanted, callback, thread_idx);
1048
- return found;
1162
+ return search(query, wanted, callback, config);
1049
1163
  }
1050
1164
 
1051
1165
  #pragma endregion
1052
1166
 
1053
1167
  #pragma region Serialization
1054
1168
 
1055
- struct state_t {
1169
+ private:
1170
+ struct serialized_state_t {
1056
1171
  // Check compatibility
1057
1172
  std::uint64_t bytes_per_label{};
1058
1173
  std::uint64_t bytes_per_id{};
@@ -1064,12 +1179,15 @@ class index_gt {
1064
1179
  std::uint64_t max_level{};
1065
1180
  };
1066
1181
 
1182
+ public:
1067
1183
  /**
1068
- * 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.
1069
1187
  */
1070
1188
  void save(char const* file_path) const noexcept(false) {
1071
1189
 
1072
- state_t state;
1190
+ serialized_state_t state;
1073
1191
  // Check compatibility
1074
1192
  state.bytes_per_label = sizeof(label_t);
1075
1193
  state.bytes_per_id = sizeof(id_t);
@@ -1115,10 +1233,12 @@ class index_gt {
1115
1233
  }
1116
1234
 
1117
1235
  /**
1118
- * 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.
1119
1239
  */
1120
1240
  void load(char const* file_path) noexcept(false) {
1121
- state_t state;
1241
+ serialized_state_t state;
1122
1242
  std::FILE* file = std::fopen(file_path, "r");
1123
1243
  if (!file)
1124
1244
  throw std::runtime_error(std::strerror(errno));
@@ -1158,7 +1278,7 @@ class index_gt {
1158
1278
  }
1159
1279
 
1160
1280
  std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
1161
- 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);
1162
1282
  read = std::fread((byte_t*)&node_ref.head + head_bytes_k, bytes_to_dump - head_bytes_k, 1, file);
1163
1283
  if (!read) {
1164
1284
  std::fclose(file);
@@ -1173,13 +1293,15 @@ class index_gt {
1173
1293
  }
1174
1294
 
1175
1295
  /**
1176
- * 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.
1177
1299
  */
1178
1300
  void view(char const* file_path) noexcept(false) {
1179
1301
  #if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
1180
1302
  throw std::logic_error("Memory-mapping is not yet available for Windows");
1181
1303
  #else
1182
- state_t state;
1304
+ serialized_state_t state;
1183
1305
  int open_flags = O_RDONLY;
1184
1306
  #if __linux__
1185
1307
  open_flags |= O_NOATIME;
@@ -1280,11 +1402,10 @@ class index_gt {
1280
1402
  node = node_t{};
1281
1403
  }
1282
1404
 
1283
- node_ref_t node_malloc( //
1284
- label_t label, scalar_t const* vector, std::size_t dim, //
1285
- 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) {
1286
1406
 
1287
1407
  // This function is rarely called and can be as expensive as needed for higher space-efficiency.
1408
+ std::size_t dim = vector.size();
1288
1409
  std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * level;
1289
1410
  std::size_t node_bytes = //
1290
1411
  pre_.mutex_bytes + // Optional concurrency-control
@@ -1298,10 +1419,10 @@ class index_gt {
1298
1419
  mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
1299
1420
  scalar_t* scalars = store_vector //
1300
1421
  ? (scalar_t*)(data + pre_.mutex_bytes + head_bytes_k + levels_bytes)
1301
- : (scalar_t*)(vector);
1422
+ : (scalar_t*)(vector.data());
1302
1423
 
1303
1424
  std::memset(data, 0, node_bytes);
1304
- 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()));
1305
1426
 
1306
1427
  node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
1307
1428
  head.label = label;
@@ -1371,17 +1492,9 @@ class index_gt {
1371
1492
  // To fit a new connection we need to drop an existing one.
1372
1493
  top.clear();
1373
1494
  top.reserve(close_header.size() + 1);
1374
- top.emplace_reserved( //
1375
- context.metric( //
1376
- new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
1377
- new_id);
1378
- for (id_t successor_id : close_header) {
1379
- node_ref_t successor_node = node(successor_id);
1380
- top.emplace_reserved( //
1381
- context.metric( //
1382
- successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
1383
- successor_id);
1384
- }
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);
1385
1498
 
1386
1499
  // Export the results:
1387
1500
  close_header.clear();
@@ -1399,13 +1512,12 @@ class index_gt {
1399
1512
  return (level_t)r;
1400
1513
  }
1401
1514
 
1402
- id_t search_for_one( //
1403
- id_t entry_id, scalar_t const* query_vec, std::size_t query_dim, //
1404
- 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 {
1405
1519
 
1406
- id_t closest_id = entry_id;
1407
- distance_t closest_dist =
1408
- 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));
1409
1521
  for (level_t level = begin_level; level > end_level; level--) {
1410
1522
  bool changed;
1411
1523
  do {
@@ -1414,23 +1526,22 @@ class index_gt {
1414
1526
  lock_t closest_lock = closest_node.lock();
1415
1527
  neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
1416
1528
  for (id_t candidate_id : closest_header) {
1417
- node_ref_t candidate_node = node(candidate_id);
1418
- distance_t candidate_dist =
1419
- context.metric(query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
1529
+ distance_t candidate_dist = context.measure(query, node(candidate_id));
1420
1530
  if (candidate_dist < closest_dist) {
1421
1531
  closest_dist = candidate_dist;
1422
1532
  closest_id = candidate_id;
1423
1533
  changed = true;
1424
1534
  }
1425
1535
  }
1536
+ context.iteration_cycles++;
1426
1537
  } while (changed);
1427
1538
  }
1428
1539
  return closest_id;
1429
1540
  }
1430
1541
 
1431
- void search_to_insert( //
1432
- id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1433
- 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) {
1434
1545
 
1435
1546
  visits_bitset_t& visits = context.visits;
1436
1547
  next_candidates_t& candidates = context.next_candidates; // pop min, push
@@ -1442,7 +1553,7 @@ class index_gt {
1442
1553
  top.clear();
1443
1554
  top.reserve(top_limit + 1);
1444
1555
 
1445
- distance_t radius = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1556
+ distance_t radius = context.measure(query, node(start_id));
1446
1557
  candidates.emplace(-radius, start_id);
1447
1558
  top.emplace_reserved(radius, start_id);
1448
1559
  visits.set(start_id);
@@ -1454,6 +1565,8 @@ class index_gt {
1454
1565
  break;
1455
1566
 
1456
1567
  candidates.pop();
1568
+ context.iteration_cycles++;
1569
+
1457
1570
  id_t candidate_id = candidacy.second;
1458
1571
  node_ref_t candidate_node = node(candidate_id);
1459
1572
  lock_t candidate_lock = candidate_node.lock();
@@ -1465,9 +1578,8 @@ class index_gt {
1465
1578
  continue;
1466
1579
 
1467
1580
  visits.set(successor_id);
1468
- node_ref_t successor_node = node(successor_id);
1469
- distance_t successor_dist =
1470
- context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
1581
+ distance_t successor_dist = context.measure(query, node(successor_id));
1582
+
1471
1583
  if (top.size() < top_limit || successor_dist < radius) {
1472
1584
  candidates.emplace(-successor_dist, successor_id);
1473
1585
  top.emplace_reserved(successor_dist, successor_id);
@@ -1479,9 +1591,9 @@ class index_gt {
1479
1591
  }
1480
1592
  }
1481
1593
 
1482
- void search_to_find_in_base( //
1483
- id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
1484
- 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) {
1485
1597
 
1486
1598
  visits_bitset_t& visits = context.visits;
1487
1599
  next_candidates_t& candidates = context.next_candidates; // pop min, push
@@ -1493,7 +1605,7 @@ class index_gt {
1493
1605
  top.clear();
1494
1606
  top.reserve(top_limit + 1);
1495
1607
 
1496
- distance_t radius = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1608
+ distance_t radius = context.measure(query, node(start_id));
1497
1609
  candidates.emplace(-radius, start_id);
1498
1610
  top.emplace_reserved(radius, start_id);
1499
1611
  visits.set(start_id);
@@ -1505,6 +1617,7 @@ class index_gt {
1505
1617
  break;
1506
1618
 
1507
1619
  candidates.pop();
1620
+ context.iteration_cycles++;
1508
1621
 
1509
1622
  id_t candidate_id = candidate.second;
1510
1623
  neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
@@ -1515,9 +1628,7 @@ class index_gt {
1515
1628
  continue;
1516
1629
 
1517
1630
  visits.set(successor_id);
1518
- node_ref_t successor_node = node(successor_id);
1519
- distance_t successor_dist =
1520
- context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
1631
+ distance_t successor_dist = context.measure(query, node(successor_id));
1521
1632
 
1522
1633
  if (top.size() < top_limit || successor_dist < radius) {
1523
1634
  candidates.emplace(-successor_dist, successor_id);
@@ -1530,6 +1641,14 @@ class index_gt {
1530
1641
  }
1531
1642
  }
1532
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
+
1533
1652
  void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
1534
1653
 
1535
1654
  /**
@@ -1537,7 +1656,10 @@ class index_gt {
1537
1656
  * that massively reduces the number of connections a point has,
1538
1657
  * to keep only the neighbors, that are from each other.
1539
1658
  */
1540
- candidates_view_t refine(top_candidates_t& top, std::size_t needed, thread_context_t& context) const noexcept {
1659
+
1660
+ candidates_view_t refine( //
1661
+ top_candidates_t& top, std::size_t needed, //
1662
+ thread_context_t& context) const noexcept {
1541
1663
 
1542
1664
  top.sort_ascending();
1543
1665
  candidate_t* top_data = top.data();
@@ -1555,9 +1677,7 @@ class index_gt {
1555
1677
  for (std::size_t idx = 0; idx < submitted_count; idx++) {
1556
1678
  candidate_t submitted = top_data[idx];
1557
1679
  node_ref_t submitted_node = node(submitted.second);
1558
- distance_t inter_result_dist = context.metric( //
1559
- submitted_node.vector, candidate_node.vector, //
1560
- submitted_node.head.dim, candidate_node.head.dim);
1680
+ distance_t inter_result_dist = context.measure(submitted_node, candidate_node);
1561
1681
  if (inter_result_dist < candidate_dist) {
1562
1682
  good = false;
1563
1683
  break;