usearch 0.9.4 → 0.9.5
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 +105 -0
- package/Cargo.toml +1 -1
- package/VERSION +1 -1
- package/conanfile.py +1 -1
- package/docs/compilation.md +14 -1
- package/include/usearch/usearch.hpp +45 -53
- package/package.json +1 -1
- package/python/scripts/bench_params.py +0 -1
- package/python/usearch/client.py +11 -4
- package/python/usearch/server.py +6 -2
- package/src/punned.hpp +40 -26
- package/src/test.cpp +26 -3
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
name: Pre-Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: ["main-dev"]
|
|
7
|
+
|
|
8
|
+
env:
|
|
9
|
+
BUILD_TYPE: Release
|
|
10
|
+
GH_TOKEN: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
|
|
11
|
+
PYTHONUTF8: 1
|
|
12
|
+
|
|
13
|
+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
|
|
19
|
+
test_python:
|
|
20
|
+
name: Test Python
|
|
21
|
+
runs-on: ${{ matrix.os }}
|
|
22
|
+
strategy:
|
|
23
|
+
matrix:
|
|
24
|
+
os: [ubuntu-20.04, macOS-11, windows-2022]
|
|
25
|
+
python-version: ["3.11"]
|
|
26
|
+
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v3
|
|
29
|
+
- run: git submodule update --init --recursive
|
|
30
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
31
|
+
uses: actions/setup-python@v4
|
|
32
|
+
with:
|
|
33
|
+
python-version: ${{ matrix.python-version }}
|
|
34
|
+
- name: Install dependencies
|
|
35
|
+
run: |
|
|
36
|
+
python -m pip install --upgrade pip
|
|
37
|
+
pip install pytest numpy
|
|
38
|
+
- name: Build locally
|
|
39
|
+
run: python -m pip install .
|
|
40
|
+
- name: Test with PyTest on NIX
|
|
41
|
+
if: matrix.os != 'windows-2022'
|
|
42
|
+
run: pytest python/scripts/test.py
|
|
43
|
+
|
|
44
|
+
test_javascript:
|
|
45
|
+
name: Test JavaScript
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
steps:
|
|
48
|
+
- uses: actions/checkout@v3
|
|
49
|
+
- run: git submodule update --init --recursive
|
|
50
|
+
- uses: actions/setup-node@v3
|
|
51
|
+
with:
|
|
52
|
+
node-version: 18
|
|
53
|
+
- run: npm install
|
|
54
|
+
- run: npm ci
|
|
55
|
+
- run: npm test
|
|
56
|
+
|
|
57
|
+
test_rust:
|
|
58
|
+
name: Test Rust
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v3
|
|
62
|
+
- run: git submodule update --init --recursive
|
|
63
|
+
- uses: actions-rs/toolchain@v1
|
|
64
|
+
with:
|
|
65
|
+
toolchain: stable
|
|
66
|
+
override: true
|
|
67
|
+
|
|
68
|
+
test_java:
|
|
69
|
+
name: Test Java
|
|
70
|
+
runs-on: ubuntu-latest
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v3
|
|
73
|
+
- run: git submodule update --init --recursive
|
|
74
|
+
- uses: actions/setup-java@v3
|
|
75
|
+
with:
|
|
76
|
+
java-version: '11'
|
|
77
|
+
distribution: 'adopt'
|
|
78
|
+
- name: Setup Gradle
|
|
79
|
+
uses: gradle/gradle-build-action@v2.4.2
|
|
80
|
+
- name: Execute Gradle build
|
|
81
|
+
run: gradle clean build
|
|
82
|
+
|
|
83
|
+
test_swift:
|
|
84
|
+
name: Test ObjC & Swift
|
|
85
|
+
runs-on: macos-latest
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v3
|
|
88
|
+
- run: git submodule update --init --recursive
|
|
89
|
+
- name: Build
|
|
90
|
+
run: swift build
|
|
91
|
+
- name: Run tests
|
|
92
|
+
run: swift test
|
|
93
|
+
|
|
94
|
+
build_docker:
|
|
95
|
+
name: Test Docker Image
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- uses: actions/checkout@v3
|
|
99
|
+
- run: git submodule update --init --recursive
|
|
100
|
+
- name: Build
|
|
101
|
+
uses: docker/build-push-action@v4
|
|
102
|
+
with:
|
|
103
|
+
context: .
|
|
104
|
+
load: true
|
|
105
|
+
push: false
|
package/Cargo.toml
CHANGED
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.5
|
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 = '0.9.
|
|
7
|
+
version = '0.9.5'
|
|
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/docs/compilation.md
CHANGED
|
@@ -35,7 +35,13 @@ cmake -B ./build_release \
|
|
|
35
35
|
-DUSEARCH_BUILD_BENCHMARK=1 \
|
|
36
36
|
-DUSEARCH_BUILD_TEST=1 \
|
|
37
37
|
&& \
|
|
38
|
-
make -C ./build_release -j
|
|
38
|
+
make -C ./build_release -j
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Linting:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
cppcheck --enable=all --suppress=cstyleCast --suppress=unusedFunction include/usearch/usearch.hpp src/punned.hpp
|
|
39
45
|
```
|
|
40
46
|
|
|
41
47
|
Benchmarking:
|
|
@@ -61,6 +67,13 @@ pip install -e .
|
|
|
61
67
|
pytest python/scripts/test.py
|
|
62
68
|
```
|
|
63
69
|
|
|
70
|
+
Linting:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
pip install ruff
|
|
74
|
+
ruff --format=github --select=E9,F63,F7,F82 --target-version=py37 python
|
|
75
|
+
```
|
|
76
|
+
|
|
64
77
|
Benchmarking:
|
|
65
78
|
|
|
66
79
|
```sh
|
|
@@ -103,7 +103,7 @@ using f64_t = double;
|
|
|
103
103
|
using f32_t = float;
|
|
104
104
|
using byte_t = char;
|
|
105
105
|
|
|
106
|
-
template <typename at> at angle_to_radians(at angle) noexcept { return angle *
|
|
106
|
+
template <typename at> at angle_to_radians(at angle) noexcept { return angle * at(3.14159265358979323846) / at(180); }
|
|
107
107
|
|
|
108
108
|
template <typename at> at square(at value) noexcept { return value * value; }
|
|
109
109
|
|
|
@@ -409,9 +409,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
409
409
|
static_assert(sizeof(byte_t) == 1, "Allocator must allocate separate addressable bytes");
|
|
410
410
|
|
|
411
411
|
#if defined(USEARCH_IS_WINDOWS)
|
|
412
|
-
using slot_t =
|
|
412
|
+
using slot_t = LONGLONG;
|
|
413
413
|
#else
|
|
414
|
-
using slot_t =
|
|
414
|
+
using slot_t = unsigned long;
|
|
415
415
|
#endif
|
|
416
416
|
|
|
417
417
|
static constexpr std::size_t bits_per_slot() { return sizeof(slot_t) * CHAR_BIT; }
|
|
@@ -422,7 +422,7 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
422
422
|
|
|
423
423
|
public:
|
|
424
424
|
visits_bitset_gt() noexcept {}
|
|
425
|
-
visits_bitset_gt(std::size_t capacity) noexcept {
|
|
425
|
+
explicit visits_bitset_gt(std::size_t capacity) noexcept {
|
|
426
426
|
slots_count_ = divide_round_up<bits_per_slot()>(capacity);
|
|
427
427
|
slots_ = (slot_t*)allocator_t{}.allocate(slots_count_ * sizeof(slot_t));
|
|
428
428
|
clear();
|
|
@@ -449,28 +449,18 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
|
449
449
|
|
|
450
450
|
#if defined(USEARCH_IS_WINDOWS)
|
|
451
451
|
|
|
452
|
-
inline bool atomic_test(std::size_t i) noexcept {
|
|
453
|
-
slot_t mask{1ul << (i & bits_mask())};
|
|
454
|
-
return InterlockedOr(&slots_[i / bits_per_slot()], 0) & mask;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
452
|
inline bool atomic_set(std::size_t i) noexcept {
|
|
458
453
|
slot_t mask{1ul << (i & bits_mask())};
|
|
459
|
-
return
|
|
454
|
+
return InterlockedOr64Acquire((slot_t volatile*)&slots_[i / bits_per_slot()], mask) & mask;
|
|
460
455
|
}
|
|
461
456
|
|
|
462
457
|
inline void atomic_reset(std::size_t i) noexcept {
|
|
463
458
|
slot_t mask{1ul << (i & bits_mask())};
|
|
464
|
-
|
|
459
|
+
InterlockedAnd64Release((slot_t volatile*)&slots_[i / bits_per_slot()], ~mask);
|
|
465
460
|
}
|
|
466
461
|
|
|
467
462
|
#else
|
|
468
463
|
|
|
469
|
-
inline bool atomic_test(std::size_t i) noexcept {
|
|
470
|
-
slot_t mask{1ul << (i & bits_mask())};
|
|
471
|
-
return __atomic_load_n(&slots_[i / bits_per_slot()], __ATOMIC_RELAXED) & mask;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
464
|
inline bool atomic_set(std::size_t i) noexcept {
|
|
475
465
|
slot_t mask{1ul << (i & bits_mask())};
|
|
476
466
|
return __atomic_fetch_or(&slots_[i / bits_per_slot()], mask, __ATOMIC_ACQUIRE) & mask;
|
|
@@ -781,7 +771,7 @@ template <typename scalar_at> class span_gt {
|
|
|
781
771
|
std::size_t size_;
|
|
782
772
|
|
|
783
773
|
public:
|
|
784
|
-
span_gt(scalar_at* begin, scalar_at* end) noexcept : data_(begin), size_(end - begin) {}
|
|
774
|
+
span_gt(scalar_at* begin, scalar_at const* end) noexcept : data_(begin), size_(end - begin) {}
|
|
785
775
|
span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
|
|
786
776
|
scalar_at* data() const noexcept { return data_; }
|
|
787
777
|
std::size_t size() const noexcept { return size_; }
|
|
@@ -928,11 +918,12 @@ class index_gt {
|
|
|
928
918
|
|
|
929
919
|
template <typename ref_at> class member_iterator_gt {
|
|
930
920
|
using ref_t = ref_at;
|
|
931
|
-
index_gt
|
|
932
|
-
std::size_t offset_;
|
|
921
|
+
index_gt* index_{};
|
|
922
|
+
std::size_t offset_{};
|
|
933
923
|
|
|
924
|
+
friend class index_gt;
|
|
934
925
|
member_iterator_gt() noexcept {}
|
|
935
|
-
member_iterator_gt(index_gt
|
|
926
|
+
member_iterator_gt(index_gt* index, std::size_t offset) noexcept : index_(index), offset_(offset) {}
|
|
936
927
|
|
|
937
928
|
public:
|
|
938
929
|
using iterator_category = std::random_access_iterator_tag;
|
|
@@ -941,7 +932,7 @@ class index_gt {
|
|
|
941
932
|
using pointer = void;
|
|
942
933
|
using reference = ref_t;
|
|
943
934
|
|
|
944
|
-
reference operator*() const noexcept { return index_
|
|
935
|
+
reference operator*() const noexcept { return index_->node_with_id_(offset_).ref(); }
|
|
945
936
|
member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
|
|
946
937
|
member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
|
|
947
938
|
member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
|
|
@@ -963,8 +954,11 @@ class index_gt {
|
|
|
963
954
|
offset_ -= d;
|
|
964
955
|
return *this;
|
|
965
956
|
}
|
|
966
|
-
bool operator
|
|
967
|
-
return
|
|
957
|
+
bool operator==(member_iterator_gt const& other) const noexcept {
|
|
958
|
+
return index_ == other.index_ && offset_ == other.offset_;
|
|
959
|
+
}
|
|
960
|
+
bool operator!=(member_iterator_gt const& other) const noexcept {
|
|
961
|
+
return index_ != other.index_ || offset_ != other.offset_;
|
|
968
962
|
}
|
|
969
963
|
};
|
|
970
964
|
|
|
@@ -1056,7 +1050,7 @@ class index_gt {
|
|
|
1056
1050
|
|
|
1057
1051
|
operator vector_view_t() const noexcept { return {vector(), dim()}; }
|
|
1058
1052
|
vector_view_t vector_view() const noexcept { return {vector(), dim()}; }
|
|
1059
|
-
member_ref_t ref() noexcept { return {
|
|
1053
|
+
member_ref_t ref() noexcept { return {{tape_}, vector_view()}; }
|
|
1060
1054
|
member_cref_t cref() const noexcept { return {label(), vector_view()}; }
|
|
1061
1055
|
};
|
|
1062
1056
|
|
|
@@ -1137,19 +1131,10 @@ class index_gt {
|
|
|
1137
1131
|
config_t const& config() const { return config_; }
|
|
1138
1132
|
bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
|
|
1139
1133
|
|
|
1140
|
-
index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
|
|
1141
|
-
: config_(normalize(config)), metric_(metric), allocator_(allocator)
|
|
1142
|
-
|
|
1143
|
-
// Externally defined hyper-parameters:
|
|
1144
|
-
pre_ = precompute_(config);
|
|
1134
|
+
explicit index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
|
|
1135
|
+
: config_(normalize(config)), metric_(metric), allocator_(allocator), pre_(precompute_(config)),
|
|
1136
|
+
viewed_file_descriptor_(0), size_(0u), max_level_(-1), entry_id_(0u) {
|
|
1145
1137
|
|
|
1146
|
-
// Configure initial empty state:
|
|
1147
|
-
size_ = 0u;
|
|
1148
|
-
max_level_ = -1;
|
|
1149
|
-
entry_id_ = 0u;
|
|
1150
|
-
viewed_file_descriptor_ = 0;
|
|
1151
|
-
|
|
1152
|
-
// Dynamic memory:
|
|
1153
1138
|
thread_contexts_.resize(config.max_threads());
|
|
1154
1139
|
for (thread_context_t& context : thread_contexts_)
|
|
1155
1140
|
context.metric = metric;
|
|
@@ -1159,7 +1144,7 @@ class index_gt {
|
|
|
1159
1144
|
/**
|
|
1160
1145
|
* @brief Clones the structure with the same hyper-parameters, but without contents.
|
|
1161
1146
|
*/
|
|
1162
|
-
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
1147
|
+
index_gt fork() noexcept(false) { return index_gt{config_, metric_, allocator_}; }
|
|
1163
1148
|
|
|
1164
1149
|
~index_gt() noexcept { clear(); }
|
|
1165
1150
|
|
|
@@ -1170,6 +1155,13 @@ class index_gt {
|
|
|
1170
1155
|
return *this;
|
|
1171
1156
|
}
|
|
1172
1157
|
|
|
1158
|
+
member_citerator_t cbegin() const noexcept { return {this, 0}; }
|
|
1159
|
+
member_citerator_t cend() const noexcept { return {this, size()}; }
|
|
1160
|
+
member_citerator_t begin() const noexcept { return {this, 0}; }
|
|
1161
|
+
member_citerator_t end() const noexcept { return {this, size()}; }
|
|
1162
|
+
member_iterator_t begin() noexcept { return {this, 0}; }
|
|
1163
|
+
member_iterator_t end() noexcept { return {this, size()}; }
|
|
1164
|
+
|
|
1173
1165
|
#pragma region Adjusting Configuration
|
|
1174
1166
|
|
|
1175
1167
|
/**
|
|
@@ -1200,12 +1192,12 @@ class index_gt {
|
|
|
1200
1192
|
std::swap(thread_contexts_, other.thread_contexts_);
|
|
1201
1193
|
|
|
1202
1194
|
// Non-atomic parts.
|
|
1203
|
-
std::size_t
|
|
1204
|
-
std::size_t
|
|
1195
|
+
std::size_t capacity_copy = capacity_;
|
|
1196
|
+
std::size_t size_copy = size_;
|
|
1205
1197
|
capacity_ = other.capacity_.load();
|
|
1206
1198
|
size_ = other.size_.load();
|
|
1207
|
-
other.capacity_ =
|
|
1208
|
-
other.size_ =
|
|
1199
|
+
other.capacity_ = capacity_copy;
|
|
1200
|
+
other.size_ = size_copy;
|
|
1209
1201
|
}
|
|
1210
1202
|
|
|
1211
1203
|
/**
|
|
@@ -1230,7 +1222,7 @@ class index_gt {
|
|
|
1230
1222
|
precomputed_constants_t pre = precompute_(config);
|
|
1231
1223
|
std::size_t bytes_per_node_base = node_head_bytes_() + pre.neighbors_base_bytes;
|
|
1232
1224
|
std::size_t rounded_size = divide_round_up<64>(bytes_per_node_base) * 64;
|
|
1233
|
-
std::size_t added_connections = (rounded_size -
|
|
1225
|
+
std::size_t added_connections = (rounded_size - bytes_per_node_base) / sizeof(id_t);
|
|
1234
1226
|
config.connectivity = config.connectivity + added_connections / base_level_multiple_();
|
|
1235
1227
|
return config;
|
|
1236
1228
|
}
|
|
@@ -1309,11 +1301,11 @@ class index_gt {
|
|
|
1309
1301
|
|
|
1310
1302
|
// Determining how much memory to allocate depends on the target level
|
|
1311
1303
|
std::unique_lock<std::mutex> new_level_lock(global_mutex_);
|
|
1312
|
-
level_t
|
|
1313
|
-
id_t
|
|
1304
|
+
level_t max_level_copy = max_level_; // Copy under lock
|
|
1305
|
+
id_t entry_id_copy = entry_id_; // Copy under lock
|
|
1314
1306
|
thread_context_t& context = thread_contexts_[config.thread];
|
|
1315
1307
|
level_t target_level = choose_random_level_(context.level_generator);
|
|
1316
|
-
if (target_level <=
|
|
1308
|
+
if (target_level <= max_level_copy)
|
|
1317
1309
|
new_level_lock.unlock();
|
|
1318
1310
|
|
|
1319
1311
|
// Allocate the neighbors
|
|
@@ -1333,10 +1325,10 @@ class index_gt {
|
|
|
1333
1325
|
result.cycles = context.iteration_cycles;
|
|
1334
1326
|
|
|
1335
1327
|
// Go down the level, tracking only the closest match
|
|
1336
|
-
id_t closest_id = search_for_one_(
|
|
1328
|
+
id_t closest_id = search_for_one_(entry_id_copy, vector, max_level_copy, target_level, context);
|
|
1337
1329
|
|
|
1338
1330
|
// From `target_level` down perform proper extensive search
|
|
1339
|
-
for (level_t level = (std::min)(target_level,
|
|
1331
|
+
for (level_t level = (std::min)(target_level, max_level_copy); level >= 0; --level) {
|
|
1340
1332
|
search_to_insert_(closest_id, vector, level, context);
|
|
1341
1333
|
closest_id = connect_new_node_(new_id, level, context);
|
|
1342
1334
|
}
|
|
@@ -1346,7 +1338,7 @@ class index_gt {
|
|
|
1346
1338
|
result.cycles = context.iteration_cycles - result.cycles;
|
|
1347
1339
|
|
|
1348
1340
|
// Updating the entry point if needed
|
|
1349
|
-
if (target_level >
|
|
1341
|
+
if (target_level > max_level_copy) {
|
|
1350
1342
|
entry_id_ = new_id;
|
|
1351
1343
|
max_level_ = target_level;
|
|
1352
1344
|
}
|
|
@@ -1423,12 +1415,12 @@ class index_gt {
|
|
|
1423
1415
|
struct stats_t {
|
|
1424
1416
|
std::size_t nodes;
|
|
1425
1417
|
std::size_t edges;
|
|
1426
|
-
std::size_t
|
|
1418
|
+
std::size_t max_edges;
|
|
1427
1419
|
};
|
|
1428
1420
|
|
|
1429
1421
|
stats_t stats() const noexcept {
|
|
1430
1422
|
stats_t result{};
|
|
1431
|
-
result.nodes =
|
|
1423
|
+
result.nodes = size();
|
|
1432
1424
|
for (std::size_t i = 0; i != result.nodes; ++i) {
|
|
1433
1425
|
node_t node = node_with_id_(i);
|
|
1434
1426
|
std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
|
|
@@ -1444,7 +1436,7 @@ class index_gt {
|
|
|
1444
1436
|
|
|
1445
1437
|
stats_t stats(std::size_t level) const noexcept {
|
|
1446
1438
|
stats_t result{};
|
|
1447
|
-
result.nodes =
|
|
1439
|
+
result.nodes = size();
|
|
1448
1440
|
for (std::size_t i = 0; i != result.nodes; ++i) {
|
|
1449
1441
|
node_t node = node_with_id_(i);
|
|
1450
1442
|
if (node.level() >= level)
|
|
@@ -1796,7 +1788,7 @@ class index_gt {
|
|
|
1796
1788
|
thread_context_t& context) const noexcept {
|
|
1797
1789
|
|
|
1798
1790
|
distance_t closest_dist = context.measure(query, node_with_id_(closest_id));
|
|
1799
|
-
for (level_t level = begin_level; level > end_level; level
|
|
1791
|
+
for (level_t level = begin_level; level > end_level; --level) {
|
|
1800
1792
|
bool changed;
|
|
1801
1793
|
do {
|
|
1802
1794
|
changed = false;
|
|
@@ -1923,7 +1915,7 @@ class index_gt {
|
|
|
1923
1915
|
top_candidates_t& top = context.top_candidates;
|
|
1924
1916
|
top.clear();
|
|
1925
1917
|
top.reserve(count);
|
|
1926
|
-
for (std::size_t i = 0; i !=
|
|
1918
|
+
for (std::size_t i = 0; i != size(); ++i)
|
|
1927
1919
|
top.emplace_reserved(context.measure(query, node_with_id_(i)), static_cast<id_t>(i));
|
|
1928
1920
|
}
|
|
1929
1921
|
|
package/package.json
CHANGED
package/python/usearch/client.py
CHANGED
|
@@ -18,9 +18,16 @@ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
|
|
|
18
18
|
return ascii
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
22
|
+
|
|
23
|
+
|
|
21
24
|
class IndexClient:
|
|
22
25
|
|
|
23
|
-
def __init__(
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
uri: str = '127.0.0.1',
|
|
29
|
+
port: int = 8545,
|
|
30
|
+
use_http: bool = True) -> None:
|
|
24
31
|
self.client = Client(uri=uri, port=port, use_http=use_http)
|
|
25
32
|
|
|
26
33
|
def add_one(self, label: int, vector: np.ndarray):
|
|
@@ -46,7 +53,7 @@ class IndexClient:
|
|
|
46
53
|
else:
|
|
47
54
|
return self.add_many(labels, vectors)
|
|
48
55
|
|
|
49
|
-
def search_one(self, vector: np.ndarray, count: int) ->
|
|
56
|
+
def search_one(self, vector: np.ndarray, count: int) -> Triplet:
|
|
50
57
|
matches: list[dict] = []
|
|
51
58
|
vector = vector.flatten()
|
|
52
59
|
ascii = _vector_to_ascii(vector)
|
|
@@ -68,7 +75,7 @@ class IndexClient:
|
|
|
68
75
|
|
|
69
76
|
return labels, distances, counts
|
|
70
77
|
|
|
71
|
-
def search_many(self, vectors: np.ndarray, count: int) ->
|
|
78
|
+
def search_many(self, vectors: np.ndarray, count: int) -> Triplet:
|
|
72
79
|
batch_size: int = vectors.shape[0]
|
|
73
80
|
list_of_matches: list[list[dict]] = self.client.search_many(
|
|
74
81
|
vectors=vectors, count=count)
|
|
@@ -84,7 +91,7 @@ class IndexClient:
|
|
|
84
91
|
|
|
85
92
|
return labels, distances, counts
|
|
86
93
|
|
|
87
|
-
def search(self, vectors: np.ndarray, count: int) ->
|
|
94
|
+
def search(self, vectors: np.ndarray, count: int) -> Triplet:
|
|
88
95
|
if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
|
|
89
96
|
return self.search_one(vectors, count)
|
|
90
97
|
else:
|
package/python/usearch/server.py
CHANGED
|
@@ -8,12 +8,16 @@ import numpy as np
|
|
|
8
8
|
from ucall.rich_posix import Server
|
|
9
9
|
from usearch.index import Index
|
|
10
10
|
|
|
11
|
+
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _results_to_json(results: Triplet, row: int) -> list[dict]:
|
|
11
15
|
|
|
12
|
-
def _results_to_json(results: tuple[np.ndarray, np.ndarray, np.ndarray], row: int) -> list[dict]:
|
|
13
16
|
count = results[2][row]
|
|
14
17
|
labels = results[0][row, :count]
|
|
15
18
|
distances = results[1][row, :count]
|
|
16
|
-
return [{'label': int(
|
|
19
|
+
return [{'label': int(label), 'distance': float(distance)}
|
|
20
|
+
for label, distance in zip(labels, distances)]
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
def _ascii_to_vector(string: str) -> np.ndarray:
|
package/src/punned.hpp
CHANGED
|
@@ -399,12 +399,15 @@ class punned_gt {
|
|
|
399
399
|
isa_t acceleration;
|
|
400
400
|
};
|
|
401
401
|
|
|
402
|
+
using member_iterator_t = typename index_t::member_iterator_t;
|
|
403
|
+
using member_citerator_t = typename index_t::member_citerator_t;
|
|
404
|
+
|
|
402
405
|
std::size_t dimensions_ = 0;
|
|
403
406
|
std::size_t casted_vector_bytes_ = 0;
|
|
404
407
|
accuracy_t accuracy_ = accuracy_t::f32_k;
|
|
405
408
|
isa_t acceleration_ = isa_t::auto_k;
|
|
406
409
|
|
|
407
|
-
index_t*
|
|
410
|
+
index_t* typed_{};
|
|
408
411
|
mutable std::vector<byte_t> cast_buffer_{};
|
|
409
412
|
struct casts_t {
|
|
410
413
|
cast_t from_f8{};
|
|
@@ -421,6 +424,7 @@ class punned_gt {
|
|
|
421
424
|
public:
|
|
422
425
|
using search_results_t = typename index_t::search_results_t;
|
|
423
426
|
using add_result_t = typename index_t::add_result_t;
|
|
427
|
+
using stats_t = typename index_t::stats_t;
|
|
424
428
|
|
|
425
429
|
punned_gt() = default;
|
|
426
430
|
punned_gt(punned_gt&& other) { swap(other); }
|
|
@@ -429,14 +433,14 @@ class punned_gt {
|
|
|
429
433
|
return *this;
|
|
430
434
|
}
|
|
431
435
|
|
|
432
|
-
~punned_gt() { aligned_index_free_(
|
|
436
|
+
~punned_gt() { aligned_index_free_(typed_); }
|
|
433
437
|
|
|
434
438
|
void swap(punned_gt& other) {
|
|
435
439
|
std::swap(dimensions_, other.dimensions_);
|
|
436
440
|
std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
|
|
437
441
|
std::swap(accuracy_, other.accuracy_);
|
|
438
442
|
std::swap(acceleration_, other.acceleration_);
|
|
439
|
-
std::swap(
|
|
443
|
+
std::swap(typed_, other.typed_);
|
|
440
444
|
std::swap(cast_buffer_, other.cast_buffer_);
|
|
441
445
|
std::swap(casts_, other.casts_);
|
|
442
446
|
std::swap(root_metric_, other.root_metric_);
|
|
@@ -446,19 +450,29 @@ class punned_gt {
|
|
|
446
450
|
static config_t optimize(config_t config) noexcept { return index_t::optimize(config); }
|
|
447
451
|
|
|
448
452
|
std::size_t dimensions() const { return dimensions_; }
|
|
449
|
-
std::size_t connectivity() const { return
|
|
450
|
-
std::size_t size() const { return
|
|
451
|
-
std::size_t capacity() const { return
|
|
452
|
-
config_t const& config() const { return
|
|
453
|
-
void clear() { return
|
|
453
|
+
std::size_t connectivity() const { return typed_->connectivity(); }
|
|
454
|
+
std::size_t size() const { return typed_->size(); }
|
|
455
|
+
std::size_t capacity() const { return typed_->capacity(); }
|
|
456
|
+
config_t const& config() const { return typed_->config(); }
|
|
457
|
+
void clear() { return typed_->clear(); }
|
|
458
|
+
|
|
459
|
+
member_citerator_t cbegin() const noexcept { return typed_->cbegin(); }
|
|
460
|
+
member_citerator_t cend() const noexcept { return typed_->cend(); }
|
|
461
|
+
member_citerator_t begin() const noexcept { return typed_->begin(); }
|
|
462
|
+
member_citerator_t end() const noexcept { return typed_->end(); }
|
|
463
|
+
member_iterator_t begin() noexcept { return typed_->begin(); }
|
|
464
|
+
member_iterator_t end() noexcept { return typed_->end(); }
|
|
465
|
+
|
|
466
|
+
stats_t stats() const noexcept { return typed_->stats(); }
|
|
467
|
+
stats_t stats(std::size_t level) const noexcept { return typed_->stats(level); }
|
|
454
468
|
|
|
455
469
|
accuracy_t accuracy() const { return accuracy_; }
|
|
456
470
|
isa_t acceleration() const { return acceleration_; }
|
|
457
471
|
|
|
458
|
-
void save(char const* path) const {
|
|
459
|
-
void load(char const* path) {
|
|
460
|
-
void view(char const* path) {
|
|
461
|
-
void reserve(std::size_t capacity) {
|
|
472
|
+
void save(char const* path) const { typed_->save(path); }
|
|
473
|
+
void load(char const* path) { typed_->load(path); }
|
|
474
|
+
void view(char const* path) { typed_->view(path); }
|
|
475
|
+
void reserve(std::size_t capacity) { typed_->reserve(capacity); }
|
|
462
476
|
|
|
463
477
|
// clang-format off
|
|
464
478
|
add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
|
|
@@ -516,7 +530,7 @@ class punned_gt {
|
|
|
516
530
|
result.root_metric_ = root_metric_;
|
|
517
531
|
index_t* raw = aligned_index_alloc_();
|
|
518
532
|
new (raw) index_t(config(), root_metric_);
|
|
519
|
-
result.
|
|
533
|
+
result.typed_ = raw;
|
|
520
534
|
|
|
521
535
|
return result;
|
|
522
536
|
}
|
|
@@ -563,7 +577,7 @@ class punned_gt {
|
|
|
563
577
|
if (casted)
|
|
564
578
|
vector_data = casted_data, vector_bytes = casted_vector_bytes_, config.store_vector = true;
|
|
565
579
|
|
|
566
|
-
return
|
|
580
|
+
return typed_->add(label, {vector_data, vector_bytes}, config);
|
|
567
581
|
}
|
|
568
582
|
|
|
569
583
|
template <typename scalar_at>
|
|
@@ -579,7 +593,7 @@ class punned_gt {
|
|
|
579
593
|
if (casted)
|
|
580
594
|
vector_data = casted_data, vector_bytes = casted_vector_bytes_;
|
|
581
595
|
|
|
582
|
-
return
|
|
596
|
+
return typed_->search({vector_data, vector_bytes}, wanted, config);
|
|
583
597
|
}
|
|
584
598
|
|
|
585
599
|
template <typename scalar_at>
|
|
@@ -595,14 +609,14 @@ class punned_gt {
|
|
|
595
609
|
if (casted)
|
|
596
610
|
vector_data = casted_data, vector_bytes = casted_vector_bytes_;
|
|
597
611
|
|
|
598
|
-
return
|
|
612
|
+
return typed_->search_around(static_cast<id_t>(hint), {vector_data, vector_bytes}, wanted, config);
|
|
599
613
|
}
|
|
600
614
|
|
|
601
615
|
template <typename scalar_at> add_result_t add_(label_t label, scalar_at const* vector, cast_t const& cast) {
|
|
602
616
|
thread_lock_t lock = thread_lock_();
|
|
603
|
-
add_config_t
|
|
604
|
-
|
|
605
|
-
return add_(label, vector,
|
|
617
|
+
add_config_t add_config;
|
|
618
|
+
add_config.thread = lock.thread_id;
|
|
619
|
+
return add_(label, vector, add_config, cast);
|
|
606
620
|
}
|
|
607
621
|
|
|
608
622
|
template <typename scalar_at>
|
|
@@ -610,9 +624,9 @@ class punned_gt {
|
|
|
610
624
|
scalar_at const* vector, std::size_t wanted, //
|
|
611
625
|
cast_t const& cast) const {
|
|
612
626
|
thread_lock_t lock = thread_lock_();
|
|
613
|
-
search_config_t
|
|
614
|
-
|
|
615
|
-
return search_(vector, wanted,
|
|
627
|
+
search_config_t search_config;
|
|
628
|
+
search_config.thread = lock.thread_id;
|
|
629
|
+
return search_(vector, wanted, search_config, cast);
|
|
616
630
|
}
|
|
617
631
|
|
|
618
632
|
template <typename scalar_at>
|
|
@@ -620,9 +634,9 @@ class punned_gt {
|
|
|
620
634
|
label_t hint, scalar_at const* vector, std::size_t wanted, //
|
|
621
635
|
cast_t const& cast) const {
|
|
622
636
|
thread_lock_t lock = thread_lock_();
|
|
623
|
-
search_config_t
|
|
624
|
-
|
|
625
|
-
return search_around_(hint, vector, wanted,
|
|
637
|
+
search_config_t search_config;
|
|
638
|
+
search_config.thread = lock.thread_id;
|
|
639
|
+
return search_around_(hint, vector, wanted, search_config, cast);
|
|
626
640
|
}
|
|
627
641
|
|
|
628
642
|
static punned_gt make_( //
|
|
@@ -650,7 +664,7 @@ class punned_gt {
|
|
|
650
664
|
// Available since C11, but only C++17, so we use the C version.
|
|
651
665
|
index_t* raw = aligned_index_alloc_();
|
|
652
666
|
new (raw) index_t(config, metric_and_meta.metric);
|
|
653
|
-
result.
|
|
667
|
+
result.typed_ = raw;
|
|
654
668
|
return result;
|
|
655
669
|
}
|
|
656
670
|
|
package/src/test.cpp
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @brief A trivial test.
|
|
3
3
|
*/
|
|
4
|
+
#include <algorithm>
|
|
4
5
|
#include <cassert>
|
|
5
6
|
|
|
6
7
|
#include "punned.hpp"
|
|
@@ -15,10 +16,13 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
|
|
|
15
16
|
using distance_t = typename index_at::distance_t;
|
|
16
17
|
|
|
17
18
|
scalar_at vec[3] = {10, 20, 15};
|
|
19
|
+
scalar_at vec_a[3] = {15, 16, 17};
|
|
20
|
+
scalar_at vec_b[3] = {16, 17, 18};
|
|
18
21
|
|
|
19
22
|
index.reserve(10);
|
|
20
23
|
index.add(42, span_t{&vec[0], 3ul});
|
|
21
24
|
|
|
25
|
+
// Default approximate search
|
|
22
26
|
point_id_t matched_labels[10] = {0};
|
|
23
27
|
distance_t matched_distances[10] = {0};
|
|
24
28
|
std::size_t matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
|
|
@@ -27,19 +31,38 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
|
|
|
27
31
|
assert(matched_labels[0] == 42);
|
|
28
32
|
assert(std::abs(matched_distances[0]) < 0.01);
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
// Add more entries
|
|
35
|
+
search_config_t search_config;
|
|
36
|
+
search_config.exact = true;
|
|
37
|
+
index.add(43, span_t{&vec_a[0], 3ul});
|
|
38
|
+
index.add(44, span_t{&vec_b[0], 3ul});
|
|
39
|
+
assert(index.size() == 3);
|
|
40
|
+
|
|
41
|
+
// Perform exact search
|
|
42
|
+
matched_count = index.search(span_t{&vec[0], 3ul}, 5, search_config).dump_to(matched_labels, matched_distances);
|
|
43
|
+
|
|
44
|
+
// Validate scans
|
|
45
|
+
std::size_t count = 0;
|
|
46
|
+
for (auto member : index) {
|
|
47
|
+
point_id_t id = member.label;
|
|
48
|
+
assert(id >= 42 && id <= 44);
|
|
49
|
+
count++;
|
|
50
|
+
}
|
|
51
|
+
assert((count == 3));
|
|
52
|
+
assert((index.stats(0).nodes == 3));
|
|
31
53
|
|
|
32
54
|
// Search again over reconstructed index
|
|
55
|
+
index.save("tmp.usearch");
|
|
33
56
|
index.load("tmp.usearch");
|
|
34
57
|
matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
|
|
35
|
-
assert(matched_count ==
|
|
58
|
+
assert(matched_count == 3);
|
|
36
59
|
assert(matched_labels[0] == 42);
|
|
37
60
|
assert(std::abs(matched_distances[0]) < 0.01);
|
|
38
61
|
|
|
39
62
|
// Search again over mapped index
|
|
40
63
|
index.view("tmp.usearch");
|
|
41
64
|
matched_count = index.search(span_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
|
|
42
|
-
assert(matched_count ==
|
|
65
|
+
assert(matched_count == 3);
|
|
43
66
|
assert(matched_labels[0] == 42);
|
|
44
67
|
assert(std::abs(matched_distances[0]) < 0.01);
|
|
45
68
|
}
|