usearch 2.3.2 → 2.4.1
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/prerelease.yml +3 -3
- package/.github/workflows/release.yml +96 -5
- package/CITATION.cff +1 -1
- package/CMakeLists.txt +2 -2
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/c/CMakeLists.txt +1 -1
- package/c/README.md +1 -1
- package/conanfile.py +14 -7
- package/cpp/test.cpp +7 -0
- package/csharp/nuget/nuget-package.props +1 -1
- package/csharp/src/Cloud.Unum.USearch.Tests/USearchIndexTests.cs +1013 -0
- package/docs/format.md +40 -17
- package/include/usearch/index.hpp +2 -2
- package/include/usearch/index_dense.hpp +47 -27
- package/include/usearch/index_plugins.hpp +516 -7
- package/package.json +2 -2
- package/python/lib.cpp +8 -5
- package/python/scripts/cluster.ipynb +0 -7
- package/python/usearch/index.py +25 -13
- package/rust/lib.rs +24 -2
- package/wasmer.toml +1 -1
- package/csharp/src/Cloud.Unum.USearch.Tests/InitTests.cs +0 -306
|
@@ -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 {
|
|
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
|
-
|
|
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::
|
|
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
|
+
"version": "2.4.1",
|
|
4
4
|
"description": "Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
5
5
|
"author": "Ash Vardanian",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@semantic-release/exec": "^6.0.3",
|
|
26
26
|
"@semantic-release/git": "^10.0.1",
|
|
27
27
|
"conventional-changelog-eslint": "^3.0.9",
|
|
28
|
-
"semantic-release": "^21.1.
|
|
28
|
+
"semantic-release": "^21.1.2",
|
|
29
29
|
"typescript": "^5.1.6"
|
|
30
30
|
}
|
|
31
31
|
}
|
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 //
|
|
@@ -606,9 +608,9 @@ static py::tuple search_many_brute_force( //
|
|
|
606
608
|
if (dataset_kind != queries_kind)
|
|
607
609
|
throw std::invalid_argument("The types of vectors don't match!");
|
|
608
610
|
|
|
609
|
-
py::array_t<dense_key_t> keys_py({
|
|
610
|
-
py::array_t<distance_t> distances_py({
|
|
611
|
-
py::array_t<Py_ssize_t> counts_py(
|
|
611
|
+
py::array_t<dense_key_t> keys_py({queries_count, static_cast<Py_ssize_t>(wanted)});
|
|
612
|
+
py::array_t<distance_t> distances_py({queries_count, static_cast<Py_ssize_t>(wanted)});
|
|
613
|
+
py::array_t<Py_ssize_t> counts_py(queries_count);
|
|
612
614
|
|
|
613
615
|
std::size_t dimensions = static_cast<std::size_t>(queries_dimensions);
|
|
614
616
|
metric_t metric = //
|
|
@@ -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( //
|