usearch 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +64 -0
- package/.github/workflows/release.yml +45 -20
- package/.github/workflows/update_version.sh +6 -0
- package/.releaserc +77 -0
- package/.vscode/settings.json +14 -2
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +12 -0
- package/Cargo.toml +1 -1
- package/README.md +246 -220
- package/VERSION +1 -1
- package/binding.gyp +13 -9
- package/build.rs +1 -0
- package/conanfile.py +1 -1
- package/docs/Compilation.md +112 -0
- package/docs/benchmarks.md +182 -0
- package/docs/benchmarks.rst +5 -0
- package/docs/compilation.rst +5 -0
- package/docs/conf.dox +2668 -0
- package/docs/index.rst +7 -0
- package/include/usearch/usearch.hpp +454 -238
- package/javascript/lib.cpp +82 -8
- package/objc/USearchObjective.mm +2 -2
- package/objc/include/USearchObjective.h +10 -9
- package/package.json +7 -1
- package/python/bench.py +37 -7
- package/python/lib.cpp +70 -43
- package/python/test.py +78 -36
- package/rust/lib.cpp +6 -2
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +68 -40
- package/src/bench.cpp +24 -23
- package/swift/Index+Sugar.swift +1 -0
- package/swift/Test.swift +1 -0
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
|
@@ -85,6 +85,9 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
85
85
|
return v;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @brief Inner (Dot) Product distance.
|
|
90
|
+
*/
|
|
88
91
|
template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
89
92
|
using scalar_t = scalar_at;
|
|
90
93
|
using result_t = result_at;
|
|
@@ -92,10 +95,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
|
92
95
|
|
|
93
96
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
94
97
|
result_type ab{};
|
|
95
|
-
#if defined(
|
|
96
|
-
#pragma GCC ivdep
|
|
97
|
-
#elif defined(__clang__)
|
|
98
|
+
#if defined(__clang__)
|
|
98
99
|
#pragma clang loop vectorize(enable)
|
|
100
|
+
#elif defined(__GNUC__)
|
|
101
|
+
#pragma GCC ivdep
|
|
99
102
|
#elif defined(_OPENMP)
|
|
100
103
|
#pragma omp simd reduction(+ : ab)
|
|
101
104
|
#endif
|
|
@@ -105,6 +108,12 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
|
105
108
|
}
|
|
106
109
|
};
|
|
107
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @brief Cosine (Angular) distance.
|
|
113
|
+
* Identical to the Inner Product of normalized vectors.
|
|
114
|
+
* Unless you are running on an tiny embedded platform, this metric
|
|
115
|
+
* is recommended over `::ip_gt` for low-precision scalars.
|
|
116
|
+
*/
|
|
108
117
|
template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
109
118
|
using scalar_t = scalar_at;
|
|
110
119
|
using result_t = result_at;
|
|
@@ -112,10 +121,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
|
112
121
|
|
|
113
122
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
114
123
|
result_t ab{}, a2{}, b2{};
|
|
115
|
-
#if defined(
|
|
116
|
-
#pragma GCC ivdep
|
|
117
|
-
#elif defined(__clang__)
|
|
124
|
+
#if defined(__clang__)
|
|
118
125
|
#pragma clang loop vectorize(enable)
|
|
126
|
+
#elif defined(__GNUC__)
|
|
127
|
+
#pragma GCC ivdep
|
|
119
128
|
#elif defined(_OPENMP)
|
|
120
129
|
#pragma omp simd reduction(+ : ab, a2, b2)
|
|
121
130
|
#endif
|
|
@@ -123,10 +132,14 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
|
123
132
|
ab += result_t(a[i]) * result_t(b[i]), //
|
|
124
133
|
a2 += square<result_t>(a[i]), //
|
|
125
134
|
b2 += square<result_t>(b[i]);
|
|
126
|
-
return ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
135
|
+
return 1 - ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
127
136
|
}
|
|
128
137
|
};
|
|
129
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @brief Squared Euclidean (L2) distance.
|
|
141
|
+
* Square root is avoided at the end, as it won't affect the ordering.
|
|
142
|
+
*/
|
|
130
143
|
template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
|
|
131
144
|
using scalar_t = scalar_at;
|
|
132
145
|
using result_t = result_at;
|
|
@@ -134,10 +147,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
|
|
|
134
147
|
|
|
135
148
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
136
149
|
result_t ab_deltas_sq{};
|
|
137
|
-
#if defined(
|
|
138
|
-
#pragma GCC ivdep
|
|
139
|
-
#elif defined(__clang__)
|
|
150
|
+
#if defined(__clang__)
|
|
140
151
|
#pragma clang loop vectorize(enable)
|
|
152
|
+
#elif defined(__GNUC__)
|
|
153
|
+
#pragma GCC ivdep
|
|
141
154
|
#elif defined(_OPENMP)
|
|
142
155
|
#pragma omp simd reduction(+ : ab_deltas_sq)
|
|
143
156
|
#endif
|
|
@@ -159,10 +172,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct hamming_g
|
|
|
159
172
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
|
|
160
173
|
std::size_t = 0) const noexcept {
|
|
161
174
|
result_t matches{};
|
|
162
|
-
#if defined(
|
|
163
|
-
#pragma GCC ivdep
|
|
164
|
-
#elif defined(__clang__)
|
|
175
|
+
#if defined(__clang__)
|
|
165
176
|
#pragma clang loop vectorize(enable)
|
|
177
|
+
#elif defined(__GNUC__)
|
|
178
|
+
#pragma GCC ivdep
|
|
166
179
|
#elif defined(_OPENMP)
|
|
167
180
|
#pragma omp simd reduction(+ : matches)
|
|
168
181
|
#endif
|
|
@@ -187,10 +200,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct bit_hammi
|
|
|
187
200
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
|
|
188
201
|
std::size_t = 0) const noexcept {
|
|
189
202
|
result_t matches{};
|
|
190
|
-
#if defined(
|
|
191
|
-
#pragma GCC ivdep
|
|
192
|
-
#elif defined(__clang__)
|
|
203
|
+
#if defined(__clang__)
|
|
193
204
|
#pragma clang loop vectorize(enable)
|
|
205
|
+
#elif defined(__GNUC__)
|
|
206
|
+
#pragma GCC ivdep
|
|
194
207
|
#elif defined(_OPENMP)
|
|
195
208
|
#pragma omp simd reduction(+ : matches)
|
|
196
209
|
#endif
|
|
@@ -239,10 +252,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct pearson_cor
|
|
|
239
252
|
scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
240
253
|
result_t a_sum{}, b_sum{}, ab_sum{};
|
|
241
254
|
result_t a_sq_sum{}, b_sq_sum{};
|
|
242
|
-
#if defined(
|
|
243
|
-
#pragma GCC ivdep
|
|
244
|
-
#elif defined(__clang__)
|
|
255
|
+
#if defined(__clang__)
|
|
245
256
|
#pragma clang loop vectorize(enable)
|
|
257
|
+
#elif defined(__GNUC__)
|
|
258
|
+
#pragma GCC ivdep
|
|
246
259
|
#elif defined(_OPENMP)
|
|
247
260
|
#pragma omp simd reduction(+ : a_sum, b_sum, ab_sum, a_sq_sum, b_sq_sum)
|
|
248
261
|
#endif
|
|
@@ -291,6 +304,9 @@ template <std::size_t multiple_ak> inline std::size_t divide_round_up(std::size_
|
|
|
291
304
|
return (num + multiple_ak - 1) / multiple_ak;
|
|
292
305
|
}
|
|
293
306
|
|
|
307
|
+
/**
|
|
308
|
+
* @brief Light-weight bitset implementation to track visited nodes during graph traversal.
|
|
309
|
+
*/
|
|
294
310
|
template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
295
311
|
using allocator_t = allocator_at;
|
|
296
312
|
using byte_t = typename allocator_t::value_type;
|
|
@@ -330,7 +346,8 @@ using visits_bitset_t = visits_bitset_gt<>;
|
|
|
330
346
|
|
|
331
347
|
/**
|
|
332
348
|
* @brief Similar to `std::priority_queue`, but allows raw access to underlying
|
|
333
|
-
* memory, in case you want to shuffle it or sort.
|
|
349
|
+
* memory, in case you want to shuffle it or sort. Good for collections
|
|
350
|
+
* from 100s to 10'000s elements.
|
|
334
351
|
*/
|
|
335
352
|
template <typename element_at, //
|
|
336
353
|
typename comparator_at = std::less<void>, // <void> is needed before C++14.
|
|
@@ -347,23 +364,19 @@ class max_heap_gt {
|
|
|
347
364
|
element_t* elements_;
|
|
348
365
|
std::size_t size_;
|
|
349
366
|
std::size_t capacity_;
|
|
350
|
-
std::size_t max_capacity_;
|
|
351
367
|
|
|
352
368
|
public:
|
|
353
|
-
max_heap_gt(
|
|
354
|
-
: elements_(nullptr), size_(0), capacity_(0), max_capacity_(max_capacity) {}
|
|
369
|
+
max_heap_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
|
|
355
370
|
|
|
356
371
|
max_heap_gt(max_heap_gt&& other) noexcept {
|
|
357
372
|
std::swap(elements_, other.elements_);
|
|
358
373
|
std::swap(size_, other.size_);
|
|
359
374
|
std::swap(capacity_, other.capacity_);
|
|
360
|
-
std::swap(max_capacity_, other.max_capacity_);
|
|
361
375
|
}
|
|
362
376
|
max_heap_gt& operator=(max_heap_gt&& other) noexcept {
|
|
363
377
|
std::swap(elements_, other.elements_);
|
|
364
378
|
std::swap(size_, other.size_);
|
|
365
379
|
std::swap(capacity_, other.capacity_);
|
|
366
|
-
std::swap(max_capacity_, other.max_capacity_);
|
|
367
380
|
return *this;
|
|
368
381
|
}
|
|
369
382
|
|
|
@@ -384,16 +397,12 @@ class max_heap_gt {
|
|
|
384
397
|
}
|
|
385
398
|
}
|
|
386
399
|
|
|
387
|
-
bool reserve(std::size_t
|
|
388
|
-
if (
|
|
400
|
+
bool reserve(std::size_t new_capacity) noexcept {
|
|
401
|
+
if (new_capacity < capacity_)
|
|
389
402
|
return true;
|
|
390
|
-
if (max_capacity_ && capacity_ == max_capacity_)
|
|
391
|
-
return false;
|
|
392
|
-
|
|
393
|
-
auto new_capacity = (std::max<std::size_t>)(capacity_ * 2u, 16u);
|
|
394
|
-
if (max_capacity_)
|
|
395
|
-
new_capacity = (std::min)(new_capacity, max_capacity_);
|
|
396
403
|
|
|
404
|
+
new_capacity = ceil2(new_capacity);
|
|
405
|
+
new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
|
|
397
406
|
auto allocator = allocator_t{};
|
|
398
407
|
auto new_elements = allocator.allocate(new_capacity);
|
|
399
408
|
if (!new_elements)
|
|
@@ -413,10 +422,15 @@ class max_heap_gt {
|
|
|
413
422
|
if (!reserve(size_ + 1))
|
|
414
423
|
return false;
|
|
415
424
|
|
|
425
|
+
emplace_reserved(std::forward<args_at>(args)...);
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
template <typename... args_at> //
|
|
430
|
+
void emplace_reserved(args_at&&... args) noexcept {
|
|
416
431
|
new (&elements_[size_]) element_t({std::forward<args_at>(args)...});
|
|
417
432
|
size_++;
|
|
418
433
|
shift_up(size_ - 1);
|
|
419
|
-
return true;
|
|
420
434
|
}
|
|
421
435
|
|
|
422
436
|
element_t pop() noexcept {
|
|
@@ -430,10 +444,7 @@ class max_heap_gt {
|
|
|
430
444
|
|
|
431
445
|
/** @brief Invalidates the "max-heap" property, transforming into ascending range. */
|
|
432
446
|
void sort_ascending() noexcept { std::sort_heap(elements_, elements_ + size_, &less); }
|
|
433
|
-
|
|
434
|
-
void sort_descending() noexcept { sort_ascending(), std::reverse(elements_, elements_ + size_); }
|
|
435
|
-
void sort_heap() noexcept { std::make_heap(elements_, elements_ + size_, &less); }
|
|
436
|
-
void shrink(std::size_t n) noexcept { size_ = n; }
|
|
447
|
+
void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
|
|
437
448
|
|
|
438
449
|
element_t* data() noexcept { return elements_; }
|
|
439
450
|
element_t const* data() const noexcept { return elements_; }
|
|
@@ -468,7 +479,109 @@ class max_heap_gt {
|
|
|
468
479
|
};
|
|
469
480
|
|
|
470
481
|
/**
|
|
471
|
-
*
|
|
482
|
+
* @brief Similar to `std::priority_queue`, but allows raw access to underlying
|
|
483
|
+
* memory and always keeps the data sorted. Ideal for small collections
|
|
484
|
+
* under 128 elements.
|
|
485
|
+
*/
|
|
486
|
+
template <typename element_at, //
|
|
487
|
+
typename comparator_at = std::less<void>, // <void> is needed before C++14.
|
|
488
|
+
typename allocator_at = std::allocator<element_at>> //
|
|
489
|
+
class sorted_buffer_gt {
|
|
490
|
+
public:
|
|
491
|
+
using element_t = element_at;
|
|
492
|
+
using comparator_t = comparator_at;
|
|
493
|
+
using allocator_t = allocator_at;
|
|
494
|
+
|
|
495
|
+
using value_type = element_t;
|
|
496
|
+
|
|
497
|
+
private:
|
|
498
|
+
element_t* elements_;
|
|
499
|
+
std::size_t size_;
|
|
500
|
+
std::size_t capacity_;
|
|
501
|
+
|
|
502
|
+
public:
|
|
503
|
+
sorted_buffer_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
|
|
504
|
+
|
|
505
|
+
sorted_buffer_gt(sorted_buffer_gt&& other) noexcept {
|
|
506
|
+
std::swap(elements_, other.elements_);
|
|
507
|
+
std::swap(size_, other.size_);
|
|
508
|
+
std::swap(capacity_, other.capacity_);
|
|
509
|
+
}
|
|
510
|
+
sorted_buffer_gt& operator=(sorted_buffer_gt&& other) noexcept {
|
|
511
|
+
std::swap(elements_, other.elements_);
|
|
512
|
+
std::swap(size_, other.size_);
|
|
513
|
+
std::swap(capacity_, other.capacity_);
|
|
514
|
+
return *this;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
sorted_buffer_gt(sorted_buffer_gt const&) = delete;
|
|
518
|
+
sorted_buffer_gt& operator=(sorted_buffer_gt const&) = delete;
|
|
519
|
+
|
|
520
|
+
~sorted_buffer_gt() {
|
|
521
|
+
clear();
|
|
522
|
+
allocator_t{}.deallocate(elements_, capacity_);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
bool empty() const noexcept { return !size_; }
|
|
526
|
+
std::size_t size() const noexcept { return size_; }
|
|
527
|
+
std::size_t capacity() const noexcept { return capacity_; }
|
|
528
|
+
element_t const& top() const noexcept { return elements_[size_ - 1]; }
|
|
529
|
+
|
|
530
|
+
void clear() noexcept {
|
|
531
|
+
while (size_)
|
|
532
|
+
size_--, elements_[size_].~element_t();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
bool reserve(std::size_t new_capacity) noexcept {
|
|
536
|
+
if (new_capacity < capacity_)
|
|
537
|
+
return true;
|
|
538
|
+
|
|
539
|
+
new_capacity = ceil2(new_capacity);
|
|
540
|
+
new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
|
|
541
|
+
auto allocator = allocator_t{};
|
|
542
|
+
auto new_elements = allocator.allocate(new_capacity);
|
|
543
|
+
if (!new_elements)
|
|
544
|
+
return false;
|
|
545
|
+
|
|
546
|
+
if (size_) {
|
|
547
|
+
std::uninitialized_copy_n(elements_, size_, new_elements);
|
|
548
|
+
allocator.deallocate(elements_, capacity_);
|
|
549
|
+
}
|
|
550
|
+
elements_ = new_elements;
|
|
551
|
+
capacity_ = new_capacity;
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
template <typename... args_at> void emplace_reserved(args_at&&... args) {
|
|
556
|
+
element_t element({std::forward<args_at>(args)...});
|
|
557
|
+
std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
|
|
558
|
+
std::size_t to_move = size_ - slot;
|
|
559
|
+
element_t* source = elements_ + size_ - 1;
|
|
560
|
+
for (; to_move; --to_move, --source)
|
|
561
|
+
source[1] = source[0];
|
|
562
|
+
elements_[slot] = element;
|
|
563
|
+
size_++;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
element_t pop() noexcept {
|
|
567
|
+
size_--;
|
|
568
|
+
element_t result = elements_[size_];
|
|
569
|
+
elements_[size_].~element_t();
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
void sort_ascending() noexcept {}
|
|
574
|
+
void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
|
|
575
|
+
|
|
576
|
+
element_t* data() noexcept { return elements_; }
|
|
577
|
+
element_t const* data() const noexcept { return elements_; }
|
|
578
|
+
|
|
579
|
+
private:
|
|
580
|
+
static bool less(element_t const& a, element_t const& b) noexcept { return comparator_t{}(a, b); }
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* @brief Tiny userspace mutex, designed to fit in a single integer.
|
|
472
585
|
*/
|
|
473
586
|
class mutex_t {
|
|
474
587
|
#if defined(WINDOWS)
|
|
@@ -517,13 +630,13 @@ static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than exp
|
|
|
517
630
|
|
|
518
631
|
using lock_t = std::unique_lock<mutex_t>;
|
|
519
632
|
|
|
520
|
-
/**
|
|
521
|
-
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
522
|
-
*/
|
|
523
633
|
#if defined(WINDOWS)
|
|
524
634
|
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
525
635
|
#endif
|
|
526
636
|
|
|
637
|
+
/**
|
|
638
|
+
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
639
|
+
*/
|
|
527
640
|
class usearch_pack_m uint40_t {
|
|
528
641
|
unsigned char octets[5];
|
|
529
642
|
|
|
@@ -561,6 +674,7 @@ class usearch_pack_m uint40_t {
|
|
|
561
674
|
return old;
|
|
562
675
|
}
|
|
563
676
|
};
|
|
677
|
+
|
|
564
678
|
#if defined(WINDOWS)
|
|
565
679
|
#pragma pack(pop) // Reset alignment to default
|
|
566
680
|
#endif
|
|
@@ -580,10 +694,9 @@ template <typename scalar_at> class span_gt {
|
|
|
580
694
|
};
|
|
581
695
|
|
|
582
696
|
/**
|
|
583
|
-
* @brief
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
* and two expansion factors - for construction and search.
|
|
697
|
+
* @brief Configuration settings for the index construction.
|
|
698
|
+
* Includes the main `::connectivity` parameter (`M` in the paper)
|
|
699
|
+
* and two expansion factors - for construction and search.
|
|
587
700
|
*/
|
|
588
701
|
struct config_t {
|
|
589
702
|
|
|
@@ -612,10 +725,35 @@ struct config_t {
|
|
|
612
725
|
std::size_t max_threads_search = 0;
|
|
613
726
|
};
|
|
614
727
|
|
|
728
|
+
struct add_config_t {
|
|
729
|
+
/// @brief Optional thread identifier for multi-threaded construction.
|
|
730
|
+
std::size_t thread{};
|
|
731
|
+
/// @brief Don't copy the ::vector, if it's persisted elsewhere.
|
|
732
|
+
bool store_vector{true};
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
struct add_result_t {
|
|
736
|
+
std::size_t new_size{};
|
|
737
|
+
std::size_t cycles{};
|
|
738
|
+
std::size_t measurements{};
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
struct search_config_t {
|
|
742
|
+
std::size_t thread{};
|
|
743
|
+
bool exact{};
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
struct search_result_t {
|
|
747
|
+
std::size_t count{};
|
|
748
|
+
std::size_t cycles{};
|
|
749
|
+
std::size_t measurements{};
|
|
750
|
+
|
|
751
|
+
inline operator std::size_t() const noexcept { return count; }
|
|
752
|
+
};
|
|
753
|
+
|
|
615
754
|
/**
|
|
616
|
-
* @brief
|
|
617
|
-
*
|
|
618
|
-
* Hierarchical Navigable Small World graph algorithm.
|
|
755
|
+
* @brief Approximate Nearest Neighbors Search index using the
|
|
756
|
+
* Hierarchical Navigable Small World graph algorithm.
|
|
619
757
|
*
|
|
620
758
|
* @section Features
|
|
621
759
|
* - Search for vectors of different dimensionality.
|
|
@@ -650,8 +788,15 @@ class index_gt {
|
|
|
650
788
|
using id_t = id_at;
|
|
651
789
|
using allocator_t = allocator_at;
|
|
652
790
|
|
|
791
|
+
using vector_view_t = span_gt<scalar_t const>;
|
|
792
|
+
|
|
793
|
+
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) // C++17 ann newer
|
|
794
|
+
using distance_t =
|
|
795
|
+
typename std::invoke_result<metric_t, scalar_t const*, scalar_t const*, std::size_t, std::size_t>::type;
|
|
796
|
+
#else
|
|
653
797
|
using distance_t =
|
|
654
798
|
typename std::result_of<metric_t(scalar_t const*, scalar_t const*, std::size_t, std::size_t)>::type;
|
|
799
|
+
#endif
|
|
655
800
|
|
|
656
801
|
private:
|
|
657
802
|
using neighbors_count_t = id_t;
|
|
@@ -672,16 +817,18 @@ class index_gt {
|
|
|
672
817
|
std::size_t neighbors_base_bytes{};
|
|
673
818
|
std::size_t mutex_bytes{};
|
|
674
819
|
};
|
|
675
|
-
struct
|
|
820
|
+
struct candidate_t {
|
|
676
821
|
distance_t first;
|
|
677
822
|
id_t second;
|
|
678
823
|
};
|
|
679
824
|
struct compare_by_distance_t {
|
|
680
|
-
inline bool operator()(
|
|
825
|
+
inline bool operator()(candidate_t a, candidate_t b) const noexcept { return a.first < b.first; }
|
|
681
826
|
};
|
|
682
827
|
|
|
683
|
-
using
|
|
684
|
-
using
|
|
828
|
+
using candidates_view_t = span_gt<candidate_t const>;
|
|
829
|
+
using candidates_allocator_t = typename allocator_traits_t::template rebind_alloc<candidate_t>;
|
|
830
|
+
using top_candidates_t = sorted_buffer_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
|
|
831
|
+
using next_candidates_t = max_heap_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
|
|
685
832
|
|
|
686
833
|
struct neighbors_ref_t {
|
|
687
834
|
neighbors_count_t& count_;
|
|
@@ -696,6 +843,8 @@ class index_gt {
|
|
|
696
843
|
inline id_t& operator[](std::size_t i) noexcept { return neighbors_[i]; }
|
|
697
844
|
inline id_t operator[](std::size_t i) const noexcept { return neighbors_[i]; }
|
|
698
845
|
inline std::size_t size() const noexcept { return count_; }
|
|
846
|
+
inline void clear() noexcept { count_ = 0u; }
|
|
847
|
+
inline void push_back(id_t id) noexcept { neighbors_[count_] = id, count_++; }
|
|
699
848
|
};
|
|
700
849
|
|
|
701
850
|
#if defined(WINDOWS)
|
|
@@ -734,16 +883,25 @@ class index_gt {
|
|
|
734
883
|
scalar_t* vector{};
|
|
735
884
|
|
|
736
885
|
inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
|
|
886
|
+
/// @brief Locks the list of neighbors for concurrent updates.
|
|
737
887
|
inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
|
|
738
888
|
inline operator node_t() const noexcept { return node_t{mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
|
|
889
|
+
inline operator vector_view_t() const noexcept { return {vector, head.dim}; }
|
|
739
890
|
};
|
|
740
891
|
|
|
741
892
|
struct usearch_align_m thread_context_t {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
visits_bitset_t visits;
|
|
745
|
-
std::default_random_engine level_generator;
|
|
746
|
-
metric_t metric;
|
|
893
|
+
top_candidates_t top_candidates{};
|
|
894
|
+
next_candidates_t next_candidates{};
|
|
895
|
+
visits_bitset_t visits{};
|
|
896
|
+
std::default_random_engine level_generator{};
|
|
897
|
+
metric_t metric{};
|
|
898
|
+
std::size_t iteration_cycles{};
|
|
899
|
+
std::size_t measurements_count{};
|
|
900
|
+
|
|
901
|
+
inline distance_t measure(vector_view_t a, vector_view_t b) noexcept {
|
|
902
|
+
measurements_count++;
|
|
903
|
+
return metric(a.data(), b.data(), a.size(), b.size());
|
|
904
|
+
}
|
|
747
905
|
};
|
|
748
906
|
|
|
749
907
|
config_t config_{};
|
|
@@ -793,6 +951,9 @@ class index_gt {
|
|
|
793
951
|
reserve(config.max_elements);
|
|
794
952
|
}
|
|
795
953
|
|
|
954
|
+
/**
|
|
955
|
+
* @brief Clones the structure with the same hyper-parameters, but without contents.
|
|
956
|
+
*/
|
|
796
957
|
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
797
958
|
|
|
798
959
|
~index_gt() noexcept { clear(); }
|
|
@@ -806,6 +967,10 @@ class index_gt {
|
|
|
806
967
|
|
|
807
968
|
#pragma region Adjusting Configuration
|
|
808
969
|
|
|
970
|
+
/**
|
|
971
|
+
* @brief Erases all the vectors from the index.
|
|
972
|
+
* Will change `size()` to zero, but will keep the same `capacity()`.
|
|
973
|
+
*/
|
|
809
974
|
void clear() noexcept {
|
|
810
975
|
std::size_t n = size_;
|
|
811
976
|
for (std::size_t i = 0; i != n; ++i)
|
|
@@ -815,6 +980,9 @@ class index_gt {
|
|
|
815
980
|
entry_id_ = 0u;
|
|
816
981
|
}
|
|
817
982
|
|
|
983
|
+
/**
|
|
984
|
+
* @brief Swaps the underlying memory buffers and thread contexts.
|
|
985
|
+
*/
|
|
818
986
|
void swap(index_gt& other) noexcept {
|
|
819
987
|
std::swap(config_, other.config_);
|
|
820
988
|
std::swap(metric_, other.metric_);
|
|
@@ -835,6 +1003,9 @@ class index_gt {
|
|
|
835
1003
|
other.size_ = size;
|
|
836
1004
|
}
|
|
837
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* @brief Increases the `capacity()` of the index to allow adding more vectors.
|
|
1008
|
+
*/
|
|
838
1009
|
void reserve(std::size_t new_capacity) noexcept(false) {
|
|
839
1010
|
|
|
840
1011
|
assert_m(new_capacity >= size_, "Can't drop existing values");
|
|
@@ -845,6 +1016,10 @@ class index_gt {
|
|
|
845
1016
|
capacity_ = new_capacity;
|
|
846
1017
|
}
|
|
847
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* @brief Optimizes configuration options to fit the maximum number
|
|
1021
|
+
* of neighbors in CPU-cache-aligned buffers.
|
|
1022
|
+
*/
|
|
848
1023
|
static config_t optimize(config_t const& config) noexcept {
|
|
849
1024
|
precomputed_constants_t pre = precompute(config);
|
|
850
1025
|
std::size_t bytes_per_node_base = head_bytes_k + pre.neighbors_base_bytes + pre.mutex_bytes;
|
|
@@ -859,103 +1034,140 @@ class index_gt {
|
|
|
859
1034
|
|
|
860
1035
|
#pragma region Construction and Search
|
|
861
1036
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* @brief Inserts a new vector into the index. Thread-safe.
|
|
1039
|
+
*
|
|
1040
|
+
* @param[in] label External identifier/name/descriptor for the vector.
|
|
1041
|
+
* @param[in] vector Contiguous range of scalars forming a vector view.
|
|
1042
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1043
|
+
*/
|
|
1044
|
+
add_result_t add(label_t label, vector_view_t vector, add_config_t config = {}) noexcept(false) {
|
|
865
1045
|
|
|
866
1046
|
assert_m(!is_immutable(), "Can't add to an immutable index");
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1047
|
+
add_result_t result;
|
|
1048
|
+
result.new_size = size_.fetch_add(1);
|
|
1049
|
+
id_t id = static_cast<id_t>(result.new_size);
|
|
870
1050
|
|
|
871
|
-
// Determining how much memory to allocate depends on the target level
|
|
1051
|
+
// Determining how much memory to allocate depends on the target level
|
|
872
1052
|
lock_t new_level_lock(global_mutex_);
|
|
873
1053
|
level_t max_level = max_level_;
|
|
874
|
-
thread_context_t& context = thread_contexts_[
|
|
875
|
-
level_t
|
|
876
|
-
if (
|
|
1054
|
+
thread_context_t& context = thread_contexts_[config.thread];
|
|
1055
|
+
level_t target_level = choose_random_level(context.level_generator);
|
|
1056
|
+
if (target_level <= max_level)
|
|
877
1057
|
new_level_lock.unlock();
|
|
878
1058
|
|
|
879
1059
|
// Allocate the neighbors
|
|
880
|
-
node_ref_t new_node = node_malloc(
|
|
1060
|
+
node_ref_t new_node = node_malloc(label, vector, target_level, config.store_vector);
|
|
881
1061
|
lock_t new_lock = new_node.lock();
|
|
882
|
-
nodes_[
|
|
1062
|
+
nodes_[id] = new_node;
|
|
883
1063
|
|
|
884
1064
|
// Do nothing for the first element
|
|
885
|
-
if (!
|
|
886
|
-
max_level_ =
|
|
887
|
-
return
|
|
1065
|
+
if (!id) {
|
|
1066
|
+
max_level_ = target_level;
|
|
1067
|
+
return result;
|
|
888
1068
|
}
|
|
889
1069
|
|
|
1070
|
+
// Pull stats
|
|
1071
|
+
result.measurements = context.measurements_count;
|
|
1072
|
+
result.cycles = context.iteration_cycles;
|
|
1073
|
+
|
|
890
1074
|
// Go down the level, tracking only the closest match
|
|
891
|
-
id_t closest_id = search_for_one(entry_id_,
|
|
1075
|
+
id_t closest_id = search_for_one(entry_id_, vector, max_level, target_level, context);
|
|
892
1076
|
|
|
893
|
-
// From `
|
|
894
|
-
for (level_t level = (std::min)(
|
|
895
|
-
search_to_insert(closest_id,
|
|
896
|
-
closest_id = connect_new_element(
|
|
1077
|
+
// From `target_level` down perform proper extensive search
|
|
1078
|
+
for (level_t level = (std::min)(target_level, max_level); level >= 0; level--) {
|
|
1079
|
+
search_to_insert(closest_id, vector, level, context);
|
|
1080
|
+
closest_id = connect_new_element(id, level, context);
|
|
897
1081
|
}
|
|
898
1082
|
|
|
899
|
-
//
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1083
|
+
// Normalize stats
|
|
1084
|
+
result.measurements = context.measurements_count - result.measurements;
|
|
1085
|
+
result.cycles = context.iteration_cycles - result.cycles;
|
|
1086
|
+
|
|
1087
|
+
// Updating the entry point if needed
|
|
1088
|
+
if (target_level > max_level) {
|
|
1089
|
+
entry_id_ = id;
|
|
1090
|
+
max_level_ = target_level;
|
|
903
1091
|
}
|
|
904
|
-
return
|
|
1092
|
+
return result;
|
|
905
1093
|
}
|
|
906
1094
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1095
|
+
/**
|
|
1096
|
+
* @brief Searches for the closest members to the given ::query. Thread-safe.
|
|
1097
|
+
*
|
|
1098
|
+
* @param[in] query Contiguous range of scalars forming a vector view.
|
|
1099
|
+
* @param[in] wanted The upper bound for the number of results to return.
|
|
1100
|
+
* @param[in] callback Function receiving the labels and distances to matches in ascending order.
|
|
1101
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1102
|
+
*/
|
|
1103
|
+
template <typename callback_at>
|
|
1104
|
+
search_result_t search( //
|
|
1105
|
+
vector_view_t query, std::size_t wanted, //
|
|
1106
|
+
callback_at&& callback, search_config_t config = {}) const noexcept(false) {
|
|
911
1107
|
|
|
1108
|
+
search_result_t result;
|
|
912
1109
|
if (!size_)
|
|
913
|
-
return;
|
|
914
|
-
|
|
915
|
-
scalar_t const* query_vec = query_span.data();
|
|
916
|
-
std::size_t query_dim = query_span.size();
|
|
1110
|
+
return result;
|
|
917
1111
|
|
|
918
1112
|
// Go down the level, tracking only the closest match
|
|
919
|
-
thread_context_t& context = thread_contexts_[
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
context
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
callback(node(top.second).head.label, top.first);
|
|
931
|
-
context.top_candidates.pop();
|
|
1113
|
+
thread_context_t& context = thread_contexts_[config.thread];
|
|
1114
|
+
result.measurements = context.measurements_count;
|
|
1115
|
+
result.cycles = context.iteration_cycles;
|
|
1116
|
+
|
|
1117
|
+
if (config.exact) {
|
|
1118
|
+
search_exact(query, wanted, context);
|
|
1119
|
+
} else {
|
|
1120
|
+
id_t closest_id = search_for_one(entry_id_, query, max_level_, 0, context);
|
|
1121
|
+
std::size_t expansion = (std::max)(config_.expansion_search, wanted);
|
|
1122
|
+
// For bottom layer we need a more optimized procedure
|
|
1123
|
+
search_to_find_in_base(closest_id, query, expansion, context);
|
|
932
1124
|
}
|
|
1125
|
+
|
|
1126
|
+
top_candidates_t& top = context.top_candidates;
|
|
1127
|
+
top.sort_ascending();
|
|
1128
|
+
top.shrink(wanted);
|
|
1129
|
+
|
|
1130
|
+
candidate_t const* top_ordered = top.data();
|
|
1131
|
+
for (std::size_t i = 0; i < top.size(); ++i)
|
|
1132
|
+
callback(node(top_ordered[i].second).head.label, top_ordered[i].first);
|
|
1133
|
+
|
|
1134
|
+
// Normalize stats
|
|
1135
|
+
result.measurements = context.measurements_count - result.measurements;
|
|
1136
|
+
result.cycles = context.iteration_cycles - result.cycles;
|
|
1137
|
+
result.count = top.size();
|
|
1138
|
+
return result;
|
|
933
1139
|
}
|
|
934
1140
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* @brief Searches for the closest members to the given ::query. Thread-safe.
|
|
1143
|
+
* @note Convenience method for callback-based `search()`.
|
|
1144
|
+
*
|
|
1145
|
+
* @param[in] query Contiguous range of scalars forming a vector view.
|
|
1146
|
+
* @param[in] wanted The upper bound for the number of results to return.
|
|
1147
|
+
* @param[out] matches Contiguous output buffer for the labels of closest members.
|
|
1148
|
+
* @param[out] distances Contiguous output buffer for the distances to the closest members.
|
|
1149
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1150
|
+
* @return The number of found approximate matches. Always equal or less then ::wanted.
|
|
1151
|
+
*/
|
|
1152
|
+
search_result_t search( //
|
|
1153
|
+
vector_view_t query, std::size_t wanted, //
|
|
1154
|
+
label_t* matches, distance_t* distances, search_config_t config = {}) const noexcept(false) {
|
|
939
1155
|
|
|
940
|
-
std::size_t found = 0;
|
|
941
1156
|
auto callback = [&](label_t label, distance_t distance) noexcept {
|
|
942
1157
|
if (matches)
|
|
943
|
-
matches
|
|
1158
|
+
*(matches++) = label;
|
|
944
1159
|
if (distances)
|
|
945
|
-
distances
|
|
946
|
-
++found;
|
|
1160
|
+
*(distances++) = distance;
|
|
947
1161
|
};
|
|
948
|
-
search(
|
|
949
|
-
std::reverse(matches, matches + found * (matches != nullptr));
|
|
950
|
-
std::reverse(distances, distances + found * (distances != nullptr));
|
|
951
|
-
return found;
|
|
1162
|
+
return search(query, wanted, callback, config);
|
|
952
1163
|
}
|
|
953
1164
|
|
|
954
1165
|
#pragma endregion
|
|
955
1166
|
|
|
956
1167
|
#pragma region Serialization
|
|
957
1168
|
|
|
958
|
-
|
|
1169
|
+
private:
|
|
1170
|
+
struct serialized_state_t {
|
|
959
1171
|
// Check compatibility
|
|
960
1172
|
std::uint64_t bytes_per_label{};
|
|
961
1173
|
std::uint64_t bytes_per_id{};
|
|
@@ -967,12 +1179,15 @@ class index_gt {
|
|
|
967
1179
|
std::uint64_t max_level{};
|
|
968
1180
|
};
|
|
969
1181
|
|
|
1182
|
+
public:
|
|
970
1183
|
/**
|
|
971
|
-
*
|
|
1184
|
+
* @brief Saves serialized binary index representation to disk,
|
|
1185
|
+
* co-locating vectors and neighbors lists.
|
|
1186
|
+
* Available on Linux, MacOS, Windows.
|
|
972
1187
|
*/
|
|
973
1188
|
void save(char const* file_path) const noexcept(false) {
|
|
974
1189
|
|
|
975
|
-
|
|
1190
|
+
serialized_state_t state;
|
|
976
1191
|
// Check compatibility
|
|
977
1192
|
state.bytes_per_label = sizeof(label_t);
|
|
978
1193
|
state.bytes_per_id = sizeof(id_t);
|
|
@@ -1018,10 +1233,12 @@ class index_gt {
|
|
|
1018
1233
|
}
|
|
1019
1234
|
|
|
1020
1235
|
/**
|
|
1021
|
-
*
|
|
1236
|
+
* @brief Loads the serialized binary index representation from disk,
|
|
1237
|
+
* copying both vectors and neighbors lists into RAM.
|
|
1238
|
+
* Available on Linux, MacOS, Windows.
|
|
1022
1239
|
*/
|
|
1023
1240
|
void load(char const* file_path) noexcept(false) {
|
|
1024
|
-
|
|
1241
|
+
serialized_state_t state;
|
|
1025
1242
|
std::FILE* file = std::fopen(file_path, "r");
|
|
1026
1243
|
if (!file)
|
|
1027
1244
|
throw std::runtime_error(std::strerror(errno));
|
|
@@ -1061,7 +1278,7 @@ class index_gt {
|
|
|
1061
1278
|
}
|
|
1062
1279
|
|
|
1063
1280
|
std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
|
|
1064
|
-
node_ref_t node_ref = node_malloc(head.label, nullptr, head.dim, head.level, true);
|
|
1281
|
+
node_ref_t node_ref = node_malloc(head.label, {nullptr, head.dim}, head.level, true);
|
|
1065
1282
|
read = std::fread((byte_t*)&node_ref.head + head_bytes_k, bytes_to_dump - head_bytes_k, 1, file);
|
|
1066
1283
|
if (!read) {
|
|
1067
1284
|
std::fclose(file);
|
|
@@ -1076,13 +1293,15 @@ class index_gt {
|
|
|
1076
1293
|
}
|
|
1077
1294
|
|
|
1078
1295
|
/**
|
|
1079
|
-
*
|
|
1296
|
+
* @brief Memory-maps the serialized binary index representation from disk,
|
|
1297
|
+
* @b without copying the vectors and neighbors lists into RAM.
|
|
1298
|
+
* Available on Linux, MacOS, but @b not on Windows.
|
|
1080
1299
|
*/
|
|
1081
1300
|
void view(char const* file_path) noexcept(false) {
|
|
1082
1301
|
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
|
|
1083
1302
|
throw std::logic_error("Memory-mapping is not yet available for Windows");
|
|
1084
1303
|
#else
|
|
1085
|
-
|
|
1304
|
+
serialized_state_t state;
|
|
1086
1305
|
int open_flags = O_RDONLY;
|
|
1087
1306
|
#if __linux__
|
|
1088
1307
|
open_flags |= O_NOATIME;
|
|
@@ -1183,11 +1402,10 @@ class index_gt {
|
|
|
1183
1402
|
node = node_t{};
|
|
1184
1403
|
}
|
|
1185
1404
|
|
|
1186
|
-
node_ref_t node_malloc(
|
|
1187
|
-
label_t label, scalar_t const* vector, std::size_t dim, //
|
|
1188
|
-
level_t level, bool store_vector = true) noexcept(false) {
|
|
1405
|
+
node_ref_t node_malloc(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept(false) {
|
|
1189
1406
|
|
|
1190
1407
|
// This function is rarely called and can be as expensive as needed for higher space-efficiency.
|
|
1408
|
+
std::size_t dim = vector.size();
|
|
1191
1409
|
std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * level;
|
|
1192
1410
|
std::size_t node_bytes = //
|
|
1193
1411
|
pre_.mutex_bytes + // Optional concurrency-control
|
|
@@ -1201,10 +1419,10 @@ class index_gt {
|
|
|
1201
1419
|
mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
|
|
1202
1420
|
scalar_t* scalars = store_vector //
|
|
1203
1421
|
? (scalar_t*)(data + pre_.mutex_bytes + head_bytes_k + levels_bytes)
|
|
1204
|
-
: (scalar_t*)(vector);
|
|
1422
|
+
: (scalar_t*)(vector.data());
|
|
1205
1423
|
|
|
1206
1424
|
std::memset(data, 0, node_bytes);
|
|
1207
|
-
std::memcpy(scalars, vector, sizeof(scalar_t) * dim * (store_vector && vector));
|
|
1425
|
+
std::memcpy(scalars, vector.data(), sizeof(scalar_t) * dim * (store_vector && vector.data()));
|
|
1208
1426
|
|
|
1209
1427
|
node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
|
|
1210
1428
|
head.label = label;
|
|
@@ -1238,68 +1456,54 @@ class index_gt {
|
|
|
1238
1456
|
id_t connect_new_element(id_t new_id, level_t level, thread_context_t& context) noexcept(false) {
|
|
1239
1457
|
|
|
1240
1458
|
node_ref_t new_node = node(new_id);
|
|
1241
|
-
|
|
1459
|
+
top_candidates_t& top = context.top_candidates;
|
|
1242
1460
|
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);
|
|
1244
|
-
|
|
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;
|
|
1248
1461
|
|
|
1249
1462
|
// Outgoing links from `new_id`:
|
|
1463
|
+
neighbors_ref_t new_neighbors = neighbors(new_node, level);
|
|
1250
1464
|
{
|
|
1251
|
-
|
|
1252
|
-
|
|
1465
|
+
assert_m(!new_neighbors.size(), "The newly inserted element should have blank link list");
|
|
1466
|
+
candidates_view_t top_view = refine(top, config_.connectivity, context);
|
|
1253
1467
|
|
|
1254
|
-
|
|
1255
|
-
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1468
|
+
for (std::size_t idx = 0; idx != top_view.size(); idx++) {
|
|
1256
1469
|
assert_m(!new_neighbors[idx], "Possible memory corruption");
|
|
1257
|
-
assert_m(level <= node(
|
|
1258
|
-
new_neighbors[idx]
|
|
1470
|
+
assert_m(level <= node(top_view[idx].second).head.level, "Linking to missing level");
|
|
1471
|
+
new_neighbors.push_back(top_view[idx].second);
|
|
1259
1472
|
}
|
|
1260
1473
|
}
|
|
1261
1474
|
|
|
1262
1475
|
// Reverse links from the neighbors:
|
|
1263
|
-
for (
|
|
1264
|
-
id_t close_id = top_ordered[idx].second;
|
|
1476
|
+
for (id_t close_id : new_neighbors) {
|
|
1265
1477
|
node_ref_t close_node = node(close_id);
|
|
1266
1478
|
lock_t close_lock = close_node.lock();
|
|
1267
1479
|
|
|
1268
1480
|
neighbors_ref_t close_header = neighbors(close_node, level);
|
|
1269
|
-
assert_m(close_header.
|
|
1481
|
+
assert_m(close_header.size() <= connectivity_max, "Possible corruption");
|
|
1270
1482
|
assert_m(close_id != new_id, "Self-loops are impossible");
|
|
1271
1483
|
assert_m(level <= close_node.head.level, "Linking to missing level");
|
|
1272
1484
|
|
|
1273
1485
|
// If `new_id` is already present in the neighboring connections of `close_id`
|
|
1274
1486
|
// then no need to modify any connections or run the heuristics.
|
|
1275
|
-
if (close_header.
|
|
1276
|
-
close_header
|
|
1277
|
-
close_header.count_++;
|
|
1487
|
+
if (close_header.size() < connectivity_max) {
|
|
1488
|
+
close_header.push_back(new_id);
|
|
1278
1489
|
continue;
|
|
1279
1490
|
}
|
|
1280
1491
|
|
|
1281
1492
|
// To fit a new connection we need to drop an existing one.
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
new_id);
|
|
1288
|
-
for (id_t successor_id : close_header) {
|
|
1289
|
-
node_ref_t successor_node = node(successor_id);
|
|
1290
|
-
candidates.emplace( //
|
|
1291
|
-
context.metric( //
|
|
1292
|
-
successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
|
|
1293
|
-
successor_id);
|
|
1294
|
-
}
|
|
1295
|
-
span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
|
|
1493
|
+
top.clear();
|
|
1494
|
+
top.reserve(close_header.size() + 1);
|
|
1495
|
+
top.emplace_reserved(context.measure(new_node, close_node), new_id);
|
|
1496
|
+
for (id_t successor_id : close_header)
|
|
1497
|
+
top.emplace_reserved(context.measure(node(successor_id), close_node), successor_id);
|
|
1296
1498
|
|
|
1297
1499
|
// Export the results:
|
|
1298
|
-
|
|
1299
|
-
|
|
1500
|
+
close_header.clear();
|
|
1501
|
+
candidates_view_t top_view = refine(top, connectivity_max, context);
|
|
1502
|
+
for (std::size_t idx = 0; idx != top_view.size(); idx++)
|
|
1503
|
+
close_header.push_back(top_view[idx].second);
|
|
1300
1504
|
}
|
|
1301
1505
|
|
|
1302
|
-
return
|
|
1506
|
+
return new_neighbors[0];
|
|
1303
1507
|
}
|
|
1304
1508
|
|
|
1305
1509
|
level_t choose_random_level(std::default_random_engine& level_generator) const noexcept {
|
|
@@ -1308,13 +1512,12 @@ class index_gt {
|
|
|
1308
1512
|
return (level_t)r;
|
|
1309
1513
|
}
|
|
1310
1514
|
|
|
1311
|
-
id_t search_for_one(
|
|
1312
|
-
id_t
|
|
1313
|
-
level_t begin_level, level_t end_level,
|
|
1515
|
+
id_t search_for_one( //
|
|
1516
|
+
id_t closest_id, vector_view_t query, //
|
|
1517
|
+
level_t begin_level, level_t end_level, //
|
|
1518
|
+
thread_context_t& context) const noexcept {
|
|
1314
1519
|
|
|
1315
|
-
|
|
1316
|
-
distance_t closest_dist =
|
|
1317
|
-
context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
|
|
1520
|
+
distance_t closest_dist = context.measure(query, node(closest_id));
|
|
1318
1521
|
for (level_t level = begin_level; level > end_level; level--) {
|
|
1319
1522
|
bool changed;
|
|
1320
1523
|
do {
|
|
@@ -1323,44 +1526,47 @@ class index_gt {
|
|
|
1323
1526
|
lock_t closest_lock = closest_node.lock();
|
|
1324
1527
|
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
1325
1528
|
for (id_t candidate_id : closest_header) {
|
|
1326
|
-
|
|
1327
|
-
distance_t candidate_dist =
|
|
1328
|
-
context.metric(query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
|
|
1529
|
+
distance_t candidate_dist = context.measure(query, node(candidate_id));
|
|
1329
1530
|
if (candidate_dist < closest_dist) {
|
|
1330
1531
|
closest_dist = candidate_dist;
|
|
1331
1532
|
closest_id = candidate_id;
|
|
1332
1533
|
changed = true;
|
|
1333
1534
|
}
|
|
1334
1535
|
}
|
|
1536
|
+
context.iteration_cycles++;
|
|
1335
1537
|
} while (changed);
|
|
1336
1538
|
}
|
|
1337
1539
|
return closest_id;
|
|
1338
1540
|
}
|
|
1339
1541
|
|
|
1340
|
-
void search_to_insert(
|
|
1341
|
-
id_t start_id,
|
|
1342
|
-
|
|
1542
|
+
void search_to_insert( //
|
|
1543
|
+
id_t start_id, vector_view_t query, level_t level, //
|
|
1544
|
+
thread_context_t& context) noexcept(false) {
|
|
1343
1545
|
|
|
1344
1546
|
visits_bitset_t& visits = context.visits;
|
|
1345
|
-
|
|
1346
|
-
|
|
1547
|
+
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
1548
|
+
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
1549
|
+
std::size_t const top_limit = config_.expansion_add;
|
|
1347
1550
|
|
|
1348
|
-
top_candidates.clear();
|
|
1349
|
-
candidates_set.clear();
|
|
1350
1551
|
visits.clear();
|
|
1552
|
+
candidates.clear();
|
|
1553
|
+
top.clear();
|
|
1554
|
+
top.reserve(top_limit + 1);
|
|
1351
1555
|
|
|
1352
|
-
distance_t
|
|
1353
|
-
|
|
1354
|
-
|
|
1556
|
+
distance_t radius = context.measure(query, node(start_id));
|
|
1557
|
+
candidates.emplace(-radius, start_id);
|
|
1558
|
+
top.emplace_reserved(radius, start_id);
|
|
1355
1559
|
visits.set(start_id);
|
|
1356
1560
|
|
|
1357
|
-
while (!
|
|
1561
|
+
while (!candidates.empty()) {
|
|
1358
1562
|
|
|
1359
|
-
|
|
1360
|
-
if ((-candidacy.first) >
|
|
1563
|
+
candidate_t candidacy = candidates.top();
|
|
1564
|
+
if ((-candidacy.first) > radius && top.size() == top_limit)
|
|
1361
1565
|
break;
|
|
1362
1566
|
|
|
1363
|
-
|
|
1567
|
+
candidates.pop();
|
|
1568
|
+
context.iteration_cycles++;
|
|
1569
|
+
|
|
1364
1570
|
id_t candidate_id = candidacy.second;
|
|
1365
1571
|
node_ref_t candidate_node = node(candidate_id);
|
|
1366
1572
|
lock_t candidate_lock = candidate_node.lock();
|
|
@@ -1372,48 +1578,48 @@ class index_gt {
|
|
|
1372
1578
|
continue;
|
|
1373
1579
|
|
|
1374
1580
|
visits.set(successor_id);
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
top_candidates.pop();
|
|
1384
|
-
if (!top_candidates.empty())
|
|
1385
|
-
closest_dist = top_candidates.top().first;
|
|
1581
|
+
distance_t successor_dist = context.measure(query, node(successor_id));
|
|
1582
|
+
|
|
1583
|
+
if (top.size() < top_limit || successor_dist < radius) {
|
|
1584
|
+
candidates.emplace(-successor_dist, successor_id);
|
|
1585
|
+
top.emplace_reserved(successor_dist, successor_id);
|
|
1586
|
+
if (top.size() > top_limit)
|
|
1587
|
+
top.pop();
|
|
1588
|
+
radius = top.top().first;
|
|
1386
1589
|
}
|
|
1387
1590
|
}
|
|
1388
1591
|
}
|
|
1389
1592
|
}
|
|
1390
1593
|
|
|
1391
|
-
void search_to_find_in_base(
|
|
1392
|
-
id_t start_id,
|
|
1393
|
-
|
|
1594
|
+
void search_to_find_in_base( //
|
|
1595
|
+
id_t start_id, vector_view_t query, std::size_t expansion, //
|
|
1596
|
+
thread_context_t& context) const noexcept(false) {
|
|
1394
1597
|
|
|
1395
1598
|
visits_bitset_t& visits = context.visits;
|
|
1396
|
-
|
|
1397
|
-
|
|
1599
|
+
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
1600
|
+
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
1601
|
+
std::size_t const top_limit = expansion;
|
|
1398
1602
|
|
|
1399
1603
|
visits.clear();
|
|
1400
|
-
|
|
1401
|
-
|
|
1604
|
+
candidates.clear();
|
|
1605
|
+
top.clear();
|
|
1606
|
+
top.reserve(top_limit + 1);
|
|
1402
1607
|
|
|
1403
|
-
distance_t
|
|
1404
|
-
|
|
1405
|
-
|
|
1608
|
+
distance_t radius = context.measure(query, node(start_id));
|
|
1609
|
+
candidates.emplace(-radius, start_id);
|
|
1610
|
+
top.emplace_reserved(radius, start_id);
|
|
1406
1611
|
visits.set(start_id);
|
|
1407
1612
|
|
|
1408
|
-
while (!
|
|
1613
|
+
while (!candidates.empty()) {
|
|
1409
1614
|
|
|
1410
|
-
|
|
1411
|
-
if ((-
|
|
1615
|
+
candidate_t candidate = candidates.top();
|
|
1616
|
+
if ((-candidate.first) > radius)
|
|
1412
1617
|
break;
|
|
1413
1618
|
|
|
1414
|
-
|
|
1619
|
+
candidates.pop();
|
|
1620
|
+
context.iteration_cycles++;
|
|
1415
1621
|
|
|
1416
|
-
id_t candidate_id =
|
|
1622
|
+
id_t candidate_id = candidate.second;
|
|
1417
1623
|
neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
|
|
1418
1624
|
|
|
1419
1625
|
prefetch_neighbors(candidate_header, visits);
|
|
@@ -1422,47 +1628,56 @@ class index_gt {
|
|
|
1422
1628
|
continue;
|
|
1423
1629
|
|
|
1424
1630
|
visits.set(successor_id);
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
if (top_candidates.size() > expansion)
|
|
1434
|
-
top_candidates.pop();
|
|
1435
|
-
if (!top_candidates.empty())
|
|
1436
|
-
closest_dist = top_candidates.top().first;
|
|
1631
|
+
distance_t successor_dist = context.measure(query, node(successor_id));
|
|
1632
|
+
|
|
1633
|
+
if (top.size() < top_limit || successor_dist < radius) {
|
|
1634
|
+
candidates.emplace(-successor_dist, successor_id);
|
|
1635
|
+
top.emplace_reserved(successor_dist, successor_id);
|
|
1636
|
+
if (top.size() > top_limit)
|
|
1637
|
+
top.pop();
|
|
1638
|
+
radius = top.top().first;
|
|
1437
1639
|
}
|
|
1438
1640
|
}
|
|
1439
1641
|
}
|
|
1440
1642
|
}
|
|
1441
1643
|
|
|
1644
|
+
void search_exact(vector_view_t query, std::size_t count, thread_context_t& context) const noexcept(false) {
|
|
1645
|
+
top_candidates_t& top = context.top_candidates;
|
|
1646
|
+
top.clear();
|
|
1647
|
+
top.reserve(count);
|
|
1648
|
+
for (std::size_t i = 0; i != nodes_.size(); ++i)
|
|
1649
|
+
top.emplace_reserved(context.measure(query, node(nodes_[i])), static_cast<id_t>(i));
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1442
1652
|
void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|
|
1443
1653
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* @brief This algorithm from the original paper implements a heuristic,
|
|
1656
|
+
* that massively reduces the number of connections a point has,
|
|
1657
|
+
* to keep only the neighbors, that are from each other.
|
|
1658
|
+
*/
|
|
1446
1659
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1660
|
+
candidates_view_t refine( //
|
|
1661
|
+
top_candidates_t& top, std::size_t needed, //
|
|
1662
|
+
thread_context_t& context) const noexcept {
|
|
1663
|
+
|
|
1664
|
+
top.sort_ascending();
|
|
1665
|
+
candidate_t* top_data = top.data();
|
|
1666
|
+
std::size_t const top_count = top.size();
|
|
1450
1667
|
if (top_count < needed)
|
|
1451
|
-
return {
|
|
1668
|
+
return {top_data, top_count};
|
|
1452
1669
|
|
|
1453
1670
|
std::size_t submitted_count = 1;
|
|
1454
1671
|
std::size_t consumed_count = 1; /// Always equal or greater than `submitted_count`.
|
|
1455
1672
|
while (submitted_count < needed && consumed_count < top_count) {
|
|
1456
|
-
|
|
1673
|
+
candidate_t candidate = top_data[consumed_count];
|
|
1457
1674
|
node_ref_t candidate_node = node(candidate.second);
|
|
1458
1675
|
distance_t candidate_dist = candidate.first;
|
|
1459
1676
|
bool good = true;
|
|
1460
1677
|
for (std::size_t idx = 0; idx < submitted_count; idx++) {
|
|
1461
|
-
|
|
1678
|
+
candidate_t submitted = top_data[idx];
|
|
1462
1679
|
node_ref_t submitted_node = node(submitted.second);
|
|
1463
|
-
distance_t inter_result_dist =
|
|
1464
|
-
submitted_node.vector, candidate_node.vector, //
|
|
1465
|
-
submitted_node.head.dim, candidate_node.head.dim);
|
|
1680
|
+
distance_t inter_result_dist = context.measure(submitted_node, candidate_node);
|
|
1466
1681
|
if (inter_result_dist < candidate_dist) {
|
|
1467
1682
|
good = false;
|
|
1468
1683
|
break;
|
|
@@ -1470,13 +1685,14 @@ class index_gt {
|
|
|
1470
1685
|
}
|
|
1471
1686
|
|
|
1472
1687
|
if (good) {
|
|
1473
|
-
|
|
1688
|
+
top_data[submitted_count] = top_data[consumed_count];
|
|
1474
1689
|
submitted_count++;
|
|
1475
1690
|
}
|
|
1476
1691
|
consumed_count++;
|
|
1477
1692
|
}
|
|
1478
1693
|
|
|
1479
|
-
|
|
1694
|
+
top.shrink(submitted_count);
|
|
1695
|
+
return {top_data, submitted_count};
|
|
1480
1696
|
}
|
|
1481
1697
|
};
|
|
1482
1698
|
|