usearch 0.19.3 → 0.21.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 +27 -0
- package/Cargo.toml +1 -1
- package/README.md +21 -2
- package/c/test.c +238 -126
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +6 -2
- package/cpp/bench.cpp +41 -4
- package/cpp/test.cpp +12 -12
- package/docs/benchmarks.md +14 -12
- package/include/usearch/index.hpp +603 -100
- package/include/usearch/index_punned_dense.hpp +68 -10
- package/include/usearch/index_punned_helpers.hpp +30 -3
- package/package.json +1 -1
- package/python/lib.cpp +57 -5
- package/python/scripts/join.py +151 -0
- package/python/scripts/test.py +19 -0
- package/python/usearch/eval.py +11 -3
- package/python/usearch/index.py +119 -10
- package/python/usearch/join.py +0 -98
|
@@ -162,6 +162,10 @@ template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num)
|
|
|
162
162
|
return (num + multiple_ak - 1) / multiple_ak;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
inline std::size_t divide_round_up(std::size_t num, std::size_t denominator) noexcept {
|
|
166
|
+
return (num + denominator - 1) / denominator;
|
|
167
|
+
}
|
|
168
|
+
|
|
165
169
|
inline std::size_t ceil2(std::size_t v) noexcept {
|
|
166
170
|
v--;
|
|
167
171
|
v |= v >> 1;
|
|
@@ -174,12 +178,16 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
174
178
|
return v;
|
|
175
179
|
}
|
|
176
180
|
|
|
177
|
-
template <typename at> void misaligned_store(void* ptr, at v) noexcept {
|
|
181
|
+
template <typename at> void misaligned_store(void* ptr, at v) noexcept {
|
|
182
|
+
static_assert(!std::is_reference<at>::value);
|
|
183
|
+
std::memcpy(ptr, &v, sizeof(at));
|
|
184
|
+
}
|
|
178
185
|
|
|
179
186
|
/// @brief Simply dereferencing misaligned pointers can be dangerous.
|
|
180
187
|
template <typename at> at misaligned_load(void* ptr) noexcept {
|
|
188
|
+
static_assert(!std::is_reference<at>::value);
|
|
181
189
|
at v;
|
|
182
|
-
std::memcpy(&v, ptr, sizeof(
|
|
190
|
+
std::memcpy(&v, ptr, sizeof(at));
|
|
183
191
|
return v;
|
|
184
192
|
}
|
|
185
193
|
|
|
@@ -250,10 +258,13 @@ template <typename scalar_at> class span_gt {
|
|
|
250
258
|
std::size_t size_;
|
|
251
259
|
|
|
252
260
|
public:
|
|
253
|
-
span_gt(
|
|
261
|
+
span_gt() noexcept : data_(nullptr), size_(0u) {}
|
|
262
|
+
span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
|
|
254
263
|
span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
|
|
255
264
|
scalar_at* data() const noexcept { return data_; }
|
|
256
265
|
std::size_t size() const noexcept { return size_; }
|
|
266
|
+
scalar_at* begin() const noexcept { return data_; }
|
|
267
|
+
scalar_at* end() const noexcept { return data_ + size_; }
|
|
257
268
|
operator scalar_at*() const noexcept { return data(); }
|
|
258
269
|
};
|
|
259
270
|
|
|
@@ -609,8 +620,8 @@ template <typename result_at> struct expected_gt {
|
|
|
609
620
|
}
|
|
610
621
|
result_at const& operator*() const noexcept { return result; }
|
|
611
622
|
explicit operator bool() const noexcept { return !error; }
|
|
612
|
-
expected_gt failed(
|
|
613
|
-
error = message;
|
|
623
|
+
expected_gt failed(error_t message) noexcept {
|
|
624
|
+
error = std::move(message);
|
|
614
625
|
return std::move(*this);
|
|
615
626
|
}
|
|
616
627
|
};
|
|
@@ -640,7 +651,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
640
651
|
|
|
641
652
|
void reset() noexcept {
|
|
642
653
|
if (slots_)
|
|
643
|
-
allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t))
|
|
654
|
+
allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t));
|
|
655
|
+
slots_ = nullptr;
|
|
656
|
+
count_ = 0;
|
|
644
657
|
}
|
|
645
658
|
|
|
646
659
|
bool resize(std::size_t capacity) noexcept {
|
|
@@ -661,10 +674,8 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
661
674
|
}
|
|
662
675
|
|
|
663
676
|
visits_bitset_gt(visits_bitset_gt&& other) noexcept {
|
|
664
|
-
slots_ = other.slots_;
|
|
665
|
-
count_ = other.count_;
|
|
666
|
-
other.slots_ = nullptr;
|
|
667
|
-
other.count_ = 0;
|
|
677
|
+
slots_ = exchange(other.slots_, nullptr);
|
|
678
|
+
count_ = exchange(other.count_, 0);
|
|
668
679
|
}
|
|
669
680
|
|
|
670
681
|
visits_bitset_gt& operator=(visits_bitset_gt&& other) noexcept {
|
|
@@ -673,6 +684,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
673
684
|
return *this;
|
|
674
685
|
}
|
|
675
686
|
|
|
687
|
+
visits_bitset_gt(visits_bitset_gt const&) = delete;
|
|
688
|
+
visits_bitset_gt& operator=(visits_bitset_gt const&) = delete;
|
|
689
|
+
|
|
676
690
|
inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
|
|
677
691
|
inline void set(std::size_t i) noexcept { slots_[i / bits_per_slot()] |= (1ul << (i & bits_mask())); }
|
|
678
692
|
|
|
@@ -911,17 +925,18 @@ class sorted_buffer_gt {
|
|
|
911
925
|
if (!new_elements)
|
|
912
926
|
return false;
|
|
913
927
|
|
|
914
|
-
if (size_)
|
|
928
|
+
if (size_)
|
|
915
929
|
std::memcpy(new_elements, elements_, size_ * sizeof(element_t));
|
|
930
|
+
if (elements_)
|
|
916
931
|
allocator.deallocate(elements_, capacity_);
|
|
917
|
-
|
|
932
|
+
|
|
918
933
|
elements_ = new_elements;
|
|
919
934
|
capacity_ = new_capacity;
|
|
920
935
|
return true;
|
|
921
936
|
}
|
|
922
937
|
|
|
923
938
|
inline void insert_reserved(element_t&& element) noexcept {
|
|
924
|
-
std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
|
|
939
|
+
std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
|
|
925
940
|
std::size_t to_move = size_ - slot;
|
|
926
941
|
element_t* source = elements_ + size_ - 1;
|
|
927
942
|
for (; to_move; --to_move, --source)
|
|
@@ -934,7 +949,7 @@ class sorted_buffer_gt {
|
|
|
934
949
|
* @return `true` if the entry was added, `false` if it wasn't relevant enough.
|
|
935
950
|
*/
|
|
936
951
|
inline bool insert(element_t&& element, std::size_t limit) noexcept {
|
|
937
|
-
std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
|
|
952
|
+
std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
|
|
938
953
|
if (slot == limit)
|
|
939
954
|
return false;
|
|
940
955
|
std::size_t to_move = size_ - slot - (size_ == limit);
|
|
@@ -975,10 +990,10 @@ class usearch_pack_m uint40_t {
|
|
|
975
990
|
|
|
976
991
|
public:
|
|
977
992
|
inline uint40_t() noexcept { std::memset(octets, 0, 5); }
|
|
978
|
-
inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets
|
|
979
|
-
inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n
|
|
993
|
+
inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets, (char*)&n, 4); }
|
|
994
|
+
inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
|
|
980
995
|
#if defined(USEARCH_DEFINED_CLANG) && defined(USEARCH_DEFINED_APPLE)
|
|
981
|
-
inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n
|
|
996
|
+
inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
|
|
982
997
|
#endif
|
|
983
998
|
|
|
984
999
|
uint40_t(uint40_t&&) = default;
|
|
@@ -986,24 +1001,40 @@ class usearch_pack_m uint40_t {
|
|
|
986
1001
|
uint40_t& operator=(uint40_t&&) = default;
|
|
987
1002
|
uint40_t& operator=(uint40_t const&) = default;
|
|
988
1003
|
|
|
989
|
-
inline uint40_t& operator+=(std::uint32_t
|
|
990
|
-
std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets
|
|
991
|
-
octets[
|
|
992
|
-
tail +=
|
|
1004
|
+
inline uint40_t& operator+=(std::uint32_t n) noexcept {
|
|
1005
|
+
std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
|
|
1006
|
+
octets[4] += static_cast<unsigned char>((tail + n) < tail);
|
|
1007
|
+
tail += n;
|
|
1008
|
+
return *this;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
inline uint40_t& operator+=(std::size_t n) noexcept {
|
|
1012
|
+
unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
|
|
1013
|
+
std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
|
|
1014
|
+
std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
|
|
1015
|
+
octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
|
|
1016
|
+
tail += n_tail;
|
|
1017
|
+
octets[4] += n_octets[4];
|
|
993
1018
|
return *this;
|
|
994
1019
|
}
|
|
995
1020
|
|
|
1021
|
+
inline uint40_t operator+(std::size_t n) noexcept {
|
|
1022
|
+
uint40_t other(*this);
|
|
1023
|
+
other += n;
|
|
1024
|
+
return other;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
996
1027
|
inline operator std::size_t() const noexcept {
|
|
997
1028
|
std::size_t result = 0;
|
|
998
|
-
std::memcpy((char*)&result
|
|
1029
|
+
std::memcpy((char*)&result, octets, 5);
|
|
999
1030
|
return result;
|
|
1000
1031
|
}
|
|
1001
1032
|
|
|
1002
|
-
inline uint40_t& operator++() noexcept { return *this +=
|
|
1033
|
+
inline uint40_t& operator++() noexcept { return *this += 1u; }
|
|
1003
1034
|
|
|
1004
1035
|
inline uint40_t operator++(int) noexcept {
|
|
1005
1036
|
uint40_t old = *this;
|
|
1006
|
-
*this +=
|
|
1037
|
+
*this += 1u;
|
|
1007
1038
|
return old;
|
|
1008
1039
|
}
|
|
1009
1040
|
};
|
|
@@ -1014,6 +1045,89 @@ class usearch_pack_m uint40_t {
|
|
|
1014
1045
|
|
|
1015
1046
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
1016
1047
|
|
|
1048
|
+
template <typename element_at, typename allocator_at = std::allocator<element_at>> //
|
|
1049
|
+
class ring_gt {
|
|
1050
|
+
public:
|
|
1051
|
+
using element_t = element_at;
|
|
1052
|
+
using allocator_t = allocator_at;
|
|
1053
|
+
|
|
1054
|
+
static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
|
|
1055
|
+
static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
|
|
1056
|
+
|
|
1057
|
+
using value_type = element_t;
|
|
1058
|
+
|
|
1059
|
+
private:
|
|
1060
|
+
element_t* elements_;
|
|
1061
|
+
std::size_t capacity_;
|
|
1062
|
+
std::size_t head_;
|
|
1063
|
+
std::size_t tail_;
|
|
1064
|
+
bool empty_;
|
|
1065
|
+
allocator_t allocator_;
|
|
1066
|
+
|
|
1067
|
+
public:
|
|
1068
|
+
explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept
|
|
1069
|
+
: elements_(nullptr), capacity_(0), head_(0), tail_(0), empty_(true), allocator_(alloc) {}
|
|
1070
|
+
|
|
1071
|
+
ring_gt(ring_gt const&) = delete;
|
|
1072
|
+
ring_gt& operator=(ring_gt const&) = delete;
|
|
1073
|
+
|
|
1074
|
+
~ring_gt() noexcept { reset(); }
|
|
1075
|
+
|
|
1076
|
+
bool empty() const noexcept { return empty_; }
|
|
1077
|
+
size_t size() const noexcept {
|
|
1078
|
+
if (empty_)
|
|
1079
|
+
return 0;
|
|
1080
|
+
else if (head_ >= tail_)
|
|
1081
|
+
return head_ - tail_;
|
|
1082
|
+
else
|
|
1083
|
+
return capacity_ - (tail_ - head_);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
void reset() noexcept {
|
|
1087
|
+
if (elements_)
|
|
1088
|
+
allocator_.deallocate(elements_, capacity_);
|
|
1089
|
+
elements_ = nullptr;
|
|
1090
|
+
capacity_ = 0;
|
|
1091
|
+
head_ = 0;
|
|
1092
|
+
tail_ = 0;
|
|
1093
|
+
empty_ = true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
bool resize(std::size_t n) noexcept {
|
|
1097
|
+
element_t* elements = allocator_.allocate(n);
|
|
1098
|
+
if (!elements)
|
|
1099
|
+
return false;
|
|
1100
|
+
reset();
|
|
1101
|
+
elements_ = elements;
|
|
1102
|
+
capacity_ = n;
|
|
1103
|
+
return true;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
void push(element_t const& value) noexcept {
|
|
1107
|
+
elements_[head_] = value;
|
|
1108
|
+
head_ = (head_ + 1) % capacity_;
|
|
1109
|
+
empty_ = false;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
bool try_push(element_t const& value) noexcept {
|
|
1113
|
+
if (head_ == tail_ && !empty_)
|
|
1114
|
+
return false; // elements_ is full
|
|
1115
|
+
|
|
1116
|
+
return push(value);
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
bool try_pop(element_t& value) noexcept {
|
|
1121
|
+
if (empty_)
|
|
1122
|
+
return false;
|
|
1123
|
+
|
|
1124
|
+
value = std::move(elements_[tail_]);
|
|
1125
|
+
tail_ = (tail_ + 1) % capacity_;
|
|
1126
|
+
empty_ = head_ == tail_;
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1017
1131
|
/// @brief Number of neighbors per graph node.
|
|
1018
1132
|
/// Defaults to 32 in FAISS and 16 in hnswlib.
|
|
1019
1133
|
/// > It is called `M` in the paper.
|
|
@@ -1095,6 +1209,21 @@ struct search_config_t {
|
|
|
1095
1209
|
bool exact = false;
|
|
1096
1210
|
};
|
|
1097
1211
|
|
|
1212
|
+
struct copy_config_t {
|
|
1213
|
+
bool copy_vectors = true;
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
struct join_config_t {
|
|
1217
|
+
/// @brief Controls maximum number of proposals per man during stable marriage.
|
|
1218
|
+
std::size_t max_proposals = 0;
|
|
1219
|
+
/// @brief Hyper-parameter controlling the quality of search.
|
|
1220
|
+
/// Defaults to 16 in FAISS and 10 in hnswlib.
|
|
1221
|
+
/// > It is called `ef` in the paper.
|
|
1222
|
+
std::size_t expansion = default_expansion_search();
|
|
1223
|
+
/// @brief Brute-forces exhaustive search over all entries in the index.
|
|
1224
|
+
bool exact = false;
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1098
1227
|
using file_header_t = byte_t[64];
|
|
1099
1228
|
|
|
1100
1229
|
/**
|
|
@@ -1199,8 +1328,8 @@ struct file_head_result_t {
|
|
|
1199
1328
|
error_t error;
|
|
1200
1329
|
|
|
1201
1330
|
explicit operator bool() const noexcept { return !error; }
|
|
1202
|
-
file_head_result_t failed(
|
|
1203
|
-
error = message;
|
|
1331
|
+
file_head_result_t failed(error_t message) noexcept {
|
|
1332
|
+
error = std::move(message);
|
|
1204
1333
|
return std::move(*this);
|
|
1205
1334
|
}
|
|
1206
1335
|
};
|
|
@@ -1234,6 +1363,37 @@ struct viewed_file_t {
|
|
|
1234
1363
|
};
|
|
1235
1364
|
#endif
|
|
1236
1365
|
|
|
1366
|
+
struct dummy_predicate_t {
|
|
1367
|
+
template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
struct dummy_progress_t {
|
|
1371
|
+
inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
struct dummy_executor_t {
|
|
1375
|
+
dummy_executor_t() noexcept {}
|
|
1376
|
+
std::size_t size() const noexcept { return 1; }
|
|
1377
|
+
|
|
1378
|
+
template <typename thread_aware_function_at>
|
|
1379
|
+
void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1380
|
+
for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
|
|
1381
|
+
thread_aware_function(0, task_idx);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
template <typename thread_aware_function_at>
|
|
1385
|
+
void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1386
|
+
thread_aware_function(0);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
struct dummy_label_to_label_mapping_t {
|
|
1391
|
+
struct member_ref_t {
|
|
1392
|
+
template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
|
|
1393
|
+
};
|
|
1394
|
+
template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
|
|
1395
|
+
};
|
|
1396
|
+
|
|
1237
1397
|
/**
|
|
1238
1398
|
* @brief Approximate Nearest Neighbors Search index using the
|
|
1239
1399
|
* Hierarchical Navigable Small World graph algorithm.
|
|
@@ -1317,11 +1477,13 @@ class index_gt {
|
|
|
1317
1477
|
struct member_ref_t {
|
|
1318
1478
|
misaligned_ref_gt<label_t> label;
|
|
1319
1479
|
vector_view_t vector;
|
|
1480
|
+
id_t id;
|
|
1320
1481
|
};
|
|
1321
1482
|
|
|
1322
1483
|
struct member_cref_t {
|
|
1323
1484
|
label_t label;
|
|
1324
1485
|
vector_view_t vector;
|
|
1486
|
+
id_t id;
|
|
1325
1487
|
};
|
|
1326
1488
|
|
|
1327
1489
|
template <typename ref_at, typename index_at> class member_iterator_gt {
|
|
@@ -1341,7 +1503,11 @@ class index_gt {
|
|
|
1341
1503
|
using pointer = void;
|
|
1342
1504
|
using reference = ref_t;
|
|
1343
1505
|
|
|
1344
|
-
reference operator*() const noexcept {
|
|
1506
|
+
reference operator*() const noexcept {
|
|
1507
|
+
node_t node = index_->node_with_id_(offset_);
|
|
1508
|
+
return {node.label(), node.vector_view(), static_cast<id_t>(offset_)};
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1345
1511
|
member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
|
|
1346
1512
|
member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
|
|
1347
1513
|
member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
|
|
@@ -1453,9 +1619,9 @@ class index_gt {
|
|
|
1453
1619
|
node_t(node_t const&) = default;
|
|
1454
1620
|
node_t& operator=(node_t const&) = default;
|
|
1455
1621
|
|
|
1456
|
-
label_t label() const noexcept { return
|
|
1457
|
-
dim_t dim() const noexcept { return
|
|
1458
|
-
level_t level() const noexcept { return
|
|
1622
|
+
misaligned_ref_gt<label_t> label() const noexcept { return {tape_}; }
|
|
1623
|
+
misaligned_ref_gt<dim_t> dim() const noexcept { return {tape_ + sizeof(label_t)}; }
|
|
1624
|
+
misaligned_ref_gt<level_t> level() const noexcept { return {tape_ + sizeof(label_t) + sizeof(dim_t)}; }
|
|
1459
1625
|
|
|
1460
1626
|
void label(label_t v) noexcept { return misaligned_store<label_t>(tape_, v); }
|
|
1461
1627
|
void dim(dim_t v) noexcept { return misaligned_store<dim_t>(tape_ + sizeof(label_t), v); }
|
|
@@ -1518,7 +1684,7 @@ class index_gt {
|
|
|
1518
1684
|
index_config_t config_{};
|
|
1519
1685
|
index_limits_t limits_{};
|
|
1520
1686
|
metric_t metric_{};
|
|
1521
|
-
allocator_t allocator_{};
|
|
1687
|
+
mutable allocator_t allocator_{};
|
|
1522
1688
|
point_allocator_t point_allocator_{};
|
|
1523
1689
|
precomputed_constants_t pre_{};
|
|
1524
1690
|
viewed_file_t viewed_file_{};
|
|
@@ -1572,6 +1738,35 @@ class index_gt {
|
|
|
1572
1738
|
return *this;
|
|
1573
1739
|
}
|
|
1574
1740
|
|
|
1741
|
+
struct copy_result_t {
|
|
1742
|
+
error_t error;
|
|
1743
|
+
index_gt index;
|
|
1744
|
+
|
|
1745
|
+
explicit operator bool() const noexcept { return !error; }
|
|
1746
|
+
copy_result_t failed(error_t message) noexcept {
|
|
1747
|
+
error = std::move(message);
|
|
1748
|
+
return std::move(*this);
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
|
|
1753
|
+
copy_result_t result;
|
|
1754
|
+
index_gt& other = result.index;
|
|
1755
|
+
other = index_gt(config_, metric_, allocator_, point_allocator_);
|
|
1756
|
+
if (!other.reserve(limits_))
|
|
1757
|
+
return result.failed("Failed to reserve the contexts");
|
|
1758
|
+
|
|
1759
|
+
// Now all is left - is to allocate new `node_t` instances and populate
|
|
1760
|
+
// the `other.nodes_` array into it.
|
|
1761
|
+
for (std::size_t i = 0; i != size_; ++i)
|
|
1762
|
+
other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
|
|
1763
|
+
|
|
1764
|
+
other.size_ = size_.load();
|
|
1765
|
+
other.max_level_ = max_level_;
|
|
1766
|
+
other.entry_id_ = entry_id_;
|
|
1767
|
+
return result;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1575
1770
|
member_citerator_t cbegin() const noexcept { return {this, 0}; }
|
|
1576
1771
|
member_citerator_t cend() const noexcept { return {this, size()}; }
|
|
1577
1772
|
member_citerator_t begin() const noexcept { return {this, 0}; }
|
|
@@ -1605,8 +1800,11 @@ class index_gt {
|
|
|
1605
1800
|
|
|
1606
1801
|
if (nodes_)
|
|
1607
1802
|
node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.elements);
|
|
1608
|
-
if (contexts_)
|
|
1803
|
+
if (contexts_) {
|
|
1804
|
+
for (std::size_t i = 0; i != limits_.threads(); ++i)
|
|
1805
|
+
contexts_[i].~context_t();
|
|
1609
1806
|
context_allocator_t{}.deallocate(exchange(contexts_, nullptr), limits_.threads());
|
|
1807
|
+
}
|
|
1610
1808
|
limits_ = index_limits_t{0, 0};
|
|
1611
1809
|
capacity_ = 0;
|
|
1612
1810
|
reset_view_();
|
|
@@ -1620,11 +1818,13 @@ class index_gt {
|
|
|
1620
1818
|
std::swap(limits_, other.limits_);
|
|
1621
1819
|
std::swap(metric_, other.metric_);
|
|
1622
1820
|
std::swap(allocator_, other.allocator_);
|
|
1821
|
+
std::swap(point_allocator_, other.point_allocator_);
|
|
1623
1822
|
std::swap(pre_, other.pre_);
|
|
1624
1823
|
std::swap(viewed_file_, other.viewed_file_);
|
|
1625
1824
|
std::swap(max_level_, other.max_level_);
|
|
1626
1825
|
std::swap(entry_id_, other.entry_id_);
|
|
1627
1826
|
std::swap(nodes_, other.nodes_);
|
|
1827
|
+
std::swap(nodes_mutexes_, other.nodes_mutexes_);
|
|
1628
1828
|
std::swap(contexts_, other.contexts_);
|
|
1629
1829
|
|
|
1630
1830
|
// Non-atomic parts.
|
|
@@ -1723,14 +1923,14 @@ class index_gt {
|
|
|
1723
1923
|
id_t id{};
|
|
1724
1924
|
|
|
1725
1925
|
explicit operator bool() const noexcept { return !error; }
|
|
1726
|
-
add_result_t failed(
|
|
1727
|
-
error = message;
|
|
1926
|
+
add_result_t failed(error_t message) noexcept {
|
|
1927
|
+
error = std::move(message);
|
|
1728
1928
|
return std::move(*this);
|
|
1729
1929
|
}
|
|
1730
1930
|
};
|
|
1731
1931
|
|
|
1732
1932
|
struct match_t {
|
|
1733
|
-
member_cref_t
|
|
1933
|
+
member_cref_t member;
|
|
1734
1934
|
distance_t distance;
|
|
1735
1935
|
};
|
|
1736
1936
|
|
|
@@ -1756,21 +1956,33 @@ class index_gt {
|
|
|
1756
1956
|
inline search_result_t& operator=(search_result_t&&) = default;
|
|
1757
1957
|
|
|
1758
1958
|
explicit operator bool() const noexcept { return !error; }
|
|
1759
|
-
search_result_t failed(
|
|
1760
|
-
error = message;
|
|
1959
|
+
search_result_t failed(error_t message) noexcept {
|
|
1960
|
+
error = std::move(message);
|
|
1761
1961
|
return std::move(*this);
|
|
1762
1962
|
}
|
|
1763
1963
|
|
|
1764
1964
|
inline operator std::size_t() const noexcept { return count; }
|
|
1765
1965
|
inline std::size_t size() const noexcept { return count; }
|
|
1766
|
-
inline
|
|
1966
|
+
inline bool empty() const noexcept { return !count; }
|
|
1967
|
+
inline match_t operator[](std::size_t i) const noexcept { return at(i); }
|
|
1968
|
+
inline match_t front() const noexcept { return at(0); }
|
|
1969
|
+
inline match_t back() const noexcept { return at(count - 1); }
|
|
1970
|
+
inline bool contains(label_t label) const noexcept {
|
|
1971
|
+
for (std::size_t i = 0; i != count; ++i)
|
|
1972
|
+
if (at(i).member.label == label)
|
|
1973
|
+
return true;
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
inline match_t at(std::size_t i) const noexcept {
|
|
1767
1977
|
candidate_t const* top_ordered = top_.data();
|
|
1768
|
-
|
|
1978
|
+
candidate_t candidate = top_ordered[i];
|
|
1979
|
+
node_t node = index_.node_with_id_(candidate.id);
|
|
1980
|
+
return {member_cref_t{node.label(), node.vector_view(), candidate.id}, candidate.distance};
|
|
1769
1981
|
}
|
|
1770
1982
|
inline std::size_t dump_to(label_t* labels, distance_t* distances) const noexcept {
|
|
1771
1983
|
for (std::size_t i = 0; i != count; ++i) {
|
|
1772
1984
|
match_t result = operator[](i);
|
|
1773
|
-
labels[i] = result.
|
|
1985
|
+
labels[i] = result.member.label;
|
|
1774
1986
|
distances[i] = result.distance;
|
|
1775
1987
|
}
|
|
1776
1988
|
return count;
|
|
@@ -1778,7 +1990,7 @@ class index_gt {
|
|
|
1778
1990
|
inline std::size_t dump_to(label_t* labels) const noexcept {
|
|
1779
1991
|
for (std::size_t i = 0; i != count; ++i) {
|
|
1780
1992
|
match_t result = operator[](i);
|
|
1781
|
-
labels[i] = result.
|
|
1993
|
+
labels[i] = result.member.label;
|
|
1782
1994
|
}
|
|
1783
1995
|
return count;
|
|
1784
1996
|
}
|
|
@@ -1820,7 +2032,7 @@ class index_gt {
|
|
|
1820
2032
|
new_level_lock.unlock();
|
|
1821
2033
|
|
|
1822
2034
|
// Allocate the neighbors
|
|
1823
|
-
node_t node =
|
|
2035
|
+
node_t node = node_make_(label, vector, target_level, config.store_vector);
|
|
1824
2036
|
if (!node)
|
|
1825
2037
|
return result.failed("Out of memory!");
|
|
1826
2038
|
std::size_t old_size = size_.fetch_add(1);
|
|
@@ -1849,6 +2061,7 @@ class index_gt {
|
|
|
1849
2061
|
// TODO: Handle out of memory conditions
|
|
1850
2062
|
search_to_insert_(closest_id, vector, level, config.expansion, context);
|
|
1851
2063
|
closest_id = connect_new_node_(new_id, level, context);
|
|
2064
|
+
reconnect_neighbor_nodes_(new_id, level, context);
|
|
1852
2065
|
}
|
|
1853
2066
|
|
|
1854
2067
|
// Normalize stats
|
|
@@ -1869,9 +2082,13 @@ class index_gt {
|
|
|
1869
2082
|
* @param[in] query Contiguous range of scalars forming a vector view.
|
|
1870
2083
|
* @param[in] wanted The upper bound for the number of results to return.
|
|
1871
2084
|
* @param[in] config Configuration options for this specific operation.
|
|
2085
|
+
* @param[in] predicate Optional filtering predicate for `member_cref_t`.
|
|
1872
2086
|
* @return Smart object referencing temporary memory. Valid until next `search()` or `add()`.
|
|
1873
2087
|
*/
|
|
1874
|
-
|
|
2088
|
+
template <typename predicate_at = dummy_predicate_t>
|
|
2089
|
+
search_result_t search( //
|
|
2090
|
+
vector_view_t query, std::size_t wanted, search_config_t config = {},
|
|
2091
|
+
predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
|
|
1875
2092
|
|
|
1876
2093
|
context_t& context = contexts_[config.thread];
|
|
1877
2094
|
top_candidates_t& top = context.top_candidates;
|
|
@@ -1886,7 +2103,7 @@ class index_gt {
|
|
|
1886
2103
|
if (config.exact) {
|
|
1887
2104
|
if (!top.reserve(wanted))
|
|
1888
2105
|
return result.failed("Out of memory!");
|
|
1889
|
-
search_exact_(query, wanted, context);
|
|
2106
|
+
search_exact_(query, wanted, context, std::forward<predicate_at>(predicate));
|
|
1890
2107
|
} else {
|
|
1891
2108
|
next_candidates_t& next = context.next_candidates;
|
|
1892
2109
|
std::size_t expansion = (std::max)(config.expansion, wanted);
|
|
@@ -1897,7 +2114,7 @@ class index_gt {
|
|
|
1897
2114
|
|
|
1898
2115
|
id_t closest_id = search_for_one_(entry_id_, query, max_level_, 0, context);
|
|
1899
2116
|
// For bottom layer we need a more optimized procedure
|
|
1900
|
-
if (!search_to_find_in_base_(closest_id, query, expansion, context))
|
|
2117
|
+
if (!search_to_find_in_base_(closest_id, query, expansion, context, std::forward<predicate_at>(predicate)))
|
|
1901
2118
|
return result.failed("Out of memory!");
|
|
1902
2119
|
}
|
|
1903
2120
|
|
|
@@ -1911,9 +2128,10 @@ class index_gt {
|
|
|
1911
2128
|
return result;
|
|
1912
2129
|
}
|
|
1913
2130
|
|
|
2131
|
+
template <typename predicate_at = dummy_predicate_t>
|
|
1914
2132
|
search_result_t search_around( //
|
|
1915
2133
|
id_t hint, vector_view_t query, std::size_t wanted, //
|
|
1916
|
-
search_config_t config = {}) const noexcept {
|
|
2134
|
+
search_config_t config = {}, predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
|
|
1917
2135
|
|
|
1918
2136
|
context_t& context = contexts_[config.thread];
|
|
1919
2137
|
top_candidates_t& top = context.top_candidates;
|
|
@@ -1933,7 +2151,7 @@ class index_gt {
|
|
|
1933
2151
|
result.measurements = context.measurements_count;
|
|
1934
2152
|
result.cycles = context.iteration_cycles;
|
|
1935
2153
|
|
|
1936
|
-
search_to_find_in_base_(hint, query, expansion, context);
|
|
2154
|
+
search_to_find_in_base_(hint, query, expansion, context, std::forward<predicate_at>(predicate));
|
|
1937
2155
|
top.sort_ascending();
|
|
1938
2156
|
top.shrink(wanted);
|
|
1939
2157
|
|
|
@@ -1944,6 +2162,10 @@ class index_gt {
|
|
|
1944
2162
|
return result;
|
|
1945
2163
|
}
|
|
1946
2164
|
|
|
2165
|
+
#pragma endregion
|
|
2166
|
+
|
|
2167
|
+
#pragma region Metadata
|
|
2168
|
+
|
|
1947
2169
|
struct stats_t {
|
|
1948
2170
|
std::size_t nodes;
|
|
1949
2171
|
std::size_t edges;
|
|
@@ -1958,7 +2180,7 @@ class index_gt {
|
|
|
1958
2180
|
node_t node = node_with_id_(i);
|
|
1959
2181
|
std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
|
|
1960
2182
|
std::size_t edges = 0;
|
|
1961
|
-
for (level_t level = 0; level
|
|
2183
|
+
for (level_t level = 0; level <= node.level(); ++level)
|
|
1962
2184
|
edges += neighbors_(node, level).size();
|
|
1963
2185
|
|
|
1964
2186
|
result.allocated_bytes += node_bytes_(node);
|
|
@@ -2007,6 +2229,8 @@ class index_gt {
|
|
|
2007
2229
|
return total;
|
|
2008
2230
|
}
|
|
2009
2231
|
|
|
2232
|
+
std::size_t memory_usage_per_node(dim_t dim, level_t level) const noexcept { return node_bytes_(dim, level); }
|
|
2233
|
+
|
|
2010
2234
|
void change_metric(metric_t const& m) noexcept {
|
|
2011
2235
|
metric_ = m;
|
|
2012
2236
|
for (std::size_t i = 0; i != limits_.threads(); ++i)
|
|
@@ -2021,8 +2245,8 @@ class index_gt {
|
|
|
2021
2245
|
error_t error;
|
|
2022
2246
|
|
|
2023
2247
|
explicit operator bool() const noexcept { return !error; }
|
|
2024
|
-
serialization_result_t failed(
|
|
2025
|
-
error = message;
|
|
2248
|
+
serialization_result_t failed(error_t message) noexcept {
|
|
2249
|
+
error = std::move(message);
|
|
2026
2250
|
return std::move(*this);
|
|
2027
2251
|
}
|
|
2028
2252
|
};
|
|
@@ -2032,7 +2256,8 @@ class index_gt {
|
|
|
2032
2256
|
* co-locating vectors and neighbors lists.
|
|
2033
2257
|
* Available on Linux, MacOS, Windows.
|
|
2034
2258
|
*/
|
|
2035
|
-
|
|
2259
|
+
template <typename progress_at = dummy_progress_t>
|
|
2260
|
+
serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
|
|
2036
2261
|
|
|
2037
2262
|
// Make sure we have right to write to that file
|
|
2038
2263
|
serialization_result_t result;
|
|
@@ -2102,6 +2327,7 @@ class index_gt {
|
|
|
2102
2327
|
write_chunk(node.vector(), node_vector_bytes);
|
|
2103
2328
|
if (result.error)
|
|
2104
2329
|
return result;
|
|
2330
|
+
progress(i, state.size);
|
|
2105
2331
|
}
|
|
2106
2332
|
|
|
2107
2333
|
std::fclose(file);
|
|
@@ -2113,7 +2339,8 @@ class index_gt {
|
|
|
2113
2339
|
* copying both vectors and neighbors lists into RAM.
|
|
2114
2340
|
* Available on Linux, MacOS, Windows.
|
|
2115
2341
|
*/
|
|
2116
|
-
|
|
2342
|
+
template <typename progress_at = dummy_progress_t>
|
|
2343
|
+
serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2117
2344
|
|
|
2118
2345
|
serialization_result_t result;
|
|
2119
2346
|
file_header_t state_buffer{};
|
|
@@ -2161,7 +2388,8 @@ class index_gt {
|
|
|
2161
2388
|
}
|
|
2162
2389
|
|
|
2163
2390
|
// Load nodes one by one
|
|
2164
|
-
|
|
2391
|
+
std::size_t const size = size_;
|
|
2392
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2165
2393
|
label_t label;
|
|
2166
2394
|
dim_t dim;
|
|
2167
2395
|
level_t level;
|
|
@@ -2176,11 +2404,15 @@ class index_gt {
|
|
|
2176
2404
|
return result;
|
|
2177
2405
|
|
|
2178
2406
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2179
|
-
node_t node = node_malloc_(
|
|
2407
|
+
node_t node = node_malloc_(dim, level);
|
|
2408
|
+
node.label(label);
|
|
2409
|
+
node.dim(dim);
|
|
2410
|
+
node.level(level);
|
|
2180
2411
|
read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
|
|
2181
2412
|
if (result.error)
|
|
2182
2413
|
return result;
|
|
2183
2414
|
nodes_[i] = node;
|
|
2415
|
+
progress(i, size);
|
|
2184
2416
|
}
|
|
2185
2417
|
|
|
2186
2418
|
std::fclose(file);
|
|
@@ -2193,7 +2425,8 @@ class index_gt {
|
|
|
2193
2425
|
* @b without copying the vectors and neighbors lists into RAM.
|
|
2194
2426
|
* Available on Linux, MacOS, but @b not on Windows.
|
|
2195
2427
|
*/
|
|
2196
|
-
|
|
2428
|
+
template <typename progress_at = dummy_progress_t>
|
|
2429
|
+
serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2197
2430
|
serialization_result_t result;
|
|
2198
2431
|
|
|
2199
2432
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
@@ -2275,17 +2508,18 @@ class index_gt {
|
|
|
2275
2508
|
}
|
|
2276
2509
|
|
|
2277
2510
|
// Locate every node packed into file
|
|
2278
|
-
std::size_t
|
|
2279
|
-
|
|
2280
|
-
|
|
2511
|
+
std::size_t progress_bytes = sizeof(file_header_t);
|
|
2512
|
+
std::size_t const size = size_;
|
|
2513
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2514
|
+
byte_t* tape = (byte_t*)(file + progress_bytes);
|
|
2281
2515
|
dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
|
|
2282
2516
|
level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
|
|
2283
2517
|
|
|
2284
2518
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2285
2519
|
std::size_t node_vector_bytes = dim * sizeof(scalar_t);
|
|
2286
2520
|
nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
|
|
2287
|
-
|
|
2288
|
-
|
|
2521
|
+
progress_bytes += node_bytes;
|
|
2522
|
+
progress(i, size);
|
|
2289
2523
|
}
|
|
2290
2524
|
|
|
2291
2525
|
return {};
|
|
@@ -2293,7 +2527,224 @@ class index_gt {
|
|
|
2293
2527
|
|
|
2294
2528
|
#pragma endregion
|
|
2295
2529
|
|
|
2530
|
+
struct join_result_t {
|
|
2531
|
+
error_t error{};
|
|
2532
|
+
std::size_t intersection_size{};
|
|
2533
|
+
std::size_t engagements{};
|
|
2534
|
+
std::size_t cycles{};
|
|
2535
|
+
std::size_t measurements{};
|
|
2536
|
+
|
|
2537
|
+
explicit operator bool() const noexcept { return !error; }
|
|
2538
|
+
join_result_t failed(error_t message) noexcept {
|
|
2539
|
+
error = std::move(message);
|
|
2540
|
+
return std::move(*this);
|
|
2541
|
+
}
|
|
2542
|
+
};
|
|
2543
|
+
|
|
2544
|
+
/**
|
|
2545
|
+
* @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
|
|
2546
|
+
* to perform fast one-to-one matching between two large collections
|
|
2547
|
+
* of vectors, using approximate nearest neighbors search.
|
|
2548
|
+
*/
|
|
2549
|
+
template < //
|
|
2550
|
+
typename first_to_second_at = dummy_label_to_label_mapping_t, //
|
|
2551
|
+
typename second_to_first_at = dummy_label_to_label_mapping_t, //
|
|
2552
|
+
typename executor_at = dummy_executor_t, //
|
|
2553
|
+
typename progress_at = dummy_progress_t //
|
|
2554
|
+
>
|
|
2555
|
+
static join_result_t join( //
|
|
2556
|
+
index_gt const& first, index_gt const& second, //
|
|
2557
|
+
join_config_t config = {}, //
|
|
2558
|
+
first_to_second_at&& first_to_second = first_to_second_at{}, //
|
|
2559
|
+
second_to_first_at&& second_to_first = second_to_first_at{}, //
|
|
2560
|
+
executor_at&& executor = executor_at{}, //
|
|
2561
|
+
progress_at&& progress = progress_at{}) noexcept {
|
|
2562
|
+
|
|
2563
|
+
if (second.size() < first.size())
|
|
2564
|
+
return join_small_and_big_( //
|
|
2565
|
+
second, first, //
|
|
2566
|
+
config, //
|
|
2567
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2568
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2569
|
+
std::forward<executor_at>(executor), //
|
|
2570
|
+
std::forward<progress_at>(progress));
|
|
2571
|
+
else
|
|
2572
|
+
return join_small_and_big_( //
|
|
2573
|
+
first, second, //
|
|
2574
|
+
config, //
|
|
2575
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2576
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2577
|
+
std::forward<executor_at>(executor), //
|
|
2578
|
+
std::forward<progress_at>(progress));
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2296
2581
|
private:
|
|
2582
|
+
template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
|
|
2583
|
+
static join_result_t join_small_and_big_( //
|
|
2584
|
+
index_gt const& men, index_gt const& women, //
|
|
2585
|
+
join_config_t config, //
|
|
2586
|
+
first_to_second_at&& man_to_woman, //
|
|
2587
|
+
second_to_first_at&& woman_to_man, //
|
|
2588
|
+
executor_at&& executor, //
|
|
2589
|
+
progress_at&& progress) noexcept {
|
|
2590
|
+
|
|
2591
|
+
join_result_t result;
|
|
2592
|
+
|
|
2593
|
+
// Sanity checks and argument validation:
|
|
2594
|
+
if (&men == &women)
|
|
2595
|
+
return result.failed("Can't join with itself, consider copying");
|
|
2596
|
+
if (config.max_proposals == 0)
|
|
2597
|
+
config.max_proposals = std::log(men.size()) + executor.size();
|
|
2598
|
+
config.max_proposals = (std::min)(men.size(), config.max_proposals);
|
|
2599
|
+
|
|
2600
|
+
using proposals_count_t = std::uint16_t;
|
|
2601
|
+
using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
|
|
2602
|
+
allocator_t& alloc = men.allocator_;
|
|
2603
|
+
|
|
2604
|
+
// Create an atomic queue, as a ring structure, from/to which
|
|
2605
|
+
// free men will be added/pulled.
|
|
2606
|
+
std::mutex free_men_mutex{};
|
|
2607
|
+
ring_gt<id_t, ids_allocator_t> free_men;
|
|
2608
|
+
free_men.resize(men.size());
|
|
2609
|
+
for (std::size_t i = 0; i != men.size(); ++i)
|
|
2610
|
+
free_men.push(static_cast<id_t>(i));
|
|
2611
|
+
|
|
2612
|
+
// We are gonna need some temporary memory.
|
|
2613
|
+
proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
|
|
2614
|
+
id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
|
|
2615
|
+
id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
|
|
2616
|
+
if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
|
|
2617
|
+
return result.failed("Can't temporary mappings");
|
|
2618
|
+
|
|
2619
|
+
id_t missing_id;
|
|
2620
|
+
std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
|
|
2621
|
+
std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
|
|
2622
|
+
std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
|
|
2623
|
+
std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
|
|
2624
|
+
|
|
2625
|
+
// Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
|
|
2626
|
+
visits_bitset_t men_locks, women_locks;
|
|
2627
|
+
if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
|
|
2628
|
+
return result.failed("Can't allocate locks");
|
|
2629
|
+
|
|
2630
|
+
// Accumulate statistics from all the contexts,
|
|
2631
|
+
// to have a baseline to compare with, by the time the `join` is finished.
|
|
2632
|
+
std::size_t old_measurements{};
|
|
2633
|
+
std::size_t old_cycles{};
|
|
2634
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2635
|
+
old_measurements += women.contexts_[thread_idx].measurements_count;
|
|
2636
|
+
old_cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2637
|
+
}
|
|
2638
|
+
std::atomic<std::size_t> rounds{0};
|
|
2639
|
+
std::atomic<std::size_t> engagements{0};
|
|
2640
|
+
|
|
2641
|
+
// Concurrently process all the men
|
|
2642
|
+
executor.execute_bulk([&](std::size_t thread_idx) {
|
|
2643
|
+
context_t& context = women.contexts_[thread_idx];
|
|
2644
|
+
search_config_t search_config;
|
|
2645
|
+
search_config.expansion = config.expansion;
|
|
2646
|
+
search_config.exact = config.exact;
|
|
2647
|
+
search_config.thread = thread_idx;
|
|
2648
|
+
id_t free_man_id;
|
|
2649
|
+
|
|
2650
|
+
// While there exist a free man who still has a woman to propose to.
|
|
2651
|
+
while (true) {
|
|
2652
|
+
std::size_t passed_rounds = 0;
|
|
2653
|
+
std::size_t total_rounds = 0;
|
|
2654
|
+
{
|
|
2655
|
+
std::unique_lock<std::mutex> pop_lock(free_men_mutex);
|
|
2656
|
+
if (!free_men.try_pop(free_man_id))
|
|
2657
|
+
// Primary exit path, we have exhausted the list of candidates
|
|
2658
|
+
break;
|
|
2659
|
+
passed_rounds = ++rounds;
|
|
2660
|
+
total_rounds = passed_rounds + free_men.size();
|
|
2661
|
+
}
|
|
2662
|
+
progress(passed_rounds, total_rounds);
|
|
2663
|
+
while (men_locks.atomic_set(free_man_id))
|
|
2664
|
+
;
|
|
2665
|
+
|
|
2666
|
+
node_t free_man = men.node_with_id_(free_man_id);
|
|
2667
|
+
proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
|
|
2668
|
+
if (free_man_proposals >= config.max_proposals)
|
|
2669
|
+
continue;
|
|
2670
|
+
|
|
2671
|
+
// Find the closest woman, to whom this man hasn't proposed yet.
|
|
2672
|
+
free_man_proposals++;
|
|
2673
|
+
search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
|
|
2674
|
+
if (!candidates) {
|
|
2675
|
+
// TODO:
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
match_t match = candidates.back();
|
|
2679
|
+
member_cref_t woman = match.member;
|
|
2680
|
+
while (women_locks.atomic_set(woman.id))
|
|
2681
|
+
;
|
|
2682
|
+
|
|
2683
|
+
id_t husband_id = woman_to_man_ids[woman.id];
|
|
2684
|
+
bool woman_is_free = husband_id == missing_id;
|
|
2685
|
+
if (woman_is_free) {
|
|
2686
|
+
// Engagement
|
|
2687
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2688
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2689
|
+
engagements++;
|
|
2690
|
+
} else {
|
|
2691
|
+
distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
|
|
2692
|
+
distance_t distance_from_candidate = match.distance;
|
|
2693
|
+
if (distance_from_husband > distance_from_candidate) {
|
|
2694
|
+
// Break-up
|
|
2695
|
+
while (men_locks.atomic_set(husband_id))
|
|
2696
|
+
;
|
|
2697
|
+
man_to_woman_ids[husband_id] = missing_id;
|
|
2698
|
+
men_locks.atomic_reset(husband_id);
|
|
2699
|
+
|
|
2700
|
+
// New Engagement
|
|
2701
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2702
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2703
|
+
engagements++;
|
|
2704
|
+
|
|
2705
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2706
|
+
free_men.push(husband_id);
|
|
2707
|
+
} else {
|
|
2708
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2709
|
+
free_men.push(free_man_id);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
men_locks.atomic_reset(free_man_id);
|
|
2714
|
+
women_locks.atomic_reset(woman.id);
|
|
2715
|
+
}
|
|
2716
|
+
});
|
|
2717
|
+
|
|
2718
|
+
// Export the IDs into labels:
|
|
2719
|
+
std::size_t intersection_size = 0;
|
|
2720
|
+
for (std::size_t i = 0; i != men.size(); ++i) {
|
|
2721
|
+
id_t woman_id = man_to_woman_ids[i];
|
|
2722
|
+
if (woman_id != missing_id) {
|
|
2723
|
+
label_t man = men.node_with_id_(i).label();
|
|
2724
|
+
label_t woman = women.node_with_id_(woman_id).label();
|
|
2725
|
+
man_to_woman[man] = woman;
|
|
2726
|
+
woman_to_man[woman] = man;
|
|
2727
|
+
intersection_size++;
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Deallocate memory
|
|
2732
|
+
alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
|
|
2733
|
+
alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
|
|
2734
|
+
alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
|
|
2735
|
+
|
|
2736
|
+
// Export stats
|
|
2737
|
+
result.engagements = engagements;
|
|
2738
|
+
result.intersection_size = intersection_size;
|
|
2739
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2740
|
+
result.measurements += women.contexts_[thread_idx].measurements_count;
|
|
2741
|
+
result.cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2742
|
+
}
|
|
2743
|
+
result.measurements -= old_measurements;
|
|
2744
|
+
result.cycles -= old_cycles;
|
|
2745
|
+
return result;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2297
2748
|
void reset_view_() noexcept {
|
|
2298
2749
|
if (!viewed_file_)
|
|
2299
2750
|
return;
|
|
@@ -2323,51 +2774,80 @@ class index_gt {
|
|
|
2323
2774
|
return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
|
|
2324
2775
|
}
|
|
2325
2776
|
|
|
2326
|
-
|
|
2777
|
+
using span_bytes_t = span_gt<byte_t>;
|
|
2778
|
+
struct node_bytes_split_t {
|
|
2779
|
+
span_bytes_t tape{};
|
|
2780
|
+
span_bytes_t vector{};
|
|
2781
|
+
|
|
2782
|
+
node_bytes_split_t() {}
|
|
2783
|
+
node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
|
|
2784
|
+
|
|
2785
|
+
std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
|
|
2786
|
+
bool colocated() const noexcept { return tape.end() == vector.begin(); }
|
|
2787
|
+
operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
|
|
2327
2791
|
std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
|
|
2328
|
-
|
|
2792
|
+
std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
|
|
2793
|
+
return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
|
|
2329
2794
|
}
|
|
2330
2795
|
|
|
2331
2796
|
inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
|
|
2332
2797
|
inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
|
|
2333
2798
|
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
if (viewed_file_)
|
|
2337
|
-
return;
|
|
2338
|
-
|
|
2339
|
-
node_t& node = nodes_[id];
|
|
2340
|
-
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
|
|
2341
|
-
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2342
|
-
node = node_t{};
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2799
|
+
node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
|
|
2346
2800
|
|
|
2347
|
-
std::size_t
|
|
2348
|
-
std::size_t
|
|
2349
|
-
std::size_t
|
|
2350
|
-
node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
|
|
2801
|
+
std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
|
|
2802
|
+
std::size_t node_bytes = node_bytes_(dims_to_store, level);
|
|
2803
|
+
std::size_t non_vector_bytes = node_bytes - vector_bytes;
|
|
2351
2804
|
|
|
2352
2805
|
byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
|
|
2353
2806
|
if (!data)
|
|
2354
|
-
return {};
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
if (vector.data())
|
|
2358
|
-
std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
|
|
2359
|
-
|
|
2360
|
-
scalar_t* scalars = store_vector //
|
|
2361
|
-
? (scalar_t*)(data + node_bytes - stored_vector_bytes)
|
|
2362
|
-
: (scalar_t*)(vector.data());
|
|
2807
|
+
return node_bytes_split_t{};
|
|
2808
|
+
return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
|
|
2809
|
+
}
|
|
2363
2810
|
|
|
2364
|
-
|
|
2811
|
+
node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2812
|
+
node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
|
|
2813
|
+
if (store_vector) {
|
|
2814
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
|
|
2815
|
+
std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
|
|
2816
|
+
} else {
|
|
2817
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
|
|
2818
|
+
}
|
|
2819
|
+
node_t node = node_bytes;
|
|
2365
2820
|
node.label(label);
|
|
2366
|
-
node.dim(static_cast<dim_t>(
|
|
2821
|
+
node.dim(static_cast<dim_t>(vector.size()));
|
|
2367
2822
|
node.level(level);
|
|
2368
2823
|
return node;
|
|
2369
2824
|
}
|
|
2370
2825
|
|
|
2826
|
+
node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
|
|
2827
|
+
if (old_bytes.colocated()) {
|
|
2828
|
+
byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
|
|
2829
|
+
std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
|
|
2830
|
+
return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
|
|
2831
|
+
} else {
|
|
2832
|
+
node_t old_node = old_bytes;
|
|
2833
|
+
node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
|
|
2834
|
+
std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
|
|
2835
|
+
std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
|
|
2836
|
+
return node_bytes;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
void node_free_(std::size_t id) noexcept {
|
|
2841
|
+
|
|
2842
|
+
if (viewed_file_)
|
|
2843
|
+
return;
|
|
2844
|
+
|
|
2845
|
+
node_t& node = nodes_[id];
|
|
2846
|
+
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
|
|
2847
|
+
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2848
|
+
node = node_t{};
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2371
2851
|
inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
|
|
2372
2852
|
inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
|
|
2373
2853
|
|
|
@@ -2396,7 +2876,6 @@ class index_gt {
|
|
|
2396
2876
|
|
|
2397
2877
|
node_t new_node = node_with_id_(new_id);
|
|
2398
2878
|
top_candidates_t& top = context.top_candidates;
|
|
2399
|
-
std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
|
|
2400
2879
|
|
|
2401
2880
|
// Outgoing links from `new_id`:
|
|
2402
2881
|
neighbors_ref_t new_neighbors = neighbors_(new_node, level);
|
|
@@ -2411,7 +2890,17 @@ class index_gt {
|
|
|
2411
2890
|
}
|
|
2412
2891
|
}
|
|
2413
2892
|
|
|
2893
|
+
return new_neighbors[0];
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
void reconnect_neighbor_nodes_(id_t new_id, level_t level, context_t& context) usearch_noexcept_m {
|
|
2897
|
+
|
|
2898
|
+
node_t new_node = node_with_id_(new_id);
|
|
2899
|
+
top_candidates_t& top = context.top_candidates;
|
|
2900
|
+
neighbors_ref_t new_neighbors = neighbors_(new_node, level);
|
|
2901
|
+
|
|
2414
2902
|
// Reverse links from the neighbors:
|
|
2903
|
+
std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
|
|
2415
2904
|
for (id_t close_id : new_neighbors) {
|
|
2416
2905
|
node_t close_node = node_with_id_(close_id);
|
|
2417
2906
|
node_lock_t close_lock = node_lock_(close_id);
|
|
@@ -2441,8 +2930,6 @@ class index_gt {
|
|
|
2441
2930
|
for (std::size_t idx = 0; idx != top_view.size(); idx++)
|
|
2442
2931
|
close_header.push_back(top_view[idx].id);
|
|
2443
2932
|
}
|
|
2444
|
-
|
|
2445
|
-
return new_neighbors[0];
|
|
2446
2933
|
}
|
|
2447
2934
|
|
|
2448
2935
|
level_t choose_random_level_(std::default_random_engine& level_generator) const noexcept {
|
|
@@ -2538,8 +3025,10 @@ class index_gt {
|
|
|
2538
3025
|
* Doesn't lock any nodes, assuming read-only simultaneous access.
|
|
2539
3026
|
* @return `true` if procedure succeeded, `false` if run out of memory.
|
|
2540
3027
|
*/
|
|
3028
|
+
template <typename predicate_at>
|
|
2541
3029
|
bool search_to_find_in_base_( //
|
|
2542
|
-
id_t start_id, vector_view_t query, std::size_t expansion, context_t& context
|
|
3030
|
+
id_t start_id, vector_view_t query, std::size_t expansion, context_t& context,
|
|
3031
|
+
predicate_at&& predicate) const noexcept {
|
|
2543
3032
|
|
|
2544
3033
|
visits_bitset_t& visits = context.visits;
|
|
2545
3034
|
next_candidates_t& next = context.next_candidates; // pop min, push
|
|
@@ -2573,14 +3062,19 @@ class index_gt {
|
|
|
2573
3062
|
continue;
|
|
2574
3063
|
|
|
2575
3064
|
visits.set(successor_id);
|
|
2576
|
-
|
|
3065
|
+
node_t successor = node_with_id_(successor_id);
|
|
3066
|
+
distance_t successor_dist = context.measure(query, successor);
|
|
2577
3067
|
|
|
2578
3068
|
if (top.size() < top_limit || successor_dist < radius) {
|
|
2579
3069
|
// This can substantially grow our priority queue:
|
|
2580
3070
|
next.insert({-successor_dist, successor_id});
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
3071
|
+
if (predicate( //
|
|
3072
|
+
match_t{member_cref_t{successor.label(), successor.vector_view(), successor_id},
|
|
3073
|
+
successor_dist})) {
|
|
3074
|
+
// This will automatically evict poor matches:
|
|
3075
|
+
top.insert({successor_dist, successor_id}, top_limit);
|
|
3076
|
+
radius = top.top().distance;
|
|
3077
|
+
}
|
|
2584
3078
|
}
|
|
2585
3079
|
}
|
|
2586
3080
|
}
|
|
@@ -2591,12 +3085,21 @@ class index_gt {
|
|
|
2591
3085
|
/**
|
|
2592
3086
|
* @brief Iterates through all managed vectors, without actually touching the index.
|
|
2593
3087
|
*/
|
|
2594
|
-
|
|
3088
|
+
template <typename predicate_at>
|
|
3089
|
+
void search_exact_( //
|
|
3090
|
+
vector_view_t query, std::size_t count, context_t& context, //
|
|
3091
|
+
predicate_at&& predicate) const noexcept {
|
|
3092
|
+
|
|
2595
3093
|
top_candidates_t& top = context.top_candidates;
|
|
2596
3094
|
top.clear();
|
|
2597
3095
|
top.reserve(count);
|
|
2598
|
-
for (std::size_t i = 0; i != size(); ++i)
|
|
2599
|
-
|
|
3096
|
+
for (std::size_t i = 0; i != size(); ++i) {
|
|
3097
|
+
id_t id = static_cast<id_t>(i);
|
|
3098
|
+
node_t node = node_with_id_(i);
|
|
3099
|
+
distance_t distance = context.measure(query, node);
|
|
3100
|
+
if (predicate(match_t{member_cref_t{node.label(), node.vector_view(), id}, distance}))
|
|
3101
|
+
top.insert(candidate_t{distance, id}, count);
|
|
3102
|
+
}
|
|
2600
3103
|
}
|
|
2601
3104
|
|
|
2602
3105
|
void prefetch_neighbors_(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|