usearch 2.17.1 → 2.19.4

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.
Files changed (47) hide show
  1. package/README.md +6 -2
  2. package/c/lib.cpp +5 -0
  3. package/c/module.modulemap +4 -0
  4. package/c/usearch.h +12 -4
  5. package/cpp/README.md +1 -1
  6. package/cpp/bench.cpp +1 -1
  7. package/cpp/test.cpp +110 -16
  8. package/include/usearch/index.hpp +48 -35
  9. package/include/usearch/index_dense.hpp +34 -19
  10. package/include/usearch/index_plugins.hpp +29 -7
  11. package/javascript/README.md +8 -6
  12. package/javascript/dist/cjs/usearch.d.ts +9 -4
  13. package/javascript/dist/cjs/usearch.js +23 -8
  14. package/javascript/dist/esm/usearch.d.ts +9 -4
  15. package/javascript/dist/esm/usearch.js +21 -7
  16. package/javascript/lib.cpp +137 -59
  17. package/javascript/usearch.test.js +359 -192
  18. package/javascript/usearch.ts +35 -11
  19. package/package.json +1 -1
  20. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  21. package/prebuilds/linux-arm64/usearch.node +0 -0
  22. package/prebuilds/linux-x64/usearch.node +0 -0
  23. package/simsimd/CONTRIBUTING.md +23 -8
  24. package/simsimd/Cargo.lock +1 -1
  25. package/simsimd/Cargo.toml +1 -1
  26. package/simsimd/README.md +49 -9
  27. package/simsimd/build.rs +7 -3
  28. package/simsimd/c/lib.c +6 -1
  29. package/simsimd/include/simsimd/binary.h +3 -3
  30. package/simsimd/include/simsimd/dot.h +2 -2
  31. package/simsimd/include/simsimd/elementwise.h +26 -17
  32. package/simsimd/include/simsimd/simsimd.h +125 -49
  33. package/simsimd/include/simsimd/sparse.h +30 -29
  34. package/simsimd/include/simsimd/spatial.h +4 -3
  35. package/simsimd/include/simsimd/types.h +19 -8
  36. package/simsimd/javascript/lib.c +0 -1
  37. package/simsimd/package-lock.json +411 -362
  38. package/simsimd/package.json +9 -9
  39. package/simsimd/rust/lib.rs +63 -29
  40. package/simsimd/scripts/bench.cxx +13 -0
  41. package/simsimd/scripts/bench_cosine.rs +8 -0
  42. package/simsimd/scripts/bench_native.rs +4 -0
  43. package/simsimd/scripts/bench_sqeuclidean.rs +8 -0
  44. package/simsimd/scripts/test.c +22 -0
  45. package/simsimd/scripts/test.py +80 -4
  46. package/simsimd/swift/SimSIMD.swift +17 -0
  47. package/simsimd/swift/Test.swift +36 -0
@@ -624,7 +624,7 @@ class index_dense_gt {
624
624
  }
625
625
  operator index_dense_gt&&() && {
626
626
  if (error)
627
- __usearch_raise_runtime_error(error.what());
627
+ usearch_raise_runtime_error(error.what());
628
628
  return std::move(index);
629
629
  }
630
630
  };
@@ -757,39 +757,45 @@ class index_dense_gt {
757
757
  };
758
758
 
759
759
  // clang-format off
760
- add_result_t add(vector_key_t key, b1x8_t const* vector, std::size_t thread = any_thread(), bool force_vector_copy = true) { return add_(key, vector, thread, force_vector_copy, casts_.from.b1x8); }
761
- add_result_t add(vector_key_t key, i8_t const* vector, std::size_t thread = any_thread(), bool force_vector_copy = true) { return add_(key, vector, thread, force_vector_copy, casts_.from.i8); }
762
- add_result_t add(vector_key_t key, f16_t const* vector, std::size_t thread = any_thread(), bool force_vector_copy = true) { return add_(key, vector, thread, force_vector_copy, casts_.from.f16); }
763
- add_result_t add(vector_key_t key, f32_t const* vector, std::size_t thread = any_thread(), bool force_vector_copy = true) { return add_(key, vector, thread, force_vector_copy, casts_.from.f32); }
764
- add_result_t add(vector_key_t key, f64_t const* vector, std::size_t thread = any_thread(), bool force_vector_copy = true) { return add_(key, vector, thread, force_vector_copy, casts_.from.f64); }
760
+ add_result_t add(vector_key_t key, b1x8_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.b1x8); }
761
+ add_result_t add(vector_key_t key, i8_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.i8); }
762
+ add_result_t add(vector_key_t key, f16_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.f16); }
763
+ add_result_t add(vector_key_t key, bf16_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.bf16); }
764
+ add_result_t add(vector_key_t key, f32_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.f32); }
765
+ add_result_t add(vector_key_t key, f64_t const* vector, std::size_t thread = any_thread(), bool copy_vector = true) { return add_(key, vector, thread, copy_vector, casts_.from.f64); }
765
766
 
766
767
  search_result_t search(b1x8_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.b1x8); }
767
768
  search_result_t search(i8_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.i8); }
768
769
  search_result_t search(f16_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.f16); }
770
+ search_result_t search(bf16_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.bf16); }
769
771
  search_result_t search(f32_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.f32); }
770
772
  search_result_t search(f64_t const* vector, std::size_t wanted, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, dummy_predicate_t {}, thread, exact, casts_.from.f64); }
771
773
 
772
774
  template <typename predicate_at> search_result_t filtered_search(b1x8_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.b1x8); }
773
775
  template <typename predicate_at> search_result_t filtered_search(i8_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.i8); }
774
776
  template <typename predicate_at> search_result_t filtered_search(f16_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.f16); }
777
+ template <typename predicate_at> search_result_t filtered_search(bf16_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.bf16); }
775
778
  template <typename predicate_at> search_result_t filtered_search(f32_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.f32); }
776
779
  template <typename predicate_at> search_result_t filtered_search(f64_t const* vector, std::size_t wanted, predicate_at&& predicate, std::size_t thread = any_thread(), bool exact = false) const { return search_(vector, wanted, std::forward<predicate_at>(predicate), thread, exact, casts_.from.f64); }
777
780
 
778
781
  std::size_t get(vector_key_t key, b1x8_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.b1x8); }
779
782
  std::size_t get(vector_key_t key, i8_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.i8); }
780
783
  std::size_t get(vector_key_t key, f16_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.f16); }
784
+ std::size_t get(vector_key_t key, bf16_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.bf16); }
781
785
  std::size_t get(vector_key_t key, f32_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.f32); }
782
786
  std::size_t get(vector_key_t key, f64_t* vector, std::size_t vectors_count = 1) const { return get_(key, vector, vectors_count, casts_.to.f64); }
783
787
 
784
788
  cluster_result_t cluster(b1x8_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.b1x8); }
785
789
  cluster_result_t cluster(i8_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.i8); }
786
790
  cluster_result_t cluster(f16_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.f16); }
791
+ cluster_result_t cluster(bf16_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.bf16); }
787
792
  cluster_result_t cluster(f32_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.f32); }
788
793
  cluster_result_t cluster(f64_t const* vector, std::size_t level, std::size_t thread = any_thread()) const { return cluster_(vector, level, thread, casts_.from.f64); }
789
794
 
790
795
  aggregated_distances_t distance_between(vector_key_t key, b1x8_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.b1x8); }
791
796
  aggregated_distances_t distance_between(vector_key_t key, i8_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.i8); }
792
797
  aggregated_distances_t distance_between(vector_key_t key, f16_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.f16); }
798
+ aggregated_distances_t distance_between(vector_key_t key, bf16_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.bf16); }
793
799
  aggregated_distances_t distance_between(vector_key_t key, f32_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.f32); }
794
800
  aggregated_distances_t distance_between(vector_key_t key, f64_t const* vector, std::size_t thread = any_thread()) const { return distance_between_(key, vector, thread, casts_.to.f64); }
795
801
  // clang-format on
@@ -940,7 +946,7 @@ class index_dense_gt {
940
946
 
941
947
  void reserve(index_limits_t limits) {
942
948
  if (!try_reserve(limits))
943
- __usearch_raise_runtime_error("failed to reserve memory");
949
+ usearch_raise_runtime_error("failed to reserve memory");
944
950
  }
945
951
 
946
952
  /**
@@ -1070,8 +1076,9 @@ class index_dense_gt {
1070
1076
 
1071
1077
  /**
1072
1078
  * @brief Parses the index from file to RAM.
1073
- * @param[in] path The path to the file.
1079
+ * @param[in] input The input stream to read from.
1074
1080
  * @param[in] config Configuration parameters for imports.
1081
+ * @param[in] progress Callback to report the execution progress.
1075
1082
  * @return Outcome descriptor explicitly convertible to boolean.
1076
1083
  */
1077
1084
  template <typename input_callback_at, typename progress_at = dummy_progress_t>
@@ -1141,7 +1148,9 @@ class index_dense_gt {
1141
1148
 
1142
1149
  config_.multi = head.multi;
1143
1150
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1144
- cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
1151
+ // available_threads_.size() will be updated to old_limits.threads() later in this
1152
+ // method, so use that as the number of threads to prepare for.
1153
+ cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1145
1154
  if (!cast_buffer_)
1146
1155
  return result.failed("Failed to allocate memory for the casts");
1147
1156
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1180,8 +1189,10 @@ class index_dense_gt {
1180
1189
 
1181
1190
  /**
1182
1191
  * @brief Parses the index from file, without loading it into RAM.
1183
- * @param[in] path The path to the file.
1192
+ * @param[in] file The input file to read from.
1193
+ * @param[in] offset The offset in the file to start reading from.
1184
1194
  * @param[in] config Configuration parameters for imports.
1195
+ * @param[in] progress Callback to report the execution progress.
1185
1196
  * @return Outcome descriptor explicitly convertible to boolean.
1186
1197
  */
1187
1198
  template <typename progress_at = dummy_progress_t>
@@ -1253,7 +1264,9 @@ class index_dense_gt {
1253
1264
 
1254
1265
  config_.multi = head.multi;
1255
1266
  metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
1256
- cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
1267
+ // available_threads_.size() will be updated to old_limits.threads() later in this
1268
+ // method, so use that as the number of threads to prepare for.
1269
+ cast_buffer_ = cast_buffer_t(old_limits.threads() * metric_.bytes_per_vector());
1257
1270
  if (!cast_buffer_)
1258
1271
  return result.failed("Failed to allocate memory for the casts");
1259
1272
  casts_ = casts_punned_t::make(head.kind_scalar);
@@ -1301,8 +1314,9 @@ class index_dense_gt {
1301
1314
 
1302
1315
  /**
1303
1316
  * @brief Saves the index to a file.
1304
- * @param[in] path The path to the file.
1317
+ * @param[in] file The output file to write to.
1305
1318
  * @param[in] config Configuration parameters for exports.
1319
+ * @param[in] progress Callback to report the execution progress.
1306
1320
  * @return Outcome descriptor explicitly convertible to boolean.
1307
1321
  */
1308
1322
  template <typename progress_at = dummy_progress_t>
@@ -1356,8 +1370,9 @@ class index_dense_gt {
1356
1370
 
1357
1371
  /**
1358
1372
  * @brief Parses the index from file to RAM.
1359
- * @param[in] path The path to the file.
1373
+ * @param[in] file The input file to read from.
1360
1374
  * @param[in] config Configuration parameters for imports.
1375
+ * @param[in] progress Progress callback.
1361
1376
  * @return Outcome descriptor explicitly convertible to boolean.
1362
1377
  */
1363
1378
  template <typename progress_at = dummy_progress_t>
@@ -1694,12 +1709,12 @@ class index_dense_gt {
1694
1709
  compaction_result_t isolate(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
1695
1710
  compaction_result_t result;
1696
1711
  std::atomic<std::size_t> pruned_edges;
1697
- auto disallow = [&](member_cref_t const& member) noexcept {
1712
+ auto allow = [&](member_cref_t const& member) noexcept {
1698
1713
  bool freed = member.key == free_key_;
1699
1714
  pruned_edges += freed;
1700
- return freed;
1715
+ return !freed;
1701
1716
  };
1702
- typed_->isolate(disallow, std::forward<executor_at>(executor), std::forward<progress_at>(progress));
1717
+ typed_->isolate(allow, std::forward<executor_at>(executor), std::forward<progress_at>(progress));
1703
1718
  result.pruned_edges = pruned_edges;
1704
1719
  return result;
1705
1720
  }
@@ -1959,6 +1974,7 @@ class index_dense_gt {
1959
1974
 
1960
1975
  result.computed_distances = computed_distances;
1961
1976
  result.visited_members = visited_members;
1977
+ result.clusters = unique_clusters;
1962
1978
 
1963
1979
  (void)progress;
1964
1980
  return result;
@@ -1986,14 +2002,13 @@ class index_dense_gt {
1986
2002
  template <typename scalar_at>
1987
2003
  add_result_t add_( //
1988
2004
  vector_key_t key, scalar_at const* vector, //
1989
- std::size_t thread, bool force_vector_copy, cast_punned_t const& cast) {
2005
+ std::size_t thread, bool copy_vector, cast_punned_t const& cast) {
1990
2006
 
1991
2007
  if (!multi() && config().enable_key_lookups && contains(key))
1992
2008
  return add_result_t{}.failed("Duplicate keys not allowed in high-level wrappers");
1993
2009
 
1994
2010
  // Cast the vector, if needed for compatibility with `metric_`
1995
2011
  thread_lock_t lock = thread_lock_(thread);
1996
- bool copy_vector = !config_.exclude_vectors || force_vector_copy;
1997
2012
  byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
1998
2013
  {
1999
2014
  byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
@@ -2255,4 +2270,4 @@ static join_result_t join( //
2255
2270
  }
2256
2271
 
2257
2272
  } // namespace usearch
2258
- } // namespace unum
2273
+ } // namespace unum
@@ -483,7 +483,7 @@ class f16_bits_t {
483
483
  inline operator float() const noexcept { return f16_to_f32(uint16_); }
484
484
  inline explicit operator bool() const noexcept { return f16_to_f32(uint16_) > 0.5f; }
485
485
 
486
- inline f16_bits_t(std::int8_t v) noexcept : uint16_(f32_to_f16(v)) {}
486
+ inline f16_bits_t(int v) noexcept : uint16_(f32_to_f16(v)) {}
487
487
  inline f16_bits_t(bool v) noexcept : uint16_(f32_to_f16(v)) {}
488
488
  inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
489
489
  inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(static_cast<float>(v))) {}
@@ -524,6 +524,11 @@ class f16_bits_t {
524
524
  }
525
525
  };
526
526
 
527
+ #if USEARCH_USE_OPENMP
528
+ #pragma omp declare reduction(+ : unum::usearch::f16_bits_t : omp_out = omp_out + omp_in) \
529
+ initializer(omp_priv = unum::usearch::f16_bits_t())
530
+ #endif
531
+
527
532
  /**
528
533
  * @brief Numeric type for brain-floating point half-precision floating point.
529
534
  * If hardware support isn't available, falls back to a hardware
@@ -542,7 +547,7 @@ class bf16_bits_t {
542
547
  inline operator float() const noexcept { return bf16_to_f32(uint16_); }
543
548
  inline explicit operator bool() const noexcept { return bf16_to_f32(uint16_) > 0.5f; }
544
549
 
545
- inline bf16_bits_t(std::int8_t v) noexcept : uint16_(f32_to_bf16(v)) {}
550
+ inline bf16_bits_t(int v) noexcept : uint16_(f32_to_bf16(v)) {}
546
551
  inline bf16_bits_t(bool v) noexcept : uint16_(f32_to_bf16(v)) {}
547
552
  inline bf16_bits_t(float v) noexcept : uint16_(f32_to_bf16(v)) {}
548
553
  inline bf16_bits_t(double v) noexcept : uint16_(f32_to_bf16(static_cast<float>(v))) {}
@@ -581,8 +586,18 @@ class bf16_bits_t {
581
586
  uint16_ = f32_to_bf16(v / bf16_to_f32(uint16_));
582
587
  return *this;
583
588
  }
589
+
590
+ inline bf16_bits_t& operator=(float v) noexcept {
591
+ uint16_ = f32_to_bf16(v);
592
+ return *this;
593
+ }
584
594
  };
585
595
 
596
+ #if USEARCH_USE_OPENMP
597
+ #pragma omp declare reduction(+ : unum::usearch::bf16_bits_t : omp_out = omp_out + omp_in) \
598
+ initializer(omp_priv = unum::usearch::bf16_bits_t())
599
+ #endif
600
+
586
601
  /**
587
602
  * @brief An STL-based executor or a "thread-pool" for parallel execution.
588
603
  * Isn't efficient for small batches, as it recreates the threads on every call.
@@ -760,7 +775,9 @@ class executor_openmp_t {
760
775
  template <typename thread_aware_function_at>
761
776
  void parallel(thread_aware_function_at&& thread_aware_function) noexcept(false) {
762
777
  #pragma omp parallel
763
- { thread_aware_function(omp_get_thread_num()); }
778
+ {
779
+ thread_aware_function(omp_get_thread_num());
780
+ }
764
781
  }
765
782
  };
766
783
 
@@ -1223,6 +1240,7 @@ struct casts_punned_t {
1223
1240
  cast_punned_t b1x8{};
1224
1241
  cast_punned_t i8{};
1225
1242
  cast_punned_t f16{};
1243
+ cast_punned_t bf16{};
1226
1244
  cast_punned_t f32{};
1227
1245
  cast_punned_t f64{};
1228
1246
 
@@ -1231,7 +1249,7 @@ struct casts_punned_t {
1231
1249
  case scalar_kind_t::f64_k: return f64;
1232
1250
  case scalar_kind_t::f32_k: return f32;
1233
1251
  case scalar_kind_t::f16_k: return f16;
1234
- case scalar_kind_t::bf16_k: return f16;
1252
+ case scalar_kind_t::bf16_k: return bf16;
1235
1253
  case scalar_kind_t::i8_k: return i8;
1236
1254
  case scalar_kind_t::b1x8_k: return b1x8;
1237
1255
  default: return nullptr;
@@ -1246,12 +1264,14 @@ struct casts_punned_t {
1246
1264
  result.from.b1x8 = &cast_gt<b1x8_t, scalar_at>::try_;
1247
1265
  result.from.i8 = &cast_gt<i8_t, scalar_at>::try_;
1248
1266
  result.from.f16 = &cast_gt<f16_t, scalar_at>::try_;
1267
+ result.from.bf16 = &cast_gt<bf16_t, scalar_at>::try_;
1249
1268
  result.from.f32 = &cast_gt<f32_t, scalar_at>::try_;
1250
1269
  result.from.f64 = &cast_gt<f64_t, scalar_at>::try_;
1251
1270
 
1252
1271
  result.to.b1x8 = &cast_gt<scalar_at, b1x8_t>::try_;
1253
1272
  result.to.i8 = &cast_gt<scalar_at, i8_t>::try_;
1254
1273
  result.to.f16 = &cast_gt<scalar_at, f16_t>::try_;
1274
+ result.to.bf16 = &cast_gt<scalar_at, bf16_t>::try_;
1255
1275
  result.to.f32 = &cast_gt<scalar_at, f32_t>::try_;
1256
1276
  result.to.f64 = &cast_gt<scalar_at, f64_t>::try_;
1257
1277
 
@@ -1283,6 +1303,8 @@ struct casts_punned_t {
1283
1303
 
1284
1304
  /**
1285
1305
  * @brief Inner (Dot) Product distance.
1306
+ * Vectors should be normalized to unit length,
1307
+ * otherwise `::metric_cos_gt` should be used instead.
1286
1308
  */
1287
1309
  template <typename scalar_at = float, typename result_at = scalar_at> struct metric_ip_gt {
1288
1310
  using scalar_t = scalar_at;
@@ -2566,7 +2588,7 @@ class flat_hash_multi_set_gt {
2566
2588
  // Allocate new memory
2567
2589
  data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
2568
2590
  if (!data_)
2569
- __usearch_raise_runtime_error("failed memory allocation");
2591
+ usearch_raise_runtime_error("failed memory allocation");
2570
2592
 
2571
2593
  // Copy metadata
2572
2594
  buckets_ = other.buckets_;
@@ -2606,7 +2628,7 @@ class flat_hash_multi_set_gt {
2606
2628
  // Allocate new memory
2607
2629
  data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
2608
2630
  if (!data_)
2609
- __usearch_raise_runtime_error("failed memory allocation");
2631
+ usearch_raise_runtime_error("failed memory allocation");
2610
2632
 
2611
2633
  // Copy metadata
2612
2634
  buckets_ = other.buckets_;
@@ -2979,7 +3001,7 @@ class flat_hash_multi_set_gt {
2979
3001
 
2980
3002
  void reserve(std::size_t capacity) {
2981
3003
  if (!try_reserve(capacity))
2982
- __usearch_raise_runtime_error("failed to reserve memory");
3004
+ usearch_raise_runtime_error("failed to reserve memory");
2983
3005
  }
2984
3006
 
2985
3007
  bool try_emplace(element_t const& element) noexcept {
@@ -10,12 +10,6 @@ For Node.js environments, install USearch using `npm`:
10
10
  npm install usearch
11
11
  ```
12
12
 
13
- For front-end applications using WASM, use the Wasmer package manager:
14
-
15
- ```sh
16
- wasmer install unum/usearch
17
- ```
18
-
19
13
  ## Quickstart
20
14
 
21
15
  Create an index, add vectors, and perform searches with ease:
@@ -78,6 +72,14 @@ const batchResults = index.search(vectors, 2);
78
72
  const firstMatch = batchResults.get(0);
79
73
  ```
80
74
 
75
+ Multi-threading is supported for batch operations:
76
+
77
+ ```js
78
+ const threads_count = 0; // Zero for auto-detection or pass an unsigned integer
79
+ index.add(keys, vectors, threads_count);
80
+ const batchResults = index.search(vectors, 2, threads_count);
81
+ ```
82
+
81
83
  ## Index Introspection
82
84
 
83
85
  Inspect and interact with the index:
@@ -115,10 +115,12 @@ export declare class Index {
115
115
  * If a single key is provided, it is associated with all provided vectors.
116
116
  * @param {Float32Array|Float64Array|Int8Array} vectors - Input matrix representing vectors,
117
117
  * matrix of size n * d, where n is the number of vectors, and d is their dimensionality.
118
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for indexing.
119
+ * If set to 0, the number of threads is determined automatically.
118
120
  * @throws Will throw an error if the length of keys doesn't match the number of vectors
119
121
  * or if it's not a single key.
120
122
  */
121
- add(keys: bigint | bigint[] | BigUint64Array, vectors: Vector): void;
123
+ add(keys: bigint | bigint[] | BigUint64Array, vectors: Vector, threads?: number): void;
122
124
  /**
123
125
  * Perform a k-nearest neighbor search on the index.
124
126
  *
@@ -136,11 +138,12 @@ export declare class Index {
136
138
  *
137
139
  * @param {Float32Array|Float64Array|Int8Array|Array<Array<number>>} vectors - Input matrix representing query vectors, can be a TypedArray or an array of TypedArray.
138
140
  * @param {number} k - The number of nearest neighbors to search for each query vector.
141
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for searching. If set to 0, the number of threads is determined automatically.
139
142
  * @return {Matches|BatchMatches} - Search results for one or more queries, containing keys, distances, and counts of the matches found.
140
143
  * @throws Will throw an error if `k` is not a positive integer or if the size of the vectors is not a multiple of dimensions.
141
144
  * @throws Will throw an error if `vectors` is not a valid input type (TypedArray or an array of TypedArray) or if its flattened size is not a multiple of dimensions.
142
145
  */
143
- search(vectors: VectorOrMatrix, k: number): Matches | BatchMatches;
146
+ search(vectors: VectorOrMatrix, k: number, threads?: number): Matches | BatchMatches;
144
147
  /**
145
148
  * Verifies the presence of one or more keys in the index.
146
149
  *
@@ -218,6 +221,7 @@ export declare class Index {
218
221
  * @param {number} dimensions - The dimensionality of the vectors in both the dataset and the queries. It defines the number of elements in each vector.
219
222
  * @param {number} count - The number of nearest neighbors to return for each query. If the dataset contains fewer vectors than the specified count, the result will contain only the available vectors.
220
223
  * @param {MetricKind} metric - The distance metric to be used for the search.
224
+ * @param {number} [threads=0] - Optional, default is 0. The number of threads to use for the search. If set to 0, the number of threads is determined automatically.
221
225
  * @return {Matches|BatchMatches} - Returns a `Matches` or `BatchMatches` object containing the results of the search.
222
226
  * @throws Will throw an error if `dimensions` and `count` are not positive integers.
223
227
  * @throws Will throw an error if `metric` is not a valid MetricKind.
@@ -231,15 +235,16 @@ export declare class Index {
231
235
  * const dimensions = 2; // The number of elements in each vector.
232
236
  * const count = 1; // The number of nearest neighbors to return for each query.
233
237
  * const metric = MetricKind.IP; // Using the Inner Product distance metric.
238
+ * const threads = 0; // How many threads to use to perform the search.
234
239
  *
235
- * const result = exactSearch(dataset, queries, dimensions, count, metric);
240
+ * const result = exactSearch(dataset, queries, dimensions, count, metric, threads);
236
241
  * // result might be:
237
242
  * // {
238
243
  * // keys: BigUint64Array [ 1n ],
239
244
  * // distances: Float32Array [ some_value ],
240
245
  * // }
241
246
  */
242
- declare function exactSearch(dataset: VectorOrMatrix, queries: VectorOrMatrix, dimensions: number, count: number, metric: MetricKind): Matches | BatchMatches;
247
+ export declare function exactSearch(dataset: VectorOrMatrix, queries: VectorOrMatrix, dimensions: number, count: number, metric: MetricKind, threads?: number): Matches | BatchMatches;
243
248
  declare const usearch: {
244
249
  Index: typeof Index;
245
250
  MetricKind: typeof MetricKind;
@@ -38,7 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  };
39
39
  var _Index_compiledIndex;
40
40
  Object.defineProperty(exports, "__esModule", { value: true });
41
- exports.Index = exports.BatchMatches = exports.Matches = exports.ScalarKind = exports.MetricKind = void 0;
41
+ exports.exactSearch = exports.Index = exports.BatchMatches = exports.Matches = exports.ScalarKind = exports.MetricKind = void 0;
42
42
  const node_gyp_build_1 = __importDefault(require("node-gyp-build"));
43
43
  const path = __importStar(require("path"));
44
44
  const fs_1 = require("fs");
@@ -266,10 +266,12 @@ class Index {
266
266
  * If a single key is provided, it is associated with all provided vectors.
267
267
  * @param {Float32Array|Float64Array|Int8Array} vectors - Input matrix representing vectors,
268
268
  * matrix of size n * d, where n is the number of vectors, and d is their dimensionality.
269
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for indexing.
270
+ * If set to 0, the number of threads is determined automatically.
269
271
  * @throws Will throw an error if the length of keys doesn't match the number of vectors
270
272
  * or if it's not a single key.
271
273
  */
272
- add(keys, vectors) {
274
+ add(keys, vectors, threads = 0) {
273
275
  let normalizedKeys = normalizeKeys(keys);
274
276
  let normalizedVectors = normalizeVectors(vectors, __classPrivateFieldGet(this, _Index_compiledIndex, "f").dimensions());
275
277
  let countVectors = normalizedVectors.length / __classPrivateFieldGet(this, _Index_compiledIndex, "f").dimensions();
@@ -281,8 +283,11 @@ class Index {
281
283
  else if (normalizedKeys.length !== countVectors) {
282
284
  throw new Error(`The length of keys (${normalizedKeys.length}) must match the number of vectors (${countVectors}) or be a single key.`);
283
285
  }
286
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
287
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
288
+ }
284
289
  // Call the compiled method
285
- __classPrivateFieldGet(this, _Index_compiledIndex, "f").add(normalizedKeys, normalizedVectors);
290
+ __classPrivateFieldGet(this, _Index_compiledIndex, "f").add(normalizedKeys, normalizedVectors, threads);
286
291
  }
287
292
  /**
288
293
  * Perform a k-nearest neighbor search on the index.
@@ -301,17 +306,21 @@ class Index {
301
306
  *
302
307
  * @param {Float32Array|Float64Array|Int8Array|Array<Array<number>>} vectors - Input matrix representing query vectors, can be a TypedArray or an array of TypedArray.
303
308
  * @param {number} k - The number of nearest neighbors to search for each query vector.
309
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for searching. If set to 0, the number of threads is determined automatically.
304
310
  * @return {Matches|BatchMatches} - Search results for one or more queries, containing keys, distances, and counts of the matches found.
305
311
  * @throws Will throw an error if `k` is not a positive integer or if the size of the vectors is not a multiple of dimensions.
306
312
  * @throws Will throw an error if `vectors` is not a valid input type (TypedArray or an array of TypedArray) or if its flattened size is not a multiple of dimensions.
307
313
  */
308
- search(vectors, k) {
314
+ search(vectors, k, threads = 0) {
309
315
  if ((!Number.isNaN(k) && typeof k !== "number") || k <= 0) {
310
316
  throw new Error("`k` must be a positive integer representing the number of nearest neighbors to search for.");
311
317
  }
318
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
319
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
320
+ }
312
321
  const normalizedVectors = normalizeVectors(vectors, __classPrivateFieldGet(this, _Index_compiledIndex, "f").dimensions());
313
322
  // Call the compiled method and create Matches or BatchMatches object with the result
314
- const result = __classPrivateFieldGet(this, _Index_compiledIndex, "f").search(normalizedVectors, k);
323
+ const result = __classPrivateFieldGet(this, _Index_compiledIndex, "f").search(normalizedVectors, k, threads);
315
324
  const countInQueries = normalizedVectors.length / Number(__classPrivateFieldGet(this, _Index_compiledIndex, "f").dimensions());
316
325
  const batchMatches = new BatchMatches(...result, k);
317
326
  if (countInQueries === 1) {
@@ -441,6 +450,7 @@ _Index_compiledIndex = new WeakMap();
441
450
  * @param {number} dimensions - The dimensionality of the vectors in both the dataset and the queries. It defines the number of elements in each vector.
442
451
  * @param {number} count - The number of nearest neighbors to return for each query. If the dataset contains fewer vectors than the specified count, the result will contain only the available vectors.
443
452
  * @param {MetricKind} metric - The distance metric to be used for the search.
453
+ * @param {number} [threads=0] - Optional, default is 0. The number of threads to use for the search. If set to 0, the number of threads is determined automatically.
444
454
  * @return {Matches|BatchMatches} - Returns a `Matches` or `BatchMatches` object containing the results of the search.
445
455
  * @throws Will throw an error if `dimensions` and `count` are not positive integers.
446
456
  * @throws Will throw an error if `metric` is not a valid MetricKind.
@@ -454,21 +464,25 @@ _Index_compiledIndex = new WeakMap();
454
464
  * const dimensions = 2; // The number of elements in each vector.
455
465
  * const count = 1; // The number of nearest neighbors to return for each query.
456
466
  * const metric = MetricKind.IP; // Using the Inner Product distance metric.
467
+ * const threads = 0; // How many threads to use to perform the search.
457
468
  *
458
- * const result = exactSearch(dataset, queries, dimensions, count, metric);
469
+ * const result = exactSearch(dataset, queries, dimensions, count, metric, threads);
459
470
  * // result might be:
460
471
  * // {
461
472
  * // keys: BigUint64Array [ 1n ],
462
473
  * // distances: Float32Array [ some_value ],
463
474
  * // }
464
475
  */
465
- function exactSearch(dataset, queries, dimensions, count, metric) {
476
+ function exactSearch(dataset, queries, dimensions, count, metric, threads = 0) {
466
477
  // Validate and normalize the dimensions and count
467
478
  dimensions = Number(dimensions);
468
479
  count = Number(count);
469
480
  if (count <= 0 || dimensions <= 0) {
470
481
  throw new Error("Dimensions and count must be positive integers.");
471
482
  }
483
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
484
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
485
+ }
472
486
  // Validate metric
473
487
  if (!Object.values(MetricKind).includes(metric)) {
474
488
  throw new Error(`Invalid metric: ${metric}. It must be one of: ${Object.values(MetricKind).join(", ")}`);
@@ -489,7 +503,7 @@ function exactSearch(dataset, queries, dimensions, count, metric) {
489
503
  throw new Error("Count must be equal or smaller than the number of vectors in the dataset.");
490
504
  }
491
505
  // Call the compiled function with the normalized input
492
- const result = compiled.exactSearch(dataset, queries, dimensions, count, metric);
506
+ const result = compiled.exactSearch(dataset, queries, dimensions, count, metric, threads);
493
507
  // Create and return a Matches or BatchMatches object with the result
494
508
  if (countInQueries == 1) {
495
509
  return new Matches(result[0], result[1]);
@@ -498,6 +512,7 @@ function exactSearch(dataset, queries, dimensions, count, metric) {
498
512
  return new BatchMatches(...result, count);
499
513
  }
500
514
  }
515
+ exports.exactSearch = exactSearch;
501
516
  const usearch = {
502
517
  Index,
503
518
  MetricKind,
@@ -115,10 +115,12 @@ export declare class Index {
115
115
  * If a single key is provided, it is associated with all provided vectors.
116
116
  * @param {Float32Array|Float64Array|Int8Array} vectors - Input matrix representing vectors,
117
117
  * matrix of size n * d, where n is the number of vectors, and d is their dimensionality.
118
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for indexing.
119
+ * If set to 0, the number of threads is determined automatically.
118
120
  * @throws Will throw an error if the length of keys doesn't match the number of vectors
119
121
  * or if it's not a single key.
120
122
  */
121
- add(keys: bigint | bigint[] | BigUint64Array, vectors: Vector): void;
123
+ add(keys: bigint | bigint[] | BigUint64Array, vectors: Vector, threads?: number): void;
122
124
  /**
123
125
  * Perform a k-nearest neighbor search on the index.
124
126
  *
@@ -136,11 +138,12 @@ export declare class Index {
136
138
  *
137
139
  * @param {Float32Array|Float64Array|Int8Array|Array<Array<number>>} vectors - Input matrix representing query vectors, can be a TypedArray or an array of TypedArray.
138
140
  * @param {number} k - The number of nearest neighbors to search for each query vector.
141
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for searching. If set to 0, the number of threads is determined automatically.
139
142
  * @return {Matches|BatchMatches} - Search results for one or more queries, containing keys, distances, and counts of the matches found.
140
143
  * @throws Will throw an error if `k` is not a positive integer or if the size of the vectors is not a multiple of dimensions.
141
144
  * @throws Will throw an error if `vectors` is not a valid input type (TypedArray or an array of TypedArray) or if its flattened size is not a multiple of dimensions.
142
145
  */
143
- search(vectors: VectorOrMatrix, k: number): Matches | BatchMatches;
146
+ search(vectors: VectorOrMatrix, k: number, threads?: number): Matches | BatchMatches;
144
147
  /**
145
148
  * Verifies the presence of one or more keys in the index.
146
149
  *
@@ -218,6 +221,7 @@ export declare class Index {
218
221
  * @param {number} dimensions - The dimensionality of the vectors in both the dataset and the queries. It defines the number of elements in each vector.
219
222
  * @param {number} count - The number of nearest neighbors to return for each query. If the dataset contains fewer vectors than the specified count, the result will contain only the available vectors.
220
223
  * @param {MetricKind} metric - The distance metric to be used for the search.
224
+ * @param {number} [threads=0] - Optional, default is 0. The number of threads to use for the search. If set to 0, the number of threads is determined automatically.
221
225
  * @return {Matches|BatchMatches} - Returns a `Matches` or `BatchMatches` object containing the results of the search.
222
226
  * @throws Will throw an error if `dimensions` and `count` are not positive integers.
223
227
  * @throws Will throw an error if `metric` is not a valid MetricKind.
@@ -231,15 +235,16 @@ export declare class Index {
231
235
  * const dimensions = 2; // The number of elements in each vector.
232
236
  * const count = 1; // The number of nearest neighbors to return for each query.
233
237
  * const metric = MetricKind.IP; // Using the Inner Product distance metric.
238
+ * const threads = 0; // How many threads to use to perform the search.
234
239
  *
235
- * const result = exactSearch(dataset, queries, dimensions, count, metric);
240
+ * const result = exactSearch(dataset, queries, dimensions, count, metric, threads);
236
241
  * // result might be:
237
242
  * // {
238
243
  * // keys: BigUint64Array [ 1n ],
239
244
  * // distances: Float32Array [ some_value ],
240
245
  * // }
241
246
  */
242
- declare function exactSearch(dataset: VectorOrMatrix, queries: VectorOrMatrix, dimensions: number, count: number, metric: MetricKind): Matches | BatchMatches;
247
+ export declare function exactSearch(dataset: VectorOrMatrix, queries: VectorOrMatrix, dimensions: number, count: number, metric: MetricKind, threads?: number): Matches | BatchMatches;
243
248
  declare const usearch: {
244
249
  Index: typeof Index;
245
250
  MetricKind: typeof MetricKind;