usearch 0.12.2 → 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 +7 -4
- package/Cargo.toml +1 -1
- package/README.md +52 -14
- package/conanfile.py +1 -1
- package/docs/compilation.md +8 -0
- package/package.json +1 -1
- package/python/lib.cpp +68 -12
- package/python/scripts/bench.py +23 -8
- package/python/scripts/bench_params.py +98 -9
- package/python/scripts/test.py +24 -5
- package/python/usearch/index.py +63 -17
- package/python/usearch/io.py +30 -21
- package/python/usearch/synthetic.py +31 -0
- 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:
|
|
@@ -37,15 +37,18 @@ jobs:
|
|
|
37
37
|
with:
|
|
38
38
|
fetch-depth: 0
|
|
39
39
|
|
|
40
|
-
- name: Perform
|
|
40
|
+
- name: Perform rebase
|
|
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/README.md
CHANGED
|
@@ -30,22 +30,25 @@ Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Met
|
|
|
30
30
|
<a href="#golang">GoLang</a> •
|
|
31
31
|
<a href="#wolfram">Wolfram</a>
|
|
32
32
|
<br/>
|
|
33
|
-
Linux • MacOS • Windows
|
|
33
|
+
Linux • MacOS • Windows • Docker • WebAssembly 🔜
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
36
|
---
|
|
37
37
|
|
|
38
38
|
- [x] Industry-leading [performance](#performance).
|
|
39
39
|
- [x] Easily-extendible [single C++11 header][usearch-header] implementation.
|
|
40
|
-
- [x] [User-defined](#define-custom-metrics)
|
|
40
|
+
- [x] SIMD-accelerated and [User-defined](#define-custom-metrics) metrics with JIT-compilation.
|
|
41
|
+
- [x] Variable dimensionality vectors - for [obscure use-cases, like GIS and Chess][obscure-use-cases].
|
|
42
|
+
- [x] Bitwise Tanimoto and Sorensen coefficients for Genomics and Chemistry.
|
|
41
43
|
- [x] [Half-precision `f16` and Quarter-precision `f8`](#quantize-on-the-fly) support on any hardware.
|
|
42
44
|
- [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
|
|
43
45
|
- [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
|
|
44
|
-
- [x]
|
|
45
|
-
- [x] [Bring your threads](#bring-your-threads), like OpenMP.
|
|
46
|
+
- [x] [Bring your threads](#bring-your-threads), like OpenMP or C++23 executors.
|
|
46
47
|
- [x] Multiple vectors per label.
|
|
47
48
|
- [ ] Thread-safe `reserve`.
|
|
48
|
-
- [
|
|
49
|
+
- [ ] On-the-fly deletions.
|
|
50
|
+
- [x] USearch + UForm Transformers = [Semantic Search](#ai--vector-search--semantic-search).
|
|
51
|
+
- [x] USearch + RDKit = [Molecular Search](#ai--vector-search--semantic-search).
|
|
49
52
|
|
|
50
53
|
[usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
|
|
51
54
|
[obscure-use-cases]: https://ashvardanian.com/posts/abusing-vector-search
|
|
@@ -500,25 +503,27 @@ func main() {
|
|
|
500
503
|
- Rust: Allow passing a custom thread ID.
|
|
501
504
|
- C# .NET bindings.
|
|
502
505
|
|
|
503
|
-
##
|
|
506
|
+
## Application Examples
|
|
507
|
+
|
|
508
|
+
### USearch + AI = Multi-Modal Semantic Search
|
|
504
509
|
|
|
505
510
|
AI has a growing number of applications, but one of the coolest classic ideas is to use it for Semantic Search.
|
|
506
|
-
One can take an encoder model, like the multi-modal UForm, and a web-programming framework, like UCall, and build
|
|
511
|
+
One can take an encoder model, like the multi-modal UForm, and a web-programming framework, like UCall, and build a text-to-image search platform in just 20 lines of Python.
|
|
507
512
|
|
|
508
513
|
```python
|
|
509
|
-
import ucall
|
|
514
|
+
import ucall
|
|
510
515
|
import uform
|
|
511
|
-
|
|
516
|
+
import usearch
|
|
512
517
|
|
|
513
518
|
import numpy as np
|
|
514
|
-
|
|
519
|
+
import PIL as pil
|
|
515
520
|
|
|
516
521
|
server = ucall.Server()
|
|
517
522
|
model = uform.get_model('unum-cloud/uform-vl-multilingual')
|
|
518
|
-
index = Index(ndim=256)
|
|
523
|
+
index = usearch.index.Index(ndim=256)
|
|
519
524
|
|
|
520
525
|
@server
|
|
521
|
-
def add(label: int, photo: Image.Image):
|
|
526
|
+
def add(label: int, photo: pil.Image.Image):
|
|
522
527
|
image = model.preprocess_image(photo)
|
|
523
528
|
vector = model.encode_image(image).detach().numpy()
|
|
524
529
|
index.add(label, vector.flatten(), copy=True)
|
|
@@ -527,10 +532,43 @@ def add(label: int, photo: Image.Image):
|
|
|
527
532
|
def search(query: str) -> np.ndarray:
|
|
528
533
|
tokens = model.preprocess_text(query)
|
|
529
534
|
vector = model.encode_text(tokens).detach().numpy()
|
|
530
|
-
|
|
531
|
-
return
|
|
535
|
+
matches = index.search(vector.flatten(), 3)
|
|
536
|
+
return matches.labels
|
|
532
537
|
|
|
533
538
|
server.run()
|
|
534
539
|
```
|
|
535
540
|
|
|
541
|
+
### USearch + RDKit = Molecular Search
|
|
542
|
+
|
|
543
|
+
Research in natural sciences is expensive.
|
|
544
|
+
To search for properties of different molecules, large databases are used.
|
|
545
|
+
Comparing the molecules, however, is hard.
|
|
546
|
+
Instead, binary hash-like fingerprints are produced from the graph structure, using packages like RDKit.
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
from usearch.index import Index, MetricKind
|
|
550
|
+
from rdkit import Chem
|
|
551
|
+
from rdkit.Chem import AllChem
|
|
552
|
+
|
|
553
|
+
import numpy as np
|
|
554
|
+
|
|
555
|
+
molecules = [Chem.MolFromSmiles('CCOC'), Chem.MolFromSmiles('CCO')]
|
|
556
|
+
encoder = AllChem.GetRDKitFPGenerator()
|
|
557
|
+
|
|
558
|
+
fingerprints = np.vstack([encoder.GetFingerprint(x) for x in molecules])
|
|
559
|
+
fingerprints = np.packbits(fingerprints, axis=1)
|
|
560
|
+
|
|
561
|
+
index = Index(ndim=2048, metric=MetricKind.BitwiseTanimoto)
|
|
562
|
+
labels = np.array(len(molecules), dtype=np.longlong)
|
|
563
|
+
|
|
564
|
+
index.add(labels, fingerprints)
|
|
565
|
+
matches = index.search(fingerprints, 10)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
RDKit [provides][rdkit-fingerprints] Atom-Pair, Topological Torsion, Morgan, and Layered Fingerprints, among other options.
|
|
569
|
+
|
|
570
|
+
[rdkit-fingerprints]: https://www.rdkit.org/docs/RDKit_Book.html#additional-information-about-the-fingerprints
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
536
574
|
Check [that](https://github.com/ashvardanian/image-search) and [other](https://github.com/unum-cloud/examples) examples on our corporate GitHub 🤗
|
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
|
|
@@ -151,6 +152,13 @@ java -cp . -Djava.library.path=/Users/av/github/usearch/java cloud.unum.usearch.
|
|
|
151
152
|
brew install --cask wolfram-engine
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
## GoLang
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
cd golang
|
|
159
|
+
go test -v
|
|
160
|
+
```
|
|
161
|
+
|
|
154
162
|
## C
|
|
155
163
|
|
|
156
164
|
### Linux
|
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,12 +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(
|
|
769
|
+
i.def_property_readonly( //
|
|
770
|
+
"memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
|
|
721
771
|
|
|
722
772
|
i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
|
|
723
773
|
i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
|
|
724
774
|
&punned_index_py_t::change_expansion_search);
|
|
725
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
|
+
|
|
726
782
|
i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
|
|
727
783
|
i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
|
|
728
784
|
i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
|
|
@@ -804,7 +860,7 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
804
860
|
auto proxy = set.unchecked<1>();
|
|
805
861
|
auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
806
862
|
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
807
|
-
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
863
|
+
auto labels_proxy = labels_py.template mutable_unchecked<1>();
|
|
808
864
|
auto result = index.search(view, count);
|
|
809
865
|
result.error.raise();
|
|
810
866
|
auto found = result.dump_to(&labels_proxy(0));
|
package/python/scripts/bench.py
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import numpy as np
|
|
3
3
|
import math
|
|
4
|
+
from typing import Tuple, Any, Optional
|
|
4
5
|
|
|
5
6
|
import fire
|
|
6
7
|
|
|
7
8
|
from usearch.index import Index
|
|
8
9
|
from usearch.io import load_matrix
|
|
10
|
+
from usearch.index import (
|
|
11
|
+
DEFAULT_CONNECTIVITY,
|
|
12
|
+
DEFAULT_EXPANSION_ADD,
|
|
13
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
14
|
+
)
|
|
9
15
|
|
|
10
16
|
|
|
11
|
-
def measure(f) -> float:
|
|
17
|
+
def measure(f) -> Tuple[float, Any]:
|
|
12
18
|
a = time.time_ns()
|
|
13
19
|
result = f()
|
|
14
20
|
b = time.time_ns()
|
|
@@ -18,7 +24,10 @@ def measure(f) -> float:
|
|
|
18
24
|
return secs, result
|
|
19
25
|
|
|
20
26
|
|
|
21
|
-
def bench_faiss(
|
|
27
|
+
def bench_faiss(
|
|
28
|
+
index,
|
|
29
|
+
vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
|
|
30
|
+
|
|
22
31
|
dt, _ = measure(lambda: index.add(vectors))
|
|
23
32
|
print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
|
|
24
33
|
|
|
@@ -33,7 +42,10 @@ def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array
|
|
|
33
42
|
print(f'- Recall@1: {recall_at_one * 100:.2f} %')
|
|
34
43
|
|
|
35
44
|
|
|
36
|
-
def bench_usearch(
|
|
45
|
+
def bench_usearch(
|
|
46
|
+
index,
|
|
47
|
+
vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
|
|
48
|
+
|
|
37
49
|
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
38
50
|
assert len(index) == 0
|
|
39
51
|
dt, _ = measure(lambda: index.add(labels, vectors))
|
|
@@ -55,15 +67,18 @@ def main(
|
|
|
55
67
|
vectors: str,
|
|
56
68
|
queries: str,
|
|
57
69
|
neighbors: str,
|
|
58
|
-
connectivity: int =
|
|
59
|
-
expansion_add: int =
|
|
60
|
-
expansion_search: int =
|
|
70
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
71
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
72
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
73
|
+
k: Optional[int] = None,
|
|
61
74
|
):
|
|
62
75
|
|
|
63
76
|
vectors_mat = load_matrix(vectors)
|
|
64
77
|
queries_mat = load_matrix(queries)
|
|
65
78
|
neighbors_mat = load_matrix(neighbors)
|
|
66
79
|
dim = vectors_mat.shape[1]
|
|
80
|
+
if k:
|
|
81
|
+
neighbors_mat = neighbors_mat[:, :1]
|
|
67
82
|
|
|
68
83
|
print('USearch: HNSW')
|
|
69
84
|
index = Index(
|
|
@@ -142,7 +157,7 @@ def main(
|
|
|
142
157
|
expansion_add=expansion_add,
|
|
143
158
|
expansion_search=expansion_search,
|
|
144
159
|
connectivity=connectivity,
|
|
145
|
-
metric=inner_product_f32
|
|
160
|
+
metric=inner_product_f32,
|
|
146
161
|
)
|
|
147
162
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
148
163
|
|
|
@@ -153,7 +168,7 @@ def main(
|
|
|
153
168
|
expansion_add=expansion_add,
|
|
154
169
|
expansion_search=expansion_search,
|
|
155
170
|
connectivity=connectivity,
|
|
156
|
-
metric=cos_f8
|
|
171
|
+
metric=cos_f8,
|
|
157
172
|
)
|
|
158
173
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
159
174
|
except ImportError:
|
|
@@ -6,9 +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
|
|
11
|
-
from usearch.
|
|
10
|
+
from usearch.index import (
|
|
11
|
+
DEFAULT_CONNECTIVITY,
|
|
12
|
+
DEFAULT_EXPANSION_ADD,
|
|
13
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
14
|
+
|
|
15
|
+
USES_OPENMP,
|
|
16
|
+
USES_SIMSIMD,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
|
|
14
20
|
def measure(f) -> float:
|
|
@@ -31,15 +37,29 @@ def bench_usearch(index, vectors: np.ndarray) -> float:
|
|
|
31
37
|
return insertions_per_second
|
|
32
38
|
|
|
33
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
|
+
|
|
34
49
|
class Main:
|
|
35
50
|
|
|
36
51
|
def dimensions(
|
|
37
52
|
self,
|
|
38
53
|
count: int = 1_000_000,
|
|
39
|
-
connectivity: int =
|
|
40
|
-
expansion_add: int =
|
|
41
|
-
expansion_search: int =
|
|
54
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
55
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
56
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
42
57
|
):
|
|
58
|
+
"""Measures indexing speed for different dimensionality vectors.
|
|
59
|
+
|
|
60
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
61
|
+
:type count: int, optional
|
|
62
|
+
"""
|
|
43
63
|
|
|
44
64
|
dimensions = [
|
|
45
65
|
2, 3, 4, 8, 16, 32, 96, 100,
|
|
@@ -75,15 +95,23 @@ class Main:
|
|
|
75
95
|
self,
|
|
76
96
|
count: int = 1_000_000,
|
|
77
97
|
ndim: int = 256,
|
|
78
|
-
expansion_add: int =
|
|
79
|
-
expansion_search: int =
|
|
98
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
99
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
80
100
|
):
|
|
101
|
+
"""Measures indexing speed and accuracy for different level of
|
|
102
|
+
connectivity in the levels of the hierarchical proximity graph.
|
|
81
103
|
|
|
82
|
-
|
|
104
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
105
|
+
:type count: int, optional
|
|
106
|
+
:param ndim: Number of dimensions per vector, defaults to 256
|
|
107
|
+
:type ndim: int, optional
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
connectivity_options = range(10, 20)
|
|
83
111
|
results = []
|
|
84
112
|
|
|
85
113
|
try:
|
|
86
|
-
for connectivity in
|
|
114
|
+
for connectivity in connectivity_options:
|
|
87
115
|
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
88
116
|
|
|
89
117
|
print(f'USearch for {connectivity} connectivity')
|
|
@@ -107,6 +135,67 @@ class Main:
|
|
|
107
135
|
__file__), 'bench_connectivity.png'))
|
|
108
136
|
fig.show()
|
|
109
137
|
|
|
138
|
+
def robustness(
|
|
139
|
+
self,
|
|
140
|
+
experiments: int = 1,
|
|
141
|
+
count: int = 10000,
|
|
142
|
+
ndim: int = 256,
|
|
143
|
+
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
144
|
+
expansion_add: int = DEFAULT_EXPANSION_ADD,
|
|
145
|
+
expansion_search: int = DEFAULT_EXPANSION_SEARCH,
|
|
146
|
+
):
|
|
147
|
+
"""How much does accuracy and speed fluctuate depending on the indexing order.
|
|
148
|
+
|
|
149
|
+
:param experiments: How many times to repeat, defaults to 10
|
|
150
|
+
:type experiments: int, optional
|
|
151
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
152
|
+
:type count: int, optional
|
|
153
|
+
:param ndim: Number of dimensions per vector, defaults to 256
|
|
154
|
+
:type ndim: int, optional
|
|
155
|
+
"""
|
|
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
|
+
|
|
165
|
+
recall_levels = []
|
|
166
|
+
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
167
|
+
|
|
168
|
+
for _ in range(experiments):
|
|
169
|
+
np.random.shuffle(vectors_mat)
|
|
170
|
+
index = Index(
|
|
171
|
+
ndim=ndim,
|
|
172
|
+
expansion_add=expansion_add,
|
|
173
|
+
expansion_search=expansion_search,
|
|
174
|
+
connectivity=connectivity,
|
|
175
|
+
)
|
|
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)
|
|
180
|
+
del index
|
|
181
|
+
|
|
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()
|
|
197
|
+
|
|
110
198
|
|
|
111
199
|
if __name__ == '__main__':
|
|
200
|
+
Main().robustness()
|
|
112
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 = (
|
|
@@ -31,18 +34,47 @@ SetsIndex = _CompiledSetsIndex
|
|
|
31
34
|
class Matches(NamedTuple):
|
|
32
35
|
labels: np.ndarray
|
|
33
36
|
distances: np.ndarray
|
|
34
|
-
counts: np.ndarray
|
|
37
|
+
counts: Union[np.ndarray, int]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def is_batch(self) -> bool:
|
|
41
|
+
return isinstance(self.counts, np.ndarray)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def batch_size(self) -> int:
|
|
45
|
+
return len(self.counts) if isinstance(self.counts, np.ndarray) else 1
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def total_matches(self) -> int:
|
|
49
|
+
return np.sum(self.counts)
|
|
50
|
+
|
|
51
|
+
def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
|
|
52
|
+
|
|
53
|
+
if not self.is_batch:
|
|
54
|
+
assert row is None, 'Exporting a single sequence is only for batch requests'
|
|
55
|
+
labels = self.labels
|
|
56
|
+
distances = self.distances
|
|
35
57
|
|
|
58
|
+
elif row is None:
|
|
59
|
+
return [self.to_list(i) for i in range(self.batch_size)]
|
|
36
60
|
|
|
37
|
-
|
|
61
|
+
else:
|
|
62
|
+
count = self.counts[row]
|
|
63
|
+
labels = self.labels[row, :count]
|
|
64
|
+
distances = self.distances[row, :count]
|
|
65
|
+
|
|
66
|
+
return [
|
|
67
|
+
{'label': int(label), 'distance': float(distance)}
|
|
68
|
+
for label, distance in zip(labels, distances)
|
|
69
|
+
]
|
|
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)
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return [
|
|
43
|
-
{'label': int(label), 'distance': float(distance)}
|
|
44
|
-
for label, distance in zip(labels, distances)
|
|
45
|
-
]
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
return f'usearch.Matches({self.total_matches})' if self.is_batch else \
|
|
77
|
+
f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
|
|
46
78
|
|
|
47
79
|
|
|
48
80
|
def jit_metric(ndim: int, metric: MetricKind, dtype: str = 'f32') -> Callable:
|
|
@@ -150,9 +182,9 @@ class Index:
|
|
|
150
182
|
:param metric: Distance function, defaults to MetricKind.IP
|
|
151
183
|
:type metric: Union[MetricKind, Callable], optional
|
|
152
184
|
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
153
|
-
Possible `MetricKind` values: IP,
|
|
154
|
-
|
|
155
|
-
Not every kind is JIT-able.
|
|
185
|
+
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
186
|
+
BitwiseHamming, BitwiseTanimoto, BitwiseSorensen.
|
|
187
|
+
Not every kind is JIT-able. For Jaccard distance, use `SetsIndex`.
|
|
156
188
|
|
|
157
189
|
:param dtype: Scalar type for internal vector storage, defaults to None
|
|
158
190
|
:type dtype: str, optional
|
|
@@ -319,7 +351,7 @@ class Index:
|
|
|
319
351
|
return Matches(*tuple_)
|
|
320
352
|
|
|
321
353
|
def __len__(self) -> int:
|
|
322
|
-
return
|
|
354
|
+
return self._compiled.__len__()
|
|
323
355
|
|
|
324
356
|
@property
|
|
325
357
|
def jit(self) -> bool:
|
|
@@ -333,6 +365,10 @@ class Index:
|
|
|
333
365
|
def ndim(self) -> int:
|
|
334
366
|
return self._compiled.ndim
|
|
335
367
|
|
|
368
|
+
@property
|
|
369
|
+
def metric(self) -> MetricKind:
|
|
370
|
+
return self._metric_kind
|
|
371
|
+
|
|
336
372
|
@property
|
|
337
373
|
def dtype(self) -> str:
|
|
338
374
|
return self._compiled.dtype
|
|
@@ -358,11 +394,11 @@ class Index:
|
|
|
358
394
|
return self._compiled.expansion_search
|
|
359
395
|
|
|
360
396
|
@expansion_add.setter
|
|
361
|
-
def
|
|
397
|
+
def expansion_add(self, v: int):
|
|
362
398
|
self._compiled.expansion_add = v
|
|
363
399
|
|
|
364
400
|
@expansion_search.setter
|
|
365
|
-
def
|
|
401
|
+
def expansion_search(self, v: int):
|
|
366
402
|
self._compiled.expansion_search = v
|
|
367
403
|
|
|
368
404
|
def save(self, path: os.PathLike):
|
|
@@ -377,5 +413,15 @@ class Index:
|
|
|
377
413
|
def clear(self):
|
|
378
414
|
self._compiled.clear()
|
|
379
415
|
|
|
380
|
-
|
|
381
|
-
|
|
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)
|
package/python/usearch/io.py
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import struct
|
|
2
|
+
|
|
2
3
|
import numpy as np
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def load_matrix(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
def load_matrix(
|
|
7
|
+
filename: str,
|
|
8
|
+
start_row: int = 0, count_rows: int = None,
|
|
9
|
+
view: bool = False) -> np.ndarray:
|
|
10
|
+
"""Read *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
|
|
11
|
+
|
|
12
|
+
:param filename: path to the matrix file
|
|
13
|
+
:param start_row: start reading vectors from this index
|
|
14
|
+
:param count_rows: number of vectors to read. If None, read all vectors
|
|
15
|
+
:param view: set to `True` to memory-map the file instead of loading to RAM
|
|
16
|
+
|
|
17
|
+
:return: parsed matrix
|
|
18
|
+
:rtype: numpy.ndarray
|
|
14
19
|
"""
|
|
15
20
|
dtype = np.float32
|
|
16
21
|
scalar_size = 4
|
|
@@ -35,17 +40,21 @@ def load_matrix(filename: str, start_row: int = 0, count_rows: int = None, view:
|
|
|
35
40
|
row_offset = start_row * scalar_size * cols
|
|
36
41
|
|
|
37
42
|
if view:
|
|
38
|
-
return np.memmap(
|
|
43
|
+
return np.memmap(
|
|
44
|
+
f, dtype=dtype, mode='r', offset=8+row_offset, shape=(rows, cols))
|
|
39
45
|
else:
|
|
40
|
-
return np.fromfile(
|
|
46
|
+
return np.fromfile(
|
|
47
|
+
f, count=rows * cols,
|
|
48
|
+
dtype=dtype, offset=row_offset).reshape(rows, cols)
|
|
41
49
|
|
|
42
50
|
|
|
43
|
-
def save_matrix(
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
def save_matrix(vectors: np.ndarray, filename: str):
|
|
52
|
+
"""Write *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
|
|
53
|
+
|
|
54
|
+
:param vectors: the matrix to serialize
|
|
55
|
+
:type vectors: numpy.ndarray
|
|
56
|
+
:param filename: path to the matrix file
|
|
57
|
+
:type filename: str
|
|
49
58
|
"""
|
|
50
59
|
dtype = np.float32
|
|
51
60
|
if filename.endswith('.fbin'):
|
|
@@ -59,9 +68,9 @@ def save_matrix(vecs: np.array, filename: str):
|
|
|
59
68
|
else:
|
|
60
69
|
raise Exception('Unknown file type')
|
|
61
70
|
|
|
62
|
-
assert len(
|
|
71
|
+
assert len(vectors.shape) == 2, 'Input array must have 2 dimensions'
|
|
63
72
|
with open(filename, 'wb') as f:
|
|
64
|
-
|
|
65
|
-
f.write(struct.pack('<i',
|
|
73
|
+
count, dim = vectors.shape
|
|
74
|
+
f.write(struct.pack('<i', count))
|
|
66
75
|
f.write(struct.pack('<i', dim))
|
|
67
|
-
|
|
76
|
+
vectors.astype(dtype).flatten().tofile(f)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from usearch.index import Index, Matches, MetricKind, BitwiseMetricKind
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def vectors(index: Index, count: int) -> np.ndarray:
|
|
7
|
+
|
|
8
|
+
if not isinstance(index, Index):
|
|
9
|
+
raise ValueError('Unsupported `index` type')
|
|
10
|
+
|
|
11
|
+
dimensions = index.ndim
|
|
12
|
+
|
|
13
|
+
if index.metric in BitwiseMetricKind:
|
|
14
|
+
bit_vectors = np.random.randint(2, size=(count, dimensions))
|
|
15
|
+
bit_vectors = np.packbits(bit_vectors, axis=1)
|
|
16
|
+
return bit_vectors
|
|
17
|
+
|
|
18
|
+
else:
|
|
19
|
+
x = np.random.rand(count, dimensions).astype(index.numpy_dtype)
|
|
20
|
+
if index.metric == MetricKind.IP:
|
|
21
|
+
return x / np.linalg.norm(x, axis=1, keepdims=True)
|
|
22
|
+
return x
|
|
23
|
+
|
|
24
|
+
|
|
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); }
|