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.
- package/.github/workflows/prerelease.yml +2 -2
- package/.github/workflows/release.yml +4 -5
- package/CITATION.cff +1 -1
- package/Cargo.toml +1 -1
- package/README.md +1 -1
- package/conanfile.py +1 -1
- package/cpp/test.cpp +7 -0
- package/csharp/nuget/nuget-package.props +1 -1
- 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 +1 -1
- package/python/lib.cpp +5 -2
- 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
|
@@ -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
|
-
|
|
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
|
|
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
|
|
447
|
+
name: Build C#
|
|
449
448
|
needs: versioning
|
|
450
449
|
strategy:
|
|
451
450
|
matrix:
|
package/CITATION.cff
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
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.
|
|
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
|
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
#define UNUM_USEARCH_HPP
|
|
11
11
|
|
|
12
12
|
#define USEARCH_VERSION_MAJOR 2
|
|
13
|
-
#define USEARCH_VERSION_MINOR
|
|
14
|
-
#define USEARCH_VERSION_PATCH
|
|
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>
|
|
5
|
-
#include <numeric>
|
|
6
|
-
#include <shared_mutex>
|
|
7
|
-
#include <thread>
|
|
8
|
-
#include <
|
|
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
|
-
|
|
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_.
|
|
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(
|
|
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
|
-
|
|
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_.
|
|
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
|
-
|
|
1274
|
-
if (
|
|
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
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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_.
|
|
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
|
-
|
|
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_.
|
|
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 {
|
|
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
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( //
|
package/python/usearch/index.py
CHANGED
|
@@ -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
|
-
"
|
|
1180
|
-
"
|
|
1181
|
-
"
|
|
1182
|
-
"
|
|
1183
|
-
"
|
|
1184
|
-
"
|
|
1185
|
-
"
|
|
1186
|
-
"
|
|
1187
|
-
"
|
|
1188
|
-
"
|
|
1189
|
-
"
|
|
1190
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
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: &[
|
|
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>;
|