usearch 2.2.1 → 2.3.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.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "2.2.1"
3
+ version = "2.3.0"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '2.2.1'
7
+ version = '2.3.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/cpp/bench.cpp CHANGED
@@ -372,6 +372,7 @@ static void single_shot(dataset_at& dataset, index_at& index, bool construct = t
372
372
  executor, [&](std::size_t progress, std::size_t total) {
373
373
  if (progress % 1000 == 0)
374
374
  printer.print(progress, total);
375
+ return true;
375
376
  });
376
377
  join_attempts = result.visited_members;
377
378
  }
@@ -1212,9 +1212,10 @@ struct dummy_callback_t {
1212
1212
  *
1213
1213
  * This is particularly helpful when handling long-running tasks, like serialization,
1214
1214
  * saving, and loading from disk, or index-level joins.
1215
+ * The reporter checks return value to continue or stop the process, `false` means need to stop.
1215
1216
  */
1216
1217
  struct dummy_progress_t {
1217
- inline void operator()(std::size_t, std::size_t) const noexcept {}
1218
+ inline bool operator()(std::size_t /*processed*/, std::size_t /*total*/) const noexcept { return true; }
1218
1219
  };
1219
1220
 
1220
1221
  /**
@@ -2690,6 +2691,10 @@ class index_gt {
2690
2691
  if (!output(&header, sizeof(header)))
2691
2692
  return result.failed("Failed to serialize the header into stream");
2692
2693
 
2694
+ // Progress status
2695
+ std::size_t processed = 0;
2696
+ std::size_t const total = 2 * header.size;
2697
+
2693
2698
  // Export the number of levels per node
2694
2699
  // That is both enough to estimate the overall memory consumption,
2695
2700
  // and to be able to estimate the offsets of every entry in the file.
@@ -2697,15 +2702,18 @@ class index_gt {
2697
2702
  node_t node = node_at_(i);
2698
2703
  level_t level = node.level();
2699
2704
  if (!output(&level, sizeof(level)))
2700
- return result.failed("Failed to serialize nodes levels into stream");
2705
+ return result.failed("Failed to serialize into stream");
2706
+ if (!progress(++processed, total))
2707
+ return result.failed("Terminated by user");
2701
2708
  }
2702
2709
 
2703
2710
  // After that dump the nodes themselves
2704
2711
  for (std::size_t i = 0; i != header.size; ++i) {
2705
2712
  span_bytes_t node_bytes = node_bytes_(node_at_(i));
2706
2713
  if (!output(node_bytes.data(), node_bytes.size()))
2707
- return result.failed("Failed to serialize nodes into stream");
2708
- progress(i, header.size);
2714
+ return result.failed("Failed to serialize into stream");
2715
+ if (!progress(++processed, total))
2716
+ return result.failed("Terminated by user");
2709
2717
  }
2710
2718
 
2711
2719
  return {};
@@ -2763,7 +2771,8 @@ class index_gt {
2763
2771
  return result.failed("Failed to pull nodes from the stream");
2764
2772
  }
2765
2773
  nodes_[i] = node_t{node_bytes.data()};
2766
- progress(i, header.size);
2774
+ if (!progress(i + 1, header.size))
2775
+ return result.failed("Terminated by user");
2767
2776
  }
2768
2777
  return {};
2769
2778
  }
@@ -2936,7 +2945,8 @@ class index_gt {
2936
2945
  // Rapidly address all the nodes
2937
2946
  for (std::size_t i = 0; i != header.size; ++i) {
2938
2947
  nodes_[i] = node_t{(byte_t*)file.data() + offsets[i]};
2939
- progress(i, header.size);
2948
+ if (!progress(i + 1, header.size))
2949
+ return result.failed("Terminated by user");
2940
2950
  }
2941
2951
  viewed_file_ = std::move(file);
2942
2952
  return {};
@@ -2986,8 +2996,13 @@ class index_gt {
2986
2996
  using slot_level_allocator_t = typename dynamic_allocator_traits_t::template rebind_alloc<slot_level_t>;
2987
2997
  buffer_gt<slot_level_t, slot_level_allocator_t> slots_and_levels(size());
2988
2998
 
2999
+ // Progress status
3000
+ std::atomic<bool> do_tasks{true};
3001
+ std::atomic<std::size_t> processed{0};
3002
+ std::size_t const total = 3 * slots_and_levels.size();
3003
+
2989
3004
  // For every bottom level node, determine its parent cluster
2990
- executor.fixed(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
3005
+ executor.dynamic(slots_and_levels.size(), [&](std::size_t thread_idx, std::size_t old_slot) {
2991
3006
  context_t& context = contexts_[thread_idx];
2992
3007
  std::size_t cluster = search_for_one_( //
2993
3008
  values[citerator_at(old_slot)], //
@@ -2997,7 +3012,13 @@ class index_gt {
2997
3012
  static_cast<compressed_slot_t>(old_slot), //
2998
3013
  static_cast<compressed_slot_t>(cluster), //
2999
3014
  node_at_(old_slot).level()};
3015
+ ++processed;
3016
+ if (thread_idx == 0)
3017
+ do_tasks = progress(processed.load(), total);
3018
+ return do_tasks.load();
3000
3019
  });
3020
+ if (!do_tasks.load())
3021
+ return;
3001
3022
 
3002
3023
  // Where the actual permutation happens:
3003
3024
  std::sort(slots_and_levels.begin(), slots_and_levels.end(), [](slot_level_t const& a, slot_level_t const& b) {
@@ -3027,8 +3048,8 @@ class index_gt {
3027
3048
  neighbor = static_cast<compressed_slot_t>(old_slot_to_new[compressed_slot_t(neighbor)]);
3028
3049
 
3029
3050
  reordered_nodes[new_slot] = new_node;
3030
-
3031
- progress(new_slot, slots_and_levels.size());
3051
+ if (!progress(++processed, total))
3052
+ return;
3032
3053
  }
3033
3054
 
3034
3055
  for (std::size_t new_slot = 0; new_slot != slots_and_levels.size(); ++new_slot) {
@@ -3036,6 +3057,8 @@ class index_gt {
3036
3057
  slot_transition(node_at_(old_slot).ckey(), //
3037
3058
  static_cast<compressed_slot_t>(old_slot), //
3038
3059
  static_cast<compressed_slot_t>(new_slot));
3060
+ if (!progress(++processed, total))
3061
+ return;
3039
3062
  }
3040
3063
 
3041
3064
  nodes_ = std::move(reordered_nodes);
@@ -3064,9 +3087,13 @@ class index_gt {
3064
3087
  executor_at&& executor = executor_at{}, //
3065
3088
  progress_at&& progress = progress_at{}) noexcept {
3066
3089
 
3090
+ // Progress status
3091
+ std::atomic<bool> do_tasks{true};
3092
+ std::atomic<std::size_t> processed{0};
3093
+
3067
3094
  // Erase all the incoming links
3068
3095
  std::size_t nodes_count = size();
3069
- executor.fixed(nodes_count, [&](std::size_t, std::size_t node_idx) {
3096
+ executor.dynamic(nodes_count, [&](std::size_t thread_idx, std::size_t node_idx) {
3070
3097
  node_t node = node_at_(node_idx);
3071
3098
  for (level_t level = 0; level <= node.level(); ++level) {
3072
3099
  neighbors_ref_t neighbors = neighbors_(node, level);
@@ -3079,8 +3106,14 @@ class index_gt {
3079
3106
  neighbors.push_back(neighbor_slot);
3080
3107
  }
3081
3108
  }
3082
- progress(node_idx, nodes_count);
3109
+ ++processed;
3110
+ if (thread_idx == 0)
3111
+ do_tasks = progress(processed.load(), nodes_count);
3112
+ return do_tasks.load();
3083
3113
  });
3114
+
3115
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
3116
+ progress(processed.load(), nodes_count);
3084
3117
  }
3085
3118
 
3086
3119
  private:
@@ -3710,7 +3743,10 @@ static join_result_t join( //
3710
3743
  passed_rounds = ++rounds;
3711
3744
  total_rounds = passed_rounds + free_men.size();
3712
3745
  }
3713
- progress(passed_rounds, total_rounds);
3746
+ if (thread_idx == 0 && !progress(passed_rounds, total_rounds)) {
3747
+ atomic_error.store("Terminated by user");
3748
+ break;
3749
+ }
3714
3750
  while (men_locks.atomic_set(free_man_slot))
3715
3751
  ;
3716
3752
 
@@ -767,7 +767,9 @@ class index_dense_gt {
767
767
  * @brief Saves serialized binary index representation to a stream.
768
768
  */
769
769
  template <typename output_callback_at, typename progress_at = dummy_progress_t>
770
- serialization_result_t save_to_stream(output_callback_at&& output, serialization_config_t config = {}) const {
770
+ serialization_result_t save_to_stream(output_callback_at&& output, //
771
+ serialization_config_t config = {}, //
772
+ progress_at&& progress = {}) const {
771
773
 
772
774
  serialization_result_t result;
773
775
  std::uint64_t matrix_rows = 0;
@@ -831,7 +833,7 @@ class index_dense_gt {
831
833
  }
832
834
 
833
835
  // Save the actual proximity graph
834
- return typed_->save_to_stream(std::forward<output_callback_at>(output));
836
+ return typed_->save_to_stream(std::forward<output_callback_at>(output), std::forward<progress_at>(progress));
835
837
  }
836
838
 
837
839
  /**
@@ -853,8 +855,10 @@ class index_dense_gt {
853
855
  * @param[in] config Configuration parameters for imports.
854
856
  * @return Outcome descriptor explicitly convertible to boolean.
855
857
  */
856
- template <typename input_callback_at>
857
- serialization_result_t load_from_stream(input_callback_at&& input, serialization_config_t config = {}) {
858
+ template <typename input_callback_at, typename progress_at = dummy_progress_t>
859
+ serialization_result_t load_from_stream(input_callback_at&& input, //
860
+ serialization_config_t config = {}, //
861
+ progress_at&& progress = {}) {
858
862
 
859
863
  // Discard all previous memory allocations of `vectors_tape_allocator_`
860
864
  reset();
@@ -915,7 +919,7 @@ class index_dense_gt {
915
919
  }
916
920
 
917
921
  // Pull the actual proximity graph
918
- result = typed_->load_from_stream(std::forward<input_callback_at>(input));
922
+ result = typed_->load_from_stream(std::forward<input_callback_at>(input), std::forward<progress_at>(progress));
919
923
  if (!result)
920
924
  return result;
921
925
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
@@ -931,7 +935,10 @@ class index_dense_gt {
931
935
  * @param[in] config Configuration parameters for imports.
932
936
  * @return Outcome descriptor explicitly convertible to boolean.
933
937
  */
934
- serialization_result_t view(memory_mapped_file_t file, std::size_t offset = 0, serialization_config_t config = {}) {
938
+ template <typename progress_at = dummy_progress_t>
939
+ serialization_result_t view(memory_mapped_file_t file, //
940
+ std::size_t offset = 0, serialization_config_t config = {}, //
941
+ progress_at&& progress = {}) {
935
942
 
936
943
  // Discard all previous memory allocations of `vectors_tape_allocator_`
937
944
  reset();
@@ -997,7 +1004,7 @@ class index_dense_gt {
997
1004
  }
998
1005
 
999
1006
  // Pull the actual proximity graph
1000
- result = typed_->view(std::move(file), offset);
1007
+ result = typed_->view(std::move(file), offset, std::forward<progress_at>(progress));
1001
1008
  if (!result)
1002
1009
  return result;
1003
1010
  if (typed_->size() != static_cast<std::size_t>(matrix_rows))
@@ -1019,7 +1026,9 @@ class index_dense_gt {
1019
1026
  * @param[in] config Configuration parameters for exports.
1020
1027
  * @return Outcome descriptor explicitly convertible to boolean.
1021
1028
  */
1022
- serialization_result_t save(output_file_t file, serialization_config_t config = {}) const {
1029
+ template <typename progress_at = dummy_progress_t>
1030
+ serialization_result_t save(output_file_t file, serialization_config_t config = {},
1031
+ progress_at&& progress = {}) const {
1023
1032
 
1024
1033
  serialization_result_t io_result = file.open_if_not();
1025
1034
  if (!io_result)
@@ -1030,7 +1039,7 @@ class index_dense_gt {
1030
1039
  io_result = file.write(buffer, length);
1031
1040
  return !!io_result;
1032
1041
  },
1033
- config);
1042
+ config, std::forward<progress_at>(progress));
1034
1043
 
1035
1044
  if (!stream_result)
1036
1045
  return stream_result;
@@ -1041,8 +1050,11 @@ class index_dense_gt {
1041
1050
  * @brief Memory-maps the serialized binary index representation from disk,
1042
1051
  * @b without copying data into RAM, and fetching it on-demand.
1043
1052
  */
1044
- serialization_result_t save(memory_mapped_file_t file, std::size_t offset = 0,
1045
- serialization_config_t config = {}) const {
1053
+ template <typename progress_at = dummy_progress_t>
1054
+ serialization_result_t save(memory_mapped_file_t file, //
1055
+ std::size_t offset = 0, //
1056
+ serialization_config_t config = {}, //
1057
+ progress_at&& progress = {}) const {
1046
1058
 
1047
1059
  serialization_result_t io_result = file.open_if_not();
1048
1060
  if (!io_result)
@@ -1056,7 +1068,7 @@ class index_dense_gt {
1056
1068
  offset += length;
1057
1069
  return true;
1058
1070
  },
1059
- config);
1071
+ config, std::forward<progress_at>(progress));
1060
1072
 
1061
1073
  return stream_result;
1062
1074
  }
@@ -1067,7 +1079,8 @@ class index_dense_gt {
1067
1079
  * @param[in] config Configuration parameters for imports.
1068
1080
  * @return Outcome descriptor explicitly convertible to boolean.
1069
1081
  */
1070
- serialization_result_t load(input_file_t file, serialization_config_t config = {}) {
1082
+ template <typename progress_at = dummy_progress_t>
1083
+ serialization_result_t load(input_file_t file, serialization_config_t config = {}, progress_at&& progress = {}) {
1071
1084
 
1072
1085
  serialization_result_t io_result = file.open_if_not();
1073
1086
  if (!io_result)
@@ -1078,7 +1091,7 @@ class index_dense_gt {
1078
1091
  io_result = file.read(buffer, length);
1079
1092
  return !!io_result;
1080
1093
  },
1081
- config);
1094
+ config, std::forward<progress_at>(progress));
1082
1095
 
1083
1096
  if (!stream_result)
1084
1097
  return stream_result;
@@ -1089,7 +1102,11 @@ class index_dense_gt {
1089
1102
  * @brief Memory-maps the serialized binary index representation from disk,
1090
1103
  * @b without copying data into RAM, and fetching it on-demand.
1091
1104
  */
1092
- serialization_result_t load(memory_mapped_file_t file, std::size_t offset = 0, serialization_config_t config = {}) {
1105
+ template <typename progress_at = dummy_progress_t>
1106
+ serialization_result_t load(memory_mapped_file_t file, //
1107
+ std::size_t offset = 0, //
1108
+ serialization_config_t config = {}, //
1109
+ progress_at&& progress = {}) {
1093
1110
 
1094
1111
  serialization_result_t io_result = file.open_if_not();
1095
1112
  if (!io_result)
@@ -1103,17 +1120,23 @@ class index_dense_gt {
1103
1120
  offset += length;
1104
1121
  return true;
1105
1122
  },
1106
- config);
1123
+ config, std::forward<progress_at>(progress));
1107
1124
 
1108
1125
  return stream_result;
1109
1126
  }
1110
1127
 
1111
- serialization_result_t save(char const* file_path, serialization_config_t config = {}) const {
1112
- return save(output_file_t(file_path), config);
1128
+ template <typename progress_at = dummy_progress_t>
1129
+ serialization_result_t save(char const* file_path, //
1130
+ serialization_config_t config = {}, //
1131
+ progress_at&& progress = {}) const {
1132
+ return save(output_file_t(file_path), config, std::forward<progress_at>(progress));
1113
1133
  }
1114
1134
 
1115
- serialization_result_t load(char const* file_path, serialization_config_t config = {}) {
1116
- return load(input_file_t(file_path), config);
1135
+ template <typename progress_at = dummy_progress_t>
1136
+ serialization_result_t load(char const* file_path, //
1137
+ serialization_config_t config = {}, //
1138
+ progress_at&& progress = {}) {
1139
+ return load(input_file_t(file_path), config, std::forward<progress_at>(progress));
1117
1140
  }
1118
1141
 
1119
1142
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
package/python/lib.cpp CHANGED
@@ -21,6 +21,7 @@
21
21
 
22
22
  #define _CRT_SECURE_NO_WARNINGS
23
23
  #define PY_SSIZE_T_CLEAN
24
+ #include <pybind11/functional.h>
24
25
  #include <pybind11/numpy.h>
25
26
  #include <pybind11/pybind11.h>
26
27
  #include <pybind11/stl.h>
@@ -55,6 +56,18 @@ using dense_labeling_result_t = typename index_dense_t::labeling_result_t;
55
56
  using dense_cluster_result_t = typename index_dense_t::cluster_result_t;
56
57
  using dense_clustering_result_t = typename index_dense_t::clustering_result_t;
57
58
 
59
+ using progress_func_t = std::function<bool(std::size_t /*processed*/, std::size_t /*total*/)>;
60
+
61
+ struct progress_t {
62
+ inline progress_t(progress_func_t const& func) : func_(func ? func : &dummy_progress) {}
63
+ inline bool operator()(std::size_t processed, std::size_t total) const noexcept { return func_(processed, total); }
64
+
65
+ private:
66
+ static inline bool dummy_progress(std::size_t /*processed*/, std::size_t /*total*/) { return true; }
67
+
68
+ progress_func_t func_;
69
+ };
70
+
58
71
  struct dense_index_py_t : public index_dense_t {
59
72
  using native_t = index_dense_t;
60
73
  using native_t::add;
@@ -208,13 +221,18 @@ template <typename scalar_at>
208
221
  static void add_typed_to_index( //
209
222
  dense_index_py_t& index, //
210
223
  py::buffer_info const& keys_info, py::buffer_info const& vectors_info, //
211
- bool force_copy, std::size_t threads) {
224
+ bool force_copy, std::size_t threads, //
225
+ progress_func_t const& progress) {
212
226
 
213
227
  Py_ssize_t vectors_count = vectors_info.shape[0];
214
228
  byte_t const* vectors_data = reinterpret_cast<byte_t const*>(vectors_info.ptr);
215
229
  byte_t const* keys_data = reinterpret_cast<byte_t const*>(keys_info.ptr);
216
230
  atomic_error_t atomic_error{nullptr};
217
231
 
232
+ // Progress status
233
+ progress_t progress_{progress};
234
+ std::atomic<std::size_t> processed{0};
235
+
218
236
  executor_default_t{threads}.dynamic(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
219
237
  dense_key_t key = *reinterpret_cast<dense_key_t const*>(keys_data + task_idx * keys_info.strides[0]);
220
238
  scalar_at const* vector = reinterpret_cast<scalar_at const*>(vectors_data + task_idx * vectors_info.strides[0]);
@@ -225,12 +243,18 @@ static void add_typed_to_index( //
225
243
  }
226
244
 
227
245
  // We don't want to check for signals from multiple threads
246
+ ++processed;
228
247
  if (thread_idx == 0)
229
- if (PyErr_CheckSignals() != 0)
248
+ if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), vectors_count)) {
249
+ atomic_error.store("Operation has been terminated");
230
250
  return false;
251
+ }
231
252
  return true;
232
253
  });
233
254
 
255
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
256
+ progress_(processed.load(), vectors_count);
257
+
234
258
  // Raise the error from a single thread
235
259
  auto error = atomic_error.load();
236
260
  if (error) {
@@ -242,7 +266,8 @@ static void add_typed_to_index( //
242
266
  template <typename index_at>
243
267
  static void add_many_to_index( //
244
268
  index_at& index, py::buffer keys, py::buffer vectors, //
245
- bool force_copy, std::size_t threads) {
269
+ bool force_copy, std::size_t threads, //
270
+ progress_func_t const& progress) {
246
271
 
247
272
  py::buffer_info keys_info = keys.request();
248
273
  py::buffer_info vectors_info = vectors.request();
@@ -272,11 +297,11 @@ static void add_many_to_index( //
272
297
 
273
298
  // clang-format off
274
299
  switch (numpy_string_to_kind(vectors_info.format)) {
275
- case scalar_kind_t::b1x8_k: add_typed_to_index<b1x8_t>(index, keys_info, vectors_info, force_copy, threads); break;
276
- case scalar_kind_t::i8_k: add_typed_to_index<i8_t>(index, keys_info, vectors_info, force_copy, threads); break;
277
- case scalar_kind_t::f16_k: add_typed_to_index<f16_t>(index, keys_info, vectors_info, force_copy, threads); break;
278
- case scalar_kind_t::f32_k: add_typed_to_index<f32_t>(index, keys_info, vectors_info, force_copy, threads); break;
279
- case scalar_kind_t::f64_k: add_typed_to_index<f64_t>(index, keys_info, vectors_info, force_copy, threads); break;
300
+ case scalar_kind_t::b1x8_k: add_typed_to_index<b1x8_t>(index, keys_info, vectors_info, force_copy, threads, progress); break;
301
+ case scalar_kind_t::i8_k: add_typed_to_index<i8_t>(index, keys_info, vectors_info, force_copy, threads, progress); break;
302
+ case scalar_kind_t::f16_k: add_typed_to_index<f16_t>(index, keys_info, vectors_info, force_copy, threads, progress); break;
303
+ case scalar_kind_t::f32_k: add_typed_to_index<f32_t>(index, keys_info, vectors_info, force_copy, threads, progress); break;
304
+ case scalar_kind_t::f64_k: add_typed_to_index<f64_t>(index, keys_info, vectors_info, force_copy, threads, progress); break;
280
305
  default: throw std::invalid_argument("Incompatible scalars in the vectors matrix: " + vectors_info.format);
281
306
  }
282
307
  // clang-format on
@@ -287,7 +312,8 @@ static void search_typed( //
287
312
  dense_index_py_t& index, py::buffer_info& vectors_info, //
288
313
  std::size_t wanted, bool exact, std::size_t threads, //
289
314
  py::array_t<dense_key_t>& keys_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py,
290
- std::atomic<std::size_t>& stats_visited_members, std::atomic<std::size_t>& stats_computed_distances) {
315
+ std::atomic<std::size_t>& stats_visited_members, std::atomic<std::size_t>& stats_computed_distances,
316
+ progress_func_t const& progress) {
291
317
 
292
318
  auto keys_py2d = keys_py.template mutable_unchecked<2>();
293
319
  auto distances_py2d = distances_py.template mutable_unchecked<2>();
@@ -301,6 +327,10 @@ static void search_typed( //
301
327
  if (!index.reserve(index_limits_t(index.size(), threads)))
302
328
  throw std::invalid_argument("Out of memory!");
303
329
 
330
+ // Progress status
331
+ progress_t progress_{progress};
332
+ std::atomic<std::size_t> processed{0};
333
+
304
334
  atomic_error_t atomic_error{nullptr};
305
335
  executor_default_t{threads}.dynamic(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
306
336
  scalar_at const* vector = (scalar_at const*)(vectors_data + task_idx * vectors_info.strides[0]);
@@ -317,12 +347,18 @@ static void search_typed( //
317
347
  stats_computed_distances += result.computed_distances;
318
348
 
319
349
  // We don't want to check for signals from multiple threads
350
+ ++processed;
320
351
  if (thread_idx == 0)
321
- if (PyErr_CheckSignals() != 0)
352
+ if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), vectors_count)) {
353
+ atomic_error.store("Operation has been terminated");
322
354
  return false;
355
+ }
323
356
  return true;
324
357
  });
325
358
 
359
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
360
+ progress_(processed.load(), vectors_count);
361
+
326
362
  // Raise the error from a single thread
327
363
  auto error = atomic_error.load();
328
364
  if (error) {
@@ -336,7 +372,8 @@ static void search_typed( //
336
372
  dense_indexes_py_t& indexes, py::buffer_info& vectors_info, //
337
373
  std::size_t wanted, bool exact, std::size_t threads, //
338
374
  py::array_t<dense_key_t>& keys_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py,
339
- std::atomic<std::size_t>& stats_visited_members, std::atomic<std::size_t>& stats_computed_distances) {
375
+ std::atomic<std::size_t>& stats_visited_members, std::atomic<std::size_t>& stats_computed_distances,
376
+ progress_func_t const& progress) {
340
377
 
341
378
  auto keys_py2d = keys_py.template mutable_unchecked<2>();
342
379
  auto distances_py2d = distances_py.template mutable_unchecked<2>();
@@ -354,6 +391,10 @@ static void search_typed( //
354
391
  if (!query_mutexes)
355
392
  throw std::bad_alloc();
356
393
 
394
+ // Progress status
395
+ progress_t progress_{progress};
396
+ std::atomic<std::size_t> processed{0};
397
+
357
398
  atomic_error_t atomic_error{nullptr};
358
399
  executor_default_t{threads}.dynamic(indexes.shards_.size(), [&](std::size_t thread_idx, std::size_t task_idx) {
359
400
  dense_index_py_t& index = *indexes.shards_[task_idx].get();
@@ -388,13 +429,19 @@ static void search_typed( //
388
429
  stats_computed_distances += result.computed_distances;
389
430
 
390
431
  // We don't want to check for signals from multiple threads
432
+ ++processed;
391
433
  if (thread_idx == 0)
392
- if (PyErr_CheckSignals() != 0)
434
+ if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), indexes.shards_.size())) {
435
+ atomic_error.store("Operation has been terminated");
393
436
  return false;
437
+ }
394
438
  return true;
395
439
  }
396
440
  });
397
441
 
442
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
443
+ progress_(processed.load(), indexes.shards_.size());
444
+
398
445
  // Raise the error from a single thread
399
446
  auto error = atomic_error.load();
400
447
  if (error) {
@@ -416,7 +463,8 @@ static void search_typed( //
416
463
  */
417
464
  template <typename index_at>
418
465
  static py::tuple search_many_in_index( //
419
- index_at& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
466
+ index_at& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads,
467
+ progress_func_t const& progress) {
420
468
 
421
469
  if (wanted == 0)
422
470
  return py::tuple(5);
@@ -441,11 +489,11 @@ static py::tuple search_many_in_index( //
441
489
 
442
490
  // clang-format off
443
491
  switch (numpy_string_to_kind(vectors_info.format)) {
444
- case scalar_kind_t::b1x8_k: search_typed<b1x8_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances); break;
445
- case scalar_kind_t::i8_k: search_typed<i8_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances); break;
446
- case scalar_kind_t::f16_k: search_typed<f16_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances); break;
447
- case scalar_kind_t::f32_k: search_typed<f32_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances); break;
448
- case scalar_kind_t::f64_k: search_typed<f64_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances); break;
492
+ case scalar_kind_t::b1x8_k: search_typed<b1x8_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances, progress); break;
493
+ case scalar_kind_t::i8_k: search_typed<i8_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances, progress); break;
494
+ case scalar_kind_t::f16_k: search_typed<f16_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances, progress); break;
495
+ case scalar_kind_t::f32_k: search_typed<f32_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances, progress); break;
496
+ case scalar_kind_t::f64_k: search_typed<f64_t>(index, vectors_info, wanted, exact, threads, keys_py, distances_py, counts_py, stats_visited_members, stats_computed_distances, progress); break;
449
497
  default: throw std::invalid_argument("Incompatible scalars in the query matrix: " + vectors_info.format);
450
498
  }
451
499
  // clang-format on
@@ -463,7 +511,8 @@ template <typename scalar_at>
463
511
  static void search_typed_brute_force( //
464
512
  py::buffer_info& dataset_info, py::buffer_info& queries_info, //
465
513
  std::size_t wanted, std::size_t threads, metric_t const& metric, //
466
- py::array_t<dense_key_t>& keys_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py) {
514
+ py::array_t<dense_key_t>& keys_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py,
515
+ progress_func_t const& progress) {
467
516
 
468
517
  auto keys_py2d = keys_py.template mutable_unchecked<2>();
469
518
  auto distances_py2d = distances_py.template mutable_unchecked<2>();
@@ -485,6 +534,10 @@ static void search_typed_brute_force( //
485
534
  if (!query_mutexes)
486
535
  throw std::bad_alloc();
487
536
 
537
+ // Progress status
538
+ progress_t progress_{progress};
539
+ std::atomic<std::size_t> processed{0};
540
+
488
541
  executor_default_t{threads}.dynamic(tasks_count, [&](std::size_t thread_idx, std::size_t task_idx) {
489
542
  //
490
543
  std::size_t dataset_idx = task_idx / queries_count;
@@ -514,11 +567,15 @@ static void search_typed_brute_force( //
514
567
  }
515
568
 
516
569
  // We don't want to check for signals from multiple threads
570
+ ++processed;
517
571
  if (thread_idx == 0)
518
- if (PyErr_CheckSignals() != 0)
572
+ if (PyErr_CheckSignals() != 0 || !progress_(processed.load(), tasks_count))
519
573
  return false;
520
574
  return true;
521
575
  });
576
+
577
+ // At the end report the latest numbers, because the reporter thread may be finished earlier
578
+ progress_(processed.load(), tasks_count);
522
579
  }
523
580
 
524
581
  static py::tuple search_many_brute_force( //
@@ -526,7 +583,8 @@ static py::tuple search_many_brute_force( //
526
583
  std::size_t wanted, std::size_t threads, //
527
584
  metric_kind_t metric_kind, //
528
585
  metric_signature_t metric_signature, //
529
- std::uintptr_t metric_uintptr) {
586
+ std::uintptr_t metric_uintptr, //
587
+ progress_func_t const& progress) {
530
588
 
531
589
  if (wanted == 0)
532
590
  return py::tuple(5);
@@ -560,11 +618,11 @@ static py::tuple search_many_brute_force( //
560
618
 
561
619
  // clang-format off
562
620
  switch (dataset_kind) {
563
- case scalar_kind_t::b1x8_k: search_typed_brute_force<b1x8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py); break;
564
- case scalar_kind_t::i8_k: search_typed_brute_force<i8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py); break;
565
- case scalar_kind_t::f16_k: search_typed_brute_force<f16_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py); break;
566
- case scalar_kind_t::f32_k: search_typed_brute_force<f32_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py); break;
567
- case scalar_kind_t::f64_k: search_typed_brute_force<f64_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py); break;
621
+ case scalar_kind_t::b1x8_k: search_typed_brute_force<b1x8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
622
+ case scalar_kind_t::i8_k: search_typed_brute_force<i8_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
623
+ case scalar_kind_t::f16_k: search_typed_brute_force<f16_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
624
+ case scalar_kind_t::f32_k: search_typed_brute_force<f32_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
625
+ case scalar_kind_t::f64_k: search_typed_brute_force<f64_t>(dataset_info, queries_info, wanted, threads, metric, keys_py, distances_py, counts_py, progress); break;
568
626
  default: throw std::invalid_argument("Incompatible vector types: " + dataset_info.format);
569
627
  }
570
628
  // clang-format on
@@ -603,7 +661,7 @@ template <typename scalar_at> struct rows_lookup_gt {
603
661
  template <typename index_at>
604
662
  static py::tuple cluster_vectors( //
605
663
  index_at& index, py::buffer queries, //
606
- std::size_t min_count, std::size_t max_count, std::size_t threads) {
664
+ std::size_t min_count, std::size_t max_count, std::size_t threads, progress_func_t const& progress) {
607
665
 
608
666
  if (index.limits().threads_search < threads)
609
667
  throw std::invalid_argument("Can't use that many threads!");
@@ -637,11 +695,11 @@ static py::tuple cluster_vectors( //
637
695
 
638
696
  // clang-format off
639
697
  switch (numpy_string_to_kind(queries_info.format)) {
640
- case scalar_kind_t::b1x8_k: cluster_result = index.cluster(queries_begin.as<b1x8_t const>(), queries_end.as<b1x8_t const>(), config, keys_ptr, distances_ptr, executor); break;
641
- case scalar_kind_t::i8_k: cluster_result = index.cluster(queries_begin.as<i8_t const>(), queries_end.as<i8_t const>(), config, keys_ptr, distances_ptr, executor); break;
642
- case scalar_kind_t::f16_k: cluster_result = index.cluster(queries_begin.as<f16_t const>(), queries_end.as<f16_t const>(), config, keys_ptr, distances_ptr, executor); break;
643
- case scalar_kind_t::f32_k: cluster_result = index.cluster(queries_begin.as<f32_t const>(), queries_end.as<f32_t const>(), config, keys_ptr, distances_ptr, executor); break;
644
- case scalar_kind_t::f64_k: cluster_result = index.cluster(queries_begin.as<f64_t const>(), queries_end.as<f64_t const>(), config, keys_ptr, distances_ptr, executor); break;
698
+ case scalar_kind_t::b1x8_k: cluster_result = index.cluster(queries_begin.as<b1x8_t const>(), queries_end.as<b1x8_t const>(), config, keys_ptr, distances_ptr, executor, progress_t{progress}); break;
699
+ case scalar_kind_t::i8_k: cluster_result = index.cluster(queries_begin.as<i8_t const>(), queries_end.as<i8_t const>(), config, keys_ptr, distances_ptr, executor, progress_t{progress}); break;
700
+ case scalar_kind_t::f16_k: cluster_result = index.cluster(queries_begin.as<f16_t const>(), queries_end.as<f16_t const>(), config, keys_ptr, distances_ptr, executor, progress_t{progress}); break;
701
+ case scalar_kind_t::f32_k: cluster_result = index.cluster(queries_begin.as<f32_t const>(), queries_end.as<f32_t const>(), config, keys_ptr, distances_ptr, executor, progress_t{progress}); break;
702
+ case scalar_kind_t::f64_k: cluster_result = index.cluster(queries_begin.as<f64_t const>(), queries_end.as<f64_t const>(), config, keys_ptr, distances_ptr, executor, progress_t{progress}); break;
645
703
  default: throw std::invalid_argument("Incompatible scalars in the query matrix: " + queries_info.format);
646
704
  }
647
705
  // clang-format on
@@ -677,7 +735,7 @@ static py::tuple cluster_vectors( //
677
735
  template <typename index_at>
678
736
  static py::tuple cluster_keys( //
679
737
  index_at& index, py::array_t<dense_key_t> queries_py, //
680
- std::size_t min_count, std::size_t max_count, std::size_t threads) {
738
+ std::size_t min_count, std::size_t max_count, std::size_t threads, progress_func_t const& progress) {
681
739
 
682
740
  if (index.limits().threads_search < threads)
683
741
  throw std::invalid_argument("Can't use that many threads!");
@@ -701,7 +759,7 @@ static py::tuple cluster_keys( //
701
759
  config.max_clusters = max_count;
702
760
 
703
761
  dense_clustering_result_t cluster_result =
704
- index.cluster(queries_begin, queries_end, config, keys_ptr, distances_ptr, executor);
762
+ index.cluster(queries_begin, queries_end, config, keys_ptr, distances_ptr, executor, progress_t{progress});
705
763
  cluster_result.error.raise();
706
764
 
707
765
  // Those would be set to 1 for all entries, in case of success
@@ -721,7 +779,8 @@ static py::tuple cluster_keys( //
721
779
 
722
780
  static std::unordered_map<dense_key_t, dense_key_t> join_index( //
723
781
  dense_index_py_t const& a, dense_index_py_t const& b, //
724
- std::size_t max_proposals, bool exact) {
782
+ std::size_t max_proposals, bool exact, //
783
+ progress_func_t const& progress) {
725
784
 
726
785
  std::unordered_map<dense_key_t, dense_key_t> a_to_b;
727
786
  dummy_key_to_key_mapping_t b_to_a;
@@ -733,7 +792,7 @@ static std::unordered_map<dense_key_t, dense_key_t> join_index( //
733
792
  config.expansion = (std::max)(a.expansion_search(), b.expansion_search());
734
793
  std::size_t threads = (std::min)(a.limits().threads(), b.limits().threads());
735
794
  executor_default_t executor{threads};
736
- join_result_t result = a.join(b, config, a_to_b, b_to_a, executor);
795
+ join_result_t result = a.join(b, config, a_to_b, b_to_a, executor, progress_t{progress});
737
796
  forward_error(result);
738
797
 
739
798
  return a_to_b;
@@ -749,14 +808,14 @@ static dense_index_py_t copy_index(dense_index_py_t const& index, bool force_cop
749
808
  return std::move(result.index);
750
809
  }
751
810
 
752
- static void compact_index(dense_index_py_t& index, std::size_t threads) {
811
+ static void compact_index(dense_index_py_t& index, std::size_t threads, progress_func_t const& progress) {
753
812
 
754
813
  if (!threads)
755
814
  threads = std::thread::hardware_concurrency();
756
815
  if (!index.reserve(index_limits_t(index.size(), threads)))
757
816
  throw std::invalid_argument("Out of memory!");
758
817
 
759
- index.compact(executor_default_t{threads});
818
+ index.compact(executor_default_t{threads}, progress_t{progress});
760
819
  }
761
820
 
762
821
  static py::dict index_metadata(index_dense_metadata_result_t const& meta) {
@@ -782,9 +841,9 @@ static py::dict index_metadata(index_dense_metadata_result_t const& meta) {
782
841
  }
783
842
 
784
843
  // clang-format off
785
- template <typename index_at> void save_index_to_path(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
786
- template <typename index_at> void load_index_from_path(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
787
- template <typename index_at> void view_index_from_path(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
844
+ template <typename index_at> void save_index_to_path(index_at const& index, std::string const& path, progress_func_t const& progress) { index.save(path.c_str(), {}, progress_t{progress}).error.raise(); }
845
+ template <typename index_at> void load_index_from_path(index_at& index, std::string const& path, progress_func_t const& progress) { index.load(path.c_str(), {}, progress_t{progress}).error.raise(); }
846
+ template <typename index_at> void view_index_from_path(index_at& index, std::string const& path, progress_func_t const& progress) { index.view(path.c_str(), 0, {}, progress_t{progress}).error.raise(); }
788
847
  template <typename index_at> void reset_index(index_at& index) { index.reset(); }
789
848
  template <typename index_at> void clear_index(index_at& index) { index.clear(); }
790
849
  template <typename index_at> std::size_t max_level(index_at const &index) { return index.max_level(); }
@@ -798,7 +857,7 @@ template <typename py_bytes_at> memory_mapped_file_t memory_map_from_bytes(py_by
798
857
  return {(byte_t*)(info.ptr), static_cast<std::size_t>(info.size)};
799
858
  }
800
859
 
801
- template <typename index_at> py::object save_index_to_buffer(index_at const& index) {
860
+ template <typename index_at> py::object save_index_to_buffer(index_at const& index, progress_func_t const& progress) {
802
861
  std::size_t serialized_length = index.serialized_length();
803
862
 
804
863
  // Create an empty bytearray object using CPython API
@@ -814,7 +873,7 @@ template <typename index_at> py::object save_index_to_buffer(index_at const& ind
814
873
 
815
874
  char* buffer = PyByteArray_AS_STRING(byte_array);
816
875
  memory_mapped_file_t memory_map((byte_t*)buffer, serialized_length);
817
- serialization_result_t result = index.save(std::move(memory_map));
876
+ serialization_result_t result = index.save(std::move(memory_map), {}, {}, progress_t{progress});
818
877
 
819
878
  if (!result) {
820
879
  Py_XDECREF(byte_array);
@@ -824,11 +883,13 @@ template <typename index_at> py::object save_index_to_buffer(index_at const& ind
824
883
  return py::reinterpret_steal<py::object>(byte_array);
825
884
  }
826
885
 
827
- template <typename index_at> void load_index_from_buffer(index_at& index, py::bytes const& buffer) {
828
- index.load(memory_map_from_bytes(buffer)).error.raise();
886
+ template <typename index_at>
887
+ void load_index_from_buffer(index_at& index, py::bytes const& buffer, progress_func_t const& progress) {
888
+ index.load(memory_map_from_bytes(buffer), {}, {}, progress_t{progress}).error.raise();
829
889
  }
830
- template <typename index_at> void view_index_from_buffer(index_at& index, py::bytes const& buffer) {
831
- index.view(memory_map_from_bytes(buffer)).error.raise();
890
+ template <typename index_at>
891
+ void view_index_from_buffer(index_at& index, py::bytes const& buffer, progress_func_t const& progress) {
892
+ index.view(memory_map_from_bytes(buffer), {}, {}, progress_t{progress}).error.raise();
832
893
  }
833
894
 
834
895
  template <typename index_at> std::vector<typename index_at::stats_t> compute_levels_stats(index_at const& index) {
@@ -954,7 +1015,8 @@ PYBIND11_MODULE(compiled, m) {
954
1015
  py::arg("threads") = 0, //
955
1016
  py::arg("metric_kind") = metric_kind_t::cos_k, //
956
1017
  py::arg("metric_signature") = metric_signature_t::array_array_k, //
957
- py::arg("metric_pointer") = 0 //
1018
+ py::arg("metric_pointer") = 0, //
1019
+ py::arg("progress") = nullptr //
958
1020
  );
959
1021
 
960
1022
  m.def(
@@ -989,7 +1051,8 @@ PYBIND11_MODULE(compiled, m) {
989
1051
  py::arg("vectors"), //
990
1052
  py::kw_only(), //
991
1053
  py::arg("copy") = true, //
992
- py::arg("threads") = 0 //
1054
+ py::arg("threads") = 0, //
1055
+ py::arg("progress") = nullptr //
993
1056
  );
994
1057
 
995
1058
  i.def( //
@@ -997,7 +1060,8 @@ PYBIND11_MODULE(compiled, m) {
997
1060
  py::arg("queries"), //
998
1061
  py::arg("count") = 10, //
999
1062
  py::arg("exact") = false, //
1000
- py::arg("threads") = 0 //
1063
+ py::arg("threads") = 0, //
1064
+ py::arg("progress") = nullptr //
1001
1065
  );
1002
1066
 
1003
1067
  i.def( //
@@ -1005,7 +1069,8 @@ PYBIND11_MODULE(compiled, m) {
1005
1069
  py::arg("queries"), //
1006
1070
  py::arg("min_count") = 0, //
1007
1071
  py::arg("max_count") = 0, //
1008
- py::arg("threads") = 0 //
1072
+ py::arg("threads") = 0, //
1073
+ py::arg("progress") = nullptr //
1009
1074
  );
1010
1075
 
1011
1076
  i.def( //
@@ -1013,7 +1078,8 @@ PYBIND11_MODULE(compiled, m) {
1013
1078
  py::arg("queries"), //
1014
1079
  py::arg("min_count") = 0, //
1015
1080
  py::arg("max_count") = 0, //
1016
- py::arg("threads") = 0 //
1081
+ py::arg("threads") = 0, //
1082
+ py::arg("progress") = nullptr //
1017
1083
  );
1018
1084
 
1019
1085
  i.def(
@@ -1205,19 +1271,24 @@ PYBIND11_MODULE(compiled, m) {
1205
1271
  },
1206
1272
  py::arg("offset"));
1207
1273
 
1208
- i.def("save_index_to_path", &save_index_to_path<dense_index_py_t>);
1209
- i.def("load_index_from_path", &load_index_from_path<dense_index_py_t>);
1210
- i.def("view_index_from_path", &view_index_from_path<dense_index_py_t>);
1274
+ i.def("save_index_to_path", &save_index_to_path<dense_index_py_t>, py::arg("path"), py::arg("progress") = nullptr);
1275
+ i.def("load_index_from_path", &load_index_from_path<dense_index_py_t>, py::arg("path"),
1276
+ py::arg("progress") = nullptr);
1277
+ i.def("view_index_from_path", &view_index_from_path<dense_index_py_t>, py::arg("path"),
1278
+ py::arg("progress") = nullptr);
1211
1279
 
1212
- i.def("save_index_to_buffer", &save_index_to_buffer<dense_index_py_t>);
1213
- i.def("load_index_from_buffer", &load_index_from_buffer<dense_index_py_t>);
1214
- i.def("view_index_from_buffer", &view_index_from_buffer<dense_index_py_t>);
1280
+ i.def("save_index_to_buffer", &save_index_to_buffer<dense_index_py_t>, py::arg("progress") = nullptr);
1281
+ i.def("load_index_from_buffer", &load_index_from_buffer<dense_index_py_t>, py::arg("buffer"),
1282
+ py::arg("progress") = nullptr);
1283
+ i.def("view_index_from_buffer", &view_index_from_buffer<dense_index_py_t>, py::arg("buffer"),
1284
+ py::arg("progress") = nullptr);
1215
1285
 
1216
1286
  i.def("reset", &reset_index<dense_index_py_t>);
1217
1287
  i.def("clear", &clear_index<dense_index_py_t>);
1218
1288
  i.def("copy", &copy_index, py::kw_only(), py::arg("copy") = true);
1219
- i.def("compact", &compact_index);
1220
- i.def("join", &join_index, py::arg("other"), py::arg("max_proposals") = 0, py::arg("exact") = false);
1289
+ i.def("compact", &compact_index, py::arg("threads"), py::arg("progress") = nullptr);
1290
+ i.def("join", &join_index, py::arg("other"), py::arg("max_proposals") = 0, py::arg("exact") = false,
1291
+ py::arg("progress") = nullptr);
1221
1292
 
1222
1293
  using punned_index_stats_t = typename dense_index_py_t::stats_t;
1223
1294
  auto i_stats = py::class_<punned_index_stats_t>(m, "IndexStats");
@@ -1242,6 +1313,7 @@ PYBIND11_MODULE(compiled, m) {
1242
1313
  py::arg("query"), //
1243
1314
  py::arg("count") = 10, //
1244
1315
  py::arg("exact") = false, //
1245
- py::arg("threads") = 0 //
1316
+ py::arg("threads") = 0, //
1317
+ py::arg("progress") = nullptr //
1246
1318
  );
1247
1319
  }
@@ -1,4 +1,5 @@
1
1
  from __future__ import annotations
2
+ from inspect import signature
2
3
 
3
4
  # The purpose of this file is to provide Pythonic wrapper on top
4
5
  # the native precompiled CPython module. It improves compatibility
@@ -8,6 +9,7 @@ import os
8
9
  import math
9
10
  from dataclasses import dataclass
10
11
  from typing import (
12
+ Any,
11
13
  Optional,
12
14
  Union,
13
15
  NamedTuple,
@@ -72,6 +74,17 @@ NoneType = type(None)
72
74
 
73
75
  PathOrBuffer = Union[str, os.PathLike, bytes]
74
76
 
77
+ ProgressCallback = Callable[[int, int], bool]
78
+
79
+
80
+ def _match_signature(
81
+ func: Callable[[Any], Any], arg_types: List[type], ret_type: type
82
+ ) -> bool:
83
+ assert callable(func), "Not callable"
84
+ sig = signature(func)
85
+ param_types = [param.annotation for param in sig.parameters.values()]
86
+ return param_types == arg_types and sig.return_annotation == ret_type
87
+
75
88
 
76
89
  def _normalize_dtype(
77
90
  dtype,
@@ -153,12 +166,16 @@ def _search_in_compiled(
153
166
  vectors: np.ndarray,
154
167
  *,
155
168
  log: Union[str, bool],
156
- batch_size: int,
169
+ progress: Optional[ProgressCallback],
157
170
  **kwargs,
158
171
  ) -> Union[Matches, BatchMatches]:
159
172
  #
160
173
  assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
161
174
  assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
175
+ assert not progress or _match_signature(
176
+ progress, [int, int], bool
177
+ ), "Invalid callback"
178
+
162
179
  if vectors.ndim == 1:
163
180
  vectors = vectors.reshape(1, len(vectors))
164
181
  count_vectors = vectors.shape[0]
@@ -168,42 +185,25 @@ def _search_in_compiled(
168
185
  ) -> Union[BatchMatches, Matches]:
169
186
  return batch_matches[0] if count_vectors == 1 else batch_matches
170
187
 
171
- if log and batch_size == 0:
172
- batch_size = int(math.ceil(count_vectors / 100))
173
-
174
- if batch_size:
175
- tasks = [
176
- vectors[start_row : start_row + batch_size, :]
177
- for start_row in range(0, count_vectors, batch_size)
178
- ]
179
- tasks_matches = []
188
+ # Create progress bar if needed
189
+ if log:
180
190
  name = log if isinstance(log, str) else "Search"
181
191
  pbar = tqdm(
182
- tasks,
183
192
  desc=name,
184
193
  total=count_vectors,
185
194
  unit="vector",
186
- disable=log is False,
187
195
  )
188
- for vectors in tasks:
189
- tuple_ = compiled_callable(vectors, **kwargs)
190
- tasks_matches.append(BatchMatches(*tuple_))
191
- pbar.update(vectors.shape[0])
192
196
 
193
- pbar.close()
194
- return distil_batch(
195
- BatchMatches(
196
- keys=np.vstack([m.keys for m in tasks_matches]),
197
- distances=np.vstack([m.distances for m in tasks_matches]),
198
- counts=np.concatenate([m.counts for m in tasks_matches], axis=None),
199
- visited_members=sum([m.visited_members for m in tasks_matches]),
200
- computed_distances=sum([m.computed_distances for m in tasks_matches]),
201
- )
202
- )
197
+ def update_progress_bar(processed: int, total: int) -> bool:
198
+ pbar.update(processed - pbar.n)
199
+ return progress if progress else True
203
200
 
201
+ tuple_ = compiled_callable(vectors, progress=update_progress_bar, **kwargs)
202
+ pbar.close()
204
203
  else:
205
204
  tuple_ = compiled_callable(vectors, **kwargs)
206
- return distil_batch(BatchMatches(*tuple_))
205
+
206
+ return distil_batch(BatchMatches(*tuple_))
207
207
 
208
208
 
209
209
  def _add_to_compiled(
@@ -214,9 +214,13 @@ def _add_to_compiled(
214
214
  copy: bool,
215
215
  threads: int,
216
216
  log: Union[str, bool],
217
- batch_size: int,
217
+ progress: Optional[ProgressCallback],
218
218
  ) -> Union[int, np.ndarray]:
219
+ #
219
220
  assert isinstance(vectors, np.ndarray), "Expects a NumPy array"
221
+ assert not progress or _match_signature(
222
+ progress, [int, int], bool
223
+ ), "Invalid callback"
220
224
  assert vectors.ndim == 1 or vectors.ndim == 2, "Expects a matrix or vector"
221
225
  if vectors.ndim == 1:
222
226
  vectors = vectors.reshape(1, len(vectors))
@@ -235,37 +239,29 @@ def _add_to_compiled(
235
239
 
236
240
  assert len(keys) == count_vectors
237
241
 
238
- # If logging is requested, and batch size is undefined, set it to grow 1% at a time:
239
- if log and batch_size == 0:
240
- batch_size = int(math.ceil(count_vectors / 100))
241
-
242
- # Split into batches and log progress, if needed
243
- if batch_size:
244
- keys = [
245
- keys[start_row : start_row + batch_size]
246
- for start_row in range(0, count_vectors, batch_size)
247
- ]
248
- vectors = [
249
- vectors[start_row : start_row + batch_size, :]
250
- for start_row in range(0, count_vectors, batch_size)
251
- ]
252
- tasks = zip(keys, vectors)
242
+ # Create progress bar if needed
243
+ if log:
253
244
  name = log if isinstance(log, str) else "Add"
254
245
  pbar = tqdm(
255
- tasks,
256
246
  desc=name,
257
247
  total=count_vectors,
258
248
  unit="vector",
259
- disable=log is False,
260
249
  )
261
- for keys, vectors in tasks:
262
- compiled.add_many(keys, vectors, copy=copy, threads=threads)
263
- pbar.update(len(keys))
264
250
 
265
- pbar.close()
251
+ def update_progress_bar(processed: int, total: int) -> bool:
252
+ pbar.update(processed - pbar.n)
253
+ return progress(processed, total) if progress else True
266
254
 
255
+ compiled.add_many(
256
+ keys,
257
+ vectors,
258
+ copy=copy,
259
+ threads=threads,
260
+ progress=update_progress_bar,
261
+ )
262
+ pbar.close()
267
263
  else:
268
- compiled.add_many(keys, vectors, copy=copy, threads=threads)
264
+ compiled.add_many(keys, vectors, copy=copy, threads=threads, progress=progress)
269
265
 
270
266
  return keys
271
267
 
@@ -625,7 +621,7 @@ class Index:
625
621
  copy: bool = True,
626
622
  threads: int = 0,
627
623
  log: Union[str, bool] = False,
628
- batch_size: int = 0,
624
+ progress: Optional[ProgressCallback] = None,
629
625
  ) -> Union[int, np.ndarray]:
630
626
  """Inserts one or move vectors into the index.
631
627
 
@@ -651,8 +647,8 @@ class Index:
651
647
  :type threads: int, defaults to 0
652
648
  :param log: Whether to print the progress bar
653
649
  :type log: Union[str, bool], defaults to False
654
- :param batch_size: Number of vectors to process at once
655
- :type batch_size: int, defaults to 0
650
+ :param progress: Callback to report stats of the progress and control it
651
+ :type progress: Optional[ProgressCallback], defaults to None
656
652
  :return: Inserted key or keys
657
653
  :type: Union[int, np.ndarray]
658
654
  """
@@ -663,7 +659,7 @@ class Index:
663
659
  copy=copy,
664
660
  threads=threads,
665
661
  log=log,
666
- batch_size=batch_size,
662
+ progress=progress,
667
663
  )
668
664
 
669
665
  def search(
@@ -675,7 +671,7 @@ class Index:
675
671
  threads: int = 0,
676
672
  exact: bool = False,
677
673
  log: Union[str, bool] = False,
678
- batch_size: int = 0,
674
+ progress: Optional[ProgressCallback] = None,
679
675
  ) -> Union[Matches, BatchMatches]:
680
676
  """
681
677
  Performs approximate nearest neighbors search for one or more queries.
@@ -690,8 +686,8 @@ class Index:
690
686
  :type exact: bool, defaults to False
691
687
  :param log: Whether to print the progress bar, default to False
692
688
  :type log: Union[str, bool], optional
693
- :param batch_size: Number of vectors to process at once
694
- :type batch_size: int, defaults to 0
689
+ :param progress: Callback to report stats of the progress and control it
690
+ :type progress: Optional[ProgressCallback], defaults to None
695
691
  :return: Matches for one or more queries
696
692
  :rtype: Union[Matches, BatchMatches]
697
693
  """
@@ -701,11 +697,11 @@ class Index:
701
697
  vectors,
702
698
  # Batch scheduling:
703
699
  log=log,
704
- batch_size=batch_size,
705
700
  # Search constraints:
706
701
  count=count,
707
702
  exact=exact,
708
703
  threads=threads,
704
+ progress=progress,
709
705
  )
710
706
 
711
707
  def contains(self, keys: KeyOrKeysLike) -> Union[bool, np.ndarray]:
@@ -932,37 +928,57 @@ class Index:
932
928
  self._compiled.expansion_search = v
933
929
 
934
930
  def save(
935
- self, path_or_buffer: Union[str, os.PathLike, NoneType] = None
931
+ self,
932
+ path_or_buffer: Union[str, os.PathLike, NoneType] = None,
933
+ progress: Optional[ProgressCallback] = None,
936
934
  ) -> Optional[bytes]:
935
+ assert not progress or _match_signature(
936
+ progress, [int, int], bool
937
+ ), "Invalid callback signature"
938
+
937
939
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
938
940
  if path_or_buffer is None:
939
- return self._compiled.save_index_to_buffer()
941
+ return self._compiled.save_index_to_buffer(progress)
940
942
  else:
941
- self._compiled.save_index_to_path(os.fspath(path_or_buffer))
943
+ self._compiled.save_index_to_path(os.fspath(path_or_buffer), progress)
944
+
945
+ def load(
946
+ self,
947
+ path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None,
948
+ progress: Optional[ProgressCallback] = None,
949
+ ):
950
+ assert not progress or _match_signature(
951
+ progress, [int, int], bool
952
+ ), "Invalid callback signature"
942
953
 
943
- def load(self, path_or_buffer: Union[str, os.PathLike, bytes, NoneType] = None):
944
954
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
945
955
  if path_or_buffer is None:
946
956
  raise Exception("Define the source")
947
957
  if isinstance(path_or_buffer, bytearray):
948
958
  path_or_buffer = bytes(path_or_buffer)
949
959
  if isinstance(path_or_buffer, bytes):
950
- self._compiled.load_index_from_buffer(path_or_buffer)
960
+ self._compiled.load_index_from_buffer(path_or_buffer, progress)
951
961
  else:
952
- self._compiled.load_index_from_path(os.fspath(path_or_buffer))
962
+ self._compiled.load_index_from_path(os.fspath(path_or_buffer), progress)
953
963
 
954
964
  def view(
955
- self, path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None
965
+ self,
966
+ path_or_buffer: Union[str, os.PathLike, bytes, bytearray, NoneType] = None,
967
+ progress: Optional[ProgressCallback] = None,
956
968
  ):
969
+ assert not progress or _match_signature(
970
+ progress, [int, int], bool
971
+ ), "Invalid callback signature"
972
+
957
973
  path_or_buffer = path_or_buffer if path_or_buffer else self.path
958
974
  if path_or_buffer is None:
959
975
  raise Exception("Define the source")
960
976
  if isinstance(path_or_buffer, bytearray):
961
977
  path_or_buffer = bytes(path_or_buffer)
962
978
  if isinstance(path_or_buffer, bytes):
963
- self._compiled.view_index_from_buffer(path_or_buffer)
979
+ self._compiled.view_index_from_buffer(path_or_buffer, progress)
964
980
  else:
965
- self._compiled.view_index_from_path(os.fspath(path_or_buffer))
981
+ self._compiled.view_index_from_path(os.fspath(path_or_buffer), progress)
966
982
 
967
983
  def clear(self):
968
984
  """Erases all the vectors from the index, preserving the space for future insertions."""
@@ -993,6 +1009,7 @@ class Index:
993
1009
  other: Index,
994
1010
  max_proposals: int = 0,
995
1011
  exact: bool = False,
1012
+ progress: Optional[ProgressCallback] = None,
996
1013
  ) -> Dict[Key, Key]:
997
1014
  """Performs "Semantic Join" or pairwise matching between `self` & `other` index.
998
1015
  Is different from `search`, as no collisions are allowed in resulting pairs.
@@ -1005,13 +1022,20 @@ class Index:
1005
1022
  :type max_proposals: int, optional
1006
1023
  :param exact: Controls if underlying `search` should be exact, defaults to False
1007
1024
  :type exact: bool, optional
1025
+ :param progress: Callback to report stats of the progress and control it
1026
+ :type progress: Optional[ProgressCallback], defaults to None
1008
1027
  :return: Mapping from keys of `self` to keys of `other`
1009
1028
  :rtype: Dict[Key, Key]
1010
1029
  """
1030
+ assert not progress or _match_signature(
1031
+ progress, [int, int], bool
1032
+ ), "Invalid callback signature"
1033
+
1011
1034
  return self._compiled.join(
1012
1035
  other=other._compiled,
1013
1036
  max_proposals=max_proposals,
1014
1037
  exact=exact,
1038
+ progress=progress,
1015
1039
  )
1016
1040
 
1017
1041
  def cluster(
@@ -1023,7 +1047,7 @@ class Index:
1023
1047
  max_count: Optional[int] = None,
1024
1048
  threads: int = 0,
1025
1049
  log: Union[str, bool] = False,
1026
- batch_size: int = 0,
1050
+ progress: Optional[ProgressCallback] = None,
1027
1051
  ) -> Clustering:
1028
1052
  """
1029
1053
  Clusters already indexed or provided `vectors`, mapping them to various centroids.
@@ -1037,11 +1061,15 @@ class Index:
1037
1061
  :type threads: int, defaults to 0
1038
1062
  :param log: Whether to print the progress bar
1039
1063
  :type log: Union[str, bool], defaults to False
1040
- :param batch_size: Number of vectors to process at once, defaults to 0
1041
- :type batch_size: int, defaults to 0
1064
+ :param progress: Callback to report stats of the progress and control it
1065
+ :type progress: Optional[ProgressCallback], defaults to None
1042
1066
  :return: Matches for one or more queries
1043
1067
  :rtype: Union[Matches, BatchMatches]
1044
1068
  """
1069
+ assert not progress or _match_signature(
1070
+ progress, [int, int], bool
1071
+ ), "Invalid callback signature"
1072
+
1045
1073
  if min_count is None:
1046
1074
  min_count = 0
1047
1075
  if max_count is None:
@@ -1054,6 +1082,7 @@ class Index:
1054
1082
  min_count=min_count,
1055
1083
  max_count=max_count,
1056
1084
  threads=threads,
1085
+ progress=progress,
1057
1086
  )
1058
1087
  else:
1059
1088
  if keys is None:
@@ -1066,6 +1095,7 @@ class Index:
1066
1095
  min_count=min_count,
1067
1096
  max_count=max_count,
1068
1097
  threads=threads,
1098
+ progress=progress,
1069
1099
  )
1070
1100
 
1071
1101
  batch_matches = BatchMatches(*results)
@@ -1231,17 +1261,18 @@ class Indexes:
1231
1261
  *,
1232
1262
  threads: int = 0,
1233
1263
  exact: bool = False,
1264
+ progress: Optional[ProgressCallback] = None,
1234
1265
  ):
1235
1266
  return _search_in_compiled(
1236
1267
  self._compiled.search_many,
1237
1268
  vectors,
1238
1269
  # Batch scheduling:
1239
1270
  log=False,
1240
- batch_size=None,
1241
1271
  # Search constraints:
1242
1272
  count=count,
1243
1273
  exact=exact,
1244
1274
  threads=threads,
1275
+ progress=progress,
1245
1276
  )
1246
1277
 
1247
1278
 
@@ -1254,7 +1285,7 @@ def search(
1254
1285
  exact: bool = False,
1255
1286
  threads: int = 0,
1256
1287
  log: Union[str, bool] = False,
1257
- batch_size: int = 0,
1288
+ progress: Optional[ProgressCallback] = None,
1258
1289
  ) -> Union[Matches, BatchMatches]:
1259
1290
  """Shortcut for search, that can avoid index construction. Particularly useful for
1260
1291
  tiny datasets, where brute-force exact search works fast enough.
@@ -1279,12 +1310,14 @@ def search(
1279
1310
  :type exact: bool, optional
1280
1311
  :param log: Whether to print the progress bar, default to False
1281
1312
  :type log: Union[str, bool], optional
1282
- :param batch_size: Number of vectors to process at once, defaults to 0
1283
- :type batch_size: int, optional
1284
-
1313
+ :param progress: Callback to report stats of the progress and control it
1314
+ :type progress: Optional[ProgressCallback], defaults to None
1285
1315
  :return: Matches for one or more queries
1286
1316
  :rtype: Union[Matches, BatchMatches]
1287
1317
  """
1318
+ assert not progress or _match_signature(
1319
+ progress, [int, int], bool
1320
+ ), "Invalid callback signature"
1288
1321
  assert dataset.ndim == 2, "Dataset must be a matrix, with a vector in each row"
1289
1322
 
1290
1323
  if not exact:
@@ -1298,14 +1331,14 @@ def search(
1298
1331
  dataset,
1299
1332
  threads=threads,
1300
1333
  log=log,
1301
- batch_size=batch_size,
1334
+ progress=progress,
1302
1335
  )
1303
1336
  return index.search(
1304
1337
  query,
1305
1338
  count,
1306
1339
  threads=threads,
1307
1340
  log=log,
1308
- batch_size=batch_size,
1341
+ progress=progress,
1309
1342
  )
1310
1343
 
1311
1344
  metric = _normalize_metric(metric)
@@ -1329,8 +1362,8 @@ def search(
1329
1362
  dataset,
1330
1363
  query,
1331
1364
  metric_kind=metric_kind,
1332
- metric_pointer=metric_pointer,
1333
1365
  metric_signature=metric_signature,
1366
+ metric_pointer=metric_pointer,
1334
1367
  **kwargs,
1335
1368
  )
1336
1369
 
@@ -1339,8 +1372,8 @@ def search(
1339
1372
  query,
1340
1373
  # Batch scheduling:
1341
1374
  log=log,
1342
- batch_size=batch_size,
1343
1375
  # Search constraints:
1344
1376
  count=count,
1345
1377
  threads=threads,
1378
+ progress=progress,
1346
1379
  )
package/wasmer.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="2.2.1"
3
+ version="2.3.0"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
5
  license="Apache-2.0"
6
6
  readme="README.md"