usearch 0.2.2 → 0.2.4
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 +39 -15
- package/.github/workflows/update_version.sh +6 -0
- package/.releaserc +77 -0
- package/.vscode/settings.json +10 -1
- package/Cargo.toml +1 -1
- package/README.md +281 -230
- package/VERSION +1 -1
- package/conanfile.py +3 -2
- package/docs/Benchmarking.md +182 -0
- package/docs/Compilation.md +112 -0
- package/include/usearch/usearch.hpp +212 -116
- package/objc/include/USearchObjective.h +10 -9
- package/package.json +7 -1
- package/python/bench.py +100 -28
- package/python/lib.cpp +51 -28
- package/python/test.py +78 -36
- package/setup.py +11 -3
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +58 -23
- package/src/bench.cpp +11 -15
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
|
@@ -123,7 +123,7 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
|
123
123
|
ab += result_t(a[i]) * result_t(b[i]), //
|
|
124
124
|
a2 += square<result_t>(a[i]), //
|
|
125
125
|
b2 += square<result_t>(b[i]);
|
|
126
|
-
return ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
126
|
+
return 1 - ab / (std::sqrt(a2) * std::sqrt(b2));
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
|
|
@@ -330,7 +330,8 @@ using visits_bitset_t = visits_bitset_gt<>;
|
|
|
330
330
|
|
|
331
331
|
/**
|
|
332
332
|
* @brief Similar to `std::priority_queue`, but allows raw access to underlying
|
|
333
|
-
* memory, in case you want to shuffle it or sort.
|
|
333
|
+
* memory, in case you want to shuffle it or sort. Good for collections
|
|
334
|
+
* from 100s to 10'000s elements.
|
|
334
335
|
*/
|
|
335
336
|
template <typename element_at, //
|
|
336
337
|
typename comparator_at = std::less<void>, // <void> is needed before C++14.
|
|
@@ -347,23 +348,19 @@ class max_heap_gt {
|
|
|
347
348
|
element_t* elements_;
|
|
348
349
|
std::size_t size_;
|
|
349
350
|
std::size_t capacity_;
|
|
350
|
-
std::size_t max_capacity_;
|
|
351
351
|
|
|
352
352
|
public:
|
|
353
|
-
max_heap_gt(
|
|
354
|
-
: elements_(nullptr), size_(0), capacity_(0), max_capacity_(max_capacity) {}
|
|
353
|
+
max_heap_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
|
|
355
354
|
|
|
356
355
|
max_heap_gt(max_heap_gt&& other) noexcept {
|
|
357
356
|
std::swap(elements_, other.elements_);
|
|
358
357
|
std::swap(size_, other.size_);
|
|
359
358
|
std::swap(capacity_, other.capacity_);
|
|
360
|
-
std::swap(max_capacity_, other.max_capacity_);
|
|
361
359
|
}
|
|
362
360
|
max_heap_gt& operator=(max_heap_gt&& other) noexcept {
|
|
363
361
|
std::swap(elements_, other.elements_);
|
|
364
362
|
std::swap(size_, other.size_);
|
|
365
363
|
std::swap(capacity_, other.capacity_);
|
|
366
|
-
std::swap(max_capacity_, other.max_capacity_);
|
|
367
364
|
return *this;
|
|
368
365
|
}
|
|
369
366
|
|
|
@@ -384,16 +381,12 @@ class max_heap_gt {
|
|
|
384
381
|
}
|
|
385
382
|
}
|
|
386
383
|
|
|
387
|
-
bool reserve(std::size_t
|
|
388
|
-
if (
|
|
384
|
+
bool reserve(std::size_t new_capacity) noexcept {
|
|
385
|
+
if (new_capacity < capacity_)
|
|
389
386
|
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
387
|
|
|
388
|
+
new_capacity = ceil2(new_capacity);
|
|
389
|
+
new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
|
|
397
390
|
auto allocator = allocator_t{};
|
|
398
391
|
auto new_elements = allocator.allocate(new_capacity);
|
|
399
392
|
if (!new_elements)
|
|
@@ -413,10 +406,15 @@ class max_heap_gt {
|
|
|
413
406
|
if (!reserve(size_ + 1))
|
|
414
407
|
return false;
|
|
415
408
|
|
|
409
|
+
emplace_reserved(std::forward<args_at>(args)...);
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
template <typename... args_at> //
|
|
414
|
+
void emplace_reserved(args_at&&... args) noexcept {
|
|
416
415
|
new (&elements_[size_]) element_t({std::forward<args_at>(args)...});
|
|
417
416
|
size_++;
|
|
418
417
|
shift_up(size_ - 1);
|
|
419
|
-
return true;
|
|
420
418
|
}
|
|
421
419
|
|
|
422
420
|
element_t pop() noexcept {
|
|
@@ -430,10 +428,7 @@ class max_heap_gt {
|
|
|
430
428
|
|
|
431
429
|
/** @brief Invalidates the "max-heap" property, transforming into ascending range. */
|
|
432
430
|
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; }
|
|
431
|
+
void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
|
|
437
432
|
|
|
438
433
|
element_t* data() noexcept { return elements_; }
|
|
439
434
|
element_t const* data() const noexcept { return elements_; }
|
|
@@ -467,6 +462,108 @@ class max_heap_gt {
|
|
|
467
462
|
}
|
|
468
463
|
};
|
|
469
464
|
|
|
465
|
+
/**
|
|
466
|
+
* @brief Similar to `std::priority_queue`, but allows raw access to underlying
|
|
467
|
+
* memory and always keeps the data sorted. Ideal for small collections
|
|
468
|
+
* under 128 elements.
|
|
469
|
+
*/
|
|
470
|
+
template <typename element_at, //
|
|
471
|
+
typename comparator_at = std::less<void>, // <void> is needed before C++14.
|
|
472
|
+
typename allocator_at = std::allocator<element_at>> //
|
|
473
|
+
class sorted_buffer_gt {
|
|
474
|
+
public:
|
|
475
|
+
using element_t = element_at;
|
|
476
|
+
using comparator_t = comparator_at;
|
|
477
|
+
using allocator_t = allocator_at;
|
|
478
|
+
|
|
479
|
+
using value_type = element_t;
|
|
480
|
+
|
|
481
|
+
private:
|
|
482
|
+
element_t* elements_;
|
|
483
|
+
std::size_t size_;
|
|
484
|
+
std::size_t capacity_;
|
|
485
|
+
|
|
486
|
+
public:
|
|
487
|
+
sorted_buffer_gt() noexcept : elements_(nullptr), size_(0), capacity_(0) {}
|
|
488
|
+
|
|
489
|
+
sorted_buffer_gt(sorted_buffer_gt&& other) noexcept {
|
|
490
|
+
std::swap(elements_, other.elements_);
|
|
491
|
+
std::swap(size_, other.size_);
|
|
492
|
+
std::swap(capacity_, other.capacity_);
|
|
493
|
+
}
|
|
494
|
+
sorted_buffer_gt& operator=(sorted_buffer_gt&& other) noexcept {
|
|
495
|
+
std::swap(elements_, other.elements_);
|
|
496
|
+
std::swap(size_, other.size_);
|
|
497
|
+
std::swap(capacity_, other.capacity_);
|
|
498
|
+
return *this;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
sorted_buffer_gt(sorted_buffer_gt const&) = delete;
|
|
502
|
+
sorted_buffer_gt& operator=(sorted_buffer_gt const&) = delete;
|
|
503
|
+
|
|
504
|
+
~sorted_buffer_gt() {
|
|
505
|
+
clear();
|
|
506
|
+
allocator_t{}.deallocate(elements_, capacity_);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
bool empty() const noexcept { return !size_; }
|
|
510
|
+
std::size_t size() const noexcept { return size_; }
|
|
511
|
+
std::size_t capacity() const noexcept { return capacity_; }
|
|
512
|
+
element_t const& top() const noexcept { return elements_[size_ - 1]; }
|
|
513
|
+
|
|
514
|
+
void clear() noexcept {
|
|
515
|
+
while (size_)
|
|
516
|
+
size_--, elements_[size_].~element_t();
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
bool reserve(std::size_t new_capacity) noexcept {
|
|
520
|
+
if (new_capacity < capacity_)
|
|
521
|
+
return true;
|
|
522
|
+
|
|
523
|
+
new_capacity = ceil2(new_capacity);
|
|
524
|
+
new_capacity = (std::max<std::size_t>)(new_capacity, (std::max<std::size_t>)(capacity_ * 2u, 16u));
|
|
525
|
+
auto allocator = allocator_t{};
|
|
526
|
+
auto new_elements = allocator.allocate(new_capacity);
|
|
527
|
+
if (!new_elements)
|
|
528
|
+
return false;
|
|
529
|
+
|
|
530
|
+
if (size_) {
|
|
531
|
+
std::uninitialized_copy_n(elements_, size_, new_elements);
|
|
532
|
+
allocator.deallocate(elements_, capacity_);
|
|
533
|
+
}
|
|
534
|
+
elements_ = new_elements;
|
|
535
|
+
capacity_ = new_capacity;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
template <typename... args_at> void emplace_reserved(args_at&&... args) {
|
|
540
|
+
element_t element({std::forward<args_at>(args)...});
|
|
541
|
+
std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
|
|
542
|
+
std::size_t to_move = size_ - slot;
|
|
543
|
+
element_t* source = elements_ + size_ - 1;
|
|
544
|
+
for (; to_move; --to_move, --source)
|
|
545
|
+
source[1] = source[0];
|
|
546
|
+
elements_[slot] = element;
|
|
547
|
+
size_++;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
element_t pop() noexcept {
|
|
551
|
+
size_--;
|
|
552
|
+
element_t result = elements_[size_];
|
|
553
|
+
elements_[size_].~element_t();
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
void sort_ascending() noexcept {}
|
|
558
|
+
void shrink(std::size_t n) noexcept { size_ = (std::min<std::size_t>)(n, size_); }
|
|
559
|
+
|
|
560
|
+
element_t* data() noexcept { return elements_; }
|
|
561
|
+
element_t const* data() const noexcept { return elements_; }
|
|
562
|
+
|
|
563
|
+
private:
|
|
564
|
+
static bool less(element_t const& a, element_t const& b) noexcept { return comparator_t{}(a, b); }
|
|
565
|
+
};
|
|
566
|
+
|
|
470
567
|
/**
|
|
471
568
|
*
|
|
472
569
|
*/
|
|
@@ -672,16 +769,18 @@ class index_gt {
|
|
|
672
769
|
std::size_t neighbors_base_bytes{};
|
|
673
770
|
std::size_t mutex_bytes{};
|
|
674
771
|
};
|
|
675
|
-
struct
|
|
772
|
+
struct candidate_t {
|
|
676
773
|
distance_t first;
|
|
677
774
|
id_t second;
|
|
678
775
|
};
|
|
679
776
|
struct compare_by_distance_t {
|
|
680
|
-
inline bool operator()(
|
|
777
|
+
inline bool operator()(candidate_t a, candidate_t b) const noexcept { return a.first < b.first; }
|
|
681
778
|
};
|
|
682
779
|
|
|
683
|
-
using
|
|
684
|
-
using
|
|
780
|
+
using candidates_view_t = span_gt<candidate_t const>;
|
|
781
|
+
using candidates_allocator_t = typename allocator_traits_t::template rebind_alloc<candidate_t>;
|
|
782
|
+
using top_candidates_t = sorted_buffer_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
|
|
783
|
+
using next_candidates_t = max_heap_gt<candidate_t, compare_by_distance_t, candidates_allocator_t>;
|
|
685
784
|
|
|
686
785
|
struct neighbors_ref_t {
|
|
687
786
|
neighbors_count_t& count_;
|
|
@@ -696,6 +795,8 @@ class index_gt {
|
|
|
696
795
|
inline id_t& operator[](std::size_t i) noexcept { return neighbors_[i]; }
|
|
697
796
|
inline id_t operator[](std::size_t i) const noexcept { return neighbors_[i]; }
|
|
698
797
|
inline std::size_t size() const noexcept { return count_; }
|
|
798
|
+
inline void clear() noexcept { count_ = 0u; }
|
|
799
|
+
inline void push_back(id_t id) noexcept { neighbors_[count_] = id, count_++; }
|
|
699
800
|
};
|
|
700
801
|
|
|
701
802
|
#if defined(WINDOWS)
|
|
@@ -739,8 +840,8 @@ class index_gt {
|
|
|
739
840
|
};
|
|
740
841
|
|
|
741
842
|
struct usearch_align_m thread_context_t {
|
|
742
|
-
|
|
743
|
-
|
|
843
|
+
top_candidates_t top_candidates;
|
|
844
|
+
next_candidates_t next_candidates;
|
|
744
845
|
visits_bitset_t visits;
|
|
745
846
|
std::default_random_engine level_generator;
|
|
746
847
|
metric_t metric;
|
|
@@ -920,16 +1021,14 @@ class index_gt {
|
|
|
920
1021
|
id_t closest_id = search_for_one(entry_id_, query_vec, query_dim, max_level_, 0, context);
|
|
921
1022
|
|
|
922
1023
|
// For bottom layer we need a more optimized procedure
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
callback(node(top.second).head.label, top.first);
|
|
931
|
-
context.top_candidates.pop();
|
|
932
|
-
}
|
|
1024
|
+
std::size_t expansion = (std::max)(config_.expansion_search, wanted);
|
|
1025
|
+
search_to_find_in_base(closest_id, query_vec, query_dim, expansion, context);
|
|
1026
|
+
context.top_candidates.sort_ascending();
|
|
1027
|
+
context.top_candidates.shrink(wanted);
|
|
1028
|
+
|
|
1029
|
+
candidate_t const* top = context.top_candidates.data();
|
|
1030
|
+
for (std::size_t i = 0; i < context.top_candidates.size(); ++i)
|
|
1031
|
+
callback(node(top[i].second).head.label, top[i].first);
|
|
933
1032
|
}
|
|
934
1033
|
|
|
935
1034
|
std::size_t search( //
|
|
@@ -946,8 +1045,6 @@ class index_gt {
|
|
|
946
1045
|
++found;
|
|
947
1046
|
};
|
|
948
1047
|
search(query_span, wanted, callback, thread_idx);
|
|
949
|
-
std::reverse(matches, matches + found * (matches != nullptr));
|
|
950
|
-
std::reverse(distances, distances + found * (distances != nullptr));
|
|
951
1048
|
return found;
|
|
952
1049
|
}
|
|
953
1050
|
|
|
@@ -1238,68 +1335,62 @@ class index_gt {
|
|
|
1238
1335
|
id_t connect_new_element(id_t new_id, level_t level, thread_context_t& context) noexcept(false) {
|
|
1239
1336
|
|
|
1240
1337
|
node_ref_t new_node = node(new_id);
|
|
1241
|
-
|
|
1338
|
+
top_candidates_t& top = context.top_candidates;
|
|
1242
1339
|
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
1340
|
|
|
1249
1341
|
// Outgoing links from `new_id`:
|
|
1342
|
+
neighbors_ref_t new_neighbors = neighbors(new_node, level);
|
|
1250
1343
|
{
|
|
1251
|
-
|
|
1252
|
-
|
|
1344
|
+
assert_m(!new_neighbors.size(), "The newly inserted element should have blank link list");
|
|
1345
|
+
candidates_view_t top_view = refine(top, config_.connectivity, context);
|
|
1253
1346
|
|
|
1254
|
-
|
|
1255
|
-
for (std::size_t idx = 0; idx < top_count; idx++) {
|
|
1347
|
+
for (std::size_t idx = 0; idx != top_view.size(); idx++) {
|
|
1256
1348
|
assert_m(!new_neighbors[idx], "Possible memory corruption");
|
|
1257
|
-
assert_m(level <= node(
|
|
1258
|
-
new_neighbors[idx]
|
|
1349
|
+
assert_m(level <= node(top_view[idx].second).head.level, "Linking to missing level");
|
|
1350
|
+
new_neighbors.push_back(top_view[idx].second);
|
|
1259
1351
|
}
|
|
1260
1352
|
}
|
|
1261
1353
|
|
|
1262
1354
|
// Reverse links from the neighbors:
|
|
1263
|
-
for (
|
|
1264
|
-
id_t close_id = top_ordered[idx].second;
|
|
1355
|
+
for (id_t close_id : new_neighbors) {
|
|
1265
1356
|
node_ref_t close_node = node(close_id);
|
|
1266
1357
|
lock_t close_lock = close_node.lock();
|
|
1267
1358
|
|
|
1268
1359
|
neighbors_ref_t close_header = neighbors(close_node, level);
|
|
1269
|
-
assert_m(close_header.
|
|
1360
|
+
assert_m(close_header.size() <= connectivity_max, "Possible corruption");
|
|
1270
1361
|
assert_m(close_id != new_id, "Self-loops are impossible");
|
|
1271
1362
|
assert_m(level <= close_node.head.level, "Linking to missing level");
|
|
1272
1363
|
|
|
1273
1364
|
// If `new_id` is already present in the neighboring connections of `close_id`
|
|
1274
1365
|
// then no need to modify any connections or run the heuristics.
|
|
1275
|
-
if (close_header.
|
|
1276
|
-
close_header
|
|
1277
|
-
close_header.count_++;
|
|
1366
|
+
if (close_header.size() < connectivity_max) {
|
|
1367
|
+
close_header.push_back(new_id);
|
|
1278
1368
|
continue;
|
|
1279
1369
|
}
|
|
1280
1370
|
|
|
1281
1371
|
// To fit a new connection we need to drop an existing one.
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
context.metric(
|
|
1372
|
+
top.clear();
|
|
1373
|
+
top.reserve(close_header.size() + 1);
|
|
1374
|
+
top.emplace_reserved( //
|
|
1375
|
+
context.metric( //
|
|
1286
1376
|
new_node.vector, close_node.vector, new_node.head.dim, close_node.head.dim),
|
|
1287
1377
|
new_id);
|
|
1288
1378
|
for (id_t successor_id : close_header) {
|
|
1289
1379
|
node_ref_t successor_node = node(successor_id);
|
|
1290
|
-
|
|
1291
|
-
context.metric(
|
|
1380
|
+
top.emplace_reserved( //
|
|
1381
|
+
context.metric( //
|
|
1292
1382
|
successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
|
|
1293
1383
|
successor_id);
|
|
1294
1384
|
}
|
|
1295
|
-
span_gt<distance_and_id_t const> top = filter_heuristic(candidates, connectivity_max, context.metric);
|
|
1296
1385
|
|
|
1297
1386
|
// Export the results:
|
|
1298
|
-
|
|
1299
|
-
|
|
1387
|
+
close_header.clear();
|
|
1388
|
+
candidates_view_t top_view = refine(top, connectivity_max, context);
|
|
1389
|
+
for (std::size_t idx = 0; idx != top_view.size(); idx++)
|
|
1390
|
+
close_header.push_back(top_view[idx].second);
|
|
1300
1391
|
}
|
|
1301
1392
|
|
|
1302
|
-
return
|
|
1393
|
+
return new_neighbors[0];
|
|
1303
1394
|
}
|
|
1304
1395
|
|
|
1305
1396
|
level_t choose_random_level(std::default_random_engine& level_generator) const noexcept {
|
|
@@ -1342,25 +1433,27 @@ class index_gt {
|
|
|
1342
1433
|
level_t level, thread_context_t& context) noexcept(false) {
|
|
1343
1434
|
|
|
1344
1435
|
visits_bitset_t& visits = context.visits;
|
|
1345
|
-
|
|
1346
|
-
|
|
1436
|
+
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
1437
|
+
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
1438
|
+
std::size_t const top_limit = config_.expansion_add;
|
|
1347
1439
|
|
|
1348
|
-
top_candidates.clear();
|
|
1349
|
-
candidates_set.clear();
|
|
1350
1440
|
visits.clear();
|
|
1441
|
+
candidates.clear();
|
|
1442
|
+
top.clear();
|
|
1443
|
+
top.reserve(top_limit + 1);
|
|
1351
1444
|
|
|
1352
|
-
distance_t
|
|
1353
|
-
|
|
1354
|
-
|
|
1445
|
+
distance_t radius = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
|
|
1446
|
+
candidates.emplace(-radius, start_id);
|
|
1447
|
+
top.emplace_reserved(radius, start_id);
|
|
1355
1448
|
visits.set(start_id);
|
|
1356
1449
|
|
|
1357
|
-
while (!
|
|
1450
|
+
while (!candidates.empty()) {
|
|
1358
1451
|
|
|
1359
|
-
|
|
1360
|
-
if ((-candidacy.first) >
|
|
1452
|
+
candidate_t candidacy = candidates.top();
|
|
1453
|
+
if ((-candidacy.first) > radius && top.size() == top_limit)
|
|
1361
1454
|
break;
|
|
1362
1455
|
|
|
1363
|
-
|
|
1456
|
+
candidates.pop();
|
|
1364
1457
|
id_t candidate_id = candidacy.second;
|
|
1365
1458
|
node_ref_t candidate_node = node(candidate_id);
|
|
1366
1459
|
lock_t candidate_lock = candidate_node.lock();
|
|
@@ -1375,14 +1468,12 @@ class index_gt {
|
|
|
1375
1468
|
node_ref_t successor_node = node(successor_id);
|
|
1376
1469
|
distance_t successor_dist =
|
|
1377
1470
|
context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
|
|
1378
|
-
if (
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
if (!top_candidates.empty())
|
|
1385
|
-
closest_dist = top_candidates.top().first;
|
|
1471
|
+
if (top.size() < top_limit || successor_dist < radius) {
|
|
1472
|
+
candidates.emplace(-successor_dist, successor_id);
|
|
1473
|
+
top.emplace_reserved(successor_dist, successor_id);
|
|
1474
|
+
if (top.size() > top_limit)
|
|
1475
|
+
top.pop();
|
|
1476
|
+
radius = top.top().first;
|
|
1386
1477
|
}
|
|
1387
1478
|
}
|
|
1388
1479
|
}
|
|
@@ -1393,27 +1484,29 @@ class index_gt {
|
|
|
1393
1484
|
std::size_t expansion, thread_context_t& context) const noexcept(false) {
|
|
1394
1485
|
|
|
1395
1486
|
visits_bitset_t& visits = context.visits;
|
|
1396
|
-
|
|
1397
|
-
|
|
1487
|
+
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
1488
|
+
top_candidates_t& top = context.top_candidates; // pop max, push
|
|
1489
|
+
std::size_t const top_limit = expansion;
|
|
1398
1490
|
|
|
1399
1491
|
visits.clear();
|
|
1400
|
-
|
|
1401
|
-
|
|
1492
|
+
candidates.clear();
|
|
1493
|
+
top.clear();
|
|
1494
|
+
top.reserve(top_limit + 1);
|
|
1402
1495
|
|
|
1403
|
-
distance_t
|
|
1404
|
-
|
|
1405
|
-
|
|
1496
|
+
distance_t radius = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
|
|
1497
|
+
candidates.emplace(-radius, start_id);
|
|
1498
|
+
top.emplace_reserved(radius, start_id);
|
|
1406
1499
|
visits.set(start_id);
|
|
1407
1500
|
|
|
1408
|
-
while (!
|
|
1501
|
+
while (!candidates.empty()) {
|
|
1409
1502
|
|
|
1410
|
-
|
|
1411
|
-
if ((-
|
|
1503
|
+
candidate_t candidate = candidates.top();
|
|
1504
|
+
if ((-candidate.first) > radius)
|
|
1412
1505
|
break;
|
|
1413
1506
|
|
|
1414
|
-
|
|
1507
|
+
candidates.pop();
|
|
1415
1508
|
|
|
1416
|
-
id_t candidate_id =
|
|
1509
|
+
id_t candidate_id = candidate.second;
|
|
1417
1510
|
neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
|
|
1418
1511
|
|
|
1419
1512
|
prefetch_neighbors(candidate_header, visits);
|
|
@@ -1426,14 +1519,12 @@ class index_gt {
|
|
|
1426
1519
|
distance_t successor_dist =
|
|
1427
1520
|
context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
|
|
1428
1521
|
|
|
1429
|
-
if (
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
if (!top_candidates.empty())
|
|
1436
|
-
closest_dist = top_candidates.top().first;
|
|
1522
|
+
if (top.size() < top_limit || successor_dist < radius) {
|
|
1523
|
+
candidates.emplace(-successor_dist, successor_id);
|
|
1524
|
+
top.emplace_reserved(successor_dist, successor_id);
|
|
1525
|
+
if (top.size() > top_limit)
|
|
1526
|
+
top.pop();
|
|
1527
|
+
radius = top.top().first;
|
|
1437
1528
|
}
|
|
1438
1529
|
}
|
|
1439
1530
|
}
|
|
@@ -1441,26 +1532,30 @@ class index_gt {
|
|
|
1441
1532
|
|
|
1442
1533
|
void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|
|
1443
1534
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1535
|
+
/**
|
|
1536
|
+
* @brief This algorithm from the original paper implements a heuristic,
|
|
1537
|
+
* that massively reduces the number of connections a point has,
|
|
1538
|
+
* to keep only the neighbors, that are from each other.
|
|
1539
|
+
*/
|
|
1540
|
+
candidates_view_t refine(top_candidates_t& top, std::size_t needed, thread_context_t& context) const noexcept {
|
|
1446
1541
|
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
std::size_t const top_count =
|
|
1542
|
+
top.sort_ascending();
|
|
1543
|
+
candidate_t* top_data = top.data();
|
|
1544
|
+
std::size_t const top_count = top.size();
|
|
1450
1545
|
if (top_count < needed)
|
|
1451
|
-
return {
|
|
1546
|
+
return {top_data, top_count};
|
|
1452
1547
|
|
|
1453
1548
|
std::size_t submitted_count = 1;
|
|
1454
1549
|
std::size_t consumed_count = 1; /// Always equal or greater than `submitted_count`.
|
|
1455
1550
|
while (submitted_count < needed && consumed_count < top_count) {
|
|
1456
|
-
|
|
1551
|
+
candidate_t candidate = top_data[consumed_count];
|
|
1457
1552
|
node_ref_t candidate_node = node(candidate.second);
|
|
1458
1553
|
distance_t candidate_dist = candidate.first;
|
|
1459
1554
|
bool good = true;
|
|
1460
1555
|
for (std::size_t idx = 0; idx < submitted_count; idx++) {
|
|
1461
|
-
|
|
1556
|
+
candidate_t submitted = top_data[idx];
|
|
1462
1557
|
node_ref_t submitted_node = node(submitted.second);
|
|
1463
|
-
distance_t inter_result_dist = metric(
|
|
1558
|
+
distance_t inter_result_dist = context.metric( //
|
|
1464
1559
|
submitted_node.vector, candidate_node.vector, //
|
|
1465
1560
|
submitted_node.head.dim, candidate_node.head.dim);
|
|
1466
1561
|
if (inter_result_dist < candidate_dist) {
|
|
@@ -1470,13 +1565,14 @@ class index_gt {
|
|
|
1470
1565
|
}
|
|
1471
1566
|
|
|
1472
1567
|
if (good) {
|
|
1473
|
-
|
|
1568
|
+
top_data[submitted_count] = top_data[consumed_count];
|
|
1474
1569
|
submitted_count++;
|
|
1475
1570
|
}
|
|
1476
1571
|
consumed_count++;
|
|
1477
1572
|
}
|
|
1478
1573
|
|
|
1479
|
-
|
|
1574
|
+
top.shrink(submitted_count);
|
|
1575
|
+
return {top_data, submitted_count};
|
|
1480
1576
|
}
|
|
1481
1577
|
};
|
|
1482
1578
|
|
|
@@ -10,6 +10,7 @@ typedef NS_ENUM(NSUInteger, Quantization) {
|
|
|
10
10
|
kDoublePrecision
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0), watchos(6.0))
|
|
13
14
|
@interface Index: NSObject
|
|
14
15
|
|
|
15
16
|
@property (readonly) UInt32 dimensions;
|
|
@@ -33,9 +34,9 @@ NS_UNAVAILABLE;
|
|
|
33
34
|
NS_SWIFT_NAME(addSingle(label:vector:));
|
|
34
35
|
|
|
35
36
|
- (UInt32)searchSingle:(Float32 const* _Nonnull)vector
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
count:(UInt32)count
|
|
38
|
+
labels:(UInt32* _Nullable)labels
|
|
39
|
+
distances:(Float32* _Nullable)distances
|
|
39
40
|
NS_SWIFT_NAME(searchSingle(vector:count:labels:distances:));
|
|
40
41
|
|
|
41
42
|
- (void)addPrecise:(UInt32)label
|
|
@@ -43,9 +44,9 @@ NS_SWIFT_NAME(searchSingle(vector:count:labels:distances:));
|
|
|
43
44
|
NS_SWIFT_NAME(addPrecise(label:vector:));
|
|
44
45
|
|
|
45
46
|
- (UInt32)searchPrecise:(Float64 const* _Nonnull)vector
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
count:(UInt32)count
|
|
48
|
+
labels:(UInt32* _Nullable)labels
|
|
49
|
+
distances:(Float32* _Nullable)distances
|
|
49
50
|
NS_SWIFT_NAME(searchPrecise(vector:count:labels:distances:));
|
|
50
51
|
|
|
51
52
|
- (void)addImprecise:(UInt32)label
|
|
@@ -53,9 +54,9 @@ NS_SWIFT_NAME(searchPrecise(vector:count:labels:distances:));
|
|
|
53
54
|
NS_SWIFT_NAME(addImprecise(label:vector:));
|
|
54
55
|
|
|
55
56
|
- (UInt32)searchImprecise:(void const* _Nonnull)vector
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
count:(UInt32)count
|
|
58
|
+
labels:(UInt32* _Nullable)labels
|
|
59
|
+
distances:(Float32* _Nullable)distances
|
|
59
60
|
NS_SWIFT_NAME(searchImprecise(vector:count:labels:distances:));
|
|
60
61
|
|
|
61
62
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usearch",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
5
5
|
"author": "Ashot Vardanian",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -18,5 +18,11 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"test": "node javascript/test.js"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@semantic-release/exec": "^6.0.3",
|
|
24
|
+
"@semantic-release/git": "^10.0.1",
|
|
25
|
+
"conventional-changelog-eslint": "^3.0.9",
|
|
26
|
+
"semantic-release": "^21.0.2"
|
|
21
27
|
}
|
|
22
28
|
}
|