usearch 0.2.0 → 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 +2 -1
- package/CMakeLists.txt +11 -6
- package/CMakeListsInstructions.md +39 -17
- package/Cargo.toml +1 -1
- package/Package.swift +40 -0
- package/README.md +24 -11
- 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 +335 -227
- 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/lib.cpp +6 -6
- package/python/test.py +4 -5
- 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 +217 -117
- package/src/bench.cpp +236 -81
- 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,6 +353,23 @@ 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_; }
|
|
@@ -356,9 +390,9 @@ class max_heap_gt {
|
|
|
356
390
|
if (max_capacity_ && capacity_ == max_capacity_)
|
|
357
391
|
return false;
|
|
358
392
|
|
|
359
|
-
auto new_capacity = std::max<std::size_t>(capacity_ * 2u, 16u);
|
|
393
|
+
auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
|
|
360
394
|
if (max_capacity_)
|
|
361
|
-
new_capacity = std::min(new_capacity, max_capacity_);
|
|
395
|
+
new_capacity = (std::min)(new_capacity, max_capacity_);
|
|
362
396
|
|
|
363
397
|
auto allocator = allocator_t{};
|
|
364
398
|
auto new_elements = allocator.allocate(new_capacity);
|
|
@@ -437,7 +471,12 @@ class max_heap_gt {
|
|
|
437
471
|
*
|
|
438
472
|
*/
|
|
439
473
|
class mutex_t {
|
|
474
|
+
#if defined(WINDOWS)
|
|
475
|
+
using slot_t = volatile LONG;
|
|
476
|
+
#else
|
|
440
477
|
using slot_t = std::int32_t;
|
|
478
|
+
#endif // WINDOWS
|
|
479
|
+
|
|
441
480
|
slot_t flag_;
|
|
442
481
|
|
|
443
482
|
public:
|
|
@@ -446,18 +485,32 @@ class mutex_t {
|
|
|
446
485
|
|
|
447
486
|
inline bool try_lock() noexcept {
|
|
448
487
|
slot_t raw = 0;
|
|
488
|
+
#if defined(WINDOWS)
|
|
489
|
+
return InterlockedCompareExchange(&flag_, 1, raw);
|
|
490
|
+
#else
|
|
449
491
|
return __atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED);
|
|
492
|
+
#endif // WINDOWS
|
|
450
493
|
}
|
|
451
494
|
|
|
452
495
|
inline void lock() noexcept {
|
|
453
|
-
slot_t raw;
|
|
496
|
+
slot_t raw = 0;
|
|
497
|
+
#if defined(WINDOWS)
|
|
498
|
+
InterlockedCompareExchange(&flag_, 1, raw);
|
|
499
|
+
#else
|
|
454
500
|
lock_again:
|
|
455
501
|
raw = 0;
|
|
456
502
|
if (!__atomic_compare_exchange_n(&flag_, &raw, 1, true, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED))
|
|
457
503
|
goto lock_again;
|
|
504
|
+
#endif // WINDOWS
|
|
458
505
|
}
|
|
459
506
|
|
|
460
|
-
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
|
+
}
|
|
461
514
|
};
|
|
462
515
|
|
|
463
516
|
static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than expected");
|
|
@@ -467,6 +520,10 @@ using lock_t = std::unique_lock<mutex_t>;
|
|
|
467
520
|
/**
|
|
468
521
|
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
469
522
|
*/
|
|
523
|
+
#if defined(WINDOWS)
|
|
524
|
+
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
525
|
+
#endif
|
|
526
|
+
|
|
470
527
|
class usearch_pack_m uint40_t {
|
|
471
528
|
unsigned char octets[5];
|
|
472
529
|
|
|
@@ -504,6 +561,9 @@ class usearch_pack_m uint40_t {
|
|
|
504
561
|
return old;
|
|
505
562
|
}
|
|
506
563
|
};
|
|
564
|
+
#if defined(WINDOWS)
|
|
565
|
+
#pragma pack(pop) // Reset alignment to default
|
|
566
|
+
#endif
|
|
507
567
|
|
|
508
568
|
static_assert(sizeof(uint40_t) == 5, "uint40_t must be exactly 5 bytes");
|
|
509
569
|
|
|
@@ -548,8 +608,8 @@ struct config_t {
|
|
|
548
608
|
|
|
549
609
|
///
|
|
550
610
|
std::size_t max_elements = 0;
|
|
551
|
-
std::size_t max_threads_add =
|
|
552
|
-
std::size_t max_threads_search =
|
|
611
|
+
std::size_t max_threads_add = 0;
|
|
612
|
+
std::size_t max_threads_search = 0;
|
|
553
613
|
};
|
|
554
614
|
|
|
555
615
|
/**
|
|
@@ -624,13 +684,23 @@ class index_gt {
|
|
|
624
684
|
using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
|
|
625
685
|
|
|
626
686
|
struct neighbors_ref_t {
|
|
627
|
-
neighbors_count_t&
|
|
628
|
-
id_t*
|
|
687
|
+
neighbors_count_t& count_;
|
|
688
|
+
id_t* neighbors_{};
|
|
629
689
|
|
|
630
690
|
inline neighbors_ref_t(byte_t* tape) noexcept
|
|
631
|
-
:
|
|
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_; }
|
|
632
699
|
};
|
|
633
700
|
|
|
701
|
+
#if defined(WINDOWS)
|
|
702
|
+
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
703
|
+
#endif
|
|
634
704
|
struct usearch_pack_m node_head_t {
|
|
635
705
|
label_t label;
|
|
636
706
|
dim_t dim;
|
|
@@ -639,11 +709,21 @@ class index_gt {
|
|
|
639
709
|
// Each starts with a `neighbors_count_t` and is followed by such number of `id_t`s.
|
|
640
710
|
byte_t neighbors[1];
|
|
641
711
|
};
|
|
712
|
+
#if defined(WINDOWS)
|
|
713
|
+
#pragma pack(pop) // Reset alignment to default
|
|
714
|
+
#endif
|
|
715
|
+
|
|
642
716
|
static constexpr std::size_t head_bytes_k = sizeof(label_t) + sizeof(dim_t) + sizeof(level_t);
|
|
643
717
|
|
|
644
718
|
struct node_t {
|
|
645
|
-
byte_t* tape_;
|
|
646
|
-
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;
|
|
647
727
|
};
|
|
648
728
|
|
|
649
729
|
class node_ref_t {
|
|
@@ -655,20 +735,21 @@ class index_gt {
|
|
|
655
735
|
|
|
656
736
|
inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
|
|
657
737
|
inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
|
|
658
|
-
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}; }
|
|
659
739
|
};
|
|
660
740
|
|
|
661
741
|
struct usearch_align_m thread_context_t {
|
|
662
742
|
distances_and_ids_t top_candidates;
|
|
663
743
|
distances_and_ids_t candidates_set;
|
|
664
|
-
distances_and_ids_t candidates_filter;
|
|
665
744
|
visits_bitset_t visits;
|
|
666
745
|
std::default_random_engine level_generator;
|
|
667
746
|
metric_t metric;
|
|
668
747
|
};
|
|
669
748
|
|
|
670
749
|
config_t config_{};
|
|
671
|
-
|
|
750
|
+
metric_t metric_{};
|
|
751
|
+
allocator_t allocator_{};
|
|
752
|
+
precomputed_constants_t pre_{};
|
|
672
753
|
int viewed_file_descriptor_{};
|
|
673
754
|
|
|
674
755
|
usearch_align_m mutable std::atomic<std::size_t> capacity_{};
|
|
@@ -682,7 +763,7 @@ class index_gt {
|
|
|
682
763
|
std::vector<node_t, node_allocator_t> nodes_{};
|
|
683
764
|
|
|
684
765
|
using thread_context_allocator_t = typename allocator_traits_t::template rebind_alloc<thread_context_t>;
|
|
685
|
-
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_{};
|
|
686
767
|
|
|
687
768
|
public:
|
|
688
769
|
std::size_t connectivity() const noexcept { return config_.connectivity; }
|
|
@@ -692,10 +773,11 @@ class index_gt {
|
|
|
692
773
|
bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
|
|
693
774
|
bool synchronize() const noexcept { return config_.max_threads_add > 1; }
|
|
694
775
|
|
|
695
|
-
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) {
|
|
696
778
|
|
|
697
779
|
// Externally defined hyper-parameters:
|
|
698
|
-
config_.expansion_add = std::max(config_.expansion_add, config_.connectivity);
|
|
780
|
+
config_.expansion_add = (std::max)(config_.expansion_add, config_.connectivity);
|
|
699
781
|
pre_ = precompute(config);
|
|
700
782
|
|
|
701
783
|
// Configure initial empty state:
|
|
@@ -705,14 +787,23 @@ class index_gt {
|
|
|
705
787
|
viewed_file_descriptor_ = 0;
|
|
706
788
|
|
|
707
789
|
// Dynamic memory:
|
|
708
|
-
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));
|
|
709
791
|
for (thread_context_t& context : thread_contexts_)
|
|
710
792
|
context.metric = metric;
|
|
711
793
|
reserve(config.max_elements);
|
|
712
794
|
}
|
|
713
795
|
|
|
796
|
+
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
797
|
+
|
|
714
798
|
~index_gt() noexcept { clear(); }
|
|
715
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
|
+
|
|
716
807
|
#pragma region Adjusting Configuration
|
|
717
808
|
|
|
718
809
|
void clear() noexcept {
|
|
@@ -724,6 +815,26 @@ class index_gt {
|
|
|
724
815
|
entry_id_ = 0u;
|
|
725
816
|
}
|
|
726
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
|
+
|
|
727
838
|
void reserve(std::size_t new_capacity) noexcept(false) {
|
|
728
839
|
|
|
729
840
|
assert_m(new_capacity >= size_, "Can't drop existing values");
|
|
@@ -777,31 +888,10 @@ class index_gt {
|
|
|
777
888
|
}
|
|
778
889
|
|
|
779
890
|
// Go down the level, tracking only the closest match
|
|
780
|
-
id_t closest_id = entry_id_;
|
|
781
|
-
distance_t closest_dist =
|
|
782
|
-
context.metric(new_vector, node(closest_id).vector, new_dim, node(closest_id).head.dim);
|
|
783
|
-
for (level_t level = max_level; level > new_target_level; level--) {
|
|
784
|
-
bool changed;
|
|
785
|
-
do {
|
|
786
|
-
changed = false;
|
|
787
|
-
node_ref_t closest_node = node(closest_id);
|
|
788
|
-
lock_t closest_lock = closest_node.lock();
|
|
789
|
-
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
790
|
-
iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
|
|
791
|
-
node_ref_t candidate_node = node(candidate_id);
|
|
792
|
-
distance_t candidate_dist =
|
|
793
|
-
context.metric(new_vector, candidate_node.vector, new_dim, candidate_node.head.dim);
|
|
794
|
-
if (candidate_dist < closest_dist) {
|
|
795
|
-
closest_dist = candidate_dist;
|
|
796
|
-
closest_id = candidate_id;
|
|
797
|
-
changed = true;
|
|
798
|
-
}
|
|
799
|
-
});
|
|
800
|
-
} while (changed);
|
|
801
|
-
}
|
|
891
|
+
id_t closest_id = search_for_one(entry_id_, new_vector, new_dim, max_level, new_target_level, context);
|
|
802
892
|
|
|
803
893
|
// From `new_target_level` down perform proper extensive search.
|
|
804
|
-
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--) {
|
|
805
895
|
search_to_insert(closest_id, new_vector, new_dim, level, context);
|
|
806
896
|
closest_id = connect_new_element(new_id, level, context);
|
|
807
897
|
}
|
|
@@ -827,32 +917,11 @@ class index_gt {
|
|
|
827
917
|
|
|
828
918
|
// Go down the level, tracking only the closest match
|
|
829
919
|
thread_context_t& context = thread_contexts_[thread_idx];
|
|
830
|
-
id_t closest_id = entry_id_;
|
|
831
|
-
distance_t closest_dist =
|
|
832
|
-
context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
|
|
833
|
-
for (level_t level = max_level_; level > 0; level--) {
|
|
834
|
-
bool changed;
|
|
835
|
-
do {
|
|
836
|
-
changed = false;
|
|
837
|
-
node_ref_t closest_node = node(closest_id);
|
|
838
|
-
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
839
|
-
iterate_through_neighbors(closest_header, [&](id_t candidate_id) noexcept {
|
|
840
|
-
node_ref_t candidate_node = node(candidate_id);
|
|
841
|
-
distance_t candidate_dist = context.metric( //
|
|
842
|
-
query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
|
|
843
|
-
if (candidate_dist < closest_dist) {
|
|
844
|
-
closest_dist = candidate_dist;
|
|
845
|
-
closest_id = candidate_id;
|
|
846
|
-
changed = true;
|
|
847
|
-
}
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
} while (changed);
|
|
851
|
-
}
|
|
920
|
+
id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
|
|
852
921
|
|
|
853
922
|
// For bottom layer we need a more optimized procedure
|
|
854
923
|
search_to_find_in_base( //
|
|
855
|
-
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);
|
|
856
925
|
while (context.top_candidates.size() > wanted)
|
|
857
926
|
context.top_candidates.pop();
|
|
858
927
|
|
|
@@ -904,6 +973,10 @@ class index_gt {
|
|
|
904
973
|
void save(char const* file_path) const noexcept(false) {
|
|
905
974
|
|
|
906
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
|
|
907
980
|
state.connectivity = config_.connectivity;
|
|
908
981
|
state.size = size_;
|
|
909
982
|
state.entry_id = entry_id_;
|
|
@@ -926,12 +999,15 @@ class index_gt {
|
|
|
926
999
|
for (std::size_t i = 0; i != state.size; ++i) {
|
|
927
1000
|
node_ref_t node_ref = node(static_cast<id_t>(i));
|
|
928
1001
|
std::size_t bytes_to_dump = node_dump_size(node_ref.head.dim, node_ref.head.level);
|
|
929
|
-
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);
|
|
930
1005
|
if (!written) {
|
|
931
1006
|
std::fclose(file);
|
|
932
1007
|
throw std::runtime_error(std::strerror(errno));
|
|
933
1008
|
}
|
|
934
|
-
|
|
1009
|
+
// Dump the vector
|
|
1010
|
+
written = std::fwrite(node_ref.vector, bytes_in_vec, 1, file);
|
|
935
1011
|
if (!written) {
|
|
936
1012
|
std::fclose(file);
|
|
937
1013
|
throw std::runtime_error(std::strerror(errno));
|
|
@@ -957,13 +1033,22 @@ class index_gt {
|
|
|
957
1033
|
std::fclose(file);
|
|
958
1034
|
throw std::runtime_error(std::strerror(errno));
|
|
959
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
|
+
|
|
960
1045
|
config_.connectivity = state.connectivity;
|
|
961
1046
|
config_.max_elements = state.size;
|
|
962
1047
|
pre_ = precompute(config_);
|
|
963
1048
|
reserve(state.size);
|
|
964
1049
|
size_ = state.size;
|
|
965
|
-
max_level_ = state.max_level;
|
|
966
|
-
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);
|
|
967
1052
|
}
|
|
968
1053
|
|
|
969
1054
|
// Load nodes one by one
|
|
@@ -1022,14 +1107,23 @@ class index_gt {
|
|
|
1022
1107
|
// Read the header
|
|
1023
1108
|
{
|
|
1024
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
|
+
|
|
1025
1119
|
config_.connectivity = state.connectivity;
|
|
1026
1120
|
config_.max_elements = state.size;
|
|
1027
1121
|
config_.max_threads_add = 0;
|
|
1028
1122
|
pre_ = precompute(config_);
|
|
1029
1123
|
reserve(state.size);
|
|
1030
1124
|
size_ = state.size;
|
|
1031
|
-
max_level_ = state.max_level;
|
|
1032
|
-
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);
|
|
1033
1127
|
}
|
|
1034
1128
|
|
|
1035
1129
|
// Locate every node packed into file
|
|
@@ -1037,14 +1131,15 @@ class index_gt {
|
|
|
1037
1131
|
for (std::size_t i = 0; i != state.size; ++i) {
|
|
1038
1132
|
node_head_t const& head = *(node_head_t const*)(file + progress);
|
|
1039
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);
|
|
1040
1135
|
nodes_[i].tape_ = (byte_t*)(file + progress);
|
|
1041
|
-
nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump -
|
|
1136
|
+
nodes_[i].vector_ = (scalar_t*)(file + progress + bytes_to_dump - bytes_in_vec);
|
|
1042
1137
|
progress += bytes_to_dump;
|
|
1043
|
-
max_level_ = std::max(max_level_, head.level);
|
|
1138
|
+
max_level_ = (std::max)(max_level_, head.level);
|
|
1044
1139
|
}
|
|
1045
1140
|
|
|
1046
1141
|
viewed_file_descriptor_ = descriptor;
|
|
1047
|
-
#endif
|
|
1142
|
+
#endif // POSIX
|
|
1048
1143
|
}
|
|
1049
1144
|
|
|
1050
1145
|
#pragma endregion
|
|
@@ -1066,7 +1161,7 @@ class index_gt {
|
|
|
1066
1161
|
|
|
1067
1162
|
void node_free(std::size_t id) noexcept {
|
|
1068
1163
|
|
|
1069
|
-
if (viewed_file_descriptor_)
|
|
1164
|
+
if (viewed_file_descriptor_ != 0)
|
|
1070
1165
|
return;
|
|
1071
1166
|
|
|
1072
1167
|
// This function is rarely called and can be as expensive as needed for higher space-efficiency.
|
|
@@ -1085,7 +1180,7 @@ class index_gt {
|
|
|
1085
1180
|
;
|
|
1086
1181
|
|
|
1087
1182
|
allocator_t{}.deallocate(node.tape_, node_bytes);
|
|
1088
|
-
node = {};
|
|
1183
|
+
node = node_t{};
|
|
1089
1184
|
}
|
|
1090
1185
|
|
|
1091
1186
|
node_ref_t node_malloc( //
|
|
@@ -1119,9 +1214,9 @@ class index_gt {
|
|
|
1119
1214
|
return {*mutex, head, scalars};
|
|
1120
1215
|
}
|
|
1121
1216
|
|
|
1122
|
-
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]); }
|
|
1123
1218
|
|
|
1124
|
-
|
|
1219
|
+
inline node_ref_t node(node_t node) const noexcept {
|
|
1125
1220
|
byte_t* data = node.tape_;
|
|
1126
1221
|
mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
|
|
1127
1222
|
node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
|
|
@@ -1144,74 +1239,64 @@ class index_gt {
|
|
|
1144
1239
|
|
|
1145
1240
|
node_ref_t new_node = node(new_id);
|
|
1146
1241
|
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1147
|
-
std::size_t connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
|
|
1148
|
-
|
|
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);
|
|
1149
1244
|
|
|
1150
|
-
distance_and_id_t const* const
|
|
1151
|
-
std::size_t const top_count =
|
|
1152
|
-
id_t next_closest_entry_id =
|
|
1153
|
-
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;
|
|
1154
1248
|
|
|
1155
1249
|
// Outgoing links from `new_id`:
|
|
1156
1250
|
{
|
|
1157
1251
|
neighbors_ref_t new_neighbors = neighbors(new_node, level);
|
|
1158
|
-
assert_m(!new_neighbors.
|
|
1252
|
+
assert_m(!new_neighbors.count_, "The newly inserted element should have blank link list");
|
|
1159
1253
|
|
|
1160
|
-
new_neighbors.
|
|
1254
|
+
new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
|
|
1161
1255
|
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1162
|
-
assert_m(!new_neighbors
|
|
1163
|
-
assert_m(level <= node(
|
|
1164
|
-
|
|
1165
|
-
new_neighbors.neighbors[idx] = top_unordered[idx].second;
|
|
1166
|
-
if (top_unordered[idx].first < next_closest_distance) {
|
|
1167
|
-
next_closest_entry_id = top_unordered[idx].second;
|
|
1168
|
-
next_closest_distance = top_unordered[idx].first;
|
|
1169
|
-
}
|
|
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;
|
|
1170
1259
|
}
|
|
1171
1260
|
}
|
|
1172
1261
|
|
|
1173
1262
|
// Reverse links from the neighbors:
|
|
1174
1263
|
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1175
|
-
id_t close_id =
|
|
1264
|
+
id_t close_id = top_ordered[idx].second;
|
|
1176
1265
|
node_ref_t close_node = node(close_id);
|
|
1177
1266
|
lock_t close_lock = close_node.lock();
|
|
1178
1267
|
|
|
1179
1268
|
neighbors_ref_t close_header = neighbors(close_node, level);
|
|
1180
|
-
assert_m(close_header.
|
|
1269
|
+
assert_m(close_header.count_ <= connectivity_max, "Possible corruption");
|
|
1181
1270
|
assert_m(close_id != new_id, "Self-loops are impossible");
|
|
1182
1271
|
assert_m(level <= close_node.head.level, "Linking to missing level");
|
|
1183
1272
|
|
|
1184
1273
|
// If `new_id` is already present in the neighboring connections of `close_id`
|
|
1185
1274
|
// then no need to modify any connections or run the heuristics.
|
|
1186
|
-
if (close_header.
|
|
1187
|
-
close_header
|
|
1188
|
-
close_header.
|
|
1275
|
+
if (close_header.count_ < connectivity_max) {
|
|
1276
|
+
close_header[close_header.count_] = new_id;
|
|
1277
|
+
close_header.count_++;
|
|
1189
1278
|
continue;
|
|
1190
1279
|
}
|
|
1191
1280
|
|
|
1192
1281
|
// To fit a new connection we need to drop an existing one.
|
|
1193
|
-
distances_and_ids_t& candidates = context.
|
|
1282
|
+
distances_and_ids_t& candidates = context.candidates_set;
|
|
1194
1283
|
candidates.clear();
|
|
1195
1284
|
candidates.emplace( //
|
|
1196
1285
|
context.metric( //
|
|
1197
1286
|
new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
|
|
1198
1287
|
new_id);
|
|
1199
|
-
|
|
1288
|
+
for (id_t successor_id : close_header) {
|
|
1200
1289
|
node_ref_t successor_node = node(successor_id);
|
|
1201
1290
|
candidates.emplace( //
|
|
1202
1291
|
context.metric( //
|
|
1203
1292
|
successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
|
|
1204
1293
|
successor_id);
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1294
|
+
}
|
|
1295
|
+
span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
|
|
1207
1296
|
|
|
1208
1297
|
// Export the results:
|
|
1209
|
-
close_header.
|
|
1210
|
-
|
|
1211
|
-
close_header.neighbors[close_header.count] = candidates.top().second;
|
|
1212
|
-
close_header.count++;
|
|
1213
|
-
candidates.pop();
|
|
1214
|
-
}
|
|
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;
|
|
1215
1300
|
}
|
|
1216
1301
|
|
|
1217
1302
|
return next_closest_entry_id;
|
|
@@ -1223,13 +1308,42 @@ class index_gt {
|
|
|
1223
1308
|
return (level_t)r;
|
|
1224
1309
|
}
|
|
1225
1310
|
|
|
1226
|
-
|
|
1227
|
-
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, //
|
|
1228
1342
|
level_t level, thread_context_t& context) noexcept(false) {
|
|
1229
1343
|
|
|
1230
1344
|
visits_bitset_t& visits = context.visits;
|
|
1231
|
-
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1232
|
-
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
|
|
1233
1347
|
|
|
1234
1348
|
top_candidates.clear();
|
|
1235
1349
|
candidates_set.clear();
|
|
@@ -1252,9 +1366,10 @@ class index_gt {
|
|
|
1252
1366
|
lock_t candidate_lock = candidate_node.lock();
|
|
1253
1367
|
neighbors_ref_t candidate_header = neighbors(candidate_node, level);
|
|
1254
1368
|
|
|
1255
|
-
|
|
1369
|
+
prefetch_neighbors(candidate_header, visits);
|
|
1370
|
+
for (id_t successor_id : candidate_header) {
|
|
1256
1371
|
if (visits.test(successor_id))
|
|
1257
|
-
|
|
1372
|
+
continue;
|
|
1258
1373
|
|
|
1259
1374
|
visits.set(successor_id);
|
|
1260
1375
|
node_ref_t successor_node = node(successor_id);
|
|
@@ -1269,17 +1384,17 @@ class index_gt {
|
|
|
1269
1384
|
if (!top_candidates.empty())
|
|
1270
1385
|
closest_dist = top_candidates.top().first;
|
|
1271
1386
|
}
|
|
1272
|
-
}
|
|
1387
|
+
}
|
|
1273
1388
|
}
|
|
1274
1389
|
}
|
|
1275
1390
|
|
|
1276
|
-
void search_to_find_in_base(
|
|
1277
|
-
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, //
|
|
1278
1393
|
std::size_t expansion, thread_context_t& context) const noexcept(false) {
|
|
1279
1394
|
|
|
1280
1395
|
visits_bitset_t& visits = context.visits;
|
|
1281
|
-
distances_and_ids_t& top_candidates = context.top_candidates;
|
|
1282
|
-
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
|
|
1283
1398
|
|
|
1284
1399
|
visits.clear();
|
|
1285
1400
|
top_candidates.clear();
|
|
@@ -1301,9 +1416,10 @@ class index_gt {
|
|
|
1301
1416
|
id_t candidate_id = current_node_node.second;
|
|
1302
1417
|
neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
|
|
1303
1418
|
|
|
1304
|
-
|
|
1419
|
+
prefetch_neighbors(candidate_header, visits);
|
|
1420
|
+
for (id_t successor_id : candidate_header) {
|
|
1305
1421
|
if (visits.test(successor_id))
|
|
1306
|
-
|
|
1422
|
+
continue;
|
|
1307
1423
|
|
|
1308
1424
|
visits.set(successor_id);
|
|
1309
1425
|
node_ref_t successor_node = node(successor_id);
|
|
@@ -1319,60 +1435,52 @@ class index_gt {
|
|
|
1319
1435
|
if (!top_candidates.empty())
|
|
1320
1436
|
closest_dist = top_candidates.top().first;
|
|
1321
1437
|
}
|
|
1322
|
-
}
|
|
1438
|
+
}
|
|
1323
1439
|
}
|
|
1324
1440
|
}
|
|
1325
1441
|
|
|
1326
|
-
|
|
1327
|
-
* @brief A simple `for`-loop that prefetches vectors of neighbors.
|
|
1328
|
-
*/
|
|
1329
|
-
template <typename neighbor_id_callback_at>
|
|
1330
|
-
inline void iterate_through_neighbors(neighbors_ref_t head, neighbor_id_callback_at&& callback) const noexcept {
|
|
1331
|
-
std::size_t n = head.count;
|
|
1332
|
-
for (std::size_t j = 0; j != n; j++)
|
|
1333
|
-
callback(head.neighbors[j]);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
void filter_top_candidates_with_heuristic( //
|
|
1337
|
-
distances_and_ids_t& top_candidates, distances_and_ids_t& temporary, std::size_t needed,
|
|
1338
|
-
thread_context_t& context) const noexcept(false) {
|
|
1339
|
-
|
|
1340
|
-
if (top_candidates.size() < needed)
|
|
1341
|
-
return;
|
|
1342
|
-
|
|
1343
|
-
// TODO: Sort ascending, then run an inplace triangular reduction.
|
|
1344
|
-
temporary.clear();
|
|
1345
|
-
while (top_candidates.size()) {
|
|
1346
|
-
temporary.emplace(-top_candidates.top().first, top_candidates.top().second);
|
|
1347
|
-
top_candidates.pop();
|
|
1348
|
-
}
|
|
1442
|
+
void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|
|
1349
1443
|
|
|
1350
|
-
|
|
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 {
|
|
1351
1446
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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;
|
|
1355
1459
|
bool good = true;
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
distance_t inter_result_dist =
|
|
1364
|
-
context.metric(other_node.vector, best_node.vector, other_node.head.dim, best_node.head.dim);
|
|
1365
|
-
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) {
|
|
1366
1467
|
good = false;
|
|
1367
1468
|
break;
|
|
1368
1469
|
}
|
|
1369
1470
|
}
|
|
1370
1471
|
|
|
1371
|
-
if (good)
|
|
1372
|
-
|
|
1472
|
+
if (good) {
|
|
1473
|
+
top_ordered[submitted_count] = top_ordered[consumed_count];
|
|
1474
|
+
submitted_count++;
|
|
1475
|
+
}
|
|
1476
|
+
consumed_count++;
|
|
1373
1477
|
}
|
|
1478
|
+
|
|
1479
|
+
return {top_ordered, submitted_count};
|
|
1374
1480
|
}
|
|
1375
1481
|
};
|
|
1376
1482
|
|
|
1377
1483
|
} // namespace usearch
|
|
1378
1484
|
} // namespace unum
|
|
1485
|
+
|
|
1486
|
+
#endif
|