usearch 0.13.0 → 0.14.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/release.yml +6 -3
- package/Cargo.toml +1 -1
- package/conanfile.py +1 -1
- package/docs/compilation.md +1 -0
- package/package.json +1 -1
- package/python/lib.cpp +68 -13
- package/python/scripts/bench.py +8 -3
- package/python/scripts/bench_params.py +56 -16
- package/python/scripts/test.py +24 -5
- package/python/usearch/index.py +22 -5
- package/python/usearch/synthetic.py +7 -8
- package/src/punned.hpp +19 -0
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
- run: npm install --save-dev @semantic-release/exec @semantic-release/git conventional-changelog-eslint semantic-release && npx semantic-release
|
|
29
29
|
|
|
30
30
|
rebase:
|
|
31
|
-
name: Rebase
|
|
31
|
+
name: Rebase Development Branch
|
|
32
32
|
needs: versioning
|
|
33
33
|
runs-on: ubuntu-latest
|
|
34
34
|
steps:
|
|
@@ -41,11 +41,14 @@ jobs:
|
|
|
41
41
|
run: |
|
|
42
42
|
git checkout main-dev
|
|
43
43
|
git rebase main
|
|
44
|
+
|
|
44
45
|
- name: Push changes
|
|
45
|
-
uses:
|
|
46
|
+
uses: CasperWA/push-protected@v2
|
|
46
47
|
with:
|
|
47
|
-
|
|
48
|
+
token: ${{ secrets.SEMANTIC_REBASE_TOKEN }}
|
|
48
49
|
branch: main-dev
|
|
50
|
+
unprotect_reviews: True
|
|
51
|
+
force: True
|
|
49
52
|
|
|
50
53
|
build_wheels:
|
|
51
54
|
name: Build Wheels for ${{ matrix.os }}
|
package/Cargo.toml
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 = '0.
|
|
7
|
+
version = '0.14.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/docs/compilation.md
CHANGED
|
@@ -94,6 +94,7 @@ Checking the effect of different embedding dimensions on construction speed:
|
|
|
94
94
|
```sh
|
|
95
95
|
python python/scripts/bench_params.py dimensions
|
|
96
96
|
python python/scripts/bench_params.py connectivity
|
|
97
|
+
python python/scripts/bench_params.py robustness
|
|
97
98
|
```
|
|
98
99
|
|
|
99
100
|
## JavaScript
|
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -347,8 +347,8 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
|
|
|
347
347
|
py::array_t<label_t> labels_py(static_cast<Py_ssize_t>(wanted));
|
|
348
348
|
py::array_t<distance_t> distances_py(static_cast<Py_ssize_t>(wanted));
|
|
349
349
|
std::size_t count{};
|
|
350
|
-
auto labels_py1d = labels_py.mutable_unchecked<1>();
|
|
351
|
-
auto distances_py1d = distances_py.mutable_unchecked<1>();
|
|
350
|
+
auto labels_py1d = labels_py.template mutable_unchecked<1>();
|
|
351
|
+
auto distances_py1d = distances_py.template mutable_unchecked<1>();
|
|
352
352
|
|
|
353
353
|
search_config_t config;
|
|
354
354
|
config.exact = exact;
|
|
@@ -416,9 +416,9 @@ static py::tuple search_many_in_index( //
|
|
|
416
416
|
py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
417
417
|
py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
418
418
|
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
419
|
-
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
420
|
-
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
421
|
-
auto counts_py1d = counts_py.mutable_unchecked<1>();
|
|
419
|
+
auto labels_py2d = labels_py.template mutable_unchecked<2>();
|
|
420
|
+
auto distances_py2d = distances_py.template mutable_unchecked<2>();
|
|
421
|
+
auto counts_py1d = counts_py.template mutable_unchecked<1>();
|
|
422
422
|
|
|
423
423
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
424
424
|
if (vectors_info.format == "c" || vectors_info.format == "b")
|
|
@@ -508,9 +508,9 @@ static py::tuple search_many_in_bits_index( //
|
|
|
508
508
|
py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
509
509
|
py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
|
|
510
510
|
py::array_t<Py_ssize_t> counts_py(vectors_count);
|
|
511
|
-
auto labels_py2d = labels_py.mutable_unchecked<2>();
|
|
512
|
-
auto distances_py2d = distances_py.mutable_unchecked<2>();
|
|
513
|
-
auto counts_py1d = counts_py.mutable_unchecked<1>();
|
|
511
|
+
auto labels_py2d = labels_py.template mutable_unchecked<2>();
|
|
512
|
+
auto distances_py2d = distances_py.template mutable_unchecked<2>();
|
|
513
|
+
auto counts_py1d = counts_py.template mutable_unchecked<1>();
|
|
514
514
|
|
|
515
515
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
516
516
|
if (vectors_info.format != "B")
|
|
@@ -548,6 +548,36 @@ template <typename index_at> std::size_t get_expansion_add(index_at const &index
|
|
|
548
548
|
template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
|
|
549
549
|
// clang-format on
|
|
550
550
|
|
|
551
|
+
template <typename scalar_at, typename index_at>
|
|
552
|
+
py::array_t<scalar_at> get_typed_member(index_at const& index, label_t label) {
|
|
553
|
+
py::array_t<scalar_at> result_py(static_cast<Py_ssize_t>(index.dimensions()));
|
|
554
|
+
auto result_py1d = result_py.template mutable_unchecked<1>();
|
|
555
|
+
index.reconstruct(label, &result_py1d(0));
|
|
556
|
+
return result_py;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
template <typename index_at>
|
|
560
|
+
py::array get_member(index_at const& index, label_t label, common_scalar_kind_t scalar_kind) {
|
|
561
|
+
if (scalar_kind == common_scalar_kind_t::f32_k)
|
|
562
|
+
return get_typed_member<f32_t>(index, label);
|
|
563
|
+
else if (scalar_kind == common_scalar_kind_t::f64_k)
|
|
564
|
+
return get_typed_member<f64_t>(index, label);
|
|
565
|
+
else if (scalar_kind == common_scalar_kind_t::f16_k)
|
|
566
|
+
return get_typed_member<f16_bits_t>(index, label);
|
|
567
|
+
else if (scalar_kind == common_scalar_kind_t::f8_k)
|
|
568
|
+
return get_typed_member<f8_bits_t>(index, label);
|
|
569
|
+
else
|
|
570
|
+
throw std::invalid_argument("Incompatible scalars in the query matrix!");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
template <typename index_at> py::array_t<label_t> get_labels(index_at const& index) {
|
|
574
|
+
std::size_t result_length = index.size();
|
|
575
|
+
py::array_t<label_t> result_py(static_cast<Py_ssize_t>(result_length));
|
|
576
|
+
auto result_py1d = result_py.template mutable_unchecked<1>();
|
|
577
|
+
index.export_labels(&result_py1d(0), result_length);
|
|
578
|
+
return result_py;
|
|
579
|
+
}
|
|
580
|
+
|
|
551
581
|
template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
|
|
552
582
|
if (begin == end)
|
|
553
583
|
return false;
|
|
@@ -611,7 +641,7 @@ py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, st
|
|
|
611
641
|
Py_ssize_t rows_stride = info.strides[1];
|
|
612
642
|
auto hashes_shape = py_shape_t{rows_count, words_per_row};
|
|
613
643
|
auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
|
|
614
|
-
auto hashes_proxy = hashes_py.mutable_unchecked<2>();
|
|
644
|
+
auto hashes_proxy = hashes_py.template mutable_unchecked<2>();
|
|
615
645
|
|
|
616
646
|
for (Py_ssize_t i = 0; i != rows_count; ++i)
|
|
617
647
|
hash_typed_row( //
|
|
@@ -622,7 +652,7 @@ py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, st
|
|
|
622
652
|
} else {
|
|
623
653
|
auto hashes_shape = py_shape_t{words_per_row};
|
|
624
654
|
auto hashes_py = py::array_t<bits_numpy_word_t>(hashes_shape);
|
|
625
|
-
auto hashes_proxy = hashes_py.mutable_unchecked<1>();
|
|
655
|
+
auto hashes_proxy = hashes_py.template mutable_unchecked<1>();
|
|
626
656
|
|
|
627
657
|
hash_typed_row( //
|
|
628
658
|
data, hashes_info.strides[0], static_cast<std::size_t>(elements_per_row), //
|
|
@@ -658,6 +688,18 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
658
688
|
m.attr("DEFAULT_EXPANSION_ADD") = py::int_(default_expansion_add());
|
|
659
689
|
m.attr("DEFAULT_EXPANSION_SEARCH") = py::int_(default_expansion_search());
|
|
660
690
|
|
|
691
|
+
#if defined(USEARCH_USE_OPENMP)
|
|
692
|
+
m.attr("USES_OPENMP") = py::int_(1);
|
|
693
|
+
#else
|
|
694
|
+
m.attr("USES_OPENMP") = py::int_(0);
|
|
695
|
+
#endif
|
|
696
|
+
|
|
697
|
+
#if defined(USEARCH_USE_SIMSIMD)
|
|
698
|
+
m.attr("USES_SIMSIMD") = py::int_(1);
|
|
699
|
+
#else
|
|
700
|
+
m.attr("USES_SIMSIMD") = py::int_(0);
|
|
701
|
+
#endif
|
|
702
|
+
|
|
661
703
|
py::enum_<common_metric_kind_t>(m, "MetricKind")
|
|
662
704
|
.value("Unknown", common_metric_kind_t::unknown_k)
|
|
663
705
|
.value("IP", common_metric_kind_t::ip_k)
|
|
@@ -670,6 +712,13 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
670
712
|
.value("BitwiseTanimoto", common_metric_kind_t::bitwise_tanimoto_k)
|
|
671
713
|
.value("BitwiseSorensen", common_metric_kind_t::bitwise_sorensen_k);
|
|
672
714
|
|
|
715
|
+
py::enum_<common_scalar_kind_t>(m, "ScalarKind")
|
|
716
|
+
.value("F64", common_scalar_kind_t::f64_k)
|
|
717
|
+
.value("F32", common_scalar_kind_t::f32_k)
|
|
718
|
+
.value("F16", common_scalar_kind_t::f16_k)
|
|
719
|
+
.value("F8", common_scalar_kind_t::f8_k)
|
|
720
|
+
.value("B1", common_scalar_kind_t::b1_k);
|
|
721
|
+
|
|
673
722
|
auto i = py::class_<punned_index_py_t>(m, "Index");
|
|
674
723
|
|
|
675
724
|
i.def(py::init(&make_index), //
|
|
@@ -717,13 +766,19 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
717
766
|
i.def_property_readonly("capacity", &punned_index_py_t::capacity);
|
|
718
767
|
i.def_property_readonly( //
|
|
719
768
|
"dtype", [](punned_index_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
|
|
720
|
-
i.def_property_readonly(
|
|
721
|
-
|
|
769
|
+
i.def_property_readonly( //
|
|
770
|
+
"memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
|
|
722
771
|
|
|
723
772
|
i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
|
|
724
773
|
i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
|
|
725
774
|
&punned_index_py_t::change_expansion_search);
|
|
726
775
|
|
|
776
|
+
i.def_property_readonly("labels", &get_labels<punned_index_py_t>);
|
|
777
|
+
i.def("__contains__", &punned_index_py_t::contains);
|
|
778
|
+
i.def( //
|
|
779
|
+
"__getitem__", &get_member<punned_index_py_t>, py::arg("label"),
|
|
780
|
+
py::arg("dtype") = common_scalar_kind_t::f32_k);
|
|
781
|
+
|
|
727
782
|
i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
|
|
728
783
|
i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
|
|
729
784
|
i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
|
|
@@ -805,7 +860,7 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
805
860
|
auto proxy = set.unchecked<1>();
|
|
806
861
|
auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
807
862
|
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
808
|
-
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
863
|
+
auto labels_proxy = labels_py.template mutable_unchecked<1>();
|
|
809
864
|
auto result = index.search(view, count);
|
|
810
865
|
result.error.raise();
|
|
811
866
|
auto found = result.dump_to(&labels_proxy(0));
|
package/python/scripts/bench.py
CHANGED
|
@@ -7,6 +7,11 @@ import fire
|
|
|
7
7
|
|
|
8
8
|
from usearch.index import Index
|
|
9
9
|
from usearch.io import load_matrix
|
|
10
|
+
from usearch.index import (
|
|
11
|
+
DEFAULT_CONNECTIVITY,
|
|
12
|
+
DEFAULT_EXPANSION_ADD,
|
|
13
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
def measure(f) -> Tuple[float, Any]:
|
|
@@ -62,9 +67,9 @@ def main(
|
|
|
62
67
|
vectors: str,
|
|
63
68
|
queries: str,
|
|
64
69
|
neighbors: str,
|
|
65
|
-
connectivity: int =
|
|
66
|
-
expansion_add: int =
|
|
67
|
-
expansion_search: int =
|
|
70
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
71
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
72
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
68
73
|
k: Optional[int] = None,
|
|
69
74
|
):
|
|
70
75
|
|
|
@@ -6,8 +6,15 @@ import plotly.express as px
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
from usearch.index import Index
|
|
10
|
+
from usearch.index import (
|
|
11
|
+
DEFAULT_CONNECTIVITY,
|
|
12
|
+
DEFAULT_EXPANSION_ADD,
|
|
13
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
14
|
+
|
|
15
|
+
USES_OPENMP,
|
|
16
|
+
USES_SIMSIMD,
|
|
17
|
+
)
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
def measure(f) -> float:
|
|
@@ -30,14 +37,23 @@ def bench_usearch(index, vectors: np.ndarray) -> float:
|
|
|
30
37
|
return insertions_per_second
|
|
31
38
|
|
|
32
39
|
|
|
40
|
+
def recall_at_one(index: Index, vectors: np.ndarray) -> float:
|
|
41
|
+
|
|
42
|
+
labels = np.arange(vectors.shape[0])
|
|
43
|
+
index.add(labels=labels, vectors=vectors)
|
|
44
|
+
matches = index.search(vectors, 1)
|
|
45
|
+
|
|
46
|
+
return np.sum(matches.labels.flatten() == labels) / len(labels)
|
|
47
|
+
|
|
48
|
+
|
|
33
49
|
class Main:
|
|
34
50
|
|
|
35
51
|
def dimensions(
|
|
36
52
|
self,
|
|
37
53
|
count: int = 1_000_000,
|
|
38
|
-
connectivity: int =
|
|
39
|
-
expansion_add: int =
|
|
40
|
-
expansion_search: int =
|
|
54
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
55
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
56
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
41
57
|
):
|
|
42
58
|
"""Measures indexing speed for different dimensionality vectors.
|
|
43
59
|
|
|
@@ -79,8 +95,8 @@ class Main:
|
|
|
79
95
|
self,
|
|
80
96
|
count: int = 1_000_000,
|
|
81
97
|
ndim: int = 256,
|
|
82
|
-
expansion_add: int =
|
|
83
|
-
expansion_search: int =
|
|
98
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
99
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
84
100
|
):
|
|
85
101
|
"""Measures indexing speed and accuracy for different level of
|
|
86
102
|
connectivity in the levels of the hierarchical proximity graph.
|
|
@@ -121,14 +137,14 @@ class Main:
|
|
|
121
137
|
|
|
122
138
|
def robustness(
|
|
123
139
|
self,
|
|
124
|
-
experiments: int =
|
|
125
|
-
count: int =
|
|
140
|
+
experiments: int = 1,
|
|
141
|
+
count: int = 10000,
|
|
126
142
|
ndim: int = 256,
|
|
127
|
-
connectivity: int =
|
|
128
|
-
expansion_add: int =
|
|
129
|
-
expansion_search: int =
|
|
143
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
144
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
145
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
130
146
|
):
|
|
131
|
-
"""How much does accuracy fluctuate depending on the indexing order.
|
|
147
|
+
"""How much does accuracy and speed fluctuate depending on the indexing order.
|
|
132
148
|
|
|
133
149
|
:param experiments: How many times to repeat, defaults to 10
|
|
134
150
|
:type experiments: int, optional
|
|
@@ -138,11 +154,19 @@ class Main:
|
|
|
138
154
|
:type ndim: int, optional
|
|
139
155
|
"""
|
|
140
156
|
|
|
157
|
+
print(f'Connectivity: {connectivity}')
|
|
158
|
+
print(f'Number of vectors: {count}')
|
|
159
|
+
print(f'Vector dimensions: {ndim}')
|
|
160
|
+
print(f'Expansion on Add: {expansion_add}')
|
|
161
|
+
print(f'Expansion on Search: {expansion_search}')
|
|
162
|
+
print(f'Uses OpenMP: {USES_OPENMP}')
|
|
163
|
+
print(f'Uses SimSIMD: {USES_SIMSIMD}')
|
|
164
|
+
|
|
141
165
|
recall_levels = []
|
|
142
166
|
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
143
167
|
|
|
144
168
|
for _ in range(experiments):
|
|
145
|
-
|
|
169
|
+
np.random.shuffle(vectors_mat)
|
|
146
170
|
index = Index(
|
|
147
171
|
ndim=ndim,
|
|
148
172
|
expansion_add=expansion_add,
|
|
@@ -150,12 +174,28 @@ class Main:
|
|
|
150
174
|
connectivity=connectivity,
|
|
151
175
|
)
|
|
152
176
|
bench_usearch(index, vectors_mat)
|
|
177
|
+
recall = recall_at_one(index, vectors_mat) * 100
|
|
178
|
+
print(f'- Recall @ 1 {recall:.2f} %')
|
|
179
|
+
recall_levels.append(recall)
|
|
153
180
|
del index
|
|
154
181
|
|
|
155
|
-
min_ = min(recall_levels)
|
|
156
|
-
max_ = max(recall_levels)
|
|
157
|
-
print(f'Recall@1 varies between {min_:.3f} and {max_:.3f}')
|
|
182
|
+
min_ = min(recall_levels) * 100
|
|
183
|
+
max_ = max(recall_levels) * 100
|
|
184
|
+
print(f'Recall @ 1 varies between {min_:.3f} and {max_:.3f}')
|
|
185
|
+
|
|
186
|
+
# Now let's try clustering and inserting in clusters
|
|
187
|
+
try:
|
|
188
|
+
from sklearn.cluster import KMeans
|
|
189
|
+
except ImportError:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
count_clusters = count / expansion_add
|
|
193
|
+
clustering = KMeans(
|
|
194
|
+
n_clusters=count_clusters,
|
|
195
|
+
random_state=0, n_init='auto').fit(vectors_mat)
|
|
196
|
+
NamedTuple()
|
|
158
197
|
|
|
159
198
|
|
|
160
199
|
if __name__ == '__main__':
|
|
200
|
+
Main().robustness()
|
|
161
201
|
fire.Fire(Main)
|
package/python/scripts/test.py
CHANGED
|
@@ -3,14 +3,21 @@ import pytest
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
5
5
|
from usearch.io import load_matrix, save_matrix
|
|
6
|
-
from usearch.
|
|
6
|
+
from usearch.synthetic import recall_members
|
|
7
|
+
|
|
8
|
+
from usearch.index import Index, SetsIndex, MetricKind, Matches
|
|
9
|
+
from usearch.index import (
|
|
10
|
+
DEFAULT_CONNECTIVITY,
|
|
11
|
+
DEFAULT_EXPANSION_ADD,
|
|
12
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
13
|
+
)
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
dimensions = [13, 99, 100, 256]
|
|
10
17
|
batch_sizes = [1, 2, 100]
|
|
11
18
|
index_types = ['f32', 'f64', 'f16', 'f8']
|
|
12
19
|
numpy_types = [np.float32, np.float64, np.float16, np.byte]
|
|
13
|
-
connectivity_options = [3, 13, 50]
|
|
20
|
+
connectivity_options = [3, 13, 50, DEFAULT_CONNECTIVITY]
|
|
14
21
|
jit_options = [False]
|
|
15
22
|
continuous_metrics = [
|
|
16
23
|
MetricKind.Cos,
|
|
@@ -26,6 +33,7 @@ hash_metrics = [
|
|
|
26
33
|
@pytest.mark.parametrize('rows', batch_sizes)
|
|
27
34
|
@pytest.mark.parametrize('cols', dimensions)
|
|
28
35
|
def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
36
|
+
|
|
29
37
|
original = np.random.rand(rows, cols).astype(np.float32)
|
|
30
38
|
save_matrix(original, 'tmp.fbin')
|
|
31
39
|
reconstructed = load_matrix('tmp.fbin')
|
|
@@ -36,6 +44,7 @@ def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
|
36
44
|
@pytest.mark.parametrize('rows', batch_sizes)
|
|
37
45
|
@pytest.mark.parametrize('cols', dimensions)
|
|
38
46
|
def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
47
|
+
|
|
39
48
|
original = np.random.randint(0, rows+1, size=(rows, cols)).astype(np.int32)
|
|
40
49
|
save_matrix(original, 'tmp.ibin')
|
|
41
50
|
reconstructed = load_matrix('tmp.ibin')
|
|
@@ -59,6 +68,8 @@ def test_index(
|
|
|
59
68
|
ndim=ndim,
|
|
60
69
|
dtype=index_type,
|
|
61
70
|
connectivity=connectivity,
|
|
71
|
+
expansion_add=DEFAULT_EXPANSION_ADD,
|
|
72
|
+
expansion_search=DEFAULT_EXPANSION_SEARCH,
|
|
62
73
|
jit=jit,
|
|
63
74
|
)
|
|
64
75
|
assert index.ndim == ndim
|
|
@@ -76,6 +87,11 @@ def test_index(
|
|
|
76
87
|
assert matches[0] == 42
|
|
77
88
|
assert distances[0] == pytest.approx(0, abs=1e-3)
|
|
78
89
|
|
|
90
|
+
assert 42 in index
|
|
91
|
+
assert 42 in index.labels
|
|
92
|
+
if numpy_type != np.byte:
|
|
93
|
+
assert np.allclose(index[42], vector, atol=0.1)
|
|
94
|
+
|
|
79
95
|
index.save('tmp.usearch')
|
|
80
96
|
index.clear()
|
|
81
97
|
assert len(index) == 0
|
|
@@ -101,10 +117,13 @@ def test_index_batch(
|
|
|
101
117
|
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
102
118
|
|
|
103
119
|
index.add(labels, vectors)
|
|
104
|
-
matches
|
|
120
|
+
matches: Matches = index.search(vectors, 10)
|
|
105
121
|
|
|
106
|
-
assert matches.shape[0] == distances.shape[0]
|
|
107
|
-
assert
|
|
122
|
+
assert matches.labels.shape[0] == matches.distances.shape[0]
|
|
123
|
+
assert matches.counts.shape[0] == batch_size
|
|
124
|
+
assert np.all(np.sort(index.labels) == np.sort(labels))
|
|
125
|
+
|
|
126
|
+
assert recall_members(index, exact=True) == 1
|
|
108
127
|
|
|
109
128
|
|
|
110
129
|
@pytest.mark.parametrize('ndim', dimensions)
|
package/python/usearch/index.py
CHANGED
|
@@ -17,6 +17,9 @@ from usearch.compiled import (
|
|
|
17
17
|
DEFAULT_CONNECTIVITY,
|
|
18
18
|
DEFAULT_EXPANSION_ADD,
|
|
19
19
|
DEFAULT_EXPANSION_SEARCH,
|
|
20
|
+
|
|
21
|
+
USES_OPENMP,
|
|
22
|
+
USES_SIMSIMD,
|
|
20
23
|
)
|
|
21
24
|
|
|
22
25
|
BitwiseMetricKind = (
|
|
@@ -65,6 +68,10 @@ class Matches(NamedTuple):
|
|
|
65
68
|
for label, distance in zip(labels, distances)
|
|
66
69
|
]
|
|
67
70
|
|
|
71
|
+
def recall_first(self, expected: np.ndarray) -> float:
|
|
72
|
+
best_matches = self.labels if self.is_batch else self.labels[:, 0]
|
|
73
|
+
return np.sum(best_matches == expected) / len(expected)
|
|
74
|
+
|
|
68
75
|
def __repr__(self) -> str:
|
|
69
76
|
return f'usearch.Matches({self.total_matches})' if self.is_batch else \
|
|
70
77
|
f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
|
|
@@ -344,7 +351,7 @@ class Index:
|
|
|
344
351
|
return Matches(*tuple_)
|
|
345
352
|
|
|
346
353
|
def __len__(self) -> int:
|
|
347
|
-
return
|
|
354
|
+
return self._compiled.__len__()
|
|
348
355
|
|
|
349
356
|
@property
|
|
350
357
|
def jit(self) -> bool:
|
|
@@ -387,11 +394,11 @@ class Index:
|
|
|
387
394
|
return self._compiled.expansion_search
|
|
388
395
|
|
|
389
396
|
@expansion_add.setter
|
|
390
|
-
def
|
|
397
|
+
def expansion_add(self, v: int):
|
|
391
398
|
self._compiled.expansion_add = v
|
|
392
399
|
|
|
393
400
|
@expansion_search.setter
|
|
394
|
-
def
|
|
401
|
+
def expansion_search(self, v: int):
|
|
395
402
|
self._compiled.expansion_search = v
|
|
396
403
|
|
|
397
404
|
def save(self, path: os.PathLike):
|
|
@@ -406,5 +413,15 @@ class Index:
|
|
|
406
413
|
def clear(self):
|
|
407
414
|
self._compiled.clear()
|
|
408
415
|
|
|
409
|
-
|
|
410
|
-
|
|
416
|
+
@property
|
|
417
|
+
def labels(self) -> np.ndarray:
|
|
418
|
+
return self._compiled.labels
|
|
419
|
+
|
|
420
|
+
def __delitem__(self, label: int):
|
|
421
|
+
raise NotImplementedError()
|
|
422
|
+
|
|
423
|
+
def __contains__(self, label: int) -> bool:
|
|
424
|
+
return self._compiled.__contains__(label)
|
|
425
|
+
|
|
426
|
+
def __getitem__(self, label: int) -> np.ndarray:
|
|
427
|
+
return self._compiled.__getitem__(label)
|
|
@@ -22,11 +22,10 @@ def vectors(index: Index, count: int) -> np.ndarray:
|
|
|
22
22
|
return x
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
matches: Matches =
|
|
31
|
-
|
|
32
|
-
return np.sum(matches.labels.flatten() == labels) / len(labels)
|
|
25
|
+
def recall_members(index: Index, *args, **kwargs) -> float:
|
|
26
|
+
vectors: np.ndarray = np.vstack([
|
|
27
|
+
index[i] for i in range(len(index))
|
|
28
|
+
])
|
|
29
|
+
labels: np.ndarray = np.arange(vectors.shape[0])
|
|
30
|
+
matches: Matches = index.search(vectors, 1, *args, **kwargs)
|
|
31
|
+
return matches.recall_first(labels)
|
package/src/punned.hpp
CHANGED
|
@@ -33,6 +33,14 @@
|
|
|
33
33
|
namespace unum {
|
|
34
34
|
namespace usearch {
|
|
35
35
|
|
|
36
|
+
enum class common_scalar_kind_t {
|
|
37
|
+
f64_k,
|
|
38
|
+
f32_k,
|
|
39
|
+
f16_k,
|
|
40
|
+
f8_k,
|
|
41
|
+
b1_k,
|
|
42
|
+
};
|
|
43
|
+
|
|
36
44
|
using byte_t = char;
|
|
37
45
|
using punned_distance_t = float;
|
|
38
46
|
using punned_metric_t = punned_distance_t (*)(byte_t const*, byte_t const*, std::size_t, std::size_t);
|
|
@@ -527,6 +535,17 @@ class punned_gt {
|
|
|
527
535
|
return typed_->memory_usage(allocator_entry_bytes);
|
|
528
536
|
}
|
|
529
537
|
|
|
538
|
+
bool contains(label_t label) const {
|
|
539
|
+
shared_lock_t lock(lookup_table_mutex_);
|
|
540
|
+
return lookup_table_.contains(label);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
void export_labels(label_t* labels, std::size_t limit) const {
|
|
544
|
+
shared_lock_t lock(lookup_table_mutex_);
|
|
545
|
+
for (auto it = lookup_table_.begin(); it != lookup_table_.end() && limit; ++it, ++labels, --limit)
|
|
546
|
+
*labels = it->first;
|
|
547
|
+
}
|
|
548
|
+
|
|
530
549
|
// clang-format off
|
|
531
550
|
add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
|
|
532
551
|
add_result_t add(label_t label, f16_bits_t const* vector) { return add_(label, vector, casts_.from_f16); }
|