usearch 2.2.1 → 2.3.1

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/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,13 +1015,14 @@ 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(
961
1023
  "hardware_acceleration",
962
- [](scalar_kind_t scalar_kind, std::size_t dimensions, metric_kind_t metric_kind) -> bool {
963
- return metric_t(dimensions, metric_kind, scalar_kind).isa_kind() != isa_kind_t::auto_k;
1024
+ [](scalar_kind_t scalar_kind, std::size_t dimensions, metric_kind_t metric_kind) -> py::str {
1025
+ return isa_name(metric_t(dimensions, metric_kind, scalar_kind).isa_kind());
964
1026
  },
965
1027
  py::kw_only(), //
966
1028
  py::arg("dtype") = scalar_kind_t::f32_k, //
@@ -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
  }