usearch 2.3.2 → 2.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.
@@ -101,7 +101,7 @@ jobs:
101
101
  run: python -m pip install .
102
102
  if: ${{ matrix.os != 'ubuntu-22.04' }}
103
103
  - name: Test with PyTest
104
- run: pytest python/scripts/
104
+ run: pytest python/scripts/ -s -x
105
105
 
106
106
 
107
107
  test_python_37:
@@ -136,7 +136,7 @@ jobs:
136
136
  if: ${{ matrix.os == 'ubuntu-22.04' }}
137
137
 
138
138
  - name: Test with PyTest
139
- run: pytest python/scripts/
139
+ run: pytest python/scripts/ -s -x
140
140
 
141
141
 
142
142
  test_javascript:
@@ -215,9 +215,8 @@ jobs:
215
215
  file: "golang/*.zip"
216
216
  update_latest_release: true
217
217
 
218
-
219
- publish_hashes:
220
- name: Add hashes to release notes
218
+ publish_checksums:
219
+ name: Compute Assets Checksums
221
220
  runs-on: ubuntu-latest
222
221
  needs: [versioning, create_linux_deb_package, create_windows_dll_library, create_macos_library, deploy_docs_pages]
223
222
  steps:
@@ -270,7 +269,7 @@ jobs:
270
269
 
271
270
 
272
271
  build_wheels:
273
- name: Build Wheels for ${{ matrix.os }}
272
+ name: Build Python
274
273
  needs: versioning
275
274
  runs-on: ${{ matrix.os }}
276
275
  strategy:
@@ -445,7 +444,7 @@ jobs:
445
444
  labels: ${{ steps.meta.outputs.labels }}
446
445
 
447
446
  build_csharp:
448
- name: Build NuGet
447
+ name: Build C#
449
448
  needs: versioning
450
449
  strategy:
451
450
  matrix:
package/CITATION.cff CHANGED
@@ -5,7 +5,7 @@ authors:
5
5
  given-names: "Ash"
6
6
  orcid: "https://orcid.org/0000-0002-4882-1815"
7
7
  title: "USearch by Unum Cloud"
8
- version: 2.3.2
8
+ version: 2.4.0
9
9
  doi: 10.5281/zenodo.7949416
10
10
  date-released: 2022-06-12
11
11
  url: "https://github.com/unum-cloud/usearch"
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "2.3.2"
3
+ version = "2.4.0"
4
4
  authors = ["Ash Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -392,7 +392,7 @@ doi = {10.5281/zenodo.7949416},
392
392
  author = {Vardanian, Ash},
393
393
  title = {{USearch by Unum Cloud}},
394
394
  url = {https://github.com/unum-cloud/usearch},
395
- version = {2.3.2},
395
+ version = {2.4.0},
396
396
  year = {2022},
397
397
  month = jun,
398
398
  }
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '2.3.2'
7
+ version = '2.4.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/cpp/test.cpp CHANGED
@@ -36,6 +36,13 @@ void test_cosine(index_at& index, std::vector<std::vector<scalar_at>> const& vec
36
36
  scalar_t const* vector_third = vectors[2].data();
37
37
  std::size_t dimensions = vectors[0].size();
38
38
 
39
+ // Try checking the empty state
40
+ if constexpr (punned_ak) {
41
+ expect(!index.contains(key_first));
42
+ expect(!index.get(key_first, (f32_t*)nullptr, 1));
43
+ }
44
+
45
+ // Add data
39
46
  index.reserve(10);
40
47
  index.add(key_first, vector_first, args...);
41
48
 
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <Project>
3
3
  <PropertyGroup>
4
- <Version Condition="'$(Version)' == ''">2.3.2</Version>
4
+ <Version Condition="'$(Version)' == ''">2.4.0</Version>
5
5
 
6
6
  <Authors>Unum</Authors>
7
7
  <Company>Unum</Company>
@@ -10,8 +10,8 @@
10
10
  #define UNUM_USEARCH_HPP
11
11
 
12
12
  #define USEARCH_VERSION_MAJOR 2
13
- #define USEARCH_VERSION_MINOR 3
14
- #define USEARCH_VERSION_PATCH 2
13
+ #define USEARCH_VERSION_MINOR 4
14
+ #define USEARCH_VERSION_PATCH 0
15
15
 
16
16
  // Inferring C++ version
17
17
  // https://stackoverflow.com/a/61552074
@@ -1,12 +1,11 @@
1
1
  #pragma once
2
2
  #include <stdlib.h> // `aligned_alloc`
3
3
 
4
- #include <functional> // `std::function`
5
- #include <numeric> // `std::iota`
6
- #include <shared_mutex> // `std::shared_mutex`
7
- #include <thread> // `std::thread`
8
- #include <unordered_set> // `std::unordered_multiset`
9
- #include <vector> // `std::vector`
4
+ #include <functional> // `std::function`
5
+ #include <numeric> // `std::iota`
6
+ #include <shared_mutex> // `std::shared_mutex`
7
+ #include <thread> // `std::thread`
8
+ #include <vector> // `std::vector`
10
9
 
11
10
  #include <usearch/index.hpp>
12
11
  #include <usearch/index_plugins.hpp>
@@ -93,6 +92,17 @@ struct index_dense_config_t : public index_config_t {
93
92
  bool exclude_vectors = false;
94
93
  bool multi = false;
95
94
 
95
+ /**
96
+ * @brief Allows you to reduce RAM consumption by avoiding
97
+ * reverse-indexing keys-to-vectors, and only keeping
98
+ * the vectors-to-keys mappings.
99
+ *
100
+ * ! This configuration parameter doesn't affect the serialized file,
101
+ * ! and is not preserved between runs. Makes sense for small vector
102
+ * ! representations that fit ina single cache line.
103
+ */
104
+ bool enable_key_lookups = true;
105
+
96
106
  index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
97
107
 
98
108
  index_dense_config_t( //
@@ -399,7 +409,7 @@ class index_dense_gt {
399
409
  };
400
410
 
401
411
  /// @brief Multi-Map from keys to IDs, and allocated vectors.
402
- std::unordered_multiset<key_and_slot_t, lookup_key_hash_t, lookup_key_same_t> slot_lookup_;
412
+ flat_hash_multi_set_gt<key_and_slot_t, lookup_key_hash_t, lookup_key_same_t> slot_lookup_;
403
413
 
404
414
  /// @brief Mutex, controlling concurrent access to `slot_lookup_`.
405
415
  mutable shared_mutex_t slot_lookup_mutex_;
@@ -1149,7 +1159,7 @@ class index_dense_gt {
1149
1159
  */
1150
1160
  bool contains(key_t key) const {
1151
1161
  shared_lock_t lock(slot_lookup_mutex_);
1152
- return slot_lookup_.find(key_and_slot_t::any_slot(key)) != slot_lookup_.end();
1162
+ return slot_lookup_.contains(key_and_slot_t::any_slot(key));
1153
1163
  }
1154
1164
 
1155
1165
  /**
@@ -1203,7 +1213,7 @@ class index_dense_gt {
1203
1213
  free_keys_.push(slot);
1204
1214
  typed_->at(slot).key = free_key_;
1205
1215
  }
1206
- slot_lookup_.erase(matching_slots.first, matching_slots.second);
1216
+ slot_lookup_.erase(key);
1207
1217
  result.completed = matching_count;
1208
1218
 
1209
1219
  return result;
@@ -1239,14 +1249,15 @@ class index_dense_gt {
1239
1249
  // - present in `free_keys_`
1240
1250
  // - missing in the `slot_lookup_`
1241
1251
  // - marked in the `typed_` index with a `free_key_`
1252
+ matching_count = 0;
1242
1253
  for (auto slots_it = matching_slots.first; slots_it != matching_slots.second; ++slots_it) {
1243
1254
  compressed_slot_t slot = (*slots_it).slot;
1244
1255
  free_keys_.push(slot);
1245
1256
  typed_->at(slot).key = free_key_;
1257
+ ++matching_count;
1246
1258
  }
1247
1259
 
1248
- matching_count = std::distance(matching_slots.first, matching_slots.second);
1249
- slot_lookup_.erase(matching_slots.first, matching_slots.second);
1260
+ slot_lookup_.erase(key);
1250
1261
  result.completed += matching_count;
1251
1262
  }
1252
1263
 
@@ -1265,20 +1276,18 @@ class index_dense_gt {
1265
1276
  labeling_result_t result;
1266
1277
  unique_lock_t lookup_lock(slot_lookup_mutex_);
1267
1278
 
1268
- if (!multi() && slot_lookup_.count(key_and_slot_t::any_slot(to)))
1279
+ if (!multi() && slot_lookup_.contains(key_and_slot_t::any_slot(to)))
1269
1280
  return result.failed("Renaming impossible, the key is already in use");
1270
1281
 
1271
1282
  // The `from` may map to multiple entries
1272
1283
  while (true) {
1273
- auto slots_it = slot_lookup_.find(key_and_slot_t::any_slot(from));
1274
- if (slots_it == slot_lookup_.end())
1284
+ key_and_slot_t key_and_slot_removed;
1285
+ if (!slot_lookup_.pop_first(key_and_slot_t::any_slot(from), key_and_slot_removed))
1275
1286
  break;
1276
1287
 
1277
- compressed_slot_t slot = (*slots_it).slot;
1278
- key_and_slot_t key_and_slot{to, slot};
1279
- slot_lookup_.erase(slots_it);
1280
- slot_lookup_.insert(key_and_slot);
1281
- typed_->at(slot).key = to;
1288
+ key_and_slot_t key_and_slot_replacing{to, key_and_slot_removed.slot};
1289
+ slot_lookup_.try_emplace(key_and_slot_replacing); // This can't fail
1290
+ typed_->at(key_and_slot_removed.slot).key = to;
1282
1291
  ++result.completed;
1283
1292
  }
1284
1293
 
@@ -1293,11 +1302,18 @@ class index_dense_gt {
1293
1302
  */
1294
1303
  void export_keys(key_t* keys, std::size_t offset, std::size_t limit) const {
1295
1304
  shared_lock_t lock(slot_lookup_mutex_);
1296
- auto it = slot_lookup_.begin();
1297
1305
  offset = (std::min)(offset, slot_lookup_.size());
1298
- std::advance(it, offset);
1299
- for (; it != slot_lookup_.end() && limit; ++it, ++keys, --limit)
1300
- *keys = (*it).key;
1306
+ slot_lookup_.for_each([&](key_and_slot_t const& key_and_slot) {
1307
+ if (offset) {
1308
+ --offset;
1309
+ return;
1310
+ }
1311
+ if (limit) {
1312
+ *keys = key_and_slot.key;
1313
+ ++keys;
1314
+ --limit;
1315
+ }
1316
+ });
1301
1317
  }
1302
1318
 
1303
1319
  struct copy_result_t {
@@ -1720,7 +1736,7 @@ class index_dense_gt {
1720
1736
  bool reuse_node = free_slot != default_free_value<compressed_slot_t>();
1721
1737
  auto on_success = [&](member_ref_t member) {
1722
1738
  unique_lock_t slot_lock(slot_lookup_mutex_);
1723
- slot_lookup_.insert(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
1739
+ slot_lookup_.try_emplace(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
1724
1740
  if (copy_vector) {
1725
1741
  if (!reuse_node)
1726
1742
  vectors_lookup_[member.slot] = vectors_tape_allocator_.allocate(metric_.bytes_per_vector());
@@ -1842,19 +1858,23 @@ class index_dense_gt {
1842
1858
  count_removed += member.key == free_key_;
1843
1859
  }
1844
1860
 
1861
+ if (!count_removed && !config_.enable_key_lookups)
1862
+ return;
1863
+
1845
1864
  // Pull entries from the underlying `typed_` into either
1846
1865
  // into `slot_lookup_`, or `free_keys_` if they are unused.
1847
1866
  unique_lock_t lock(slot_lookup_mutex_);
1848
1867
  slot_lookup_.clear();
1849
- slot_lookup_.reserve(count_total - count_removed);
1868
+ if (config_.enable_key_lookups)
1869
+ slot_lookup_.reserve(count_total - count_removed);
1850
1870
  free_keys_.clear();
1851
1871
  free_keys_.reserve(count_removed);
1852
1872
  for (std::size_t i = 0; i != typed_->size(); ++i) {
1853
1873
  member_cref_t member = typed_->at(i);
1854
1874
  if (member.key == free_key_)
1855
1875
  free_keys_.push(static_cast<compressed_slot_t>(i));
1856
- else
1857
- slot_lookup_.insert(key_and_slot_t{key_t(member.key), static_cast<compressed_slot_t>(i)});
1876
+ else if (config_.enable_key_lookups)
1877
+ slot_lookup_.try_emplace(key_and_slot_t{key_t(member.key), static_cast<compressed_slot_t>(i)});
1858
1878
  }
1859
1879
  }
1860
1880
 
@@ -649,7 +649,9 @@ class aligned_allocator_gt {
649
649
  using size_type = std::size_t;
650
650
  using pointer = element_at*;
651
651
  using const_pointer = element_at const*;
652
- template <typename other_element_at> struct rebind { using other = aligned_allocator_gt<other_element_at>; };
652
+ template <typename other_element_at> struct rebind {
653
+ using other = aligned_allocator_gt<other_element_at>;
654
+ };
653
655
 
654
656
  constexpr std::size_t alignment() const { return alignment_ak; }
655
657
 
@@ -1315,17 +1317,16 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct met
1315
1317
  result_t lat_a = a[0], lon_a = a[1];
1316
1318
  result_t lat_b = b[0], lon_b = b[1];
1317
1319
 
1318
- result_t lat_delta = angle_to_radians<result_t>(lat_b - lat_a);
1319
- result_t lon_delta = angle_to_radians<result_t>(lon_b - lon_a);
1320
+ result_t lat_delta = angle_to_radians<result_t>(lat_b - lat_a) / 2;
1321
+ result_t lon_delta = angle_to_radians<result_t>(lon_b - lon_a) / 2;
1320
1322
 
1321
1323
  result_t converted_lat_a = angle_to_radians<result_t>(lat_a);
1322
1324
  result_t converted_lat_b = angle_to_radians<result_t>(lat_b);
1323
1325
 
1324
- result_t x = //
1325
- square(std::sin(lat_delta / 2.f)) +
1326
- std::cos(converted_lat_a) * std::cos(converted_lat_b) * square(std::sin(lon_delta / 2.f));
1326
+ result_t x = square(std::sin(lat_delta)) + //
1327
+ std::cos(converted_lat_a) * std::cos(converted_lat_b) * square(std::sin(lon_delta));
1327
1328
 
1328
- return std::atan2(std::sqrt(x), std::sqrt(1.f - x));
1329
+ return 2 * std::asin(std::sqrt(x));
1329
1330
  }
1330
1331
  };
1331
1332
 
@@ -1556,5 +1557,513 @@ class metric_punned_t {
1556
1557
  // clang-format on
1557
1558
  };
1558
1559
 
1560
+ /**
1561
+ * @brief C++11 Multi-Hash-Set with Linear Probing.
1562
+ *
1563
+ * - Allows multiple equivalent values,
1564
+ * - Supports transparent hashing and equality operator.
1565
+ * - Doesn't throw exceptions, if forbidden.
1566
+ * - Doesn't need reserving a value for deletions.
1567
+ *
1568
+ * @section Layout
1569
+ *
1570
+ * For every slot we store 2 extra bits for 3 possible states: empty, populated, or deleted.
1571
+ */
1572
+ template <typename element_at, typename hash_at, typename equals_at, typename allocator_at = std::allocator<char>>
1573
+ class flat_hash_multi_set_gt {
1574
+ public:
1575
+ using element_t = element_at;
1576
+ using hash_t = hash_at;
1577
+ using equals_t = equals_at;
1578
+ using allocator_t = allocator_at;
1579
+
1580
+ static constexpr std::size_t slots_per_bucket() { return 64; }
1581
+ static constexpr std::size_t bytes_per_bucket() {
1582
+ return slots_per_bucket() * sizeof(element_t) + sizeof(bucket_header_t);
1583
+ }
1584
+
1585
+ private:
1586
+ struct bucket_header_t {
1587
+ std::uint64_t populated{};
1588
+ std::uint64_t deleted{};
1589
+ };
1590
+ char* data_ = nullptr;
1591
+ std::size_t buckets_ = 0;
1592
+ std::size_t populated_slots_ = 0;
1593
+ /// @brief Number of slots
1594
+ std::size_t capacity_slots_ = 0;
1595
+
1596
+ struct slot_ref_t {
1597
+ bucket_header_t& header;
1598
+ std::uint64_t mask;
1599
+ element_t& element;
1600
+ };
1601
+
1602
+ slot_ref_t slot_ref(char* data, std::size_t slot_index) const noexcept {
1603
+ std::size_t bucket_index = slot_index / slots_per_bucket();
1604
+ std::size_t in_bucket_index = slot_index % slots_per_bucket();
1605
+ auto bucket_pointer = data + bytes_per_bucket() * bucket_index;
1606
+ auto slot_pointer = bucket_pointer + sizeof(bucket_header_t) + sizeof(element_t) * in_bucket_index;
1607
+ return {
1608
+ *reinterpret_cast<bucket_header_t*>(bucket_pointer),
1609
+ std::uint64_t(1ull) << in_bucket_index,
1610
+ *reinterpret_cast<element_t*>(slot_pointer),
1611
+ };
1612
+ }
1613
+
1614
+ slot_ref_t slot_ref(std::size_t slot_index) const noexcept { return slot_ref(data_, slot_index); }
1615
+
1616
+ bool populate_slot(slot_ref_t slot, element_t const& new_element) {
1617
+ if (slot.header.populated & slot.mask) {
1618
+ slot.element = new_element;
1619
+ slot.header.deleted &= ~slot.mask;
1620
+ return false;
1621
+ } else {
1622
+ new (&slot.element) element_t(new_element);
1623
+ slot.header.populated |= slot.mask;
1624
+ return true;
1625
+ }
1626
+ }
1627
+
1628
+ public:
1629
+ std::size_t size() const noexcept { return populated_slots_; }
1630
+ std::size_t capacity() const noexcept { return capacity_slots_; }
1631
+
1632
+ flat_hash_multi_set_gt() noexcept {}
1633
+ ~flat_hash_multi_set_gt() noexcept { reset(); }
1634
+
1635
+ flat_hash_multi_set_gt(flat_hash_multi_set_gt const& other) {
1636
+
1637
+ // On Windows allocating a zero-size array would fail
1638
+ if (!other.buckets_) {
1639
+ reset();
1640
+ return;
1641
+ }
1642
+
1643
+ // Allocate new memory
1644
+ data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
1645
+ if (!data_)
1646
+ throw std::bad_alloc();
1647
+
1648
+ // Copy metadata
1649
+ buckets_ = other.buckets_;
1650
+ populated_slots_ = other.populated_slots_;
1651
+ capacity_slots_ = other.capacity_slots_;
1652
+
1653
+ // Initialize new buckets to empty
1654
+ std::memset(data_, 0, buckets_ * bytes_per_bucket());
1655
+
1656
+ // Copy elements and bucket headers
1657
+ for (std::size_t i = 0; i < capacity_slots_; ++i) {
1658
+ slot_ref_t old_slot = other.slot_ref(i);
1659
+ if ((old_slot.header.populated & old_slot.mask) && !(old_slot.header.deleted & old_slot.mask)) {
1660
+ slot_ref_t new_slot = slot_ref(i);
1661
+ populate_slot(new_slot, old_slot.element);
1662
+ }
1663
+ }
1664
+ }
1665
+
1666
+ flat_hash_multi_set_gt& operator=(flat_hash_multi_set_gt const& other) {
1667
+
1668
+ // On Windows allocating a zero-size array would fail
1669
+ if (!other.buckets_) {
1670
+ reset();
1671
+ return *this;
1672
+ }
1673
+
1674
+ // Handle self-assignment
1675
+ if (this == &other)
1676
+ return *this;
1677
+
1678
+ // Clear existing data
1679
+ clear();
1680
+ if (data_)
1681
+ allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
1682
+
1683
+ // Allocate new memory
1684
+ data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
1685
+ if (!data_)
1686
+ throw std::bad_alloc();
1687
+
1688
+ // Copy metadata
1689
+ buckets_ = other.buckets_;
1690
+ populated_slots_ = other.populated_slots_;
1691
+ capacity_slots_ = other.capacity_slots_;
1692
+
1693
+ // Initialize new buckets to empty
1694
+ std::memset(data_, 0, buckets_ * bytes_per_bucket());
1695
+
1696
+ // Copy elements and bucket headers
1697
+ for (std::size_t i = 0; i < capacity_slots_; ++i) {
1698
+ slot_ref_t old_slot = other.slot_ref(i);
1699
+ if ((old_slot.header.populated & old_slot.mask) && !(old_slot.header.deleted & old_slot.mask)) {
1700
+ slot_ref_t new_slot = slot_ref(i);
1701
+ populate_slot(new_slot, old_slot.element);
1702
+ }
1703
+ }
1704
+
1705
+ return *this;
1706
+ }
1707
+
1708
+ void clear() noexcept {
1709
+ // Call the destructors
1710
+ for (std::size_t i = 0; i < capacity_slots_; ++i) {
1711
+ slot_ref_t slot = slot_ref(i);
1712
+ if ((slot.header.populated & slot.mask) & (~slot.header.deleted & slot.mask))
1713
+ slot.element.~element_t();
1714
+ }
1715
+
1716
+ // Reset populated slots count
1717
+ std::memset(data_, 0, buckets_ * bytes_per_bucket());
1718
+ populated_slots_ = 0;
1719
+ }
1720
+
1721
+ void reset() noexcept {
1722
+ clear(); // Clear all elements
1723
+ if (data_)
1724
+ allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
1725
+ buckets_ = 0;
1726
+ populated_slots_ = 0;
1727
+ capacity_slots_ = 0;
1728
+ }
1729
+
1730
+ bool try_reserve(std::size_t capacity) noexcept {
1731
+ if (capacity * 3u <= capacity_slots_ * 2u)
1732
+ return true;
1733
+
1734
+ // Calculate new sizes
1735
+ std::size_t new_slots = ceil2((capacity * 3ul) / 2ul);
1736
+ std::size_t new_buckets = divide_round_up<slots_per_bucket()>(new_slots);
1737
+ new_slots = new_buckets * slots_per_bucket(); // This must be a power of two!
1738
+ std::size_t new_bytes = new_buckets * bytes_per_bucket();
1739
+
1740
+ // Allocate new memory
1741
+ char* new_data = (char*)allocator_t{}.allocate(new_bytes);
1742
+ if (!new_data)
1743
+ return false;
1744
+
1745
+ // Initialize new buckets to empty
1746
+ std::memset(new_data, 0, new_bytes);
1747
+
1748
+ // Rehash and copy existing elements to new_data
1749
+ hash_t hasher;
1750
+ for (std::size_t i = 0; i < capacity_slots_; ++i) {
1751
+ slot_ref_t old_slot = slot_ref(i);
1752
+ if ((~old_slot.header.populated & old_slot.mask) | (old_slot.header.deleted & old_slot.mask))
1753
+ continue;
1754
+
1755
+ // Rehash
1756
+ std::size_t hash_value = hasher(old_slot.element);
1757
+ std::size_t new_slot_index = hash_value & (new_slots - 1);
1758
+
1759
+ // Linear probing to find an empty slot in new_data
1760
+ while (true) {
1761
+ slot_ref_t new_slot = slot_ref(new_data, new_slot_index);
1762
+ if (!(new_slot.header.populated & new_slot.mask) || (new_slot.header.deleted & new_slot.mask)) {
1763
+ populate_slot(new_slot, std::move(old_slot.element));
1764
+ new_slot.header.populated |= new_slot.mask;
1765
+ break;
1766
+ }
1767
+ new_slot_index = (new_slot_index + 1) & (new_slots - 1);
1768
+ }
1769
+ }
1770
+
1771
+ // Deallocate old data and update pointers and sizes
1772
+ if (data_)
1773
+ allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
1774
+ data_ = new_data;
1775
+ buckets_ = new_buckets;
1776
+ capacity_slots_ = new_slots;
1777
+
1778
+ return true;
1779
+ }
1780
+
1781
+ template <typename query_at> class equal_iterator_gt {
1782
+ public:
1783
+ using iterator_category = std::forward_iterator_tag;
1784
+ using value_type = element_t;
1785
+ using difference_type = std::ptrdiff_t;
1786
+ using pointer = element_t*;
1787
+ using reference = element_t&;
1788
+
1789
+ equal_iterator_gt(std::size_t index, flat_hash_multi_set_gt* parent, const query_at& query,
1790
+ const equals_t& equals)
1791
+ : index_(index), parent_(parent), query_(query), equals_(equals) {}
1792
+
1793
+ // Pre-increment
1794
+ equal_iterator_gt& operator++() {
1795
+ do {
1796
+ ++index_;
1797
+ if (index_ >= parent_->capacity_slots_) {
1798
+ break;
1799
+ }
1800
+ } while (!equals_(parent_->slot_ref(index_).element, query_) ||
1801
+ !(parent_->slot_ref(index_).header.populated & parent_->slot_ref(index_).mask));
1802
+ return *this;
1803
+ }
1804
+
1805
+ equal_iterator_gt operator++(int) {
1806
+ equal_iterator_gt temp = *this;
1807
+ ++(*this);
1808
+ return temp;
1809
+ }
1810
+
1811
+ reference operator*() { return parent_->slot_ref(index_).element; }
1812
+ pointer operator->() { return &parent_->slot_ref(index_).element; }
1813
+ bool operator!=(equal_iterator_gt const& other) const { return !(*this == other); }
1814
+ bool operator==(equal_iterator_gt const& other) const {
1815
+ return index_ == other.index_ && parent_ == other.parent_;
1816
+ }
1817
+
1818
+ private:
1819
+ std::size_t index_;
1820
+ flat_hash_multi_set_gt* parent_;
1821
+ query_at query_; // Store the query object
1822
+ equals_t equals_; // Store the equals functor
1823
+ };
1824
+
1825
+ template <typename query_at>
1826
+ std::pair<equal_iterator_gt<query_at>, equal_iterator_gt<query_at>>
1827
+ equal_range(query_at const& query) const noexcept {
1828
+
1829
+ equals_t equals;
1830
+ auto this_ptr = const_cast<flat_hash_multi_set_gt*>(this);
1831
+ auto end = equal_iterator_gt<query_at>(capacity_slots_, this_ptr, query, equals);
1832
+ if (!capacity_slots_)
1833
+ return {end, end};
1834
+
1835
+ hash_t hasher;
1836
+ std::size_t hash_value = hasher(query);
1837
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1);
1838
+ std::size_t const start_index = slot_index;
1839
+ std::size_t first_equal_index = capacity_slots_;
1840
+
1841
+ // Linear probing to find the first equal element
1842
+ do {
1843
+ slot_ref_t slot = slot_ref(slot_index);
1844
+ if (slot.header.populated & ~slot.header.deleted & slot.mask) {
1845
+ if (equals(slot.element, query)) {
1846
+ first_equal_index = slot_index;
1847
+ break;
1848
+ }
1849
+ }
1850
+ // Stop if we find an empty slot
1851
+ else if (~slot.header.populated & slot.mask)
1852
+ break;
1853
+
1854
+ // Move to the next slot
1855
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1);
1856
+ } while (slot_index != start_index);
1857
+
1858
+ return {equal_iterator_gt<query_at>(first_equal_index, this_ptr, query, equals), end};
1859
+ }
1860
+
1861
+ template <typename similar_at> bool pop_first(similar_at&& query, element_t& popped_value) noexcept {
1862
+
1863
+ if (!capacity_slots_)
1864
+ return false;
1865
+
1866
+ hash_t hasher;
1867
+ equals_t equals;
1868
+ std::size_t hash_value = hasher(query);
1869
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1870
+ std::size_t start_index = slot_index; // To detect loop in probing
1871
+
1872
+ // Linear probing to find the first match
1873
+ do {
1874
+ slot_ref_t slot = slot_ref(slot_index);
1875
+ if (slot.header.populated & slot.mask) {
1876
+ if ((~slot.header.deleted & slot.mask) && equals(slot.element, query)) {
1877
+ // Found a match, mark as deleted
1878
+ slot.header.deleted |= slot.mask;
1879
+ --populated_slots_;
1880
+ popped_value = slot.element;
1881
+ return true; // Successfully removed
1882
+ }
1883
+ } else {
1884
+ // Stop if we find an empty slot
1885
+ break;
1886
+ }
1887
+
1888
+ // Move to the next slot
1889
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1890
+ } while (slot_index != start_index);
1891
+
1892
+ return false; // No match found
1893
+ }
1894
+
1895
+ template <typename similar_at> std::size_t erase(similar_at&& query) noexcept {
1896
+
1897
+ if (!capacity_slots_)
1898
+ return 0;
1899
+
1900
+ hash_t hasher;
1901
+ equals_t equals;
1902
+ std::size_t hash_value = hasher(query);
1903
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1904
+ std::size_t start_index = slot_index; // To detect loop in probing
1905
+ std::size_t count = 0; // Count of elements removed
1906
+
1907
+ // Linear probing to find all matches
1908
+ do {
1909
+ slot_ref_t slot = slot_ref(slot_index);
1910
+ if (slot.header.populated & slot.mask) {
1911
+ if ((~slot.header.deleted & slot.mask) && equals(slot.element, query)) {
1912
+ // Found a match, mark as deleted
1913
+ slot.header.deleted |= slot.mask;
1914
+ --populated_slots_;
1915
+ ++count; // Increment count of elements removed
1916
+ }
1917
+ } else {
1918
+ // Stop if we find an empty slot
1919
+ break;
1920
+ }
1921
+
1922
+ // Move to the next slot
1923
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1924
+ } while (slot_index != start_index);
1925
+
1926
+ return count; // Return the number of elements removed
1927
+ }
1928
+
1929
+ template <typename similar_at> element_t const* find(similar_at&& query) const noexcept {
1930
+
1931
+ if (!capacity_slots_)
1932
+ return nullptr;
1933
+
1934
+ hash_t hasher;
1935
+ equals_t equals;
1936
+ std::size_t hash_value = hasher(query);
1937
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1938
+ std::size_t start_index = slot_index; // To detect loop in probing
1939
+
1940
+ // Linear probing to find the first match
1941
+ do {
1942
+ slot_ref_t slot = slot_ref(slot_index);
1943
+ if (slot.header.populated & slot.mask) {
1944
+ if ((~slot.header.deleted & slot.mask) && equals(slot.element, query))
1945
+ return &slot.element; // Found a match, return pointer to the element
1946
+ } else {
1947
+ // Stop if we find an empty slot
1948
+ break;
1949
+ }
1950
+
1951
+ // Move to the next slot
1952
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1); // Assuming capacity_slots_ is a power of 2
1953
+ } while (slot_index != start_index);
1954
+
1955
+ return nullptr; // No match found
1956
+ }
1957
+
1958
+ element_t const* end() const noexcept { return nullptr; }
1959
+
1960
+ template <typename func_at> void for_each(func_at&& func) const {
1961
+ for (std::size_t bucket_index = 0; bucket_index < buckets_; ++bucket_index) {
1962
+ auto bucket_pointer = data_ + bytes_per_bucket() * bucket_index;
1963
+ bucket_header_t& header = *reinterpret_cast<bucket_header_t*>(bucket_pointer);
1964
+ std::uint64_t populated = header.populated;
1965
+ std::uint64_t deleted = header.deleted;
1966
+
1967
+ // Iterate through slots in the bucket
1968
+ for (std::size_t in_bucket_index = 0; in_bucket_index < slots_per_bucket(); ++in_bucket_index) {
1969
+ std::uint64_t mask = std::uint64_t(1ull) << in_bucket_index;
1970
+
1971
+ // Check if the slot is populated and not deleted
1972
+ if ((populated & ~deleted) & mask) {
1973
+ auto slot_pointer = bucket_pointer + sizeof(bucket_header_t) + sizeof(element_t) * in_bucket_index;
1974
+ element_t const& element = *reinterpret_cast<element_t const*>(slot_pointer);
1975
+ func(element);
1976
+ }
1977
+ }
1978
+ }
1979
+ }
1980
+
1981
+ template <typename similar_at> std::size_t count(similar_at&& query) const noexcept {
1982
+
1983
+ if (!capacity_slots_)
1984
+ return 0;
1985
+
1986
+ hash_t hasher;
1987
+ equals_t equals;
1988
+ std::size_t hash_value = hasher(query);
1989
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1);
1990
+ std::size_t start_index = slot_index; // To detect loop in probing
1991
+ std::size_t count = 0;
1992
+
1993
+ // Linear probing to find the range
1994
+ do {
1995
+ slot_ref_t slot = slot_ref(slot_index);
1996
+ if ((slot.header.populated & slot.mask) && (~slot.header.deleted & slot.mask)) {
1997
+ if (equals(slot.element, query))
1998
+ ++count;
1999
+ } else if (~slot.header.populated & slot.mask) {
2000
+ // Stop if we find an empty slot
2001
+ break;
2002
+ }
2003
+
2004
+ // Move to the next slot
2005
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1);
2006
+ } while (slot_index != start_index);
2007
+
2008
+ return count;
2009
+ }
2010
+
2011
+ template <typename similar_at> bool contains(similar_at&& query) const noexcept {
2012
+
2013
+ if (!capacity_slots_)
2014
+ return false;
2015
+
2016
+ hash_t hasher;
2017
+ equals_t equals;
2018
+ std::size_t hash_value = hasher(query);
2019
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1);
2020
+ std::size_t start_index = slot_index; // To detect loop in probing
2021
+
2022
+ // Linear probing to find the first match
2023
+ do {
2024
+ slot_ref_t slot = slot_ref(slot_index);
2025
+ if (slot.header.populated & slot.mask) {
2026
+ if ((~slot.header.deleted & slot.mask) && equals(slot.element, query))
2027
+ return true; // Found a match, exit early
2028
+ } else
2029
+ // Stop if we find an empty slot
2030
+ break;
2031
+
2032
+ // Move to the next slot
2033
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1);
2034
+ } while (slot_index != start_index);
2035
+
2036
+ return false; // No match found
2037
+ }
2038
+
2039
+ void reserve(std::size_t capacity) {
2040
+ if (!try_reserve(capacity))
2041
+ throw std::bad_alloc();
2042
+ }
2043
+
2044
+ bool try_emplace(element_t const& element) noexcept {
2045
+ // Check if we need to resize
2046
+ if (populated_slots_ * 3u >= capacity_slots_ * 2u)
2047
+ if (!try_reserve(populated_slots_ + 1))
2048
+ return false;
2049
+
2050
+ hash_t hasher;
2051
+ std::size_t hash_value = hasher(element);
2052
+ std::size_t slot_index = hash_value & (capacity_slots_ - 1);
2053
+
2054
+ // Linear probing
2055
+ while (true) {
2056
+ slot_ref_t slot = slot_ref(slot_index);
2057
+ if ((~slot.header.populated & slot.mask) | (slot.header.deleted & slot.mask)) {
2058
+ // Found an empty or deleted slot
2059
+ populated_slots_ += populate_slot(slot, element);
2060
+ return true;
2061
+ }
2062
+ // Move to the next slot
2063
+ slot_index = (slot_index + 1) & (capacity_slots_ - 1);
2064
+ }
2065
+ }
2066
+ };
2067
+
1559
2068
  } // namespace usearch
1560
2069
  } // namespace unum
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian",
6
6
  "license": "Apache 2.0",
package/python/lib.cpp CHANGED
@@ -177,10 +177,12 @@ static dense_index_py_t make_index( //
177
177
  metric_kind_t metric_kind, //
178
178
  metric_signature_t metric_signature, //
179
179
  std::uintptr_t metric_uintptr, //
180
- bool multi) {
180
+ bool multi, //
181
+ bool enable_key_lookups) {
181
182
 
182
183
  index_dense_config_t config(connectivity, expansion_add, expansion_search);
183
184
  config.multi = multi;
185
+ config.enable_key_lookups = enable_key_lookups;
184
186
 
185
187
  metric_t metric = //
186
188
  metric_uintptr //
@@ -1042,7 +1044,8 @@ PYBIND11_MODULE(compiled, m) {
1042
1044
  py::arg("metric_kind") = metric_kind_t::cos_k, //
1043
1045
  py::arg("metric_signature") = metric_signature_t::array_array_k, //
1044
1046
  py::arg("metric_pointer") = 0, //
1045
- py::arg("multi") = false //
1047
+ py::arg("multi") = false, //
1048
+ py::arg("enable_key_lookups") = true //
1046
1049
  );
1047
1050
 
1048
1051
  i.def( //
@@ -133,13 +133,6 @@
133
133
  "plt.tight_layout()\n",
134
134
  "plt.show()"
135
135
  ]
136
- },
137
- {
138
- "cell_type": "code",
139
- "execution_count": null,
140
- "metadata": {},
141
- "outputs": [],
142
- "source": []
143
136
  }
144
137
  ],
145
138
  "metadata": {
@@ -476,6 +476,7 @@ class Index:
476
476
  multi: bool = False,
477
477
  path: Optional[os.PathLike] = None,
478
478
  view: bool = False,
479
+ enable_key_lookups: bool = True,
479
480
  ) -> None:
480
481
  """Construct the index and compiles the functions, if requested (expensive).
481
482
 
@@ -565,6 +566,7 @@ class Index:
565
566
  expansion_add=expansion_add,
566
567
  expansion_search=expansion_search,
567
568
  multi=multi,
569
+ enable_key_lookups=enable_key_lookups,
568
570
  metric_kind=self._metric_kind,
569
571
  metric_pointer=self._metric_pointer,
570
572
  metric_signature=self._metric_signature,
@@ -986,6 +988,8 @@ class Index:
986
988
 
987
989
  def reset(self):
988
990
  """Erases all members from index, closing files, and returning RAM to OS."""
991
+ if not hasattr(self, "_compiled"):
992
+ return
989
993
  self._compiled.reset()
990
994
 
991
995
  def __del__(self):
@@ -1175,23 +1179,28 @@ class Index:
1175
1179
 
1176
1180
  @property
1177
1181
  def specs(self) -> Dict[str, Union[str, int, bool]]:
1182
+ if not hasattr(self, "_compiled"):
1183
+ return "usearch.Index(failed)"
1178
1184
  return {
1179
- "Class": "usearch.Index",
1180
- "Connectivity": self.connectivity,
1181
- "Size": self.size,
1182
- "Dimensions": self.ndim,
1183
- "Expansion@Add": self.expansion_add,
1184
- "Expansion@Search": self.expansion_search,
1185
- "OpenMP": USES_OPENMP,
1186
- "SimSIMD": USES_SIMSIMD,
1187
- "NativeF16": USES_NATIVE_F16,
1188
- "JIT": self.jit,
1189
- "DType": self.dtype,
1190
- "Path": self.path,
1185
+ "type": "usearch.Index",
1186
+ "ndim": self.ndim,
1187
+ "connectivity": self.connectivity,
1188
+ "expansion_add": self.expansion_add,
1189
+ "expansion_search": self.expansion_search,
1190
+ "size": self.size,
1191
+ "jit": self.jit,
1192
+ "hardware_acceleration": self.hardware_acceleration,
1193
+ "dtype": self.dtype,
1194
+ "path": self.path,
1195
+ "compiled_with_openmp": USES_OPENMP,
1196
+ "compiled_with_simsimd": USES_SIMSIMD,
1197
+ "compiled_with_native_f16": USES_NATIVE_F16,
1191
1198
  }
1192
1199
 
1193
1200
  def __repr__(self) -> str:
1194
- f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {} vectors in {} levels)"
1201
+ if not hasattr(self, "_compiled"):
1202
+ return "usearch.Index(failed)"
1203
+ f = "usearch.Index({} x {}, {}, connectivity: {}, expansion: {} & {}, {:,} vectors in {} levels, {} hardware accerlation)"
1195
1204
  return f.format(
1196
1205
  self.dtype,
1197
1206
  self.ndim,
@@ -1201,9 +1210,12 @@ class Index:
1201
1210
  self.expansion_search,
1202
1211
  len(self),
1203
1212
  self.nlevels,
1213
+ self.hardware_acceleration,
1204
1214
  )
1205
1215
 
1206
1216
  def _repr_pretty_(self, printer, cycle) -> str:
1217
+ if not hasattr(self, "_compiled"):
1218
+ return "usearch.Index(failed)"
1207
1219
  level_stats = [
1208
1220
  f"--- {i}. {self.level_stats(i).nodes:,} nodes" for i in range(self.nlevels)
1209
1221
  ]
package/rust/lib.rs CHANGED
@@ -81,7 +81,18 @@ pub mod ffi {
81
81
  ///
82
82
  /// * `key` - The key associated with the vector.
83
83
  /// * `vector` - A slice containing the vector data.
84
- pub fn add(self: &Index, key: u64, vector: &[f32]) -> Result<()>;
84
+ fn add<T>(self: &Index, key: u64, vector: &[T]) -> Result<()> {
85
+ // Use type-based dispatch
86
+ if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i8>() {
87
+ add_i8(key, unsafe { std::mem::transmute::<&[T], &[i8]>(vector) })
88
+ } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
89
+ add_f32(key, unsafe { std::mem::transmute::<&[T], &[f32]>(vector) })
90
+ } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
91
+ add_f64(key, unsafe { std::mem::transmute::<&[T], &[f64]>(vector) })
92
+ } else {
93
+ Err(Box::new("Unsupported type"))
94
+ }
95
+ }
85
96
 
86
97
  pub fn add_i8(self: &Index, key: u64, vector: &[i8]) -> Result<()>;
87
98
  pub fn add_f16(self: &Index, key: u64, vector: &[u16]) -> Result<()>;
@@ -98,7 +109,18 @@ pub mod ffi {
98
109
  /// # Returns
99
110
  ///
100
111
  /// A `Result` containing the matches found.
101
- pub fn search(self: &Index, query: &[f32], count: usize) -> Result<Matches>;
112
+ pub fn search<T>(self: &Index, query: &[T], count: usize) -> Result<Matches> {
113
+ // Use type-based dispatch
114
+ if std::any::TypeId::of::<T>() == std::any::TypeId::of::<i8>() {
115
+ search_i8(unsafe { std::mem::transmute::<&[T], &[i8]>(query) }, count)
116
+ } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f32>() {
117
+ search_f32(unsafe { std::mem::transmute::<&[T], &[f32]>(query) }, count)
118
+ } else if std::any::TypeId::of::<T>() == std::any::TypeId::of::<f64>() {
119
+ search_f64(unsafe { std::mem::transmute::<&[T], &[f64]>(query) }, count)
120
+ } else {
121
+ Err(Box::new("Unsupported type"))
122
+ }
123
+ }
102
124
 
103
125
  pub fn search_i8(self: &Index, query: &[i8], count: usize) -> Result<Matches>;
104
126
  pub fn search_f16(self: &Index, query: &[u16], count: usize) -> Result<Matches>;
package/wasmer.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name="unum/usearch"
3
- version="2.3.2"
3
+ version="2.4.0"
4
4
  description="Smaller & Faster Single-File Vector Search Engine from Unum"
5
5
  license="Apache-2.0"
6
6
  readme="README.md"