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.
- package/.github/workflows/prerelease.yml +71 -0
- package/.github/workflows/release.yml +77 -2
- package/.github/workflows/update_version.sh +1 -0
- package/.gitignore +11 -1
- package/.vscode/settings.json +5 -1
- package/CMakeLists.txt +1 -0
- package/Cargo.toml +1 -1
- package/README.md +31 -18
- package/c/lib.cpp +38 -4
- package/c/test.c +117 -114
- package/c/usearch.h +28 -1
- package/conanfile.py +1 -1
- package/cpp/test.cpp +2 -2
- package/csharp/.editorconfig +361 -0
- package/csharp/Cloud.Unum.USearch.sln +34 -0
- package/csharp/DEV_SETUP.md +51 -0
- package/csharp/Directory.Build.props +29 -0
- package/csharp/README.md +77 -0
- package/csharp/build_and_test.cmd +31 -0
- package/csharp/build_and_test.sh +30 -0
- package/csharp/eng/build_lib.sh +65 -0
- package/csharp/eng/clean.sh +12 -0
- package/csharp/nuget/NUGET.md +11 -0
- package/csharp/nuget/nuget-package.props +33 -0
- package/csharp/pack.sh +56 -0
- package/csharp/src/Cloud.Unum.USearch.Tests/Cloud.Unum.USearch.Tests.csproj +39 -0
- package/csharp/src/Cloud.Unum.USearch.Tests/InitTests.cs +306 -0
- package/csharp/src/Cloud.Unum.USearch.Tests/Usings.cs +1 -0
- package/docs/benchmarks.md +6 -6
- package/docs/compilation.md +1 -1
- package/include/usearch/index.hpp +172 -52
- package/include/usearch/index_dense.hpp +201 -64
- package/include/usearch/index_plugins.hpp +90 -69
- package/package.json +2 -2
- package/python/lib.cpp +99 -37
- package/python/scripts/test_index.py +38 -10
- package/python/usearch/index.py +87 -37
- package/rust/README.md +2 -2
- package/rust/lib.cpp +42 -19
- package/rust/lib.hpp +14 -0
- package/rust/lib.rs +85 -43
- 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,
|
|
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(
|
|
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
|
-
|
|
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
|
|
769
|
-
std::size_t new_cap = last_capacity_ * capacity_multiplier();
|
|
770
|
-
|
|
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<
|
|
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 =
|
|
1216
|
+
using scalar_t = i8_t;
|
|
1195
1217
|
using result_t = f32_t;
|
|
1196
1218
|
|
|
1197
|
-
inline result_t operator()(
|
|
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 =
|
|
1240
|
+
using scalar_t = i8_t;
|
|
1219
1241
|
using result_t = f32_t;
|
|
1220
1242
|
|
|
1221
|
-
inline result_t operator()(
|
|
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<
|
|
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": "
|
|
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.
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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<
|
|
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
|
|
764
|
-
template <typename index_at> void
|
|
765
|
-
template <typename index_at> void
|
|
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<
|
|
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("
|
|
869
|
-
index_dense_metadata_result_t meta =
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
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("
|
|
1142
|
-
i.def("
|
|
1143
|
-
i.def("
|
|
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", ©_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("
|
|
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.
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
154
|
+
if batch_size > 0:
|
|
155
|
+
assert len(index[0].flatten()) == ndim
|
|
149
156
|
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|