usearch 0.20.0 → 0.22.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 +46 -0
- package/.github/workflows/release.yml +28 -0
- package/.github/workflows/update_version.sh +2 -1
- package/CMakeLists.txt +4 -0
- package/Cargo.toml +1 -1
- package/README.md +93 -585
- package/c/lib.cpp +19 -15
- package/c/usearch.h +26 -23
- package/cmake/emscripten.cmake +8 -0
- package/conanfile.py +1 -1
- package/cpp/CMakeLists.txt +6 -2
- package/cpp/README.md +88 -0
- package/cpp/bench.cpp +72 -44
- package/cpp/test.cpp +13 -7
- package/docs/benchmarks.md +14 -12
- package/docs/golang/index.rst +6 -0
- package/docs/index.rst +5 -2
- package/docs/python/reference.rst +9 -2
- package/docs/rust/index.rst +6 -0
- package/docs/swift/index.rst +6 -0
- package/golang/README.md +28 -0
- package/include/usearch/index.hpp +668 -123
- package/include/usearch/index_punned_dense.hpp +305 -93
- package/include/usearch/index_punned_helpers.hpp +28 -3
- package/java/README.md +22 -0
- package/javascript/README.md +27 -0
- package/javascript/lib.cpp +1 -1
- package/package.json +1 -1
- package/python/README.md +283 -0
- package/python/lib.cpp +118 -16
- package/python/scripts/join.py +155 -0
- package/python/scripts/test.py +45 -241
- package/python/scripts/test_jit.py +244 -0
- package/python/usearch/eval.py +11 -3
- package/python/usearch/index.py +207 -26
- package/rust/README.md +67 -0
- package/swift/README.md +20 -0
- package/wasm/CMakeLists.txt +15 -0
- package/wasm/lib.cpp +5 -0
- package/wasmer.toml +8 -0
- package/python/usearch/join.py +0 -98
|
@@ -41,6 +41,13 @@
|
|
|
41
41
|
#define USEARCH_DEFINED_ARM
|
|
42
42
|
#endif
|
|
43
43
|
|
|
44
|
+
// Inferring hardware bitness: 32 vs 64
|
|
45
|
+
#if __WORDSIZE == 64
|
|
46
|
+
#define USEARCH_64BIT_ENV
|
|
47
|
+
#else
|
|
48
|
+
#define USEARCH_32BIT_ENV
|
|
49
|
+
#endif
|
|
50
|
+
|
|
44
51
|
#if !defined(USEARCH_USE_OPENMP)
|
|
45
52
|
#define USEARCH_USE_OPENMP 0
|
|
46
53
|
#endif
|
|
@@ -173,19 +180,21 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
173
180
|
v |= v >> 4;
|
|
174
181
|
v |= v >> 8;
|
|
175
182
|
v |= v >> 16;
|
|
183
|
+
#ifdef USEARCH_64BIT_ENV
|
|
176
184
|
v |= v >> 32;
|
|
185
|
+
#endif
|
|
177
186
|
v++;
|
|
178
187
|
return v;
|
|
179
188
|
}
|
|
180
189
|
|
|
181
190
|
template <typename at> void misaligned_store(void* ptr, at v) noexcept {
|
|
182
|
-
static_assert(!std::is_reference<at>::value);
|
|
191
|
+
static_assert(!std::is_reference<at>::value, "Can't store a reference");
|
|
183
192
|
std::memcpy(ptr, &v, sizeof(at));
|
|
184
193
|
}
|
|
185
194
|
|
|
186
195
|
/// @brief Simply dereferencing misaligned pointers can be dangerous.
|
|
187
196
|
template <typename at> at misaligned_load(void* ptr) noexcept {
|
|
188
|
-
static_assert(!std::is_reference<at>::value);
|
|
197
|
+
static_assert(!std::is_reference<at>::value, "Can't load a reference");
|
|
189
198
|
at v;
|
|
190
199
|
std::memcpy(&v, ptr, sizeof(at));
|
|
191
200
|
return v;
|
|
@@ -258,10 +267,13 @@ template <typename scalar_at> class span_gt {
|
|
|
258
267
|
std::size_t size_;
|
|
259
268
|
|
|
260
269
|
public:
|
|
261
|
-
span_gt(
|
|
270
|
+
span_gt() noexcept : data_(nullptr), size_(0u) {}
|
|
271
|
+
span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
|
|
262
272
|
span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
|
|
263
273
|
scalar_at* data() const noexcept { return data_; }
|
|
264
274
|
std::size_t size() const noexcept { return size_; }
|
|
275
|
+
scalar_at* begin() const noexcept { return data_; }
|
|
276
|
+
scalar_at* end() const noexcept { return data_ + size_; }
|
|
265
277
|
operator scalar_at*() const noexcept { return data(); }
|
|
266
278
|
};
|
|
267
279
|
|
|
@@ -617,8 +629,8 @@ template <typename result_at> struct expected_gt {
|
|
|
617
629
|
}
|
|
618
630
|
result_at const& operator*() const noexcept { return result; }
|
|
619
631
|
explicit operator bool() const noexcept { return !error; }
|
|
620
|
-
expected_gt failed(
|
|
621
|
-
error = message;
|
|
632
|
+
expected_gt failed(error_t message) noexcept {
|
|
633
|
+
error = std::move(message);
|
|
622
634
|
return std::move(*this);
|
|
623
635
|
}
|
|
624
636
|
};
|
|
@@ -981,12 +993,19 @@ class sorted_buffer_gt {
|
|
|
981
993
|
|
|
982
994
|
/**
|
|
983
995
|
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
996
|
+
*
|
|
997
|
+
* @note Avoid usage in 32bit environment
|
|
984
998
|
*/
|
|
985
999
|
class usearch_pack_m uint40_t {
|
|
986
1000
|
unsigned char octets[5];
|
|
987
1001
|
|
|
1002
|
+
inline uint40_t& broadcast(unsigned char c) {
|
|
1003
|
+
std::memset(octets, c, 5);
|
|
1004
|
+
return *this;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
988
1007
|
public:
|
|
989
|
-
inline uint40_t() noexcept {
|
|
1008
|
+
inline uint40_t() noexcept { broadcast(0); }
|
|
990
1009
|
inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets, (char*)&n, 4); }
|
|
991
1010
|
inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
|
|
992
1011
|
#if defined(USEARCH_DEFINED_CLANG) && defined(USEARCH_DEFINED_APPLE)
|
|
@@ -1007,7 +1026,7 @@ class usearch_pack_m uint40_t {
|
|
|
1007
1026
|
|
|
1008
1027
|
inline uint40_t& operator+=(std::size_t n) noexcept {
|
|
1009
1028
|
unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
|
|
1010
|
-
std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(
|
|
1029
|
+
std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(n_octets);
|
|
1011
1030
|
std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
|
|
1012
1031
|
octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
|
|
1013
1032
|
tail += n_tail;
|
|
@@ -1023,7 +1042,11 @@ class usearch_pack_m uint40_t {
|
|
|
1023
1042
|
|
|
1024
1043
|
inline operator std::size_t() const noexcept {
|
|
1025
1044
|
std::size_t result = 0;
|
|
1026
|
-
|
|
1045
|
+
#ifdef USEARCH_64BIT_ENV
|
|
1046
|
+
std::memcpy((char*)&result + 3, octets, 5);
|
|
1047
|
+
#else
|
|
1048
|
+
std::memcpy((char*)&result, octets + 1, 4);
|
|
1049
|
+
#endif
|
|
1027
1050
|
return result;
|
|
1028
1051
|
}
|
|
1029
1052
|
|
|
@@ -1034,6 +1057,9 @@ class usearch_pack_m uint40_t {
|
|
|
1034
1057
|
*this += 1u;
|
|
1035
1058
|
return old;
|
|
1036
1059
|
}
|
|
1060
|
+
|
|
1061
|
+
inline static uint40_t max() noexcept { return uint40_t{}.broadcast(0xFF); }
|
|
1062
|
+
inline static uint40_t min() noexcept { return uint40_t{}.broadcast(0); }
|
|
1037
1063
|
};
|
|
1038
1064
|
|
|
1039
1065
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
@@ -1042,6 +1068,123 @@ class usearch_pack_m uint40_t {
|
|
|
1042
1068
|
|
|
1043
1069
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
1044
1070
|
|
|
1071
|
+
template <typename element_at, typename allocator_at = std::allocator<element_at>> //
|
|
1072
|
+
class ring_gt {
|
|
1073
|
+
public:
|
|
1074
|
+
using element_t = element_at;
|
|
1075
|
+
using allocator_t = allocator_at;
|
|
1076
|
+
|
|
1077
|
+
static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
|
|
1078
|
+
static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
|
|
1079
|
+
|
|
1080
|
+
using value_type = element_t;
|
|
1081
|
+
|
|
1082
|
+
private:
|
|
1083
|
+
element_t* elements_{};
|
|
1084
|
+
std::size_t capacity_{};
|
|
1085
|
+
std::size_t head_{};
|
|
1086
|
+
std::size_t tail_{};
|
|
1087
|
+
bool empty_{true};
|
|
1088
|
+
allocator_t allocator_{};
|
|
1089
|
+
|
|
1090
|
+
public:
|
|
1091
|
+
explicit ring_gt(allocator_t const& alloc = allocator_t()) noexcept : allocator_(alloc) {}
|
|
1092
|
+
|
|
1093
|
+
ring_gt(ring_gt const&) = delete;
|
|
1094
|
+
ring_gt& operator=(ring_gt const&) = delete;
|
|
1095
|
+
|
|
1096
|
+
ring_gt(ring_gt&& other) noexcept { swap(other); }
|
|
1097
|
+
ring_gt& operator=(ring_gt&& other) noexcept {
|
|
1098
|
+
swap(other);
|
|
1099
|
+
return *this;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
void swap(ring_gt& other) noexcept {
|
|
1103
|
+
std::swap(elements_, other.elements_);
|
|
1104
|
+
std::swap(capacity_, other.capacity_);
|
|
1105
|
+
std::swap(head_, other.head_);
|
|
1106
|
+
std::swap(tail_, other.tail_);
|
|
1107
|
+
std::swap(empty_, other.empty_);
|
|
1108
|
+
std::swap(allocator_, other.allocator_);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
~ring_gt() noexcept { reset(); }
|
|
1112
|
+
|
|
1113
|
+
bool empty() const noexcept { return empty_; }
|
|
1114
|
+
size_t capacity() const noexcept { return capacity_; }
|
|
1115
|
+
size_t size() const noexcept {
|
|
1116
|
+
if (empty_)
|
|
1117
|
+
return 0;
|
|
1118
|
+
else if (head_ >= tail_)
|
|
1119
|
+
return head_ - tail_;
|
|
1120
|
+
else
|
|
1121
|
+
return capacity_ - (tail_ - head_);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
void clear() noexcept {
|
|
1125
|
+
head_ = 0;
|
|
1126
|
+
tail_ = 0;
|
|
1127
|
+
empty_ = true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
void reset() noexcept {
|
|
1131
|
+
if (elements_)
|
|
1132
|
+
allocator_.deallocate(elements_, capacity_);
|
|
1133
|
+
elements_ = nullptr;
|
|
1134
|
+
capacity_ = 0;
|
|
1135
|
+
head_ = 0;
|
|
1136
|
+
tail_ = 0;
|
|
1137
|
+
empty_ = true;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
bool reserve(std::size_t n) noexcept {
|
|
1141
|
+
if (n < size())
|
|
1142
|
+
return false; // prevent data loss
|
|
1143
|
+
n = (std::max<std::size_t>)(n, 64u);
|
|
1144
|
+
element_t* elements = allocator_.allocate(n);
|
|
1145
|
+
if (!elements)
|
|
1146
|
+
return false;
|
|
1147
|
+
|
|
1148
|
+
std::size_t i = 0;
|
|
1149
|
+
while (try_pop(elements[i]))
|
|
1150
|
+
i++;
|
|
1151
|
+
|
|
1152
|
+
reset();
|
|
1153
|
+
elements_ = elements;
|
|
1154
|
+
capacity_ = n;
|
|
1155
|
+
head_ = i;
|
|
1156
|
+
tail_ = 0;
|
|
1157
|
+
empty_ = (i == 0);
|
|
1158
|
+
return true;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
void push(element_t const& value) noexcept {
|
|
1162
|
+
elements_[head_] = value;
|
|
1163
|
+
head_ = (head_ + 1) % capacity_;
|
|
1164
|
+
empty_ = false;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
bool try_push(element_t const& value) noexcept {
|
|
1168
|
+
if (head_ == tail_ && !empty_)
|
|
1169
|
+
return false; // elements_ is full
|
|
1170
|
+
|
|
1171
|
+
return push(value);
|
|
1172
|
+
return true;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
bool try_pop(element_t& value) noexcept {
|
|
1176
|
+
if (empty_)
|
|
1177
|
+
return false;
|
|
1178
|
+
|
|
1179
|
+
value = std::move(elements_[tail_]);
|
|
1180
|
+
tail_ = (tail_ + 1) % capacity_;
|
|
1181
|
+
empty_ = head_ == tail_;
|
|
1182
|
+
return true;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
element_t const& operator[](std::size_t i) const noexcept { return elements_[(tail_ + i) % capacity_]; }
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1045
1188
|
/// @brief Number of neighbors per graph node.
|
|
1046
1189
|
/// Defaults to 32 in FAISS and 16 in hnswlib.
|
|
1047
1190
|
/// > It is called `M` in the paper.
|
|
@@ -1087,11 +1230,11 @@ struct index_config_t {
|
|
|
1087
1230
|
};
|
|
1088
1231
|
|
|
1089
1232
|
struct index_limits_t {
|
|
1090
|
-
std::size_t
|
|
1233
|
+
std::size_t members = 0;
|
|
1091
1234
|
std::size_t threads_add = std::thread::hardware_concurrency();
|
|
1092
1235
|
std::size_t threads_search = std::thread::hardware_concurrency();
|
|
1093
1236
|
|
|
1094
|
-
index_limits_t(std::size_t n, std::size_t t) noexcept :
|
|
1237
|
+
index_limits_t(std::size_t n, std::size_t t) noexcept : members(n), threads_add(t), threads_search(t) {}
|
|
1095
1238
|
index_limits_t(std::size_t n = 0) noexcept : index_limits_t(n, std::thread::hardware_concurrency()) {}
|
|
1096
1239
|
std::size_t threads() const noexcept { return (std::max)(threads_add, threads_search); }
|
|
1097
1240
|
std::size_t concurrency() const noexcept { return (std::min)(threads_add, threads_search); }
|
|
@@ -1123,6 +1266,21 @@ struct search_config_t {
|
|
|
1123
1266
|
bool exact = false;
|
|
1124
1267
|
};
|
|
1125
1268
|
|
|
1269
|
+
struct copy_config_t {
|
|
1270
|
+
bool copy_vectors = true;
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
struct join_config_t {
|
|
1274
|
+
/// @brief Controls maximum number of proposals per man during stable marriage.
|
|
1275
|
+
std::size_t max_proposals = 0;
|
|
1276
|
+
/// @brief Hyper-parameter controlling the quality of search.
|
|
1277
|
+
/// Defaults to 16 in FAISS and 10 in hnswlib.
|
|
1278
|
+
/// > It is called `ef` in the paper.
|
|
1279
|
+
std::size_t expansion = default_expansion_search();
|
|
1280
|
+
/// @brief Brute-forces exhaustive search over all entries in the index.
|
|
1281
|
+
bool exact = false;
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1126
1284
|
using file_header_t = byte_t[64];
|
|
1127
1285
|
|
|
1128
1286
|
/**
|
|
@@ -1227,8 +1385,8 @@ struct file_head_result_t {
|
|
|
1227
1385
|
error_t error;
|
|
1228
1386
|
|
|
1229
1387
|
explicit operator bool() const noexcept { return !error; }
|
|
1230
|
-
file_head_result_t failed(
|
|
1231
|
-
error = message;
|
|
1388
|
+
file_head_result_t failed(error_t message) noexcept {
|
|
1389
|
+
error = std::move(message);
|
|
1232
1390
|
return std::move(*this);
|
|
1233
1391
|
}
|
|
1234
1392
|
};
|
|
@@ -1262,6 +1420,37 @@ struct viewed_file_t {
|
|
|
1262
1420
|
};
|
|
1263
1421
|
#endif
|
|
1264
1422
|
|
|
1423
|
+
struct dummy_predicate_t {
|
|
1424
|
+
template <typename match_at> constexpr bool operator()(match_at&&) const noexcept { return true; }
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
struct dummy_progress_t {
|
|
1428
|
+
inline void operator()(std::size_t /*progress*/, std::size_t /*total*/) const noexcept {}
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
struct dummy_executor_t {
|
|
1432
|
+
dummy_executor_t() noexcept {}
|
|
1433
|
+
std::size_t size() const noexcept { return 1; }
|
|
1434
|
+
|
|
1435
|
+
template <typename thread_aware_function_at>
|
|
1436
|
+
void execute_bulk(std::size_t tasks, thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1437
|
+
for (std::size_t task_idx = 0; task_idx != tasks; ++task_idx)
|
|
1438
|
+
thread_aware_function(0, task_idx);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
template <typename thread_aware_function_at>
|
|
1442
|
+
void execute_bulk(thread_aware_function_at&& thread_aware_function) noexcept {
|
|
1443
|
+
thread_aware_function(0);
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
struct dummy_label_to_label_mapping_t {
|
|
1448
|
+
struct member_ref_t {
|
|
1449
|
+
template <typename label_at> member_ref_t& operator=(label_at&&) noexcept { return *this; }
|
|
1450
|
+
};
|
|
1451
|
+
template <typename label_at> member_ref_t operator[](label_at&&) const noexcept { return {}; }
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1265
1454
|
/**
|
|
1266
1455
|
* @brief Approximate Nearest Neighbors Search index using the
|
|
1267
1456
|
* Hierarchical Navigable Small World graph algorithm.
|
|
@@ -1325,7 +1514,7 @@ struct viewed_file_t {
|
|
|
1325
1514
|
*
|
|
1326
1515
|
*/
|
|
1327
1516
|
template <typename metric_at = ip_gt<float>, //
|
|
1328
|
-
typename label_at = std::
|
|
1517
|
+
typename label_at = std::int64_t, //
|
|
1329
1518
|
typename id_at = std::uint32_t, //
|
|
1330
1519
|
typename allocator_at = std::allocator<char>, //
|
|
1331
1520
|
typename point_allocator_at = allocator_at> //
|
|
@@ -1342,16 +1531,18 @@ class index_gt {
|
|
|
1342
1531
|
using vector_view_t = span_gt<scalar_t const>;
|
|
1343
1532
|
using distance_t = return_type_gt<metric_t, vector_view_t, vector_view_t>;
|
|
1344
1533
|
|
|
1345
|
-
struct
|
|
1346
|
-
|
|
1534
|
+
struct member_cref_t {
|
|
1535
|
+
label_t label;
|
|
1347
1536
|
vector_view_t vector;
|
|
1348
1537
|
id_t id;
|
|
1349
1538
|
};
|
|
1350
1539
|
|
|
1351
|
-
struct
|
|
1352
|
-
label_t label;
|
|
1540
|
+
struct member_ref_t {
|
|
1541
|
+
misaligned_ref_gt<label_t> label;
|
|
1353
1542
|
vector_view_t vector;
|
|
1354
1543
|
id_t id;
|
|
1544
|
+
|
|
1545
|
+
inline operator member_cref_t() const noexcept { return {label, vector, id}; }
|
|
1355
1546
|
};
|
|
1356
1547
|
|
|
1357
1548
|
template <typename ref_at, typename index_at> class member_iterator_gt {
|
|
@@ -1552,7 +1743,7 @@ class index_gt {
|
|
|
1552
1743
|
index_config_t config_{};
|
|
1553
1744
|
index_limits_t limits_{};
|
|
1554
1745
|
metric_t metric_{};
|
|
1555
|
-
allocator_t allocator_{};
|
|
1746
|
+
mutable allocator_t allocator_{};
|
|
1556
1747
|
point_allocator_t point_allocator_{};
|
|
1557
1748
|
precomputed_constants_t pre_{};
|
|
1558
1749
|
viewed_file_t viewed_file_{};
|
|
@@ -1606,6 +1797,35 @@ class index_gt {
|
|
|
1606
1797
|
return *this;
|
|
1607
1798
|
}
|
|
1608
1799
|
|
|
1800
|
+
struct copy_result_t {
|
|
1801
|
+
error_t error;
|
|
1802
|
+
index_gt index;
|
|
1803
|
+
|
|
1804
|
+
explicit operator bool() const noexcept { return !error; }
|
|
1805
|
+
copy_result_t failed(error_t message) noexcept {
|
|
1806
|
+
error = std::move(message);
|
|
1807
|
+
return std::move(*this);
|
|
1808
|
+
}
|
|
1809
|
+
};
|
|
1810
|
+
|
|
1811
|
+
copy_result_t copy(copy_config_t /*config*/ = {}) const noexcept {
|
|
1812
|
+
copy_result_t result;
|
|
1813
|
+
index_gt& other = result.index;
|
|
1814
|
+
other = index_gt(config_, metric_, allocator_, point_allocator_);
|
|
1815
|
+
if (!other.reserve(limits_))
|
|
1816
|
+
return result.failed("Failed to reserve the contexts");
|
|
1817
|
+
|
|
1818
|
+
// Now all is left - is to allocate new `node_t` instances and populate
|
|
1819
|
+
// the `other.nodes_` array into it.
|
|
1820
|
+
for (std::size_t i = 0; i != size_; ++i)
|
|
1821
|
+
other.nodes_[i] = other.node_make_copy_(node_bytes_split_(nodes_[i]));
|
|
1822
|
+
|
|
1823
|
+
other.size_ = size_.load();
|
|
1824
|
+
other.max_level_ = max_level_;
|
|
1825
|
+
other.entry_id_ = entry_id_;
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1609
1829
|
member_citerator_t cbegin() const noexcept { return {this, 0}; }
|
|
1610
1830
|
member_citerator_t cend() const noexcept { return {this, size()}; }
|
|
1611
1831
|
member_citerator_t begin() const noexcept { return {this, 0}; }
|
|
@@ -1613,6 +1833,9 @@ class index_gt {
|
|
|
1613
1833
|
member_iterator_t begin() noexcept { return {this, 0}; }
|
|
1614
1834
|
member_iterator_t end() noexcept { return {this, size()}; }
|
|
1615
1835
|
|
|
1836
|
+
member_cref_t at(std::size_t i) const noexcept { return member_cref_t(node_with_id_(i)); }
|
|
1837
|
+
member_ref_t at(std::size_t i) noexcept { return member_ref_t(node_with_id_(i)); }
|
|
1838
|
+
|
|
1616
1839
|
#pragma region Adjusting Configuration
|
|
1617
1840
|
|
|
1618
1841
|
/**
|
|
@@ -1638,7 +1861,7 @@ class index_gt {
|
|
|
1638
1861
|
clear();
|
|
1639
1862
|
|
|
1640
1863
|
if (nodes_)
|
|
1641
|
-
node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.
|
|
1864
|
+
node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.members);
|
|
1642
1865
|
if (contexts_) {
|
|
1643
1866
|
for (std::size_t i = 0; i != limits_.threads(); ++i)
|
|
1644
1867
|
contexts_[i].~context_t();
|
|
@@ -1657,11 +1880,13 @@ class index_gt {
|
|
|
1657
1880
|
std::swap(limits_, other.limits_);
|
|
1658
1881
|
std::swap(metric_, other.metric_);
|
|
1659
1882
|
std::swap(allocator_, other.allocator_);
|
|
1883
|
+
std::swap(point_allocator_, other.point_allocator_);
|
|
1660
1884
|
std::swap(pre_, other.pre_);
|
|
1661
1885
|
std::swap(viewed_file_, other.viewed_file_);
|
|
1662
1886
|
std::swap(max_level_, other.max_level_);
|
|
1663
1887
|
std::swap(entry_id_, other.entry_id_);
|
|
1664
1888
|
std::swap(nodes_, other.nodes_);
|
|
1889
|
+
std::swap(nodes_mutexes_, other.nodes_mutexes_);
|
|
1665
1890
|
std::swap(contexts_, other.contexts_);
|
|
1666
1891
|
|
|
1667
1892
|
// Non-atomic parts.
|
|
@@ -1679,13 +1904,16 @@ class index_gt {
|
|
|
1679
1904
|
*/
|
|
1680
1905
|
bool reserve(index_limits_t limits) usearch_noexcept_m {
|
|
1681
1906
|
|
|
1682
|
-
|
|
1907
|
+
if (limits.threads_add <= limits_.threads_add //
|
|
1908
|
+
&& limits.threads_search <= limits_.threads_search //
|
|
1909
|
+
&& limits.members <= limits_.members)
|
|
1910
|
+
return true;
|
|
1683
1911
|
|
|
1684
|
-
if (!nodes_mutexes_.resize(limits.
|
|
1912
|
+
if (!nodes_mutexes_.resize(limits.members))
|
|
1685
1913
|
return false;
|
|
1686
1914
|
|
|
1687
1915
|
node_allocator_t node_allocator;
|
|
1688
|
-
node_t* new_nodes = node_allocator.allocate(limits.
|
|
1916
|
+
node_t* new_nodes = node_allocator.allocate(limits.members);
|
|
1689
1917
|
if (!new_nodes)
|
|
1690
1918
|
return false;
|
|
1691
1919
|
|
|
@@ -1693,17 +1921,17 @@ class index_gt {
|
|
|
1693
1921
|
context_allocator_t context_allocator;
|
|
1694
1922
|
context_t* new_contexts = context_allocator.allocate(limits_threads);
|
|
1695
1923
|
if (!new_contexts) {
|
|
1696
|
-
node_allocator.deallocate(new_nodes, limits.
|
|
1924
|
+
node_allocator.deallocate(new_nodes, limits.members);
|
|
1697
1925
|
return false;
|
|
1698
1926
|
}
|
|
1699
1927
|
for (std::size_t i = 0; i != limits_threads; ++i) {
|
|
1700
1928
|
context_t& context = new_contexts[i];
|
|
1701
1929
|
new (&context) context_t();
|
|
1702
1930
|
context.metric = metric_;
|
|
1703
|
-
if (!context.visits.resize(limits.
|
|
1931
|
+
if (!context.visits.resize(limits.members)) {
|
|
1704
1932
|
for (std::size_t j = 0; j != i; ++j)
|
|
1705
1933
|
context.visits.reset();
|
|
1706
|
-
node_allocator.deallocate(new_nodes, limits.
|
|
1934
|
+
node_allocator.deallocate(new_nodes, limits.members);
|
|
1707
1935
|
context_allocator.deallocate(new_contexts, limits_threads);
|
|
1708
1936
|
return false;
|
|
1709
1937
|
}
|
|
@@ -1723,13 +1951,12 @@ class index_gt {
|
|
|
1723
1951
|
|
|
1724
1952
|
// Move the nodes info, and deallocate previous buffers.
|
|
1725
1953
|
if (nodes_)
|
|
1726
|
-
std::memcpy(new_nodes, nodes_, sizeof(node_t) * size()),
|
|
1727
|
-
node_allocator.deallocate(nodes_, limits_.elements);
|
|
1954
|
+
std::memcpy(new_nodes, nodes_, sizeof(node_t) * size()), node_allocator.deallocate(nodes_, limits_.members);
|
|
1728
1955
|
if (contexts_)
|
|
1729
1956
|
context_allocator.deallocate(contexts_, limits_.threads());
|
|
1730
1957
|
|
|
1731
1958
|
limits_ = limits;
|
|
1732
|
-
capacity_ = limits.
|
|
1959
|
+
capacity_ = limits.members;
|
|
1733
1960
|
nodes_ = new_nodes;
|
|
1734
1961
|
contexts_ = new_contexts;
|
|
1735
1962
|
return true;
|
|
@@ -1760,8 +1987,8 @@ class index_gt {
|
|
|
1760
1987
|
id_t id{};
|
|
1761
1988
|
|
|
1762
1989
|
explicit operator bool() const noexcept { return !error; }
|
|
1763
|
-
add_result_t failed(
|
|
1764
|
-
error = message;
|
|
1990
|
+
add_result_t failed(error_t message) noexcept {
|
|
1991
|
+
error = std::move(message);
|
|
1765
1992
|
return std::move(*this);
|
|
1766
1993
|
}
|
|
1767
1994
|
};
|
|
@@ -1793,14 +2020,17 @@ class index_gt {
|
|
|
1793
2020
|
inline search_result_t& operator=(search_result_t&&) = default;
|
|
1794
2021
|
|
|
1795
2022
|
explicit operator bool() const noexcept { return !error; }
|
|
1796
|
-
search_result_t failed(
|
|
1797
|
-
error = message;
|
|
2023
|
+
search_result_t failed(error_t message) noexcept {
|
|
2024
|
+
error = std::move(message);
|
|
1798
2025
|
return std::move(*this);
|
|
1799
2026
|
}
|
|
1800
2027
|
|
|
1801
2028
|
inline operator std::size_t() const noexcept { return count; }
|
|
1802
2029
|
inline std::size_t size() const noexcept { return count; }
|
|
2030
|
+
inline bool empty() const noexcept { return !count; }
|
|
1803
2031
|
inline match_t operator[](std::size_t i) const noexcept { return at(i); }
|
|
2032
|
+
inline match_t front() const noexcept { return at(0); }
|
|
2033
|
+
inline match_t back() const noexcept { return at(count - 1); }
|
|
1804
2034
|
inline bool contains(label_t label) const noexcept {
|
|
1805
2035
|
for (std::size_t i = 0; i != count; ++i)
|
|
1806
2036
|
if (at(i).member.label == label)
|
|
@@ -1866,7 +2096,7 @@ class index_gt {
|
|
|
1866
2096
|
new_level_lock.unlock();
|
|
1867
2097
|
|
|
1868
2098
|
// Allocate the neighbors
|
|
1869
|
-
node_t node =
|
|
2099
|
+
node_t node = node_make_(label, vector, target_level, config.store_vector);
|
|
1870
2100
|
if (!node)
|
|
1871
2101
|
return result.failed("Out of memory!");
|
|
1872
2102
|
std::size_t old_size = size_.fetch_add(1);
|
|
@@ -1887,16 +2117,7 @@ class index_gt {
|
|
|
1887
2117
|
result.measurements = context.measurements_count;
|
|
1888
2118
|
result.cycles = context.iteration_cycles;
|
|
1889
2119
|
|
|
1890
|
-
|
|
1891
|
-
id_t closest_id = search_for_one_(entry_id_copy, vector, max_level_copy, target_level, context);
|
|
1892
|
-
|
|
1893
|
-
// From `target_level` down perform proper extensive search
|
|
1894
|
-
for (level_t level = (std::min)(target_level, max_level_copy); level >= 0; --level) {
|
|
1895
|
-
// TODO: Handle out of memory conditions
|
|
1896
|
-
search_to_insert_(closest_id, vector, level, config.expansion, context);
|
|
1897
|
-
closest_id = connect_new_node_(new_id, level, context);
|
|
1898
|
-
reconnect_neighbor_nodes_(new_id, level, context);
|
|
1899
|
-
}
|
|
2120
|
+
connect_node_across_levels_(new_id, vector, entry_id_copy, max_level_copy, target_level, config, context);
|
|
1900
2121
|
|
|
1901
2122
|
// Normalize stats
|
|
1902
2123
|
result.measurements = context.measurements_count - result.measurements;
|
|
@@ -1910,9 +2131,50 @@ class index_gt {
|
|
|
1910
2131
|
return result;
|
|
1911
2132
|
}
|
|
1912
2133
|
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2134
|
+
/**
|
|
2135
|
+
* @brief Update an existing entry, replacing a vector and a label. Thread-safe.
|
|
2136
|
+
*
|
|
2137
|
+
* @param[in] old_id Existing internal identifier for a node to be replaced.
|
|
2138
|
+
* @param[in] label External identifier/name/descriptor for the vector.
|
|
2139
|
+
* @param[in] vector Contiguous range of scalars forming a vector view.
|
|
2140
|
+
* @param[in] config Configuration options for this specific operation.
|
|
2141
|
+
*/
|
|
2142
|
+
add_result_t update(id_t old_id, label_t label, vector_view_t vector, add_config_t config = {}) usearch_noexcept_m {
|
|
2143
|
+
|
|
2144
|
+
usearch_assert_m(!is_immutable(), "Can't add to an immutable index");
|
|
2145
|
+
add_result_t result;
|
|
2146
|
+
|
|
2147
|
+
// Make sure we have enough local memory to perform this request
|
|
2148
|
+
context_t& context = contexts_[config.thread];
|
|
2149
|
+
top_candidates_t& top = context.top_candidates;
|
|
2150
|
+
next_candidates_t& next = context.next_candidates;
|
|
2151
|
+
top.clear();
|
|
2152
|
+
next.clear();
|
|
2153
|
+
|
|
2154
|
+
// The top list needs one more slot than the connectivity of the base level
|
|
2155
|
+
// for the heuristic, that tries to squeeze one more element into saturated list.
|
|
2156
|
+
std::size_t top_limit = (std::max)(base_level_multiple_() * config_.connectivity + 1, config.expansion);
|
|
2157
|
+
if (!top.reserve(top_limit))
|
|
2158
|
+
return result.failed("Out of memory!");
|
|
2159
|
+
if (!next.reserve(config.expansion))
|
|
2160
|
+
return result.failed("Out of memory!");
|
|
2161
|
+
|
|
2162
|
+
node_lock_t new_lock = node_lock_(old_id);
|
|
2163
|
+
node_t node = node_with_id_(old_id);
|
|
2164
|
+
|
|
2165
|
+
// Pull stats
|
|
2166
|
+
result.measurements = context.measurements_count;
|
|
2167
|
+
result.cycles = context.iteration_cycles;
|
|
2168
|
+
|
|
2169
|
+
connect_node_across_levels_(old_id, vector, entry_id_, max_level_, node.level(), config, context);
|
|
2170
|
+
node.label(label);
|
|
2171
|
+
|
|
2172
|
+
// Normalize stats
|
|
2173
|
+
result.measurements = context.measurements_count - result.measurements;
|
|
2174
|
+
result.cycles = context.iteration_cycles - result.cycles;
|
|
2175
|
+
|
|
2176
|
+
return result;
|
|
2177
|
+
}
|
|
1916
2178
|
|
|
1917
2179
|
/**
|
|
1918
2180
|
* @brief Searches for the closest elements to the given ::query. Thread-safe.
|
|
@@ -1966,40 +2228,6 @@ class index_gt {
|
|
|
1966
2228
|
return result;
|
|
1967
2229
|
}
|
|
1968
2230
|
|
|
1969
|
-
template <typename predicate_at = dummy_predicate_t>
|
|
1970
|
-
search_result_t search_around( //
|
|
1971
|
-
id_t hint, vector_view_t query, std::size_t wanted, //
|
|
1972
|
-
search_config_t config = {}, predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
|
|
1973
|
-
|
|
1974
|
-
context_t& context = contexts_[config.thread];
|
|
1975
|
-
top_candidates_t& top = context.top_candidates;
|
|
1976
|
-
next_candidates_t& next = context.next_candidates;
|
|
1977
|
-
search_result_t result{*this, top};
|
|
1978
|
-
|
|
1979
|
-
if (!size_)
|
|
1980
|
-
return result;
|
|
1981
|
-
|
|
1982
|
-
std::size_t expansion = (std::max)(config.expansion, wanted);
|
|
1983
|
-
if (!next.reserve(expansion))
|
|
1984
|
-
return result.failed("Out of memory!");
|
|
1985
|
-
if (!top.reserve(expansion))
|
|
1986
|
-
return result.failed("Out of memory!");
|
|
1987
|
-
|
|
1988
|
-
// Go down the level, tracking only the closest match
|
|
1989
|
-
result.measurements = context.measurements_count;
|
|
1990
|
-
result.cycles = context.iteration_cycles;
|
|
1991
|
-
|
|
1992
|
-
search_to_find_in_base_(hint, query, expansion, context, std::forward<predicate_at>(predicate));
|
|
1993
|
-
top.sort_ascending();
|
|
1994
|
-
top.shrink(wanted);
|
|
1995
|
-
|
|
1996
|
-
// Normalize stats
|
|
1997
|
-
result.measurements = context.measurements_count - result.measurements;
|
|
1998
|
-
result.cycles = context.iteration_cycles - result.cycles;
|
|
1999
|
-
result.count = top.size();
|
|
2000
|
-
return result;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
2231
|
#pragma endregion
|
|
2004
2232
|
|
|
2005
2233
|
#pragma region Metadata
|
|
@@ -2060,7 +2288,7 @@ class index_gt {
|
|
|
2060
2288
|
}
|
|
2061
2289
|
|
|
2062
2290
|
// Temporary data-structures, proportional to the number of nodes:
|
|
2063
|
-
total += limits_.
|
|
2291
|
+
total += limits_.members * sizeof(node_t) + allocator_entry_bytes;
|
|
2064
2292
|
|
|
2065
2293
|
// Temporary data-structures, proportional to the number of threads:
|
|
2066
2294
|
total += limits_.threads() * sizeof(context_t) + allocator_entry_bytes * 3;
|
|
@@ -2083,8 +2311,8 @@ class index_gt {
|
|
|
2083
2311
|
error_t error;
|
|
2084
2312
|
|
|
2085
2313
|
explicit operator bool() const noexcept { return !error; }
|
|
2086
|
-
serialization_result_t failed(
|
|
2087
|
-
error = message;
|
|
2314
|
+
serialization_result_t failed(error_t message) noexcept {
|
|
2315
|
+
error = std::move(message);
|
|
2088
2316
|
return std::move(*this);
|
|
2089
2317
|
}
|
|
2090
2318
|
};
|
|
@@ -2094,7 +2322,8 @@ class index_gt {
|
|
|
2094
2322
|
* co-locating vectors and neighbors lists.
|
|
2095
2323
|
* Available on Linux, MacOS, Windows.
|
|
2096
2324
|
*/
|
|
2097
|
-
|
|
2325
|
+
template <typename progress_at = dummy_progress_t>
|
|
2326
|
+
serialization_result_t save(char const* file_path, progress_at&& progress = {}) const noexcept {
|
|
2098
2327
|
|
|
2099
2328
|
// Make sure we have right to write to that file
|
|
2100
2329
|
serialization_result_t result;
|
|
@@ -2164,6 +2393,7 @@ class index_gt {
|
|
|
2164
2393
|
write_chunk(node.vector(), node_vector_bytes);
|
|
2165
2394
|
if (result.error)
|
|
2166
2395
|
return result;
|
|
2396
|
+
progress(i, state.size);
|
|
2167
2397
|
}
|
|
2168
2398
|
|
|
2169
2399
|
std::fclose(file);
|
|
@@ -2175,7 +2405,8 @@ class index_gt {
|
|
|
2175
2405
|
* copying both vectors and neighbors lists into RAM.
|
|
2176
2406
|
* Available on Linux, MacOS, Windows.
|
|
2177
2407
|
*/
|
|
2178
|
-
|
|
2408
|
+
template <typename progress_at = dummy_progress_t>
|
|
2409
|
+
serialization_result_t load(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2179
2410
|
|
|
2180
2411
|
serialization_result_t result;
|
|
2181
2412
|
file_header_t state_buffer{};
|
|
@@ -2212,7 +2443,7 @@ class index_gt {
|
|
|
2212
2443
|
pre_ = precompute_(config_);
|
|
2213
2444
|
|
|
2214
2445
|
index_limits_t limits;
|
|
2215
|
-
limits.
|
|
2446
|
+
limits.members = state.size;
|
|
2216
2447
|
if (!reserve(limits)) {
|
|
2217
2448
|
std::fclose(file);
|
|
2218
2449
|
return result.failed("Out of memory");
|
|
@@ -2223,7 +2454,8 @@ class index_gt {
|
|
|
2223
2454
|
}
|
|
2224
2455
|
|
|
2225
2456
|
// Load nodes one by one
|
|
2226
|
-
|
|
2457
|
+
std::size_t const size = size_;
|
|
2458
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2227
2459
|
label_t label;
|
|
2228
2460
|
dim_t dim;
|
|
2229
2461
|
level_t level;
|
|
@@ -2238,11 +2470,15 @@ class index_gt {
|
|
|
2238
2470
|
return result;
|
|
2239
2471
|
|
|
2240
2472
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2241
|
-
node_t node = node_malloc_(
|
|
2473
|
+
node_t node = node_malloc_(dim, level);
|
|
2474
|
+
node.label(label);
|
|
2475
|
+
node.dim(dim);
|
|
2476
|
+
node.level(level);
|
|
2242
2477
|
read_chunk(node.tape() + node_head_bytes_(), node_bytes - node_head_bytes_());
|
|
2243
2478
|
if (result.error)
|
|
2244
2479
|
return result;
|
|
2245
2480
|
nodes_[i] = node;
|
|
2481
|
+
progress(i, size);
|
|
2246
2482
|
}
|
|
2247
2483
|
|
|
2248
2484
|
std::fclose(file);
|
|
@@ -2255,7 +2491,8 @@ class index_gt {
|
|
|
2255
2491
|
* @b without copying the vectors and neighbors lists into RAM.
|
|
2256
2492
|
* Available on Linux, MacOS, but @b not on Windows.
|
|
2257
2493
|
*/
|
|
2258
|
-
|
|
2494
|
+
template <typename progress_at = dummy_progress_t>
|
|
2495
|
+
serialization_result_t view(char const* file_path, progress_at&& progress = {}) noexcept {
|
|
2259
2496
|
serialization_result_t result;
|
|
2260
2497
|
|
|
2261
2498
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
@@ -2326,7 +2563,7 @@ class index_gt {
|
|
|
2326
2563
|
pre_ = precompute_(config_);
|
|
2327
2564
|
|
|
2328
2565
|
index_limits_t limits;
|
|
2329
|
-
limits.
|
|
2566
|
+
limits.members = state.size;
|
|
2330
2567
|
limits.threads_add = 0;
|
|
2331
2568
|
if (!reserve(limits))
|
|
2332
2569
|
return result.failed("Out of memory!");
|
|
@@ -2337,16 +2574,18 @@ class index_gt {
|
|
|
2337
2574
|
}
|
|
2338
2575
|
|
|
2339
2576
|
// Locate every node packed into file
|
|
2340
|
-
std::size_t
|
|
2341
|
-
|
|
2342
|
-
|
|
2577
|
+
std::size_t progress_bytes = sizeof(file_header_t);
|
|
2578
|
+
std::size_t const size = size_;
|
|
2579
|
+
for (std::size_t i = 0; i != size; ++i) {
|
|
2580
|
+
byte_t* tape = (byte_t*)(file + progress_bytes);
|
|
2343
2581
|
dim_t dim = misaligned_load<dim_t>(tape + sizeof(label_t));
|
|
2344
2582
|
level_t level = misaligned_load<level_t>(tape + sizeof(label_t) + sizeof(dim_t));
|
|
2345
2583
|
|
|
2346
2584
|
std::size_t node_bytes = node_bytes_(dim, level);
|
|
2347
2585
|
std::size_t node_vector_bytes = dim * sizeof(scalar_t);
|
|
2348
2586
|
nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
|
|
2349
|
-
|
|
2587
|
+
progress_bytes += node_bytes;
|
|
2588
|
+
progress(i, size);
|
|
2350
2589
|
}
|
|
2351
2590
|
|
|
2352
2591
|
return {};
|
|
@@ -2354,7 +2593,267 @@ class index_gt {
|
|
|
2354
2593
|
|
|
2355
2594
|
#pragma endregion
|
|
2356
2595
|
|
|
2596
|
+
struct join_result_t {
|
|
2597
|
+
error_t error{};
|
|
2598
|
+
std::size_t intersection_size{};
|
|
2599
|
+
std::size_t engagements{};
|
|
2600
|
+
std::size_t cycles{};
|
|
2601
|
+
std::size_t measurements{};
|
|
2602
|
+
|
|
2603
|
+
explicit operator bool() const noexcept { return !error; }
|
|
2604
|
+
join_result_t failed(error_t message) noexcept {
|
|
2605
|
+
error = std::move(message);
|
|
2606
|
+
return std::move(*this);
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
|
|
2610
|
+
/**
|
|
2611
|
+
* @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
|
|
2612
|
+
* to perform fast one-to-one matching between two large collections
|
|
2613
|
+
* of vectors, using approximate nearest neighbors search.
|
|
2614
|
+
*
|
|
2615
|
+
* @param[inout] first_to_second Container to map ::first labels to ::second.
|
|
2616
|
+
* @param[inout] second_to_first Container to map ::second labels to ::first.
|
|
2617
|
+
* @param[in] executor Thread-pool to execute the job in parallel.
|
|
2618
|
+
* @param[in] progress Callback to report the execution progress.
|
|
2619
|
+
*/
|
|
2620
|
+
template < //
|
|
2621
|
+
typename first_to_second_at = dummy_label_to_label_mapping_t, //
|
|
2622
|
+
typename second_to_first_at = dummy_label_to_label_mapping_t, //
|
|
2623
|
+
typename executor_at = dummy_executor_t, //
|
|
2624
|
+
typename progress_at = dummy_progress_t //
|
|
2625
|
+
>
|
|
2626
|
+
static join_result_t join( //
|
|
2627
|
+
index_gt const& first, index_gt const& second, //
|
|
2628
|
+
join_config_t config = {}, //
|
|
2629
|
+
first_to_second_at&& first_to_second = first_to_second_at{}, //
|
|
2630
|
+
second_to_first_at&& second_to_first = second_to_first_at{}, //
|
|
2631
|
+
executor_at&& executor = executor_at{}, //
|
|
2632
|
+
progress_at&& progress = progress_at{}) noexcept {
|
|
2633
|
+
|
|
2634
|
+
if (second.size() < first.size())
|
|
2635
|
+
return join_small_and_big_( //
|
|
2636
|
+
second, first, //
|
|
2637
|
+
config, //
|
|
2638
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2639
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2640
|
+
std::forward<executor_at>(executor), //
|
|
2641
|
+
std::forward<progress_at>(progress));
|
|
2642
|
+
else
|
|
2643
|
+
return join_small_and_big_( //
|
|
2644
|
+
first, second, //
|
|
2645
|
+
config, //
|
|
2646
|
+
std::forward<first_to_second_at>(first_to_second), //
|
|
2647
|
+
std::forward<second_to_first_at>(second_to_first), //
|
|
2648
|
+
std::forward<executor_at>(executor), //
|
|
2649
|
+
std::forward<progress_at>(progress));
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/**
|
|
2653
|
+
* @brief Scans the whole collection, removing the links leading towards
|
|
2654
|
+
* banned entries. This essentially isolates some nodes from the rest
|
|
2655
|
+
* of the graph, while keeping their outgoing links, in case the node
|
|
2656
|
+
* is structurally relevant and has a crucial role in the index.
|
|
2657
|
+
* It won't reclaim the memory.
|
|
2658
|
+
*
|
|
2659
|
+
* @param[in] allow_member Predicate to mark nodes for isolation.
|
|
2660
|
+
* @param[in] executor Thread-pool to execute the job in parallel.
|
|
2661
|
+
* @param[in] progress Callback to report the execution progress.
|
|
2662
|
+
*/
|
|
2663
|
+
template < //
|
|
2664
|
+
typename allow_member_at = dummy_predicate_t, //
|
|
2665
|
+
typename executor_at = dummy_executor_t, //
|
|
2666
|
+
typename progress_at = dummy_progress_t //
|
|
2667
|
+
>
|
|
2668
|
+
void isolate( //
|
|
2669
|
+
allow_member_at&& allow_member, //
|
|
2670
|
+
executor_at&& executor = executor_at{}, //
|
|
2671
|
+
progress_at&& progress = progress_at{}) noexcept {
|
|
2672
|
+
|
|
2673
|
+
// Erase all the incoming links
|
|
2674
|
+
executor.execute_bulk(size(), [&](std::size_t node_idx, std::size_t) {
|
|
2675
|
+
node_t node = node_with_id_(node_idx);
|
|
2676
|
+
for (level_t level = 0; level <= node.level(); ++level) {
|
|
2677
|
+
neighbors_ref_t neighbors = neighbors_(node, level);
|
|
2678
|
+
std::size_t old_size = neighbors.size();
|
|
2679
|
+
neighbors.clear();
|
|
2680
|
+
for (std::size_t i = 0; i != old_size; ++i) {
|
|
2681
|
+
id_t neighbor_id = neighbors[i];
|
|
2682
|
+
node_t neighbor = node_with_id_(neighbor_id);
|
|
2683
|
+
if (allow_member(member_cref_t{neighbor.label(), neighbor.vector_view(), neighbor_id}))
|
|
2684
|
+
neighbors.push_back(neighbor_id);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2357
2690
|
private:
|
|
2691
|
+
template <typename first_to_second_at, typename second_to_first_at, typename executor_at, typename progress_at>
|
|
2692
|
+
static join_result_t join_small_and_big_( //
|
|
2693
|
+
index_gt const& men, index_gt const& women, //
|
|
2694
|
+
join_config_t config, //
|
|
2695
|
+
first_to_second_at&& man_to_woman, //
|
|
2696
|
+
second_to_first_at&& woman_to_man, //
|
|
2697
|
+
executor_at&& executor, //
|
|
2698
|
+
progress_at&& progress) noexcept {
|
|
2699
|
+
|
|
2700
|
+
join_result_t result;
|
|
2701
|
+
|
|
2702
|
+
// Sanity checks and argument validation:
|
|
2703
|
+
if (&men == &women)
|
|
2704
|
+
return result.failed("Can't join with itself, consider copying");
|
|
2705
|
+
if (config.max_proposals == 0)
|
|
2706
|
+
config.max_proposals = std::log(men.size()) + executor.size();
|
|
2707
|
+
config.max_proposals = (std::min)(men.size(), config.max_proposals);
|
|
2708
|
+
|
|
2709
|
+
using proposals_count_t = std::uint16_t;
|
|
2710
|
+
using ids_allocator_t = typename allocator_traits_t::template rebind_alloc<id_t>;
|
|
2711
|
+
allocator_t& alloc = men.allocator_;
|
|
2712
|
+
|
|
2713
|
+
// Create an atomic queue, as a ring structure, from/to which
|
|
2714
|
+
// free men will be added/pulled.
|
|
2715
|
+
std::mutex free_men_mutex{};
|
|
2716
|
+
ring_gt<id_t, ids_allocator_t> free_men;
|
|
2717
|
+
free_men.reserve(men.size());
|
|
2718
|
+
for (std::size_t i = 0; i != men.size(); ++i)
|
|
2719
|
+
free_men.push(static_cast<id_t>(i));
|
|
2720
|
+
|
|
2721
|
+
// We are gonna need some temporary memory.
|
|
2722
|
+
proposals_count_t* proposal_counts = (proposals_count_t*)alloc.allocate(sizeof(proposals_count_t) * men.size());
|
|
2723
|
+
id_t* man_to_woman_ids = (id_t*)alloc.allocate(sizeof(id_t) * men.size());
|
|
2724
|
+
id_t* woman_to_man_ids = (id_t*)alloc.allocate(sizeof(id_t) * women.size());
|
|
2725
|
+
if (!proposal_counts || !man_to_woman_ids || !woman_to_man_ids)
|
|
2726
|
+
return result.failed("Can't temporary mappings");
|
|
2727
|
+
|
|
2728
|
+
id_t missing_id;
|
|
2729
|
+
std::memset((void*)&missing_id, 0xFF, sizeof(id_t));
|
|
2730
|
+
std::memset((void*)man_to_woman_ids, 0xFF, sizeof(id_t) * men.size());
|
|
2731
|
+
std::memset((void*)woman_to_man_ids, 0xFF, sizeof(id_t) * women.size());
|
|
2732
|
+
std::memset(proposal_counts, 0, sizeof(proposals_count_t) * men.size());
|
|
2733
|
+
|
|
2734
|
+
// Define locks, to limit concurrent accesses to `man_to_woman_ids` and `woman_to_man_ids`.
|
|
2735
|
+
visits_bitset_t men_locks, women_locks;
|
|
2736
|
+
if (!men_locks.resize(men.size()) || !women_locks.resize(women.size()))
|
|
2737
|
+
return result.failed("Can't allocate locks");
|
|
2738
|
+
|
|
2739
|
+
// Accumulate statistics from all the contexts,
|
|
2740
|
+
// to have a baseline to compare with, by the time the `join` is finished.
|
|
2741
|
+
std::size_t old_measurements{};
|
|
2742
|
+
std::size_t old_cycles{};
|
|
2743
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2744
|
+
old_measurements += women.contexts_[thread_idx].measurements_count;
|
|
2745
|
+
old_cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2746
|
+
}
|
|
2747
|
+
std::atomic<std::size_t> rounds{0};
|
|
2748
|
+
std::atomic<std::size_t> engagements{0};
|
|
2749
|
+
|
|
2750
|
+
// Concurrently process all the men
|
|
2751
|
+
executor.execute_bulk([&](std::size_t thread_idx) {
|
|
2752
|
+
context_t& context = women.contexts_[thread_idx];
|
|
2753
|
+
search_config_t search_config;
|
|
2754
|
+
search_config.expansion = config.expansion;
|
|
2755
|
+
search_config.exact = config.exact;
|
|
2756
|
+
search_config.thread = thread_idx;
|
|
2757
|
+
id_t free_man_id;
|
|
2758
|
+
|
|
2759
|
+
// While there exist a free man who still has a woman to propose to.
|
|
2760
|
+
while (true) {
|
|
2761
|
+
std::size_t passed_rounds = 0;
|
|
2762
|
+
std::size_t total_rounds = 0;
|
|
2763
|
+
{
|
|
2764
|
+
std::unique_lock<std::mutex> pop_lock(free_men_mutex);
|
|
2765
|
+
if (!free_men.try_pop(free_man_id))
|
|
2766
|
+
// Primary exit path, we have exhausted the list of candidates
|
|
2767
|
+
break;
|
|
2768
|
+
passed_rounds = ++rounds;
|
|
2769
|
+
total_rounds = passed_rounds + free_men.size();
|
|
2770
|
+
}
|
|
2771
|
+
progress(passed_rounds, total_rounds);
|
|
2772
|
+
while (men_locks.atomic_set(free_man_id))
|
|
2773
|
+
;
|
|
2774
|
+
|
|
2775
|
+
node_t free_man = men.node_with_id_(free_man_id);
|
|
2776
|
+
proposals_count_t& free_man_proposals = proposal_counts[free_man_id];
|
|
2777
|
+
if (free_man_proposals >= config.max_proposals)
|
|
2778
|
+
continue;
|
|
2779
|
+
|
|
2780
|
+
// Find the closest woman, to whom this man hasn't proposed yet.
|
|
2781
|
+
free_man_proposals++;
|
|
2782
|
+
search_result_t candidates = women.search(free_man.vector_view(), free_man_proposals, search_config);
|
|
2783
|
+
if (!candidates) {
|
|
2784
|
+
// TODO:
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
match_t match = candidates.back();
|
|
2788
|
+
member_cref_t woman = match.member;
|
|
2789
|
+
while (women_locks.atomic_set(woman.id))
|
|
2790
|
+
;
|
|
2791
|
+
|
|
2792
|
+
id_t husband_id = woman_to_man_ids[woman.id];
|
|
2793
|
+
bool woman_is_free = husband_id == missing_id;
|
|
2794
|
+
if (woman_is_free) {
|
|
2795
|
+
// Engagement
|
|
2796
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2797
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2798
|
+
engagements++;
|
|
2799
|
+
} else {
|
|
2800
|
+
distance_t distance_from_husband = context.measure(woman.vector, men.node_with_id_(husband_id));
|
|
2801
|
+
distance_t distance_from_candidate = match.distance;
|
|
2802
|
+
if (distance_from_husband > distance_from_candidate) {
|
|
2803
|
+
// Break-up
|
|
2804
|
+
while (men_locks.atomic_set(husband_id))
|
|
2805
|
+
;
|
|
2806
|
+
man_to_woman_ids[husband_id] = missing_id;
|
|
2807
|
+
men_locks.atomic_reset(husband_id);
|
|
2808
|
+
|
|
2809
|
+
// New Engagement
|
|
2810
|
+
man_to_woman_ids[free_man_id] = woman.id;
|
|
2811
|
+
woman_to_man_ids[woman.id] = free_man_id;
|
|
2812
|
+
engagements++;
|
|
2813
|
+
|
|
2814
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2815
|
+
free_men.push(husband_id);
|
|
2816
|
+
} else {
|
|
2817
|
+
std::unique_lock<std::mutex> push_lock(free_men_mutex);
|
|
2818
|
+
free_men.push(free_man_id);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
men_locks.atomic_reset(free_man_id);
|
|
2823
|
+
women_locks.atomic_reset(woman.id);
|
|
2824
|
+
}
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
// Export the IDs into labels:
|
|
2828
|
+
std::size_t intersection_size = 0;
|
|
2829
|
+
for (std::size_t i = 0; i != men.size(); ++i) {
|
|
2830
|
+
id_t woman_id = man_to_woman_ids[i];
|
|
2831
|
+
if (woman_id != missing_id) {
|
|
2832
|
+
label_t man = men.node_with_id_(i).label();
|
|
2833
|
+
label_t woman = women.node_with_id_(woman_id).label();
|
|
2834
|
+
man_to_woman[man] = woman;
|
|
2835
|
+
woman_to_man[woman] = man;
|
|
2836
|
+
intersection_size++;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// Deallocate memory
|
|
2841
|
+
alloc.deallocate((byte_t*)proposal_counts, sizeof(proposals_count_t) * men.size());
|
|
2842
|
+
alloc.deallocate((byte_t*)man_to_woman_ids, sizeof(id_t) * men.size());
|
|
2843
|
+
alloc.deallocate((byte_t*)woman_to_man_ids, sizeof(id_t) * women.size());
|
|
2844
|
+
|
|
2845
|
+
// Export stats
|
|
2846
|
+
result.engagements = engagements;
|
|
2847
|
+
result.intersection_size = intersection_size;
|
|
2848
|
+
for (std::size_t thread_idx = 0; thread_idx != executor.size(); ++thread_idx) {
|
|
2849
|
+
result.measurements += women.contexts_[thread_idx].measurements_count;
|
|
2850
|
+
result.cycles += women.contexts_[thread_idx].iteration_cycles;
|
|
2851
|
+
}
|
|
2852
|
+
result.measurements -= old_measurements;
|
|
2853
|
+
result.cycles -= old_cycles;
|
|
2854
|
+
return result;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2358
2857
|
void reset_view_() noexcept {
|
|
2359
2858
|
if (!viewed_file_)
|
|
2360
2859
|
return;
|
|
@@ -2384,51 +2883,80 @@ class index_gt {
|
|
|
2384
2883
|
return node_head_bytes_() + pre_.neighbors_base_bytes + pre_.neighbors_bytes * level + sizeof(scalar_t) * dim;
|
|
2385
2884
|
}
|
|
2386
2885
|
|
|
2387
|
-
|
|
2886
|
+
using span_bytes_t = span_gt<byte_t>;
|
|
2887
|
+
struct node_bytes_split_t {
|
|
2888
|
+
span_bytes_t tape{};
|
|
2889
|
+
span_bytes_t vector{};
|
|
2890
|
+
|
|
2891
|
+
node_bytes_split_t() {}
|
|
2892
|
+
node_bytes_split_t(span_bytes_t tape, span_bytes_t vector) noexcept : tape(tape), vector(vector) {}
|
|
2893
|
+
|
|
2894
|
+
std::size_t memory_usage() const noexcept { return tape.size() + vector.size(); }
|
|
2895
|
+
bool colocated() const noexcept { return tape.end() == vector.begin(); }
|
|
2896
|
+
operator node_t() const noexcept { return node_t{tape.begin(), reinterpret_cast<scalar_t*>(vector.begin())}; }
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
inline node_bytes_split_t node_bytes_split_(node_t node) const noexcept {
|
|
2388
2900
|
std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * node.level();
|
|
2389
|
-
|
|
2901
|
+
std::size_t bytes_in_tape = node_head_bytes_() + levels_bytes;
|
|
2902
|
+
return {{node.tape(), bytes_in_tape}, {(byte_t*)node.vector(), node_vector_bytes_(node)}};
|
|
2390
2903
|
}
|
|
2391
2904
|
|
|
2392
2905
|
inline std::size_t node_vector_bytes_(dim_t dim) const noexcept { return dim * sizeof(scalar_t); }
|
|
2393
2906
|
inline std::size_t node_vector_bytes_(node_t node) const noexcept { return node_vector_bytes_(node.dim()); }
|
|
2394
2907
|
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
if (viewed_file_)
|
|
2398
|
-
return;
|
|
2399
|
-
|
|
2400
|
-
node_t& node = nodes_[id];
|
|
2401
|
-
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_stored_(node);
|
|
2402
|
-
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2403
|
-
node = node_t{};
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
node_t node_malloc_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2908
|
+
node_bytes_split_t node_malloc_(dim_t dims_to_store, level_t level) noexcept {
|
|
2407
2909
|
|
|
2408
|
-
std::size_t
|
|
2409
|
-
std::size_t
|
|
2410
|
-
std::size_t
|
|
2411
|
-
node_vector_bytes_(static_cast<dim_t>(dim)) * std::size_t(!store_vector);
|
|
2910
|
+
std::size_t vector_bytes = node_vector_bytes_(dims_to_store);
|
|
2911
|
+
std::size_t node_bytes = node_bytes_(dims_to_store, level);
|
|
2912
|
+
std::size_t non_vector_bytes = node_bytes - vector_bytes;
|
|
2412
2913
|
|
|
2413
2914
|
byte_t* data = (byte_t*)point_allocator_.allocate(node_bytes);
|
|
2414
2915
|
if (!data)
|
|
2415
|
-
return {};
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (vector.data())
|
|
2419
|
-
std::memcpy(data + node_bytes - stored_vector_bytes, vector.data(), stored_vector_bytes);
|
|
2420
|
-
|
|
2421
|
-
scalar_t* scalars = store_vector //
|
|
2422
|
-
? (scalar_t*)(data + node_bytes - stored_vector_bytes)
|
|
2423
|
-
: (scalar_t*)(vector.data());
|
|
2916
|
+
return node_bytes_split_t{};
|
|
2917
|
+
return {{data, non_vector_bytes}, {data + non_vector_bytes, vector_bytes}};
|
|
2918
|
+
}
|
|
2424
2919
|
|
|
2425
|
-
|
|
2920
|
+
node_t node_make_(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept {
|
|
2921
|
+
node_bytes_split_t node_bytes = node_malloc_(vector.size() * store_vector, level);
|
|
2922
|
+
if (store_vector) {
|
|
2923
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.tape.size());
|
|
2924
|
+
std::memcpy(node_bytes.vector.data(), vector.data(), node_bytes.vector.size());
|
|
2925
|
+
} else {
|
|
2926
|
+
std::memset(node_bytes.tape.data(), 0, node_bytes.memory_usage());
|
|
2927
|
+
}
|
|
2928
|
+
node_t node = node_bytes;
|
|
2426
2929
|
node.label(label);
|
|
2427
|
-
node.dim(static_cast<dim_t>(
|
|
2930
|
+
node.dim(static_cast<dim_t>(vector.size()));
|
|
2428
2931
|
node.level(level);
|
|
2429
2932
|
return node;
|
|
2430
2933
|
}
|
|
2431
2934
|
|
|
2935
|
+
node_t node_make_copy_(node_bytes_split_t old_bytes) noexcept {
|
|
2936
|
+
if (old_bytes.colocated()) {
|
|
2937
|
+
byte_t* data = (byte_t*)point_allocator_.allocate(old_bytes.memory_usage());
|
|
2938
|
+
std::memcpy(data, old_bytes.tape.data(), old_bytes.memory_usage());
|
|
2939
|
+
return node_t{data, reinterpret_cast<scalar_t*>(data + old_bytes.tape.size())};
|
|
2940
|
+
} else {
|
|
2941
|
+
node_t old_node = old_bytes;
|
|
2942
|
+
node_bytes_split_t node_bytes = node_malloc_(old_node.vector_view().size(), old_node.level());
|
|
2943
|
+
std::memcpy(node_bytes.tape.data(), old_bytes.tape.data(), old_bytes.tape.size());
|
|
2944
|
+
std::memcpy(node_bytes.vector.data(), old_bytes.vector.data(), old_bytes.vector.size());
|
|
2945
|
+
return node_bytes;
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
void node_free_(std::size_t id) noexcept {
|
|
2950
|
+
|
|
2951
|
+
if (viewed_file_)
|
|
2952
|
+
return;
|
|
2953
|
+
|
|
2954
|
+
node_t& node = nodes_[id];
|
|
2955
|
+
std::size_t node_bytes = node_bytes_(node) - node_vector_bytes_(node) * !node_bytes_split_(node).colocated();
|
|
2956
|
+
point_allocator_.deallocate(node.tape(), node_bytes);
|
|
2957
|
+
node = node_t{};
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2432
2960
|
inline node_t node_with_id_(std::size_t idx) const noexcept { return nodes_[idx]; }
|
|
2433
2961
|
inline neighbors_ref_t neighbors_base_(node_t node) const noexcept { return {node.neighbors_tape()}; }
|
|
2434
2962
|
|
|
@@ -2453,6 +2981,23 @@ class index_gt {
|
|
|
2453
2981
|
return {nodes_mutexes_, idx};
|
|
2454
2982
|
}
|
|
2455
2983
|
|
|
2984
|
+
void connect_node_across_levels_( //
|
|
2985
|
+
id_t node_id, vector_view_t vector, //
|
|
2986
|
+
id_t entry_id, level_t max_level, level_t target_level, //
|
|
2987
|
+
add_config_t const& config, context_t& context) usearch_noexcept_m {
|
|
2988
|
+
|
|
2989
|
+
// Go down the level, tracking only the closest match
|
|
2990
|
+
id_t closest_id = search_for_one_(entry_id, vector, max_level, target_level, context);
|
|
2991
|
+
|
|
2992
|
+
// From `target_level` down perform proper extensive search
|
|
2993
|
+
for (level_t level = (std::min)(target_level, max_level); level >= 0; --level) {
|
|
2994
|
+
// TODO: Handle out of memory conditions
|
|
2995
|
+
search_to_insert_(closest_id, vector, level, config.expansion, context);
|
|
2996
|
+
closest_id = connect_new_node_(node_id, level, context);
|
|
2997
|
+
reconnect_neighbor_nodes_(node_id, level, context);
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2456
3001
|
id_t connect_new_node_(id_t new_id, level_t level, context_t& context) usearch_noexcept_m {
|
|
2457
3002
|
|
|
2458
3003
|
node_t new_node = node_with_id_(new_id);
|