usearch 0.9.3 → 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 +68 -72
- package/package.json +1 -1
- package/pyproject.toml +1 -0
- 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,72 +409,66 @@ 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 =
|
|
413
|
-
static_assert(sizeof(slot_t) == 8);
|
|
412
|
+
using slot_t = LONGLONG;
|
|
414
413
|
#else
|
|
415
|
-
using slot_t =
|
|
414
|
+
using slot_t = unsigned long;
|
|
416
415
|
#endif
|
|
417
416
|
|
|
418
|
-
std::
|
|
419
|
-
|
|
417
|
+
static constexpr std::size_t bits_per_slot() { return sizeof(slot_t) * CHAR_BIT; }
|
|
418
|
+
static constexpr slot_t bits_mask() { return sizeof(slot_t) * CHAR_BIT - 1; }
|
|
419
|
+
|
|
420
|
+
slot_t* slots_{};
|
|
421
|
+
std::size_t slots_count_{};
|
|
420
422
|
|
|
421
423
|
public:
|
|
422
424
|
visits_bitset_gt() noexcept {}
|
|
423
|
-
visits_bitset_gt(std::size_t capacity) noexcept {
|
|
424
|
-
|
|
425
|
-
|
|
425
|
+
explicit visits_bitset_gt(std::size_t capacity) noexcept {
|
|
426
|
+
slots_count_ = divide_round_up<bits_per_slot()>(capacity);
|
|
427
|
+
slots_ = (slot_t*)allocator_t{}.allocate(slots_count_ * sizeof(slot_t));
|
|
426
428
|
clear();
|
|
427
429
|
}
|
|
428
|
-
~visits_bitset_gt() noexcept {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
~visits_bitset_gt() noexcept {
|
|
431
|
+
allocator_t{}.deallocate((byte_t*)slots_, slots_count_ * sizeof(slot_t)), slots_ = nullptr;
|
|
432
|
+
}
|
|
433
|
+
void clear() noexcept { std::memset(slots_, 0, slots_count_ * sizeof(slot_t)); }
|
|
434
|
+
inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
|
|
435
|
+
inline void set(std::size_t i) noexcept { slots_[i / bits_per_slot()] |= (1ul << (i & bits_mask())); }
|
|
432
436
|
|
|
433
437
|
visits_bitset_gt(visits_bitset_gt&& other) noexcept {
|
|
434
|
-
u64s_ = other.u64s_;
|
|
435
438
|
slots_ = other.slots_;
|
|
436
|
-
|
|
437
|
-
other.slots_ =
|
|
439
|
+
slots_count_ = other.slots_count_;
|
|
440
|
+
other.slots_ = nullptr;
|
|
441
|
+
other.slots_count_ = 0;
|
|
438
442
|
}
|
|
439
443
|
|
|
440
444
|
visits_bitset_gt& operator=(visits_bitset_gt&& other) noexcept {
|
|
441
|
-
std::swap(u64s_, other.u64s_);
|
|
442
445
|
std::swap(slots_, other.slots_);
|
|
446
|
+
std::swap(slots_count_, other.slots_count_);
|
|
443
447
|
return *this;
|
|
444
448
|
}
|
|
445
449
|
|
|
446
450
|
#if defined(USEARCH_IS_WINDOWS)
|
|
447
451
|
|
|
448
|
-
inline bool atomic_test(std::size_t i) noexcept {
|
|
449
|
-
slot_t mask{1ul << (i & 63ul)};
|
|
450
|
-
return InterlockedOr(&u64s_[i / 64], 0) & mask;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
452
|
inline bool atomic_set(std::size_t i) noexcept {
|
|
454
|
-
slot_t mask{1ul << (i &
|
|
455
|
-
return
|
|
453
|
+
slot_t mask{1ul << (i & bits_mask())};
|
|
454
|
+
return InterlockedOr64Acquire((slot_t volatile*)&slots_[i / bits_per_slot()], mask) & mask;
|
|
456
455
|
}
|
|
457
456
|
|
|
458
457
|
inline void atomic_reset(std::size_t i) noexcept {
|
|
459
|
-
slot_t mask{1ul << (i &
|
|
460
|
-
|
|
458
|
+
slot_t mask{1ul << (i & bits_mask())};
|
|
459
|
+
InterlockedAnd64Release((slot_t volatile*)&slots_[i / bits_per_slot()], ~mask);
|
|
461
460
|
}
|
|
462
461
|
|
|
463
462
|
#else
|
|
464
463
|
|
|
465
|
-
inline bool atomic_test(std::size_t i) noexcept {
|
|
466
|
-
slot_t mask{1ul << (i & 63ul)};
|
|
467
|
-
return __atomic_load_n(&u64s_[i / 64], __ATOMIC_RELAXED) & mask;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
464
|
inline bool atomic_set(std::size_t i) noexcept {
|
|
471
|
-
slot_t mask{1ul << (i &
|
|
472
|
-
return __atomic_fetch_or(&
|
|
465
|
+
slot_t mask{1ul << (i & bits_mask())};
|
|
466
|
+
return __atomic_fetch_or(&slots_[i / bits_per_slot()], mask, __ATOMIC_ACQUIRE) & mask;
|
|
473
467
|
}
|
|
474
468
|
|
|
475
469
|
inline void atomic_reset(std::size_t i) noexcept {
|
|
476
|
-
slot_t mask{1ul << (i &
|
|
477
|
-
__atomic_fetch_and(&
|
|
470
|
+
slot_t mask{1ul << (i & bits_mask())};
|
|
471
|
+
__atomic_fetch_and(&slots_[i / bits_per_slot()], ~mask, __ATOMIC_RELEASE);
|
|
478
472
|
}
|
|
479
473
|
|
|
480
474
|
#endif
|
|
@@ -777,7 +771,7 @@ template <typename scalar_at> class span_gt {
|
|
|
777
771
|
std::size_t size_;
|
|
778
772
|
|
|
779
773
|
public:
|
|
780
|
-
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) {}
|
|
781
775
|
span_gt(scalar_at* begin, std::size_t size) noexcept : data_(begin), size_(size) {}
|
|
782
776
|
scalar_at* data() const noexcept { return data_; }
|
|
783
777
|
std::size_t size() const noexcept { return size_; }
|
|
@@ -924,11 +918,12 @@ class index_gt {
|
|
|
924
918
|
|
|
925
919
|
template <typename ref_at> class member_iterator_gt {
|
|
926
920
|
using ref_t = ref_at;
|
|
927
|
-
index_gt
|
|
928
|
-
std::size_t offset_;
|
|
921
|
+
index_gt* index_{};
|
|
922
|
+
std::size_t offset_{};
|
|
929
923
|
|
|
924
|
+
friend class index_gt;
|
|
930
925
|
member_iterator_gt() noexcept {}
|
|
931
|
-
member_iterator_gt(index_gt
|
|
926
|
+
member_iterator_gt(index_gt* index, std::size_t offset) noexcept : index_(index), offset_(offset) {}
|
|
932
927
|
|
|
933
928
|
public:
|
|
934
929
|
using iterator_category = std::random_access_iterator_tag;
|
|
@@ -937,7 +932,7 @@ class index_gt {
|
|
|
937
932
|
using pointer = void;
|
|
938
933
|
using reference = ref_t;
|
|
939
934
|
|
|
940
|
-
reference operator*() const noexcept { return index_
|
|
935
|
+
reference operator*() const noexcept { return index_->node_with_id_(offset_).ref(); }
|
|
941
936
|
member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
|
|
942
937
|
member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
|
|
943
938
|
member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
|
|
@@ -959,8 +954,11 @@ class index_gt {
|
|
|
959
954
|
offset_ -= d;
|
|
960
955
|
return *this;
|
|
961
956
|
}
|
|
962
|
-
bool operator
|
|
963
|
-
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_;
|
|
964
962
|
}
|
|
965
963
|
};
|
|
966
964
|
|
|
@@ -1052,7 +1050,7 @@ class index_gt {
|
|
|
1052
1050
|
|
|
1053
1051
|
operator vector_view_t() const noexcept { return {vector(), dim()}; }
|
|
1054
1052
|
vector_view_t vector_view() const noexcept { return {vector(), dim()}; }
|
|
1055
|
-
member_ref_t ref() noexcept { return {
|
|
1053
|
+
member_ref_t ref() noexcept { return {{tape_}, vector_view()}; }
|
|
1056
1054
|
member_cref_t cref() const noexcept { return {label(), vector_view()}; }
|
|
1057
1055
|
};
|
|
1058
1056
|
|
|
@@ -1133,19 +1131,10 @@ class index_gt {
|
|
|
1133
1131
|
config_t const& config() const { return config_; }
|
|
1134
1132
|
bool is_immutable() const noexcept { return viewed_file_descriptor_ != 0; }
|
|
1135
1133
|
|
|
1136
|
-
index_gt(config_t config = {}, metric_t metric = {}, allocator_t allocator = {}) noexcept(false)
|
|
1137
|
-
: config_(normalize(config)), metric_(metric), allocator_(allocator)
|
|
1138
|
-
|
|
1139
|
-
// Externally defined hyper-parameters:
|
|
1140
|
-
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) {
|
|
1141
1137
|
|
|
1142
|
-
// Configure initial empty state:
|
|
1143
|
-
size_ = 0u;
|
|
1144
|
-
max_level_ = -1;
|
|
1145
|
-
entry_id_ = 0u;
|
|
1146
|
-
viewed_file_descriptor_ = 0;
|
|
1147
|
-
|
|
1148
|
-
// Dynamic memory:
|
|
1149
1138
|
thread_contexts_.resize(config.max_threads());
|
|
1150
1139
|
for (thread_context_t& context : thread_contexts_)
|
|
1151
1140
|
context.metric = metric;
|
|
@@ -1155,7 +1144,7 @@ class index_gt {
|
|
|
1155
1144
|
/**
|
|
1156
1145
|
* @brief Clones the structure with the same hyper-parameters, but without contents.
|
|
1157
1146
|
*/
|
|
1158
|
-
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
1147
|
+
index_gt fork() noexcept(false) { return index_gt{config_, metric_, allocator_}; }
|
|
1159
1148
|
|
|
1160
1149
|
~index_gt() noexcept { clear(); }
|
|
1161
1150
|
|
|
@@ -1166,6 +1155,13 @@ class index_gt {
|
|
|
1166
1155
|
return *this;
|
|
1167
1156
|
}
|
|
1168
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
|
+
|
|
1169
1165
|
#pragma region Adjusting Configuration
|
|
1170
1166
|
|
|
1171
1167
|
/**
|
|
@@ -1196,12 +1192,12 @@ class index_gt {
|
|
|
1196
1192
|
std::swap(thread_contexts_, other.thread_contexts_);
|
|
1197
1193
|
|
|
1198
1194
|
// Non-atomic parts.
|
|
1199
|
-
std::size_t
|
|
1200
|
-
std::size_t
|
|
1195
|
+
std::size_t capacity_copy = capacity_;
|
|
1196
|
+
std::size_t size_copy = size_;
|
|
1201
1197
|
capacity_ = other.capacity_.load();
|
|
1202
1198
|
size_ = other.size_.load();
|
|
1203
|
-
other.capacity_ =
|
|
1204
|
-
other.size_ =
|
|
1199
|
+
other.capacity_ = capacity_copy;
|
|
1200
|
+
other.size_ = size_copy;
|
|
1205
1201
|
}
|
|
1206
1202
|
|
|
1207
1203
|
/**
|
|
@@ -1226,7 +1222,7 @@ class index_gt {
|
|
|
1226
1222
|
precomputed_constants_t pre = precompute_(config);
|
|
1227
1223
|
std::size_t bytes_per_node_base = node_head_bytes_() + pre.neighbors_base_bytes;
|
|
1228
1224
|
std::size_t rounded_size = divide_round_up<64>(bytes_per_node_base) * 64;
|
|
1229
|
-
std::size_t added_connections = (rounded_size -
|
|
1225
|
+
std::size_t added_connections = (rounded_size - bytes_per_node_base) / sizeof(id_t);
|
|
1230
1226
|
config.connectivity = config.connectivity + added_connections / base_level_multiple_();
|
|
1231
1227
|
return config;
|
|
1232
1228
|
}
|
|
@@ -1305,11 +1301,11 @@ class index_gt {
|
|
|
1305
1301
|
|
|
1306
1302
|
// Determining how much memory to allocate depends on the target level
|
|
1307
1303
|
std::unique_lock<std::mutex> new_level_lock(global_mutex_);
|
|
1308
|
-
level_t
|
|
1309
|
-
id_t
|
|
1304
|
+
level_t max_level_copy = max_level_; // Copy under lock
|
|
1305
|
+
id_t entry_id_copy = entry_id_; // Copy under lock
|
|
1310
1306
|
thread_context_t& context = thread_contexts_[config.thread];
|
|
1311
1307
|
level_t target_level = choose_random_level_(context.level_generator);
|
|
1312
|
-
if (target_level <=
|
|
1308
|
+
if (target_level <= max_level_copy)
|
|
1313
1309
|
new_level_lock.unlock();
|
|
1314
1310
|
|
|
1315
1311
|
// Allocate the neighbors
|
|
@@ -1329,10 +1325,10 @@ class index_gt {
|
|
|
1329
1325
|
result.cycles = context.iteration_cycles;
|
|
1330
1326
|
|
|
1331
1327
|
// Go down the level, tracking only the closest match
|
|
1332
|
-
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);
|
|
1333
1329
|
|
|
1334
1330
|
// From `target_level` down perform proper extensive search
|
|
1335
|
-
for (level_t level = (std::min)(target_level,
|
|
1331
|
+
for (level_t level = (std::min)(target_level, max_level_copy); level >= 0; --level) {
|
|
1336
1332
|
search_to_insert_(closest_id, vector, level, context);
|
|
1337
1333
|
closest_id = connect_new_node_(new_id, level, context);
|
|
1338
1334
|
}
|
|
@@ -1342,7 +1338,7 @@ class index_gt {
|
|
|
1342
1338
|
result.cycles = context.iteration_cycles - result.cycles;
|
|
1343
1339
|
|
|
1344
1340
|
// Updating the entry point if needed
|
|
1345
|
-
if (target_level >
|
|
1341
|
+
if (target_level > max_level_copy) {
|
|
1346
1342
|
entry_id_ = new_id;
|
|
1347
1343
|
max_level_ = target_level;
|
|
1348
1344
|
}
|
|
@@ -1419,12 +1415,12 @@ class index_gt {
|
|
|
1419
1415
|
struct stats_t {
|
|
1420
1416
|
std::size_t nodes;
|
|
1421
1417
|
std::size_t edges;
|
|
1422
|
-
std::size_t
|
|
1418
|
+
std::size_t max_edges;
|
|
1423
1419
|
};
|
|
1424
1420
|
|
|
1425
1421
|
stats_t stats() const noexcept {
|
|
1426
1422
|
stats_t result{};
|
|
1427
|
-
result.nodes =
|
|
1423
|
+
result.nodes = size();
|
|
1428
1424
|
for (std::size_t i = 0; i != result.nodes; ++i) {
|
|
1429
1425
|
node_t node = node_with_id_(i);
|
|
1430
1426
|
std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
|
|
@@ -1440,7 +1436,7 @@ class index_gt {
|
|
|
1440
1436
|
|
|
1441
1437
|
stats_t stats(std::size_t level) const noexcept {
|
|
1442
1438
|
stats_t result{};
|
|
1443
|
-
result.nodes =
|
|
1439
|
+
result.nodes = size();
|
|
1444
1440
|
for (std::size_t i = 0; i != result.nodes; ++i) {
|
|
1445
1441
|
node_t node = node_with_id_(i);
|
|
1446
1442
|
if (node.level() >= level)
|
|
@@ -1792,7 +1788,7 @@ class index_gt {
|
|
|
1792
1788
|
thread_context_t& context) const noexcept {
|
|
1793
1789
|
|
|
1794
1790
|
distance_t closest_dist = context.measure(query, node_with_id_(closest_id));
|
|
1795
|
-
for (level_t level = begin_level; level > end_level; level
|
|
1791
|
+
for (level_t level = begin_level; level > end_level; --level) {
|
|
1796
1792
|
bool changed;
|
|
1797
1793
|
do {
|
|
1798
1794
|
changed = false;
|
|
@@ -1919,7 +1915,7 @@ class index_gt {
|
|
|
1919
1915
|
top_candidates_t& top = context.top_candidates;
|
|
1920
1916
|
top.clear();
|
|
1921
1917
|
top.reserve(count);
|
|
1922
|
-
for (std::size_t i = 0; i !=
|
|
1918
|
+
for (std::size_t i = 0; i != size(); ++i)
|
|
1923
1919
|
top.emplace_reserved(context.measure(query, node_with_id_(i)), static_cast<id_t>(i));
|
|
1924
1920
|
}
|
|
1925
1921
|
|
package/package.json
CHANGED
package/pyproject.toml
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
|
}
|