usearch 1.2.2 → 2.0.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.
Files changed (42) hide show
  1. package/.github/workflows/prerelease.yml +71 -0
  2. package/.github/workflows/release.yml +77 -2
  3. package/.github/workflows/update_version.sh +1 -0
  4. package/.gitignore +11 -1
  5. package/.vscode/settings.json +5 -1
  6. package/CMakeLists.txt +1 -0
  7. package/Cargo.toml +1 -1
  8. package/README.md +31 -18
  9. package/c/lib.cpp +38 -4
  10. package/c/test.c +117 -114
  11. package/c/usearch.h +28 -1
  12. package/conanfile.py +1 -1
  13. package/cpp/test.cpp +2 -2
  14. package/csharp/.editorconfig +361 -0
  15. package/csharp/Cloud.Unum.USearch.sln +34 -0
  16. package/csharp/DEV_SETUP.md +51 -0
  17. package/csharp/Directory.Build.props +29 -0
  18. package/csharp/README.md +77 -0
  19. package/csharp/build_and_test.cmd +31 -0
  20. package/csharp/build_and_test.sh +30 -0
  21. package/csharp/eng/build_lib.sh +65 -0
  22. package/csharp/eng/clean.sh +12 -0
  23. package/csharp/nuget/NUGET.md +11 -0
  24. package/csharp/nuget/nuget-package.props +33 -0
  25. package/csharp/pack.sh +56 -0
  26. package/csharp/src/Cloud.Unum.USearch.Tests/Cloud.Unum.USearch.Tests.csproj +39 -0
  27. package/csharp/src/Cloud.Unum.USearch.Tests/InitTests.cs +306 -0
  28. package/csharp/src/Cloud.Unum.USearch.Tests/Usings.cs +1 -0
  29. package/docs/benchmarks.md +6 -6
  30. package/docs/compilation.md +1 -1
  31. package/include/usearch/index.hpp +172 -52
  32. package/include/usearch/index_dense.hpp +201 -64
  33. package/include/usearch/index_plugins.hpp +90 -69
  34. package/package.json +2 -2
  35. package/python/lib.cpp +99 -37
  36. package/python/scripts/test_index.py +38 -10
  37. package/python/usearch/index.py +87 -37
  38. package/rust/README.md +2 -2
  39. package/rust/lib.cpp +42 -19
  40. package/rust/lib.hpp +14 -0
  41. package/rust/lib.rs +85 -43
  42. package/wasmer.toml +1 -1
@@ -54,8 +54,8 @@ struct uuid_t {
54
54
  std::uint8_t octets[16];
55
55
  };
56
56
 
57
- class i8_bits_t;
58
57
  class f16_bits_t;
58
+ class i8_converted_t;
59
59
 
60
60
  #if USEARCH_USE_NATIVE_F16
61
61
  #if defined(USEARCH_DEFINED_ARM)
@@ -149,7 +149,7 @@ template <typename scalar_at> scalar_kind_t scalar_kind() noexcept {
149
149
  return scalar_kind_t::f32_k;
150
150
  if (std::is_same<scalar_at, f16_t>())
151
151
  return scalar_kind_t::f16_k;
152
- if (std::is_same<scalar_at, i8_bits_t>())
152
+ if (std::is_same<scalar_at, i8_t>())
153
153
  return scalar_kind_t::i8_k;
154
154
  if (std::is_same<scalar_at, u64_t>())
155
155
  return scalar_kind_t::u64_k;
@@ -364,7 +364,7 @@ class f16_bits_t {
364
364
  inline operator float() const noexcept { return f16_to_f32(uint16_); }
365
365
  inline explicit operator bool() const noexcept { return f16_to_f32(uint16_) > 0.5f; }
366
366
 
367
- inline f16_bits_t(i8_bits_t) noexcept;
367
+ inline f16_bits_t(i8_converted_t) noexcept;
368
368
  inline f16_bits_t(bool v) noexcept : uint16_(f32_to_f16(v)) {}
369
369
  inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
370
370
  inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(v)) {}
@@ -403,45 +403,6 @@ class f16_bits_t {
403
403
  }
404
404
  };
405
405
 
406
- /**
407
- * @brief Numeric type for uniformly-distributed floating point
408
- * values within [-1,1] range, quantized to integers [-100,100].
409
- */
410
- class i8_bits_t {
411
- std::int8_t int8_{};
412
-
413
- public:
414
- constexpr static float divisor_k = 100.f;
415
- constexpr static std::int8_t min_k = -100;
416
- constexpr static std::int8_t max_k = 100;
417
-
418
- inline i8_bits_t() noexcept : int8_(0) {}
419
- inline i8_bits_t(bool v) noexcept : int8_(v ? max_k : 0) {}
420
-
421
- inline i8_bits_t(i8_bits_t&&) = default;
422
- inline i8_bits_t& operator=(i8_bits_t&&) = default;
423
- inline i8_bits_t(i8_bits_t const&) = default;
424
- inline i8_bits_t& operator=(i8_bits_t const&) = default;
425
-
426
- inline operator float() const noexcept { return float(int8_) / divisor_k; }
427
- inline operator f16_t() const noexcept { return float(int8_) / divisor_k; }
428
- inline operator double() const noexcept { return double(int8_) / divisor_k; }
429
- inline explicit operator bool() const noexcept { return int8_ > (max_k / 2); }
430
- inline explicit operator std::int8_t() const noexcept { return int8_; }
431
- inline explicit operator std::int16_t() const noexcept { return int8_; }
432
- inline explicit operator std::int32_t() const noexcept { return int8_; }
433
- inline explicit operator std::int64_t() const noexcept { return int8_; }
434
-
435
- inline i8_bits_t(f16_t v)
436
- : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
437
- inline i8_bits_t(float v)
438
- : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
439
- inline i8_bits_t(double v)
440
- : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
441
- };
442
-
443
- inline f16_bits_t::f16_bits_t(i8_bits_t v) noexcept : f16_bits_t(float(v)) {}
444
-
445
406
  /**
446
407
  * @brief An STL-based executor or a "thread-pool" for parallel execution.
447
408
  * Isn't efficient for small batches, as it recreates the threads on every call.
@@ -669,6 +630,34 @@ class aligned_allocator_gt {
669
630
 
670
631
  using aligned_allocator_t = aligned_allocator_gt<>;
671
632
 
633
+ class page_allocator_t {
634
+ public:
635
+ static constexpr std::size_t page_size() { return 4096; }
636
+
637
+ /**
638
+ * @brief Allocates an @b uninitialized block of memory of the specified size.
639
+ * @param count_bytes The number of bytes to allocate.
640
+ * @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
641
+ */
642
+ byte_t* allocate(std::size_t count_bytes) const noexcept {
643
+ count_bytes = divide_round_up(count_bytes, page_size()) * page_size();
644
+ #if defined(USEARCH_DEFINED_WINDOWS)
645
+ return (byte_t*)(::VirtualAlloc(NULL, count_bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
646
+ #else
647
+ return (byte_t*)mmap(NULL, count_bytes, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
648
+ #endif
649
+ }
650
+
651
+ void deallocate(byte_t* page_pointer, std::size_t count_bytes) const noexcept {
652
+ #if defined(USEARCH_DEFINED_WINDOWS)
653
+ ::VirtualFree(page_pointer, 0, MEM_RELEASE);
654
+ #else
655
+ count_bytes = divide_round_up(count_bytes, page_size()) * page_size();
656
+ munmap(page_pointer, count_bytes);
657
+ #endif
658
+ }
659
+ };
660
+
672
661
  /**
673
662
  * @brief Memory-mapping allocator designed for "alloc many, free at once" usage patterns.
674
663
  * @b Thread-safe, @b except constructors and destructors.
@@ -718,15 +707,11 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
718
707
  void reset() noexcept {
719
708
  byte_t* last_arena = last_arena_;
720
709
  while (last_arena) {
721
- byte_t* previous_arena;
710
+ byte_t* previous_arena = nullptr;
722
711
  std::memcpy(&previous_arena, last_arena, sizeof(byte_t*));
723
- std::size_t last_cap;
712
+ std::size_t last_cap = 0;
724
713
  std::memcpy(&last_cap, last_arena + sizeof(byte_t*), sizeof(std::size_t));
725
- #if defined(USEARCH_DEFINED_WINDOWS)
726
- ::VirtualFree(last_arena, 0, MEM_RELEASE);
727
- #else
728
- munmap(last_arena, last_cap);
729
- #endif
714
+ page_allocator_t{}.deallocate(last_arena, last_cap);
730
715
  last_arena = previous_arena;
731
716
  }
732
717
 
@@ -760,22 +745,12 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
760
745
  */
761
746
  inline byte_t* allocate(std::size_t count_bytes) noexcept {
762
747
  std::size_t extended_bytes = divide_round_up<alignment_ak>(count_bytes) * alignment_ak;
763
- // Check if the requested allocation size is absurd
764
- if (extended_bytes > min_capacity())
765
- return nullptr;
766
-
767
748
  std::unique_lock<std::mutex> lock(mutex_);
768
- if (!last_arena_ || (last_usage_ + extended_bytes > last_capacity_)) {
769
- std::size_t new_cap = last_capacity_ * capacity_multiplier();
770
- #if defined(USEARCH_DEFINED_WINDOWS)
771
- byte_t* new_arena = (byte_t*)(::VirtualAlloc(NULL, new_cap, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
772
- if (new_arena == nullptr)
773
- return nullptr;
774
- #else
775
- byte_t* new_arena = (byte_t*)mmap(NULL, new_cap, PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
749
+ if (!last_arena_ || (last_usage_ + extended_bytes >= last_capacity_)) {
750
+ std::size_t new_cap = (std::max)(last_capacity_, ceil2(extended_bytes)) * capacity_multiplier();
751
+ byte_t* new_arena = page_allocator_t{}.allocate(new_cap);
776
752
  if (!new_arena)
777
753
  return nullptr;
778
- #endif
779
754
  std::memcpy(new_arena, &last_arena_, sizeof(byte_t*));
780
755
  std::memcpy(new_arena + sizeof(byte_t*), &new_cap, sizeof(std::size_t));
781
756
 
@@ -939,7 +914,7 @@ template <> struct cast_gt<f16_bits_t, f16_bits_t> {
939
914
  bool operator()(byte_t const*, std::size_t, byte_t*) const { return false; }
940
915
  };
941
916
 
942
- template <> struct cast_gt<i8_bits_t, i8_bits_t> {
917
+ template <> struct cast_gt<i8_t, i8_t> {
943
918
  bool operator()(byte_t const*, std::size_t, byte_t*) const { return false; }
944
919
  };
945
920
 
@@ -967,6 +942,53 @@ template <typename to_scalar_at> struct cast_gt<b1x8_t, to_scalar_at> {
967
942
  }
968
943
  };
969
944
 
945
+ /**
946
+ * @brief Numeric type for uniformly-distributed floating point
947
+ * values within [-1,1] range, quantized to integers [-100,100].
948
+ */
949
+ class i8_converted_t {
950
+ std::int8_t int8_{};
951
+
952
+ public:
953
+ constexpr static float divisor_k = 100.f;
954
+ constexpr static std::int8_t min_k = -100;
955
+ constexpr static std::int8_t max_k = 100;
956
+
957
+ inline i8_converted_t() noexcept : int8_(0) {}
958
+ inline i8_converted_t(bool v) noexcept : int8_(v ? max_k : 0) {}
959
+
960
+ inline i8_converted_t(i8_converted_t&&) = default;
961
+ inline i8_converted_t& operator=(i8_converted_t&&) = default;
962
+ inline i8_converted_t(i8_converted_t const&) = default;
963
+ inline i8_converted_t& operator=(i8_converted_t const&) = default;
964
+
965
+ inline operator float() const noexcept { return float(int8_) / divisor_k; }
966
+ inline operator f16_t() const noexcept { return float(int8_) / divisor_k; }
967
+ inline operator double() const noexcept { return double(int8_) / divisor_k; }
968
+ inline explicit operator bool() const noexcept { return int8_ > (max_k / 2); }
969
+ inline explicit operator std::int8_t() const noexcept { return int8_; }
970
+ inline explicit operator std::int16_t() const noexcept { return int8_; }
971
+ inline explicit operator std::int32_t() const noexcept { return int8_; }
972
+ inline explicit operator std::int64_t() const noexcept { return int8_; }
973
+
974
+ inline i8_converted_t(f16_t v)
975
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
976
+ inline i8_converted_t(float v)
977
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
978
+ inline i8_converted_t(double v)
979
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
980
+ };
981
+
982
+ f16_bits_t::f16_bits_t(i8_converted_t v) noexcept : uint16_(f32_to_f16(v)) {}
983
+
984
+ template <> struct cast_gt<i8_t, f16_t> : public cast_gt<i8_converted_t, f16_t> {};
985
+ template <> struct cast_gt<i8_t, f32_t> : public cast_gt<i8_converted_t, f32_t> {};
986
+ template <> struct cast_gt<i8_t, f64_t> : public cast_gt<i8_converted_t, f64_t> {};
987
+
988
+ template <> struct cast_gt<f16_t, i8_t> : public cast_gt<f16_t, i8_converted_t> {};
989
+ template <> struct cast_gt<f32_t, i8_t> : public cast_gt<f32_t, i8_converted_t> {};
990
+ template <> struct cast_gt<f64_t, i8_t> : public cast_gt<f64_t, i8_converted_t> {};
991
+
970
992
  /**
971
993
  * @brief Inner (Dot) Product distance.
972
994
  */
@@ -1191,10 +1213,10 @@ template <typename scalar_at = float, typename result_at = float> struct metric_
1191
1213
  };
1192
1214
 
1193
1215
  struct cos_i8_t {
1194
- using scalar_t = i8_bits_t;
1216
+ using scalar_t = i8_t;
1195
1217
  using result_t = f32_t;
1196
1218
 
1197
- inline result_t operator()(i8_bits_t const* a, i8_bits_t const* b, std::size_t dim) const noexcept {
1219
+ inline result_t operator()(i8_t const* a, i8_t const* b, std::size_t dim) const noexcept {
1198
1220
  std::int32_t ab{}, a2{}, b2{};
1199
1221
  #if USEARCH_USE_OPENMP
1200
1222
  #pragma omp simd reduction(+ : ab, a2, b2)
@@ -1215,10 +1237,10 @@ struct cos_i8_t {
1215
1237
  };
1216
1238
 
1217
1239
  struct l2sq_i8_t {
1218
- using scalar_t = i8_bits_t;
1240
+ using scalar_t = i8_t;
1219
1241
  using result_t = f32_t;
1220
1242
 
1221
- inline result_t operator()(i8_bits_t const* a, i8_bits_t const* b, std::size_t dim) const noexcept {
1243
+ inline result_t operator()(i8_t const* a, i8_t const* b, std::size_t dim) const noexcept {
1222
1244
  std::int32_t ab_deltas_sq{};
1223
1245
  #if USEARCH_USE_OPENMP
1224
1246
  #pragma omp simd reduction(+ : ab_deltas_sq)
@@ -1417,7 +1439,6 @@ class metric_punned_t {
1417
1439
  static metric_punned_t haversine_metric_(scalar_kind_t scalar_kind) {
1418
1440
  std::size_t bytes_per_vector = 2u * bits_per_scalar(scalar_kind) / CHAR_BIT;
1419
1441
  switch (scalar_kind) {
1420
- case scalar_kind_t::i8_k: return {to_stl_<metric_haversine_gt<i8_bits_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1421
1442
  case scalar_kind_t::f16_k: return {to_stl_<metric_haversine_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1422
1443
  case scalar_kind_t::f32_k: return {to_stl_<metric_haversine_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1423
1444
  case scalar_kind_t::f64_k: return {to_stl_<metric_haversine_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::haversine_k, scalar_kind_t::f64_k, isa_kind_t::auto_k};
@@ -1427,7 +1448,7 @@ class metric_punned_t {
1427
1448
 
1428
1449
  static metric_punned_t pearson_metric_(std::size_t bytes_per_vector, scalar_kind_t scalar_kind) {
1429
1450
  switch (scalar_kind) {
1430
- case scalar_kind_t::i8_k: return {to_stl_<metric_pearson_gt<i8_bits_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1451
+ case scalar_kind_t::i8_k: return {to_stl_<metric_pearson_gt<i8_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::i8_k, isa_kind_t::auto_k};
1431
1452
  case scalar_kind_t::f16_k: return {to_stl_<metric_pearson_gt<f16_t, f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f16_k, isa_kind_t::auto_k};
1432
1453
  case scalar_kind_t::f32_k: return {to_stl_<metric_pearson_gt<f32_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f32_k, isa_kind_t::auto_k};
1433
1454
  case scalar_kind_t::f64_k: return {to_stl_<metric_pearson_gt<f64_t>>(bytes_per_vector), bytes_per_vector, metric_kind_t::pearson_k, scalar_kind_t::f64_k, isa_kind_t::auto_k};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "1.2.2",
3
+ "version": "2.0.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
@@ -25,7 +25,7 @@
25
25
  "@semantic-release/exec": "^6.0.3",
26
26
  "@semantic-release/git": "^10.0.1",
27
27
  "conventional-changelog-eslint": "^3.0.9",
28
- "semantic-release": "^21.1.0",
28
+ "semantic-release": "^21.1.1",
29
29
  "typescript": "^5.1.6"
30
30
  }
31
31
  }
package/python/lib.cpp CHANGED
@@ -144,7 +144,7 @@ metric_t wrap_user_defined_metric(
144
144
  case scalar_kind_t::b1x8_k:
145
145
  return wrap_typed_user_defined_metric<b1x8_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
146
146
  case scalar_kind_t::i8_k:
147
- return wrap_typed_user_defined_metric<i8_bits_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
147
+ return wrap_typed_user_defined_metric<i8_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
148
148
  case scalar_kind_t::f16_k:
149
149
  return wrap_typed_user_defined_metric<f16_t>(kind, signature, metric_uintptr, scalar_kind, dimensions);
150
150
  case scalar_kind_t::f32_k:
@@ -273,7 +273,7 @@ static void add_many_to_index( //
273
273
  // clang-format off
274
274
  switch (numpy_string_to_kind(vectors_info.format)) {
275
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_bits_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
277
  case scalar_kind_t::f16_k: add_typed_to_index<f16_t>(index, keys_info, vectors_info, force_copy, threads); break;
278
278
  case scalar_kind_t::f32_k: add_typed_to_index<f32_t>(index, keys_info, vectors_info, force_copy, threads); break;
279
279
  case scalar_kind_t::f64_k: add_typed_to_index<f64_t>(index, keys_info, vectors_info, force_copy, threads); break;
@@ -442,7 +442,7 @@ static py::tuple search_many_in_index( //
442
442
  // clang-format off
443
443
  switch (numpy_string_to_kind(vectors_info.format)) {
444
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_bits_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
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
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
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;
@@ -561,7 +561,7 @@ static py::tuple search_many_brute_force( //
561
561
  // clang-format off
562
562
  switch (dataset_kind) {
563
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_bits_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
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
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
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;
@@ -638,7 +638,7 @@ static py::tuple cluster_vectors( //
638
638
  // clang-format off
639
639
  switch (numpy_string_to_kind(queries_info.format)) {
640
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_bits_t const>(), queries_end.as<i8_bits_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
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
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
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;
@@ -759,17 +759,86 @@ static void compact_index(dense_index_py_t& index, std::size_t threads) {
759
759
  index.compact(executor_default_t{threads});
760
760
  }
761
761
 
762
+ static py::dict index_metadata(index_dense_metadata_result_t const& meta) {
763
+ py::dict result;
764
+ result["matrix_included"] = !meta.config.exclude_vectors;
765
+ result["matrix_uses_64_bit_dimensions"] = meta.config.use_64_bit_dimensions;
766
+
767
+ index_dense_head_t const& head = meta.head;
768
+ result["version"] = std::to_string(head.version_major) + "." + //
769
+ std::to_string(head.version_minor) + "." + //
770
+ std::to_string(head.version_patch);
771
+
772
+ result["kind_metric"] = metric_kind_t(head.kind_metric);
773
+ result["kind_scalar"] = scalar_kind_t(head.kind_scalar);
774
+ result["kind_key"] = scalar_kind_t(head.kind_key);
775
+ result["kind_compressed_slot"] = scalar_kind_t(head.kind_compressed_slot);
776
+
777
+ result["count_present"] = std::uint64_t(head.count_present);
778
+ result["count_deleted"] = std::uint64_t(head.count_deleted);
779
+ result["dimensions"] = std::uint64_t(head.dimensions);
780
+
781
+ return result;
782
+ }
783
+
762
784
  // clang-format off
763
- template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
764
- template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
765
- template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
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(); }
766
788
  template <typename index_at> void reset_index(index_at& index) { index.reset(); }
767
789
  template <typename index_at> void clear_index(index_at& index) { index.clear(); }
768
790
  template <typename index_at> std::size_t max_level(index_at const &index) { return index.max_level(); }
791
+ template <typename index_at> std::size_t serialized_length(index_at const &index) { return index.serialized_length(); }
769
792
  template <typename index_at> typename index_at::stats_t compute_stats(index_at const &index) { return index.stats(); }
770
793
  template <typename index_at> typename index_at::stats_t compute_level_stats(index_at const &index, std::size_t level) { return index.stats(level); }
771
794
  // clang-format on
772
795
 
796
+ template <typename py_bytes_at> memory_mapped_file_t memory_map_from_bytes(py_bytes_at&& bytes) {
797
+ py::buffer_info info(py::buffer(bytes).request());
798
+ return {(byte_t*)(info.ptr), static_cast<std::size_t>(info.size)};
799
+ }
800
+
801
+ template <typename index_at> py::object save_index_to_buffer(index_at const& index) {
802
+ std::size_t serialized_length = index.serialized_length();
803
+
804
+ // Create an empty bytearray object using CPython API
805
+ PyObject* byte_array = PyByteArray_FromStringAndSize(nullptr, 0);
806
+ if (!byte_array)
807
+ throw std::runtime_error("Could not allocate bytearray object");
808
+
809
+ // Resize the bytearray object to the desired length
810
+ if (PyByteArray_Resize(byte_array, static_cast<Py_ssize_t>(serialized_length)) != 0) {
811
+ Py_XDECREF(byte_array);
812
+ throw std::runtime_error("Could not resize bytearray object");
813
+ }
814
+
815
+ char* buffer = PyByteArray_AS_STRING(byte_array);
816
+ memory_mapped_file_t memory_map((byte_t*)buffer, serialized_length);
817
+ serialization_result_t result = index.save(std::move(memory_map));
818
+
819
+ if (!result) {
820
+ Py_XDECREF(byte_array);
821
+ result.error.raise();
822
+ }
823
+
824
+ return py::reinterpret_steal<py::object>(byte_array);
825
+ }
826
+
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();
829
+ }
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();
832
+ }
833
+
834
+ template <typename index_at> std::vector<typename index_at::stats_t> compute_levels_stats(index_at const& index) {
835
+ using stats_t = typename index_at::stats_t;
836
+ std::size_t max_level = index.max_level();
837
+ std::vector<stats_t> result(max_level + 1);
838
+ index.stats(result.data(), max_level);
839
+ return result;
840
+ }
841
+
773
842
  template <typename internal_at, typename external_at = internal_at, typename index_at = void>
774
843
  static py::tuple get_typed_vectors_for_keys(index_at const& index, py::buffer keys) {
775
844
 
@@ -807,7 +876,7 @@ template <typename index_at> py::tuple get_many(index_at const& index, py::buffe
807
876
  else if (scalar_kind == scalar_kind_t::f16_k)
808
877
  return get_typed_vectors_for_keys<f16_t, std::uint16_t>(index, keys);
809
878
  else if (scalar_kind == scalar_kind_t::i8_k)
810
- return get_typed_vectors_for_keys<i8_bits_t, std::int8_t>(index, keys);
879
+ return get_typed_vectors_for_keys<i8_t, std::int8_t>(index, keys);
811
880
  else if (scalar_kind == scalar_kind_t::b1x8_k)
812
881
  return get_typed_vectors_for_keys<b1x8_t, std::uint8_t>(index, keys);
813
882
  else
@@ -865,30 +934,16 @@ PYBIND11_MODULE(compiled, m) {
865
934
  .value("I16", scalar_kind_t::i16_k)
866
935
  .value("I8", scalar_kind_t::i8_k);
867
936
 
868
- m.def("index_dense_metadata", [](std::string const& path) -> py::dict {
869
- index_dense_metadata_result_t meta = index_dense_metadata(path.c_str());
937
+ m.def("index_dense_metadata_from_path", [](std::string const& path) -> py::dict {
938
+ index_dense_metadata_result_t meta = index_dense_metadata_from_path(path.c_str());
870
939
  forward_error(meta);
940
+ return index_metadata(meta);
941
+ });
871
942
 
872
- index_dense_head_t const& head = meta.head;
873
-
874
- py::dict result;
875
- result["matrix_included"] = !meta.config.exclude_vectors;
876
- result["matrix_uses_64_bit_dimensions"] = meta.config.use_64_bit_dimensions;
877
-
878
- result["version"] = std::to_string(head.version_major) + "." + //
879
- std::to_string(head.version_minor) + "." + //
880
- std::to_string(head.version_patch);
881
-
882
- result["kind_metric"] = metric_kind_t(head.kind_metric);
883
- result["kind_scalar"] = scalar_kind_t(head.kind_scalar);
884
- result["kind_key"] = scalar_kind_t(head.kind_key);
885
- result["kind_compressed_slot"] = scalar_kind_t(head.kind_compressed_slot);
886
-
887
- result["count_present"] = std::uint64_t(head.count_present);
888
- result["count_deleted"] = std::uint64_t(head.count_deleted);
889
- result["dimensions"] = std::uint64_t(head.dimensions);
890
-
891
- return result;
943
+ m.def("index_dense_metadata_from_buffer", [](py::bytes const& buffer) -> py::dict {
944
+ index_dense_metadata_result_t meta = index_dense_metadata_from_buffer(memory_map_from_bytes(buffer));
945
+ forward_error(meta);
946
+ return index_metadata(meta);
892
947
  });
893
948
 
894
949
  m.def("exact_search", &search_many_brute_force, //
@@ -1034,8 +1089,9 @@ PYBIND11_MODULE(compiled, m) {
1034
1089
  [](dense_index_py_t const& index) -> std::size_t { return index.metric().dimensions(); });
1035
1090
  i.def_property_readonly( //
1036
1091
  "dtype", [](dense_index_py_t const& index) -> scalar_kind_t { return index.scalar_kind(); });
1037
- i.def_property_readonly( //
1038
- "memory_usage", [](dense_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
1092
+
1093
+ i.def_property_readonly("serialized_length", &dense_index_py_t::serialized_length);
1094
+ i.def_property_readonly("memory_usage", &dense_index_py_t::memory_usage);
1039
1095
 
1040
1096
  i.def_property("expansion_add", &dense_index_py_t::expansion_add, &dense_index_py_t::change_expansion_add);
1041
1097
  i.def_property("expansion_search", &dense_index_py_t::expansion_search, &dense_index_py_t::change_expansion_search);
@@ -1138,9 +1194,14 @@ PYBIND11_MODULE(compiled, m) {
1138
1194
  },
1139
1195
  py::arg("offset"));
1140
1196
 
1141
- i.def("save", &save_index<dense_index_py_t>, py::arg("path"));
1142
- i.def("load", &load_index<dense_index_py_t>, py::arg("path"));
1143
- i.def("view", &view_index<dense_index_py_t>, py::arg("path"));
1197
+ i.def("save_index_to_path", &save_index_to_path<dense_index_py_t>);
1198
+ i.def("load_index_from_path", &load_index_from_path<dense_index_py_t>);
1199
+ i.def("view_index_from_path", &view_index_from_path<dense_index_py_t>);
1200
+
1201
+ i.def("save_index_to_buffer", &save_index_to_buffer<dense_index_py_t>);
1202
+ i.def("load_index_from_buffer", &load_index_from_buffer<dense_index_py_t>);
1203
+ i.def("view_index_from_buffer", &view_index_from_buffer<dense_index_py_t>);
1204
+
1144
1205
  i.def("reset", &reset_index<dense_index_py_t>);
1145
1206
  i.def("clear", &clear_index<dense_index_py_t>);
1146
1207
  i.def("copy", &copy_index, py::kw_only(), py::arg("copy") = true);
@@ -1155,7 +1216,8 @@ PYBIND11_MODULE(compiled, m) {
1155
1216
  i_stats.def_readonly("allocated_bytes", &punned_index_stats_t::allocated_bytes);
1156
1217
 
1157
1218
  i.def_property_readonly("max_level", &max_level<dense_index_py_t>);
1158
- i.def_property_readonly("levels_stats", &compute_stats<dense_index_py_t>);
1219
+ i.def_property_readonly("stats", &compute_stats<dense_index_py_t>);
1220
+ i.def_property_readonly("levels_stats", &compute_levels_stats<dense_index_py_t>);
1159
1221
  i.def("level_stats", &compute_level_stats<dense_index_py_t>, py::arg("level"));
1160
1222
 
1161
1223
  auto is = py::class_<dense_indexes_py_t>(m, "Indexes");
@@ -126,35 +126,63 @@ def test_index_stats(batch_size):
126
126
  index.add(keys, vectors, threads=threads)
127
127
 
128
128
  assert index.max_level >= 0
129
- assert index.levels_stats.nodes >= batch_size
129
+ assert index.stats.nodes >= batch_size
130
+ assert index.levels_stats[0].nodes == batch_size
130
131
  assert index.level_stats(0).nodes == batch_size
131
132
 
133
+ assert index.levels_stats[index.max_level].nodes > 0
134
+
132
135
 
133
136
  @pytest.mark.parametrize("ndim", [1, 3, 8, 32, 256, 4096])
134
- @pytest.mark.parametrize("batch_size", [1, 7, 1024])
137
+ @pytest.mark.parametrize("batch_size", [0, 1, 7, 1024])
135
138
  @pytest.mark.parametrize("quantization", [ScalarKind.F32, ScalarKind.I8])
136
139
  def test_index_save_load_restore_copy(ndim, quantization, batch_size):
137
140
  index = Index(ndim=ndim, dtype=quantization, multi=False)
138
- keys = np.arange(batch_size)
139
- vectors = random_vectors(count=batch_size, ndim=ndim)
140
- index.add(keys, vectors, threads=threads)
141
+
142
+ if batch_size > 0:
143
+ keys = np.arange(batch_size)
144
+ vectors = random_vectors(count=batch_size, ndim=ndim)
145
+ index.add(keys, vectors, threads=threads)
141
146
 
142
147
  index.save("tmp.usearch")
143
148
  index.clear()
144
149
  assert len(index) == 0
150
+ assert os.path.exists("tmp.usearch")
145
151
 
146
152
  index.load("tmp.usearch")
147
153
  assert len(index) == batch_size
148
- assert len(index[0].flatten()) == ndim
154
+ if batch_size > 0:
155
+ assert len(index[0].flatten()) == ndim
149
156
 
150
- index = Index.restore("tmp.usearch", view=True)
157
+ index_meta = Index.metadata("tmp.usearch")
158
+ assert index_meta is not None
159
+
160
+ index = Index.restore("tmp.usearch", view=False)
151
161
  assert len(index) == batch_size
152
- assert len(index[0].flatten()) == ndim
162
+ if batch_size > 0:
163
+ assert len(index[0].flatten()) == ndim
153
164
 
154
165
  copied_index = index.copy()
155
166
  assert len(copied_index) == len(index)
156
- assert np.allclose(np.vstack(copied_index.get(keys)), np.vstack(index.get(keys)))
157
-
167
+ if batch_size > 0:
168
+ assert np.allclose(
169
+ np.vstack(copied_index.get(keys)), np.vstack(index.get(keys))
170
+ )
171
+
172
+ # Perform the same operations in RAM, without touching the filesystem
173
+ serialized_index = index.save()
174
+ deserialized_metadata = Index.metadata(serialized_index)
175
+ assert deserialized_metadata is not None
176
+
177
+ deserialized_index = Index.restore(serialized_index)
178
+ assert len(deserialized_index) == len(index)
179
+ assert set(np.array(deserialized_index.keys)) == set(np.array(index.keys))
180
+ if batch_size > 0:
181
+ assert np.allclose(
182
+ np.vstack(deserialized_index.get(keys)), np.vstack(index.get(keys))
183
+ )
184
+
185
+ deserialized_index.reset()
158
186
  index.reset()
159
187
  os.remove("tmp.usearch")
160
188