usearch 0.1.10 → 0.2.2
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/release.yml +12 -1
- package/.vscode/launch.json +5 -4
- package/.vscode/settings.json +7 -1
- package/CMakeLists.txt +11 -6
- package/CMakeListsInstructions.md +39 -17
- package/Cargo.toml +1 -1
- package/Package.swift +40 -0
- package/README.md +36 -18
- package/VERSION +1 -1
- package/build.gradle +5 -1
- package/cmake/jemalloc.cmake +0 -2
- package/conanfile.py +18 -0
- package/include/usearch/usearch.hpp +337 -228
- package/javascript/lib.cpp +8 -2
- package/objc/USearchObjective.mm +150 -0
- package/objc/include/USearchObjective.h +69 -0
- package/package.json +1 -1
- package/pyproject.toml +18 -4
- package/python/bench.py +14 -2
- package/python/lib.cpp +353 -43
- package/python/test.py +10 -4
- package/python/usearch/__init__.py +0 -0
- package/rust/lib.cpp +13 -2
- package/setup.py +8 -3
- package/simsimd/include/simsimd/simsimd.h +15 -0
- package/src/advanced.hpp +232 -122
- package/src/bench.cpp +249 -79
- package/swift/Index+Sugar.swift +70 -0
- package/swift/Test.swift +20 -0
- package/swift/USearch.swift +8 -0
- package/java/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/java/gradle/wrapper/gradle-wrapper.properties +0 -6
- package/java/gradlew +0 -244
|
@@ -6,7 +6,30 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @copyright Copyright (c) 2023
|
|
8
8
|
*/
|
|
9
|
-
#
|
|
9
|
+
#ifndef UNUM_USEARCH_H
|
|
10
|
+
#define UNUM_USEARCH_H
|
|
11
|
+
|
|
12
|
+
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
|
|
13
|
+
#define _USE_MATH_DEFINES
|
|
14
|
+
|
|
15
|
+
#include <Windows.h>
|
|
16
|
+
|
|
17
|
+
#define usearch_pack_m
|
|
18
|
+
#define usearch_align_m __declspec(align(64))
|
|
19
|
+
#define WINDOWS
|
|
20
|
+
|
|
21
|
+
#else
|
|
22
|
+
#include <fcntl.h> // `fallocate`
|
|
23
|
+
#include <stdlib.h> // `posix_memalign`
|
|
24
|
+
#include <sys/mman.h> // `mmap`
|
|
25
|
+
#include <unistd.h> // `open`, `close`
|
|
26
|
+
|
|
27
|
+
#define usearch_pack_m __attribute__((packed))
|
|
28
|
+
#define usearch_align_m __attribute__((aligned(64)))
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
#include <sys/stat.h> // `fstat` for file size
|
|
32
|
+
|
|
10
33
|
#include <algorithm> // `std::sort_heap`
|
|
11
34
|
#include <atomic> // `std::atomic`
|
|
12
35
|
#include <bitset> // `std::bitset`
|
|
@@ -15,18 +38,10 @@
|
|
|
15
38
|
#include <cstring> // `std::memset`
|
|
16
39
|
#include <mutex> // `std::unique_lock` - replacement candidate
|
|
17
40
|
#include <random> // `std::default_random_engine` - replacement candidate
|
|
18
|
-
#include <
|
|
41
|
+
#include <stdexcept> // `std::runtime_exception`
|
|
19
42
|
#include <utility> // `std::exchange`
|
|
20
43
|
#include <vector> // `std::vector`
|
|
21
44
|
|
|
22
|
-
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
|
|
23
|
-
#else
|
|
24
|
-
#include <fcntl.h> // `fallocate`
|
|
25
|
-
#include <stdlib.h> // `posix_memalign`
|
|
26
|
-
#include <sys/mman.h> // `mmap`
|
|
27
|
-
#include <sys/stat.h> // `fstat` for file size
|
|
28
|
-
#endif
|
|
29
|
-
|
|
30
45
|
#if defined(__GNUC__)
|
|
31
46
|
// https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
|
|
32
47
|
// Zero means we are only going to read from that memory.
|
|
@@ -39,7 +54,7 @@
|
|
|
39
54
|
#define prefetch_m(ptr)
|
|
40
55
|
#endif
|
|
41
56
|
|
|
42
|
-
#
|
|
57
|
+
#if defined(NDEBUG)
|
|
43
58
|
#define assert_m(must_be_true, message)
|
|
44
59
|
#else
|
|
45
60
|
#define assert_m(must_be_true, message) \
|
|
@@ -48,21 +63,12 @@
|
|
|
48
63
|
}
|
|
49
64
|
#endif
|
|
50
65
|
|
|
51
|
-
#define usearch_align_m __attribute__((aligned(64)))
|
|
52
|
-
#define usearch_pack_m __attribute__((packed))
|
|
53
|
-
|
|
54
66
|
namespace unum {
|
|
55
67
|
namespace usearch {
|
|
56
68
|
|
|
57
69
|
using f64_t = double;
|
|
58
70
|
using f32_t = float;
|
|
59
71
|
|
|
60
|
-
/**
|
|
61
|
-
* GCC supports half-precision types.
|
|
62
|
-
* https://gcc.gnu.org/onlinedocs/gcc/Half-Precision.html
|
|
63
|
-
*/
|
|
64
|
-
enum class f16_t : std::uint16_t {};
|
|
65
|
-
|
|
66
72
|
template <typename at> at angle_to_radians(at angle) noexcept { return angle * M_PI / 180.f; }
|
|
67
73
|
|
|
68
74
|
template <typename at> at square(at value) noexcept { return value * value; }
|
|
@@ -79,11 +85,12 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
79
85
|
return v;
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
template <typename scalar_at> struct ip_gt {
|
|
88
|
+
template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
83
89
|
using scalar_t = scalar_at;
|
|
84
|
-
using
|
|
90
|
+
using result_t = result_at;
|
|
91
|
+
using result_type = result_t;
|
|
85
92
|
|
|
86
|
-
inline
|
|
93
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
87
94
|
result_type ab{};
|
|
88
95
|
#if defined(__GNUC__)
|
|
89
96
|
#pragma GCC ivdep
|
|
@@ -93,17 +100,18 @@ template <typename scalar_at> struct ip_gt {
|
|
|
93
100
|
#pragma omp simd reduction(+ : ab)
|
|
94
101
|
#endif
|
|
95
102
|
for (std::size_t i = 0; i != dim; ++i)
|
|
96
|
-
ab += a[i] * b[i];
|
|
103
|
+
ab += result_t(a[i]) * result_t(b[i]);
|
|
97
104
|
return 1 - ab;
|
|
98
105
|
}
|
|
99
106
|
};
|
|
100
107
|
|
|
101
|
-
template <typename scalar_at> struct cos_gt {
|
|
108
|
+
template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
102
109
|
using scalar_t = scalar_at;
|
|
103
|
-
using
|
|
110
|
+
using result_t = result_at;
|
|
111
|
+
using result_type = result_t;
|
|
104
112
|
|
|
105
|
-
inline
|
|
106
|
-
|
|
113
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
114
|
+
result_t ab{}, a2{}, b2{};
|
|
107
115
|
#if defined(__GNUC__)
|
|
108
116
|
#pragma GCC ivdep
|
|
109
117
|
#elif defined(__clang__)
|
|
@@ -112,17 +120,20 @@ template <typename scalar_at> struct cos_gt {
|
|
|
112
120
|
#pragma omp simd reduction(+ : ab, a2, b2)
|
|
113
121
|
#endif
|
|
114
122
|
for (std::size_t i = 0; i != dim; ++i)
|
|
115
|
-
ab += a[i] * b[i],
|
|
123
|
+
ab += result_t(a[i]) * result_t(b[i]), //
|
|
124
|
+
a2 += square<result_t>(a[i]), //
|
|
125
|
+
b2 += square<result_t>(b[i]);
|
|
116
126
|
return ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
117
127
|
}
|
|
118
128
|
};
|
|
119
129
|
|
|
120
|
-
template <typename scalar_at> struct
|
|
130
|
+
template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
|
|
121
131
|
using scalar_t = scalar_at;
|
|
122
|
-
using
|
|
132
|
+
using result_t = result_at;
|
|
133
|
+
using result_type = result_t;
|
|
123
134
|
|
|
124
|
-
inline
|
|
125
|
-
|
|
135
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
136
|
+
result_t ab_deltas_sq{};
|
|
126
137
|
#if defined(__GNUC__)
|
|
127
138
|
#pragma GCC ivdep
|
|
128
139
|
#elif defined(__clang__)
|
|
@@ -131,7 +142,7 @@ template <typename scalar_at> struct l2_squared_gt {
|
|
|
131
142
|
#pragma omp simd reduction(+ : ab_deltas_sq)
|
|
132
143
|
#endif
|
|
133
144
|
for (std::size_t i = 0; i != dim; ++i)
|
|
134
|
-
ab_deltas_sq += (a[i] -
|
|
145
|
+
ab_deltas_sq += square(result_t(a[i]) - result_t(b[i]));
|
|
135
146
|
return ab_deltas_sq;
|
|
136
147
|
}
|
|
137
148
|
};
|
|
@@ -140,13 +151,14 @@ template <typename scalar_at> struct l2_squared_gt {
|
|
|
140
151
|
* @brief Hamming distance computes the number of differing elements in
|
|
141
152
|
* two arrays. An example would be a chess board.
|
|
142
153
|
*/
|
|
143
|
-
template <typename scalar_at> struct hamming_gt {
|
|
154
|
+
template <typename scalar_at, typename result_at = std::size_t> struct hamming_gt {
|
|
144
155
|
using scalar_t = scalar_at;
|
|
145
|
-
using
|
|
156
|
+
using result_t = result_at;
|
|
157
|
+
using result_type = result_t;
|
|
146
158
|
|
|
147
|
-
inline
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
|
|
160
|
+
std::size_t = 0) const noexcept {
|
|
161
|
+
result_t matches{};
|
|
150
162
|
#if defined(__GNUC__)
|
|
151
163
|
#pragma GCC ivdep
|
|
152
164
|
#elif defined(__clang__)
|
|
@@ -165,15 +177,16 @@ template <typename scalar_at> struct hamming_gt {
|
|
|
165
177
|
* two arrays of integers. An example would be a textual document,
|
|
166
178
|
* tokenized and hashed into a fixed-capacity bitset.
|
|
167
179
|
*/
|
|
168
|
-
template <typename scalar_at> struct bit_hamming_gt {
|
|
180
|
+
template <typename scalar_at, typename result_at = std::size_t> struct bit_hamming_gt {
|
|
169
181
|
using scalar_t = scalar_at;
|
|
170
|
-
using
|
|
182
|
+
using result_t = result_at;
|
|
183
|
+
using result_type = result_t;
|
|
171
184
|
static_assert(std::is_unsigned<scalar_t>::value, "Hamming distance requires unsigned integral words");
|
|
172
185
|
static constexpr std::size_t bits_per_word_k = sizeof(scalar_t) * CHAR_BIT;
|
|
173
186
|
|
|
174
|
-
inline
|
|
175
|
-
|
|
176
|
-
|
|
187
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
|
|
188
|
+
std::size_t = 0) const noexcept {
|
|
189
|
+
result_t matches{};
|
|
177
190
|
#if defined(__GNUC__)
|
|
178
191
|
#pragma GCC ivdep
|
|
179
192
|
#elif defined(__clang__)
|
|
@@ -192,14 +205,15 @@ template <typename scalar_at> struct bit_hamming_gt {
|
|
|
192
205
|
* Can be used to compute the similarity between two textual documents
|
|
193
206
|
* using the IDs of tokens present in them.
|
|
194
207
|
*/
|
|
195
|
-
template <typename scalar_at> struct jaccard_gt {
|
|
208
|
+
template <typename scalar_at, typename result_at = scalar_at> struct jaccard_gt {
|
|
196
209
|
using scalar_t = scalar_at;
|
|
197
|
-
using
|
|
210
|
+
using result_t = result_at;
|
|
211
|
+
using result_type = result_t;
|
|
198
212
|
static_assert(!std::is_floating_point<scalar_t>::value, "Jaccard distance requires integral scalars");
|
|
199
213
|
|
|
200
|
-
inline
|
|
214
|
+
inline result_t operator()( //
|
|
201
215
|
scalar_t const* a, scalar_t const* b, std::size_t a_length, std::size_t b_length) const noexcept {
|
|
202
|
-
|
|
216
|
+
result_t intersection{};
|
|
203
217
|
std::size_t i{};
|
|
204
218
|
std::size_t j{};
|
|
205
219
|
while (i != a_length && j != b_length) {
|
|
@@ -216,14 +230,15 @@ template <typename scalar_at> struct jaccard_gt {
|
|
|
216
230
|
* Can be used to compute the similarity between two textual documents
|
|
217
231
|
* using the IDs of tokens present in them.
|
|
218
232
|
*/
|
|
219
|
-
template <typename scalar_at> struct pearson_correlation_gt {
|
|
233
|
+
template <typename scalar_at, typename result_at = scalar_at> struct pearson_correlation_gt {
|
|
220
234
|
using scalar_t = scalar_at;
|
|
221
|
-
using
|
|
235
|
+
using result_t = result_at;
|
|
236
|
+
using result_type = result_t;
|
|
222
237
|
|
|
223
|
-
inline
|
|
238
|
+
inline result_t operator()( //
|
|
224
239
|
scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
result_t a_sum{}, b_sum{}, ab_sum{};
|
|
241
|
+
result_t a_sq_sum{}, b_sq_sum{};
|
|
227
242
|
#if defined(__GNUC__)
|
|
228
243
|
#pragma GCC ivdep
|
|
229
244
|
#elif defined(__clang__)
|
|
@@ -238,8 +253,8 @@ template <typename scalar_at> struct pearson_correlation_gt {
|
|
|
238
253
|
a_sq_sum += a[i] * a[i];
|
|
239
254
|
b_sq_sum += b[i] * b[i];
|
|
240
255
|
}
|
|
241
|
-
|
|
242
|
-
|
|
256
|
+
result_t denom = std::sqrt((dim * a_sq_sum - a_sum * a_sum) * (dim * b_sq_sum - b_sum * b_sum));
|
|
257
|
+
result_t corr = (dim * ab_sum - a_sum * b_sum) / denom;
|
|
243
258
|
return -corr;
|
|
244
259
|
}
|
|
245
260
|
};
|
|
@@ -248,23 +263,25 @@ template <typename scalar_at> struct pearson_correlation_gt {
|
|
|
248
263
|
* @brief Haversine distance for the shortest distance between two nodes on
|
|
249
264
|
* the surface of a 3D sphere, defined with latitude and longitude.
|
|
250
265
|
*/
|
|
251
|
-
template <typename scalar_at> struct haversine_gt {
|
|
266
|
+
template <typename scalar_at, typename result_at = scalar_at> struct haversine_gt {
|
|
252
267
|
using scalar_t = scalar_at;
|
|
253
|
-
using
|
|
268
|
+
using result_t = result_at;
|
|
269
|
+
using result_type = result_t;
|
|
254
270
|
static_assert(!std::is_integral<scalar_t>::value, "Latitude and longitude must be floating-node");
|
|
255
271
|
|
|
256
|
-
inline
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t = 2, std::size_t = 2) const noexcept {
|
|
273
|
+
result_t lat_a = a[0], lon_a = a[1];
|
|
274
|
+
result_t lat_b = b[0], lon_b = b[1];
|
|
259
275
|
|
|
260
|
-
|
|
261
|
-
|
|
276
|
+
result_t lat_delta = angle_to_radians<result_t>(lat_b - lat_a);
|
|
277
|
+
result_t lon_delta = angle_to_radians<result_t>(lon_b - lon_a);
|
|
262
278
|
|
|
263
|
-
|
|
264
|
-
|
|
279
|
+
result_t converted_lat_a = angle_to_radians<result_t>(lat_a);
|
|
280
|
+
result_t converted_lat_b = angle_to_radians<result_t>(lat_b);
|
|
265
281
|
|
|
266
|
-
|
|
267
|
-
|
|
282
|
+
result_t x = //
|
|
283
|
+
square(std::sin(lat_delta / 2.f)) +
|
|
284
|
+
std::cos(converted_lat_a) * std::cos(converted_lat_b) * square(std::sin(lon_delta / 2.f));
|
|
268
285
|
|
|
269
286
|
return std::atan2(std::sqrt(x), std::sqrt(1.f - x));
|
|
270
287
|
}
|
|
@@ -336,11 +353,29 @@ class max_heap_gt {
|
|
|
336
353
|
max_heap_gt(std::size_t max_capacity = 0) noexcept
|
|
337
354
|
: elements_(nullptr), size_(0), capacity_(0), max_capacity_(max_capacity) {}
|
|
338
355
|
|
|
356
|
+
max_heap_gt(max_heap_gt&& other) noexcept {
|
|
357
|
+
std::swap(elements_, other.elements_);
|
|
358
|
+
std::swap(size_, other.size_);
|
|
359
|
+
std::swap(capacity_, other.capacity_);
|
|
360
|
+
std::swap(max_capacity_, other.max_capacity_);
|
|
361
|
+
}
|
|
362
|
+
max_heap_gt& operator=(max_heap_gt&& other) noexcept {
|
|
363
|
+
std::swap(elements_, other.elements_);
|
|
364
|
+
std::swap(size_, other.size_);
|
|
365
|
+
std::swap(capacity_, other.capacity_);
|
|
366
|
+
std::swap(max_capacity_, other.max_capacity_);
|
|
367
|
+
return *this;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
max_heap_gt(max_heap_gt const&) = delete;
|
|
371
|
+
max_heap_gt& operator=(max_heap_gt const&) = delete;
|
|
372
|
+
|
|
339
373
|
bool empty() const noexcept { return !size_; }
|
|
340
374
|
std::size_t size() const noexcept { return size_; }
|
|
341
375
|
std::size_t capacity() const noexcept { return capacity_; }
|
|
376
|
+
/// @brief Selects the largest element in the heap.
|
|
377
|
+
/// @return Reference to the stored element.
|
|
342
378
|
element_t const& top() const noexcept { return elements_[0]; }
|
|
343
|
-
element_t const& max() const noexcept { return elements_[0]; }
|
|
344
379
|
|
|
345
380
|
void clear() noexcept {
|
|
346
381
|
while (size_) {
|
|
@@ -355,9 +390,9 @@ class max_heap_gt {
|
|
|
355
390
|
if (max_capacity_ && capacity_ == max_capacity_)
|
|
356
391
|
return false;
|
|
357
392
|
|
|
358
|
-
auto new_capacity = std::max<std::size_t>(capacity_ * 2u, 16u);
|
|
393
|
+
auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
|
|
359
394
|
if (max_capacity_)
|
|
360
|
-
new_capacity = std::min(new_capacity, max_capacity_);
|
|
395
|
+
new_capacity = (std::min)(new_capacity, max_capacity_);
|
|
361
396
|
|
|
362
397
|
auto allocator = allocator_t{};
|
|
363
398
|
auto new_elements = allocator.allocate(new_capacity);
|
|
@@ -436,7 +471,12 @@ class max_heap_gt {
|
|
|
436
471
|
*
|
|
437
472
|
*/
|
|
438
473
|
class mutex_t {
|
|
474
|
+
#if defined(WINDOWS)
|
|
475
|
+
using slot_t = volatile LONG;
|
|
476
|
+
#else
|
|
439
477
|
using slot_t = std::int32_t;
|
|
478
|
+
#endif // WINDOWS
|
|
479
|
+
|
|
440
480
|
slot_t flag_;
|
|
441
481
|
|
|
442
482
|
public:
|
|
@@ -445,18 +485,32 @@ class mutex_t {
|
|
|
445
485
|
|
|
446
486
|
inline bool try_lock() noexcept {
|
|
447
487
|
slot_t raw = 0;
|
|
488
|
+
#if defined(WINDOWS)
|
|
489
|
+
return InterlockedCompareExchange(&flag_, 1, raw);
|
|
490
|
+
#else
|
|
448
491
|
return __atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED);
|
|
492
|
+
#endif // WINDOWS
|
|
449
493
|
}
|
|
450
494
|
|
|
451
495
|
inline void lock() noexcept {
|
|
452
|
-
slot_t raw;
|
|
496
|
+
slot_t raw = 0;
|
|
497
|
+
#if defined(WINDOWS)
|
|
498
|
+
InterlockedCompareExchange(&flag_, 1, raw);
|
|
499
|
+
#else
|
|
453
500
|
lock_again:
|
|
454
501
|
raw = 0;
|
|
455
502
|
if (!__atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
|
|
456
503
|
goto lock_again;
|
|
504
|
+
#endif // WINDOWS
|
|
457
505
|
}
|
|
458
506
|
|
|
459
|
-
inline void unlock() noexcept {
|
|
507
|
+
inline void unlock() noexcept {
|
|
508
|
+
#if defined(WINDOWS)
|
|
509
|
+
InterlockedExchange(&flag_, 0);
|
|
510
|
+
#else
|
|
511
|
+
__atomic_store_n(&flag_, 0, __ATOMIC_RELEASE);
|
|
512
|
+
#endif
|
|
513
|
+
}
|
|
460
514
|
};
|
|
461
515
|
|
|
462
516
|
static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than expected");
|
|
@@ -466,6 +520,10 @@ using lock_t = std::unique_lock<mutex_t>;
|
|
|
466
520
|
/**
|
|
467
521
|
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
468
522
|
*/
|
|
523
|
+
#if defined(WINDOWS)
|
|
524
|
+
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
525
|
+
#endif
|
|
526
|
+
|
|
469
527
|
class usearch_pack_m uint40_t {
|
|
470
528
|
unsigned char octets[5];
|
|
471
529
|
|
|
@@ -503,6 +561,9 @@ class usearch_pack_m uint40_t {
|
|
|
503
561
|
return old;
|
|
504
562
|
}
|
|
505
563
|
};
|
|
564
|
+
#if defined(WINDOWS)
|
|
565
|
+
#pragma pack(pop) // Reset alignment to default
|
|
566
|
+
#endif
|
|
506
567
|
|
|
507
568
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
508
569
|
|
|
@@ -547,8 +608,8 @@ struct config_t {
|
|
|
547
608
|
|
|
548
609
|
///
|
|
549
610
|
std::size_t max_elements = 0;
|
|
550
|
-
std::size_t max_threads_add =
|
|
551
|
-
std::size_t max_threads_search =
|
|
611
|
+
std::size_t max_threads_add = 0;
|
|
612
|
+
std::size_t max_threads_search = 0;
|
|
552
613
|
};
|
|
553
614
|
|
|
554
615
|
/**
|
|
@@ -623,13 +684,23 @@ class index_gt {
|
|
|
623
684
|
using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
|
|
624
685
|
|
|
625
686
|
struct neighbors_ref_t {
|
|
626
|
-
neighbors_count_t&
|
|
627
|
-
id_t*
|
|
687
|
+
neighbors_count_t& count_;
|
|
688
|
+
id_t* neighbors_{};
|
|
628
689
|
|
|
629
690
|
inline neighbors_ref_t(byte_t* tape) noexcept
|
|
630
|
-
:
|
|
691
|
+
: count_(*(neighbors_count_t*)tape), neighbors_((neighbors_count_t*)tape + 1) {}
|
|
692
|
+
inline id_t* begin() noexcept { return neighbors_; }
|
|
693
|
+
inline id_t* end() noexcept { return neighbors_ + count_; }
|
|
694
|
+
inline id_t const* begin() const noexcept { return neighbors_; }
|
|
695
|
+
inline id_t const* end() const noexcept { return neighbors_ + count_; }
|
|
696
|
+
inline id_t& operator[](std::size_t i) noexcept { return neighbors_[i]; }
|
|
697
|
+
inline id_t operator[](std::size_t i) const noexcept { return neighbors_[i]; }
|
|
698
|
+
inline std::size_t size() const noexcept { return count_; }
|
|
631
699
|
};
|
|
632
700
|
|
|
701
|
+
#if defined(WINDOWS)
|
|
702
|
+
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
703
|
+
#endif
|
|
633
704
|
struct usearch_pack_m node_head_t {
|
|
634
705
|
label_t label;
|
|
635
706
|
dim_t dim;
|
|
@@ -638,11 +709,21 @@ class index_gt {
|
|
|
638
709
|
// Each starts with a `neighbors_count_t` and is followed by such number of `id_t`s.
|
|
639
710
|
byte_t neighbors[1];
|
|
640
711
|
};
|
|
712
|
+
#if defined(WINDOWS)
|
|
713
|
+
#pragma pack(pop) // Reset alignment to default
|
|
714
|
+
#endif
|
|
715
|
+
|
|
641
716
|
static constexpr std::size_t head_bytes_k = sizeof(label_t) + sizeof(dim_t) + sizeof(level_t);
|
|
642
717
|
|
|
643
718
|
struct node_t {
|
|
644
|
-
byte_t* tape_;
|
|
645
|
-
scalar_t* vector_;
|
|
719
|
+
byte_t* tape_{};
|
|
720
|
+
scalar_t* vector_{};
|
|
721
|
+
|
|
722
|
+
explicit node_t(byte_t* tape, scalar_t* vector) noexcept : tape_(tape), vector_(vector) {}
|
|
723
|
+
|
|
724
|
+
node_t() = default;
|
|
725
|
+
node_t(node_t const&) = default;
|
|
726
|
+
node_t& operator=(node_t const&) = default;
|
|
646
727
|
};
|
|
647
728
|
|
|
648
729
|
class node_ref_t {
|
|
@@ -654,20 +735,21 @@ class index_gt {
|
|
|
654
735
|
|
|
655
736
|
inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
|
|
656
737
|
inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
|
|
657
|
-
inline operator node_t() const noexcept { return {mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
|
|
738
|
+
inline operator node_t() const noexcept { return node_t{mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
|
|
658
739
|
};
|
|
659
740
|
|
|
660
741
|
struct usearch_align_m thread_context_t {
|
|
661
742
|
distances_and_ids_t top_candidates;
|
|
662
743
|
distances_and_ids_t candidates_set;
|
|
663
|
-
distances_and_ids_t candidates_filter;
|
|
664
744
|
visits_bitset_t visits;
|
|
665
745
|
std::default_random_engine level_generator;
|
|
666
746
|
metric_t metric;
|
|
667
747
|
};
|
|
668
748
|
|
|
669
749
|
config_t config_{};
|
|
670
|
-
|
|
750
|
+
metric_t metric_{};
|
|
751
|
+
allocator_t allocator_{};
|
|
752
|
+
precomputed_constants_t pre_{};
|
|
671
753
|
int viewed_file_descriptor_{};
|
|
672
754
|
|
|
673
755
|
usearch_align_m mutable std::atomic<std::size_t> capacity_{};
|
|
@@ -681,7 +763,7 @@ class index_gt {
|
|
|
681
763
|
std::vector<node_t, node_allocator_t> nodes_{};
|
|
682
764
|
|
|
683
765
|
using thread_context_allocator_t = typename allocator_traits_t::template rebind_alloc<thread_context_t>;
|
|
684
|
-
mutable std::vector<thread_context_t, thread_context_allocator_t> thread_contexts_;
|
|
766
|
+
mutable std::vector<thread_context_t, thread_context_allocator_t> thread_contexts_{};
|
|
685
767
|
|
|
686
768
|
public:
|
|
687
769
|
std::size_t connectivity() const noexcept { return config_.connectivity; }
|
|
@@ -691,10 +773,11 @@ class index_gt {
|
|
|
691
773
|
bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
|
|
692
774
|
bool synchronize() const noexcept { return config_.max_threads_add > 1; }
|
|
693
775
|
|
|
694
|
-
index_gt(config_t config = {}, metric_t metric = {}, allocator_t = {})
|
|
776
|
+
index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
|
|
777
|
+
: config_(config), metric_(metric), allocator_(allocator) {
|
|
695
778
|
|
|
696
779
|
// Externally defined hyper-parameters:
|
|
697
|
-
config_.expansion_add = std::max(config_.expansion_add, config_.connectivity);
|
|
780
|
+
config_.expansion_add = (std::max)(config_.expansion_add, config_.connectivity);
|
|
698
781
|
pre_ = precompute(config);
|
|
699
782
|
|
|
700
783
|
// Configure initial empty state:
|
|
@@ -704,14 +787,23 @@ class index_gt {
|
|
|
704
787
|
viewed_file_descriptor_ = 0;
|
|
705
788
|
|
|
706
789
|
// Dynamic memory:
|
|
707
|
-
thread_contexts_.resize(std::max(config.max_threads_search, config.max_threads_add));
|
|
790
|
+
thread_contexts_.resize((std::max)(config.max_threads_search, config.max_threads_add));
|
|
708
791
|
for (thread_context_t& context : thread_contexts_)
|
|
709
792
|
context.metric = metric;
|
|
710
793
|
reserve(config.max_elements);
|
|
711
794
|
}
|
|
712
795
|
|
|
796
|
+
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
797
|
+
|
|
713
798
|
~index_gt() noexcept { clear(); }
|
|
714
799
|
|
|
800
|
+
index_gt(index_gt&& other) noexcept { swap(other); }
|
|
801
|
+
|
|
802
|
+
index_gt& operator=(index_gt&& other) noexcept {
|
|
803
|
+
swap(other);
|
|
804
|
+
return *this;
|
|
805
|
+
}
|
|
806
|
+
|
|
715
807
|
#pragma region Adjusting Configuration
|
|
716
808
|
|
|
717
809
|
void clear() noexcept {
|
|
@@ -723,6 +815,26 @@ class index_gt {
|
|
|
723
815
|
entry_id_ = 0u;
|
|
724
816
|
}
|
|
725
817
|
|
|
818
|
+
void swap(index_gt& other) noexcept {
|
|
819
|
+
std::swap(config_, other.config_);
|
|
820
|
+
std::swap(metric_, other.metric_);
|
|
821
|
+
std::swap(allocator_, other.allocator_);
|
|
822
|
+
std::swap(pre_, other.pre_);
|
|
823
|
+
std::swap(viewed_file_descriptor_, other.viewed_file_descriptor_);
|
|
824
|
+
std::swap(max_level_, other.max_level_);
|
|
825
|
+
std::swap(entry_id_, other.entry_id_);
|
|
826
|
+
std::swap(nodes_, other.nodes_);
|
|
827
|
+
std::swap(thread_contexts_, other.thread_contexts_);
|
|
828
|
+
|
|
829
|
+
// Non-atomic parts.
|
|
830
|
+
std::size_t capacity = capacity_;
|
|
831
|
+
std::size_t size = size_;
|
|
832
|
+
capacity_ = other.capacity_.load();
|
|
833
|
+
size_ = other.size_.load();
|
|
834
|
+
other.capacity_ = capacity;
|
|
835
|
+
other.size_ = size;
|
|
836
|
+
}
|
|
837
|
+
|
|
726
838
|
void reserve(std::size_t new_capacity) noexcept(false) {
|
|
727
839
|
|
|
728
840
|
assert_m(new_capacity >= size_, "Can't drop existing values");
|
|
@@ -776,31 +888,10 @@ class index_gt {
|
|
|
776
888
|
}
|
|
777
889
|
|
|
778
890
|
// Go down the level, tracking only the closest match
|
|
779
|
-
id_t closest_id = entry_id_;
|
|
780
|
-
distance_t closest_dist =
|
|
781
|
-
context.metric(new_vector, node(closest_id).vector, new_dim, node(closest_id).head.dim);
|
|
782
|
-
for (level_t level = max_level; level > new_target_level; level--) {
|
|
783
|
-
bool changed;
|
|
784
|
-
do {
|
|
785
|
-
changed = false;
|
|
786
|
-
node_ref_t closest_node = node(closest_id);
|
|
787
|
-
lock_t closest_lock = closest_node.lock();
|
|
788
|
-
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
789
|
-
iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
|
|
790
|
-
node_ref_t candidate_node = node(candidate_id);
|
|
791
|
-
distance_t candidate_dist =
|
|
792
|
-
context.metric(new_vector, candidate_node.vector, new_dim, candidate_node.head.dim);
|
|
793
|
-
if (candidate_dist < closest_dist) {
|
|
794
|
-
closest_dist = candidate_dist;
|
|
795
|
-
closest_id = candidate_id;
|
|
796
|
-
changed = true;
|
|
797
|
-
}
|
|
798
|
-
});
|
|
799
|
-
} while (changed);
|
|
800
|
-
}
|
|
891
|
+
id_t closest_id = search_for_one(entry_id_, new_vector, new_dim, max_level, new_target_level, context);
|
|
801
892
|
|
|
802
893
|
// From `new_target_level` down perform proper extensive search.
|
|
803
|
-
for (level_t level = std::min(new_target_level, max_level); level >= 0; level--) {
|
|
894
|
+
for (level_t level = (std::min)(new_target_level, max_level); level >= 0; level--) {
|
|
804
895
|
search_to_insert(closest_id, new_vector, new_dim, level, context);
|
|
805
896
|
closest_id = connect_new_element(new_id, level, context);
|
|
806
897
|
}
|
|
@@ -826,32 +917,11 @@ class index_gt {
|
|
|
826
917
|
|
|
827
918
|
// Go down the level, tracking only the closest match
|
|
828
919
|
thread_context_t& context = thread_contexts_[thread_idx];
|
|
829
|
-
id_t closest_id = entry_id_;
|
|
830
|
-
distance_t closest_dist =
|
|
831
|
-
context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
|
|
832
|
-
for (level_t level = max_level_; level > 0; level--) {
|
|
833
|
-
bool changed;
|
|
834
|
-
do {
|
|
835
|
-
changed = false;
|
|
836
|
-
node_ref_t closest_node = node(closest_id);
|
|
837
|
-
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
838
|
-
iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
|
|
839
|
-
node_ref_t candidate_node = node(candidate_id);
|
|
840
|
-
distance_t candidate_dist = context.metric( //
|
|
841
|
-
query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
|
|
842
|
-
if (candidate_dist < closest_dist) {
|
|
843
|
-
closest_dist = candidate_dist;
|
|
844
|
-
closest_id = candidate_id;
|
|
845
|
-
changed = true;
|
|
846
|
-
}
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
} while (changed);
|
|
850
|
-
}
|
|
920
|
+
id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
|
|
851
921
|
|
|
852
922
|
// For bottom layer we need a more optimized procedure
|
|
853
923
|
search_to_find_in_base( //
|
|
854
|
-
closest_id, query_vec, query_dim, std::max(config_.expansion_search, wanted), context);
|
|
924
|
+
closest_id, query_vec, query_dim, (std::max)(config_.expansion_search, wanted), context);
|
|
855
925
|
while (context.top_candidates.size() > wanted)
|
|
856
926
|
context.top_candidates.pop();
|
|
857
927
|
|
|
@@ -903,6 +973,10 @@ class index_gt {
|
|
|
903
973
|
void save(char const* file_path) const noexcept(false) {
|
|
904
974
|
|
|
905
975
|
state_t state;
|
|
976
|
+
// Check compatibility
|
|
977
|
+
state.bytes_per_label = sizeof(label_t);
|
|
978
|
+
state.bytes_per_id = sizeof(id_t);
|
|
979
|
+
// Describe state
|
|
906
980
|
state.connectivity = config_.connectivity;
|
|
907
981
|
state.size = size_;
|
|
908
982
|
state.entry_id = entry_id_;
|
|
@@ -925,12 +999,15 @@ class index_gt {
|
|
|
925
999
|
for (std::size_t i = 0; i != state.size; ++i) {
|
|
926
1000
|
node_ref_t node_ref = node(static_cast<id_t>(i));
|
|
927
1001
|
std::size_t bytes_to_dump = node_dump_size(node_ref.head.dim, node_ref.head.level);
|
|
928
|
-
std::size_t
|
|
1002
|
+
std::size_t bytes_in_vec = node_ref.head.dim * sizeof(scalar_t);
|
|
1003
|
+
// Dump just neighbors, as vectors may be in a disjoint location
|
|
1004
|
+
std::size_t written = std::fwrite(&node_ref.head, bytes_to_dump - bytes_in_vec, 1, file);
|
|
929
1005
|
if (!written) {
|
|
930
1006
|
std::fclose(file);
|
|
931
1007
|
throw std::runtime_error(std::strerror(errno));
|
|
932
1008
|
}
|
|
933
|
-
|
|
1009
|
+
// Dump the vector
|
|
1010
|
+
written = std::fwrite(node_ref.vector, bytes_in_vec, 1, file);
|
|
934
1011
|
if (!written) {
|
|
935
1012
|
std::fclose(file);
|
|
936
1013
|
throw std::runtime_error(std::strerror(errno));
|
|
@@ -956,13 +1033,22 @@ class index_gt {
|
|
|
956
1033
|
std::fclose(file);
|
|
957
1034
|
throw std::runtime_error(std::strerror(errno));
|
|
958
1035
|
}
|
|
1036
|
+
if (state.bytes_per_label != sizeof(label_t)) {
|
|
1037
|
+
std::fclose(file);
|
|
1038
|
+
throw std::runtime_error("Incompatible label type!");
|
|
1039
|
+
}
|
|
1040
|
+
if (state.bytes_per_id != sizeof(id_t)) {
|
|
1041
|
+
std::fclose(file);
|
|
1042
|
+
throw std::runtime_error("Incompatible ID type!");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
959
1045
|
config_.connectivity = state.connectivity;
|
|
960
1046
|
config_.max_elements = state.size;
|
|
961
1047
|
pre_ = precompute(config_);
|
|
962
1048
|
reserve(state.size);
|
|
963
1049
|
size_ = state.size;
|
|
964
|
-
max_level_ = state.max_level;
|
|
965
|
-
entry_id_ = state.entry_id;
|
|
1050
|
+
max_level_ = static_cast<level_t>(state.max_level);
|
|
1051
|
+
entry_id_ = static_cast<id_t>(state.entry_id);
|
|
966
1052
|
}
|
|
967
1053
|
|
|
968
1054
|
// Load nodes one by one
|
|
@@ -1021,14 +1107,23 @@ class index_gt {
|
|
|
1021
1107
|
// Read the header
|
|
1022
1108
|
{
|
|
1023
1109
|
std::memcpy(&state, file, sizeof(state));
|
|
1110
|
+
if (state.bytes_per_label != sizeof(label_t)) {
|
|
1111
|
+
close(descriptor);
|
|
1112
|
+
throw std::runtime_error("Incompatible label type!");
|
|
1113
|
+
}
|
|
1114
|
+
if (state.bytes_per_id != sizeof(id_t)) {
|
|
1115
|
+
close(descriptor);
|
|
1116
|
+
throw std::runtime_error("Incompatible ID type!");
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1024
1119
|
config_.connectivity = state.connectivity;
|
|
1025
1120
|
config_.max_elements = state.size;
|
|
1026
1121
|
config_.max_threads_add = 0;
|
|
1027
1122
|
pre_ = precompute(config_);
|
|
1028
1123
|
reserve(state.size);
|
|
1029
1124
|
size_ = state.size;
|
|
1030
|
-
max_level_ = state.max_level;
|
|
1031
|
-
entry_id_ = state.entry_id;
|
|
1125
|
+
max_level_ = static_cast<level_t>(state.max_level);
|
|
1126
|
+
entry_id_ = static_cast<id_t>(state.entry_id);
|
|
1032
1127
|
}
|
|
1033
1128
|
|
|
1034
1129
|
// Locate every node packed into file
|
|
@@ -1036,14 +1131,15 @@ class index_gt {
|
|
|
1036
1131
|
for (std::size_t i = 0; i != state.size; ++i) {
|
|
1037
1132
|
node_head_t const& head = *(node_head_t const*)(file + progress);
|
|
1038
1133
|
std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
|
|
1134
|
+
std::size_t bytes_in_vec = head.dim * sizeof(scalar_t);
|
|
1039
1135
|
nodes_[i].tape_ = (byte_t*)(file + progress);
|
|
1040
|
-
nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump -
|
|
1136
|
+
nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump - bytes_in_vec);
|
|
1041
1137
|
progress += bytes_to_dump;
|
|
1042
|
-
max_level_ = std::max(max_level_, head.level);
|
|
1138
|
+
max_level_ = (std::max)(max_level_, head.level);
|
|
1043
1139
|
}
|
|
1044
1140
|
|
|
1045
1141
|
viewed_file_descriptor_ = descriptor;
|
|
1046
|
-
#endif
|
|
1142
|
+
#endif // POSIX
|
|
1047
1143
|
}
|
|
1048
1144
|
|
|
1049
1145
|
#pragma endregion
|
|
@@ -1065,7 +1161,7 @@ class index_gt {
|
|
|
1065
1161
|
|
|
1066
1162
|
void node_free(std::size_t id) noexcept {
|
|
1067
1163
|
|
|
1068
|
-
if (viewed_file_descriptor_)
|
|
1164
|
+
if (viewed_file_descriptor_ != 0)
|
|
1069
1165
|
return;
|
|
1070
1166
|
|
|
1071
1167
|
// This function is rarely called and can be as expensive as needed for higher space-efficiency.
|
|
@@ -1084,7 +1180,7 @@ class index_gt {
|
|
|
1084
1180
|
;
|
|
1085
1181
|
|
|
1086
1182
|
allocator_t{}.deallocate(node.tape_, node_bytes);
|
|
1087
|
-
node = {};
|
|
1183
|
+
node = node_t{};
|
|
1088
1184
|
}
|
|
1089
1185
|
|
|
1090
1186
|
node_ref_t node_malloc( //
|
|
@@ -1118,9 +1214,9 @@ class index_gt {
|
|
|
1118
1214
|
return {*mutex, head, scalars};
|
|
1119
1215
|
}
|
|
1120
1216
|
|
|
1121
|
-
inline node_ref_t node(id_t id) const noexcept {
|
|
1217
|
+
inline node_ref_t node(id_t id) const noexcept { return node(nodes_[id]); }
|
|
1122
1218
|
|
|
1123
|
-
|
|
1219
|
+
inline node_ref_t node(node_t node) const noexcept {
|
|
1124
1220
|
byte_t* data = node.tape_;
|
|
1125
1221
|
mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
|
|
1126
1222
|
node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
|
|
@@ -1143,74 +1239,64 @@ class index_gt {
|
|
|
1143
1239
|
|
|
1144
1240
|
node_ref_t new_node = node(new_id);
|
|
1145
1241
|
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1146
|
-
std::size_t connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
|
|
1147
|
-
|
|
1242
|
+
std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
|
|
1243
|
+
span_gt<distance_and_id_t const> top = filter_heuristic(top_candidates, config_.connectivity, context.metric);
|
|
1148
1244
|
|
|
1149
|
-
distance_and_id_t const* const
|
|
1150
|
-
std::size_t const top_count =
|
|
1151
|
-
id_t next_closest_entry_id =
|
|
1152
|
-
distance_t next_closest_distance = top_unordered[0].first;
|
|
1245
|
+
distance_and_id_t const* const top_ordered = top.data();
|
|
1246
|
+
std::size_t const top_count = top.size();
|
|
1247
|
+
id_t const next_closest_entry_id = top_ordered[0].second;
|
|
1153
1248
|
|
|
1154
1249
|
// Outgoing links from `new_id`:
|
|
1155
1250
|
{
|
|
1156
1251
|
neighbors_ref_t new_neighbors = neighbors(new_node, level);
|
|
1157
|
-
assert_m(!new_neighbors.
|
|
1252
|
+
assert_m(!new_neighbors.count_, "The newly inserted element should have blank link list");
|
|
1158
1253
|
|
|
1159
|
-
new_neighbors.
|
|
1254
|
+
new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
|
|
1160
1255
|
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1161
|
-
assert_m(!new_neighbors
|
|
1162
|
-
assert_m(level <= node(
|
|
1163
|
-
|
|
1164
|
-
new_neighbors.neighbors[idx] = top_unordered[idx].second;
|
|
1165
|
-
if (top_unordered[idx].first < next_closest_distance) {
|
|
1166
|
-
next_closest_entry_id = top_unordered[idx].second;
|
|
1167
|
-
next_closest_distance = top_unordered[idx].first;
|
|
1168
|
-
}
|
|
1256
|
+
assert_m(!new_neighbors[idx], "Possible memory corruption");
|
|
1257
|
+
assert_m(level <= node(top_ordered[idx].second).head.level, "Linking to missing level");
|
|
1258
|
+
new_neighbors[idx] = top_ordered[idx].second;
|
|
1169
1259
|
}
|
|
1170
1260
|
}
|
|
1171
1261
|
|
|
1172
1262
|
// Reverse links from the neighbors:
|
|
1173
1263
|
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1174
|
-
id_t close_id =
|
|
1264
|
+
id_t close_id = top_ordered[idx].second;
|
|
1175
1265
|
node_ref_t close_node = node(close_id);
|
|
1176
1266
|
lock_t close_lock = close_node.lock();
|
|
1177
1267
|
|
|
1178
1268
|
neighbors_ref_t close_header = neighbors(close_node, level);
|
|
1179
|
-
assert_m(close_header.
|
|
1269
|
+
assert_m(close_header.count_ <= connectivity_max, "Possible corruption");
|
|
1180
1270
|
assert_m(close_id != new_id, "Self-loops are impossible");
|
|
1181
1271
|
assert_m(level <= close_node.head.level, "Linking to missing level");
|
|
1182
1272
|
|
|
1183
1273
|
// If `new_id` is already present in the neighboring connections of `close_id`
|
|
1184
1274
|
// then no need to modify any connections or run the heuristics.
|
|
1185
|
-
if (close_header.
|
|
1186
|
-
close_header
|
|
1187
|
-
close_header.
|
|
1275
|
+
if (close_header.count_ < connectivity_max) {
|
|
1276
|
+
close_header[close_header.count_] = new_id;
|
|
1277
|
+
close_header.count_++;
|
|
1188
1278
|
continue;
|
|
1189
1279
|
}
|
|
1190
1280
|
|
|
1191
1281
|
// To fit a new connection we need to drop an existing one.
|
|
1192
|
-
distances_and_ids_t& candidates = context.
|
|
1282
|
+
distances_and_ids_t& candidates = context.candidates_set;
|
|
1193
1283
|
candidates.clear();
|
|
1194
1284
|
candidates.emplace( //
|
|
1195
1285
|
context.metric( //
|
|
1196
1286
|
new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
|
|
1197
1287
|
new_id);
|
|
1198
|
-
|
|
1288
|
+
for (id_t successor_id : close_header) {
|
|
1199
1289
|
node_ref_t successor_node = node(successor_id);
|
|
1200
1290
|
candidates.emplace( //
|
|
1201
1291
|
context.metric( //
|
|
1202
1292
|
successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
|
|
1203
1293
|
successor_id);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1294
|
+
}
|
|
1295
|
+
span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
|
|
1206
1296
|
|
|
1207
1297
|
// Export the results:
|
|
1208
|
-
close_header.
|
|
1209
|
-
|
|
1210
|
-
close_header.neighbors[close_header.count] = candidates.top().second;
|
|
1211
|
-
close_header.count++;
|
|
1212
|
-
candidates.pop();
|
|
1213
|
-
}
|
|
1298
|
+
for (close_header.count_ = 0u; close_header.count_ != top.size(); ++close_header.count_)
|
|
1299
|
+
close_header[close_header.count_] = top[close_header.count_].second;
|
|
1214
1300
|
}
|
|
1215
1301
|
|
|
1216
1302
|
return next_closest_entry_id;
|
|
@@ -1222,13 +1308,42 @@ class index_gt {
|
|
|
1222
1308
|
return (level_t)r;
|
|
1223
1309
|
}
|
|
1224
1310
|
|
|
1225
|
-
|
|
1226
|
-
id_t
|
|
1311
|
+
id_t search_for_one( //
|
|
1312
|
+
id_t entry_id, scalar_t const* query_vec, std::size_t query_dim, //
|
|
1313
|
+
level_t begin_level, level_t end_level, thread_context_t& context) const noexcept {
|
|
1314
|
+
|
|
1315
|
+
id_t closest_id = entry_id;
|
|
1316
|
+
distance_t closest_dist =
|
|
1317
|
+
context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
|
|
1318
|
+
for (level_t level = begin_level; level > end_level; level--) {
|
|
1319
|
+
bool changed;
|
|
1320
|
+
do {
|
|
1321
|
+
changed = false;
|
|
1322
|
+
node_ref_t closest_node = node(closest_id);
|
|
1323
|
+
lock_t closest_lock = closest_node.lock();
|
|
1324
|
+
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
1325
|
+
for (id_t candidate_id : closest_header) {
|
|
1326
|
+
node_ref_t candidate_node = node(candidate_id);
|
|
1327
|
+
distance_t candidate_dist =
|
|
1328
|
+
context.metric(query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
|
|
1329
|
+
if (candidate_dist < closest_dist) {
|
|
1330
|
+
closest_dist = candidate_dist;
|
|
1331
|
+
closest_id = candidate_id;
|
|
1332
|
+
changed = true;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
} while (changed);
|
|
1336
|
+
}
|
|
1337
|
+
return closest_id;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
void search_to_insert( //
|
|
1341
|
+
id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
|
|
1227
1342
|
level_t level, thread_context_t& context) noexcept(false) {
|
|
1228
1343
|
|
|
1229
1344
|
visits_bitset_t& visits = context.visits;
|
|
1230
|
-
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1231
|
-
distances_and_ids_t& candidates_set = context.candidates_set;
|
|
1345
|
+
distances_and_ids_t& top_candidates = context.top_candidates; // pop max, push
|
|
1346
|
+
distances_and_ids_t& candidates_set = context.candidates_set; // pop min, push
|
|
1232
1347
|
|
|
1233
1348
|
top_candidates.clear();
|
|
1234
1349
|
candidates_set.clear();
|
|
@@ -1251,9 +1366,10 @@ class index_gt {
|
|
|
1251
1366
|
lock_t candidate_lock = candidate_node.lock();
|
|
1252
1367
|
neighbors_ref_t candidate_header = neighbors(candidate_node, level);
|
|
1253
1368
|
|
|
1254
|
-
|
|
1369
|
+
prefetch_neighbors(candidate_header, visits);
|
|
1370
|
+
for (id_t successor_id : candidate_header) {
|
|
1255
1371
|
if (visits.test(successor_id))
|
|
1256
|
-
|
|
1372
|
+
continue;
|
|
1257
1373
|
|
|
1258
1374
|
visits.set(successor_id);
|
|
1259
1375
|
node_ref_t successor_node = node(successor_id);
|
|
@@ -1268,17 +1384,17 @@ class index_gt {
|
|
|
1268
1384
|
if (!top_candidates.empty())
|
|
1269
1385
|
closest_dist = top_candidates.top().first;
|
|
1270
1386
|
}
|
|
1271
|
-
}
|
|
1387
|
+
}
|
|
1272
1388
|
}
|
|
1273
1389
|
}
|
|
1274
1390
|
|
|
1275
|
-
void search_to_find_in_base(
|
|
1276
|
-
id_t start_id, scalar_t const* query_vec,
|
|
1391
|
+
void search_to_find_in_base( //
|
|
1392
|
+
id_t start_id, scalar_t const* query_vec, std::size_t query_dim, //
|
|
1277
1393
|
std::size_t expansion, thread_context_t& context) const noexcept(false) {
|
|
1278
1394
|
|
|
1279
1395
|
visits_bitset_t& visits = context.visits;
|
|
1280
|
-
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1281
|
-
distances_and_ids_t& candidates_set = context.candidates_set;
|
|
1396
|
+
distances_and_ids_t& top_candidates = context.top_candidates; // pop max, push
|
|
1397
|
+
distances_and_ids_t& candidates_set = context.candidates_set; // pop min, push
|
|
1282
1398
|
|
|
1283
1399
|
visits.clear();
|
|
1284
1400
|
top_candidates.clear();
|
|
@@ -1300,9 +1416,10 @@ class index_gt {
|
|
|
1300
1416
|
id_t candidate_id = current_node_node.second;
|
|
1301
1417
|
neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
|
|
1302
1418
|
|
|
1303
|
-
|
|
1419
|
+
prefetch_neighbors(candidate_header, visits);
|
|
1420
|
+
for (id_t successor_id : candidate_header) {
|
|
1304
1421
|
if (visits.test(successor_id))
|
|
1305
|
-
|
|
1422
|
+
continue;
|
|
1306
1423
|
|
|
1307
1424
|
visits.set(successor_id);
|
|
1308
1425
|
node_ref_t successor_node = node(successor_id);
|
|
@@ -1318,60 +1435,52 @@ class index_gt {
|
|
|
1318
1435
|
if (!top_candidates.empty())
|
|
1319
1436
|
closest_dist = top_candidates.top().first;
|
|
1320
1437
|
}
|
|
1321
|
-
}
|
|
1438
|
+
}
|
|
1322
1439
|
}
|
|
1323
1440
|
}
|
|
1324
1441
|
|
|
1325
|
-
|
|
1326
|
-
* @brief A simple `for`-loop that prefetches vectors of neighbors.
|
|
1327
|
-
*/
|
|
1328
|
-
template <typename neighbor_id_callback_at>
|
|
1329
|
-
inline void iterate_through_neighbors(neighbors_ref_t head, neighbor_id_callback_at&& callback) const noexcept {
|
|
1330
|
-
std::size_t n = head.count;
|
|
1331
|
-
for (std::size_t j = 0; j != n; j++)
|
|
1332
|
-
callback(head.neighbors[j]);
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
void filter_top_candidates_with_heuristic( //
|
|
1336
|
-
distances_and_ids_t& top_candidates, distances_and_ids_t& temporary, std::size_t needed,
|
|
1337
|
-
thread_context_t& context) const noexcept(false) {
|
|
1338
|
-
|
|
1339
|
-
if (top_candidates.size() < needed)
|
|
1340
|
-
return;
|
|
1341
|
-
|
|
1342
|
-
// TODO: Sort ascending, then run an inplace triangular reduction.
|
|
1343
|
-
temporary.clear();
|
|
1344
|
-
while (top_candidates.size()) {
|
|
1345
|
-
temporary.emplace(-top_candidates.top().first, top_candidates.top().second);
|
|
1346
|
-
top_candidates.pop();
|
|
1347
|
-
}
|
|
1442
|
+
void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|
|
1348
1443
|
|
|
1349
|
-
|
|
1444
|
+
span_gt<distance_and_id_t const> filter_heuristic( //
|
|
1445
|
+
distances_and_ids_t& top_candidates, std::size_t needed, metric_t const& metric) const noexcept {
|
|
1350
1446
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1447
|
+
top_candidates.sort_ascending();
|
|
1448
|
+
distance_and_id_t* top_ordered = top_candidates.data();
|
|
1449
|
+
std::size_t const top_count = top_candidates.size();
|
|
1450
|
+
if (top_count < needed)
|
|
1451
|
+
return {top_ordered, top_count};
|
|
1452
|
+
|
|
1453
|
+
std::size_t submitted_count = 1;
|
|
1454
|
+
std::size_t consumed_count = 1; /// Always equal or greater than `submitted_count`.
|
|
1455
|
+
while (submitted_count < needed && consumed_count < top_count) {
|
|
1456
|
+
distance_and_id_t candidate = top_ordered[consumed_count];
|
|
1457
|
+
node_ref_t candidate_node = node(candidate.second);
|
|
1458
|
+
distance_t candidate_dist = candidate.first;
|
|
1354
1459
|
bool good = true;
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
distance_t inter_result_dist =
|
|
1363
|
-
context.metric(other_node.vector, best_node.vector, other_node.head.dim, best_node.head.dim);
|
|
1364
|
-
if (inter_result_dist < dist_to_query) {
|
|
1460
|
+
for (std::size_t idx = 0; idx < submitted_count; idx++) {
|
|
1461
|
+
distance_and_id_t submitted = top_ordered[idx];
|
|
1462
|
+
node_ref_t submitted_node = node(submitted.second);
|
|
1463
|
+
distance_t inter_result_dist = metric( //
|
|
1464
|
+
submitted_node.vector, candidate_node.vector, //
|
|
1465
|
+
submitted_node.head.dim, candidate_node.head.dim);
|
|
1466
|
+
if (inter_result_dist < candidate_dist) {
|
|
1365
1467
|
good = false;
|
|
1366
1468
|
break;
|
|
1367
1469
|
}
|
|
1368
1470
|
}
|
|
1369
1471
|
|
|
1370
|
-
if (good)
|
|
1371
|
-
|
|
1472
|
+
if (good) {
|
|
1473
|
+
top_ordered[submitted_count] = top_ordered[consumed_count];
|
|
1474
|
+
submitted_count++;
|
|
1475
|
+
}
|
|
1476
|
+
consumed_count++;
|
|
1372
1477
|
}
|
|
1478
|
+
|
|
1479
|
+
return {top_ordered, submitted_count};
|
|
1373
1480
|
}
|
|
1374
1481
|
};
|
|
1375
1482
|
|
|
1376
1483
|
} // namespace usearch
|
|
1377
1484
|
} // namespace unum
|
|
1485
|
+
|
|
1486
|
+
#endif
|