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.
@@ -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(std::size_t max_capacity = 0) noexcept
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 n) noexcept {
388
- if (n < capacity_)
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
- /** @brief Invalidates the "max-heap" property, transforming into descending range. */
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 distance_and_id_t {
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()(distance_and_id_t a, distance_and_id_t b) const noexcept { return a.first < b.first; }
777
+ inline bool operator()(candidate_t a, candidate_t b) const noexcept { return a.first < b.first; }
681
778
  };
682
779
 
683
- using distances_and_ids_allocator_t = typename allocator_traits_t::template rebind_alloc<distance_and_id_t>;
684
- using distances_and_ids_t = max_heap_gt<distance_and_id_t, compare_by_distance_t, distances_and_ids_allocator_t>;
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
- distances_and_ids_t top_candidates;
743
- distances_and_ids_t candidates_set;
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
- search_to_find_in_base( //
924
- closest_id, query_vec, query_dim, (std::max)(config_.expansion_search, wanted), context);
925
- while (context.top_candidates.size() > wanted)
926
- context.top_candidates.pop();
927
-
928
- while (context.top_candidates.size()) {
929
- distance_and_id_t top = context.top_candidates.top();
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
- distances_and_ids_t& top_candidates = context.top_candidates;
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
- neighbors_ref_t new_neighbors = neighbors(new_node, level);
1252
- assert_m(!new_neighbors.count_, "The newly inserted element should have blank link list");
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
- new_neighbors.count_ = static_cast<neighbors_count_t>(top_count);
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(top_ordered[idx].second).head.level, "Linking to missing level");
1258
- new_neighbors[idx] = top_ordered[idx].second;
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 (std::size_t idx = 0; idx < top_count; idx++) {
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.count_ <= connectivity_max, "Possible corruption");
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.count_ < connectivity_max) {
1276
- close_header[close_header.count_] = new_id;
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
- distances_and_ids_t& candidates = context.candidates_set;
1283
- candidates.clear();
1284
- candidates.emplace( //
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
- candidates.emplace( //
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
- for (close_header.count_ = 0u; close_header.count_ != top.size(); ++close_header.count_)
1299
- close_header[close_header.count_] = top[close_header.count_].second;
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 next_closest_entry_id;
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
- 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
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 closest_dist = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1353
- top_candidates.emplace(closest_dist, start_id);
1354
- candidates_set.emplace(-closest_dist, start_id);
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 (!candidates_set.empty()) {
1450
+ while (!candidates.empty()) {
1358
1451
 
1359
- distance_and_id_t candidacy = candidates_set.top();
1360
- if ((-candidacy.first) > closest_dist && top_candidates.size() == config_.expansion_add)
1452
+ candidate_t candidacy = candidates.top();
1453
+ if ((-candidacy.first) > radius && top.size() == top_limit)
1361
1454
  break;
1362
1455
 
1363
- candidates_set.pop();
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 (top_candidates.size() < config_.expansion_add || closest_dist > successor_dist) {
1379
- candidates_set.emplace(-successor_dist, successor_id);
1380
-
1381
- top_candidates.emplace(successor_dist, successor_id);
1382
- if (top_candidates.size() > config_.expansion_add)
1383
- top_candidates.pop();
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
- 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
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
- top_candidates.clear();
1401
- candidates_set.clear();
1492
+ candidates.clear();
1493
+ top.clear();
1494
+ top.reserve(top_limit + 1);
1402
1495
 
1403
- distance_t closest_dist = context.metric(query_vec, node(start_id).vector, query_dim, node(start_id).head.dim);
1404
- top_candidates.emplace(closest_dist, start_id);
1405
- candidates_set.emplace(-closest_dist, start_id);
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 (!candidates_set.empty()) {
1501
+ while (!candidates.empty()) {
1409
1502
 
1410
- distance_and_id_t current_node_node = candidates_set.top();
1411
- if ((-current_node_node.first) > closest_dist)
1503
+ candidate_t candidate = candidates.top();
1504
+ if ((-candidate.first) > radius)
1412
1505
  break;
1413
1506
 
1414
- candidates_set.pop();
1507
+ candidates.pop();
1415
1508
 
1416
- id_t candidate_id = current_node_node.second;
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 (top_candidates.size() < expansion || closest_dist > successor_dist) {
1430
- candidates_set.emplace(-successor_dist, successor_id);
1431
-
1432
- top_candidates.emplace(successor_dist, successor_id);
1433
- if (top_candidates.size() > expansion)
1434
- top_candidates.pop();
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
- 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 {
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
- top_candidates.sort_ascending();
1448
- distance_and_id_t* top_ordered = top_candidates.data();
1449
- std::size_t const top_count = top_candidates.size();
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 {top_ordered, top_count};
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
- distance_and_id_t candidate = top_ordered[consumed_count];
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
- distance_and_id_t submitted = top_ordered[idx];
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
- top_ordered[submitted_count] = top_ordered[consumed_count];
1568
+ top_data[submitted_count] = top_data[consumed_count];
1474
1569
  submitted_count++;
1475
1570
  }
1476
1571
  consumed_count++;
1477
1572
  }
1478
1573
 
1479
- return {top_ordered, submitted_count};
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
- count:(UInt32)count
37
- labels:(UInt32* _Nullable)labels
38
- distances:(Float32* _Nullable)distances
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
- count:(UInt32)count
47
- labels:(UInt32* _Nullable)labels
48
- distances:(Float32* _Nullable)distances
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
- count:(UInt32)count
57
- labels:(UInt32* _Nullable)labels
58
- distances:(Float32* _Nullable)distances
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.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
  }