usearch 0.12.2 → 0.13.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 +3 -3
- package/Cargo.toml +1 -1
- package/README.md +52 -14
- package/conanfile.py +1 -1
- package/docs/compilation.md +7 -0
- package/package.json +1 -1
- package/python/lib.cpp +2 -1
- package/python/scripts/bench.py +15 -5
- package/python/scripts/bench_params.py +52 -3
- package/python/usearch/index.py +41 -12
- package/python/usearch/io.py +30 -21
- package/python/usearch/synthetic.py +32 -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 Developmtn Branch
|
|
32
32
|
needs: versioning
|
|
33
33
|
runs-on: ubuntu-latest
|
|
34
34
|
steps:
|
|
@@ -37,14 +37,14 @@ 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
|
- name: Push changes
|
|
45
45
|
uses: ad-m/github-push-action@master
|
|
46
46
|
with:
|
|
47
|
-
github_token: ${{ secrets.
|
|
47
|
+
github_token: ${{ secrets.SEMANTIC_REBASE_TOKEN }}
|
|
48
48
|
branch: main-dev
|
|
49
49
|
|
|
50
50
|
build_wheels:
|
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.13.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
package/package.json
CHANGED
package/python/lib.cpp
CHANGED
|
@@ -717,7 +717,8 @@ PYBIND11_MODULE(compiled, m) {
|
|
|
717
717
|
i.def_property_readonly("capacity", &punned_index_py_t::capacity);
|
|
718
718
|
i.def_property_readonly( //
|
|
719
719
|
"dtype", [](punned_index_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
|
|
720
|
-
i.def_property_readonly("memory_usage",
|
|
720
|
+
i.def_property_readonly("memory_usage",
|
|
721
|
+
[](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
|
|
721
722
|
|
|
722
723
|
i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
|
|
723
724
|
i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
|
package/python/scripts/bench.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
|
|
@@ -8,7 +9,7 @@ from usearch.index import Index
|
|
|
8
9
|
from usearch.io import load_matrix
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
def measure(f) -> float:
|
|
12
|
+
def measure(f) -> Tuple[float, Any]:
|
|
12
13
|
a = time.time_ns()
|
|
13
14
|
result = f()
|
|
14
15
|
b = time.time_ns()
|
|
@@ -18,7 +19,10 @@ def measure(f) -> float:
|
|
|
18
19
|
return secs, result
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def bench_faiss(
|
|
22
|
+
def bench_faiss(
|
|
23
|
+
index,
|
|
24
|
+
vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
|
|
25
|
+
|
|
22
26
|
dt, _ = measure(lambda: index.add(vectors))
|
|
23
27
|
print(f'- Performance: {vectors.shape[0]/dt:.2f} insertions/s')
|
|
24
28
|
|
|
@@ -33,7 +37,10 @@ def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array
|
|
|
33
37
|
print(f'- Recall@1: {recall_at_one * 100:.2f} %')
|
|
34
38
|
|
|
35
39
|
|
|
36
|
-
def bench_usearch(
|
|
40
|
+
def bench_usearch(
|
|
41
|
+
index,
|
|
42
|
+
vectors: np.ndarray, queries: np.ndarray, neighbors: np.ndarray):
|
|
43
|
+
|
|
37
44
|
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
38
45
|
assert len(index) == 0
|
|
39
46
|
dt, _ = measure(lambda: index.add(labels, vectors))
|
|
@@ -58,12 +65,15 @@ def main(
|
|
|
58
65
|
connectivity: int = 16,
|
|
59
66
|
expansion_add: int = 128,
|
|
60
67
|
expansion_search: int = 64,
|
|
68
|
+
k: Optional[int] = None,
|
|
61
69
|
):
|
|
62
70
|
|
|
63
71
|
vectors_mat = load_matrix(vectors)
|
|
64
72
|
queries_mat = load_matrix(queries)
|
|
65
73
|
neighbors_mat = load_matrix(neighbors)
|
|
66
74
|
dim = vectors_mat.shape[1]
|
|
75
|
+
if k:
|
|
76
|
+
neighbors_mat = neighbors_mat[:, :1]
|
|
67
77
|
|
|
68
78
|
print('USearch: HNSW')
|
|
69
79
|
index = Index(
|
|
@@ -142,7 +152,7 @@ def main(
|
|
|
142
152
|
expansion_add=expansion_add,
|
|
143
153
|
expansion_search=expansion_search,
|
|
144
154
|
connectivity=connectivity,
|
|
145
|
-
metric=inner_product_f32
|
|
155
|
+
metric=inner_product_f32,
|
|
146
156
|
)
|
|
147
157
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
148
158
|
|
|
@@ -153,7 +163,7 @@ def main(
|
|
|
153
163
|
expansion_add=expansion_add,
|
|
154
164
|
expansion_search=expansion_search,
|
|
155
165
|
connectivity=connectivity,
|
|
156
|
-
metric=cos_f8
|
|
166
|
+
metric=cos_f8,
|
|
157
167
|
)
|
|
158
168
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
159
169
|
except ImportError:
|
|
@@ -8,7 +8,6 @@ import pandas as pd
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
from usearch.index import Index
|
|
11
|
-
from usearch.io import load_matrix
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
def measure(f) -> float:
|
|
@@ -40,6 +39,11 @@ class Main:
|
|
|
40
39
|
expansion_add: int = 128,
|
|
41
40
|
expansion_search: int = 64,
|
|
42
41
|
):
|
|
42
|
+
"""Measures indexing speed for different dimensionality vectors.
|
|
43
|
+
|
|
44
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
45
|
+
:type count: int, optional
|
|
46
|
+
"""
|
|
43
47
|
|
|
44
48
|
dimensions = [
|
|
45
49
|
2, 3, 4, 8, 16, 32, 96, 100,
|
|
@@ -78,12 +82,20 @@ class Main:
|
|
|
78
82
|
expansion_add: int = 128,
|
|
79
83
|
expansion_search: int = 64,
|
|
80
84
|
):
|
|
85
|
+
"""Measures indexing speed and accuracy for different level of
|
|
86
|
+
connectivity in the levels of the hierarchical proximity graph.
|
|
87
|
+
|
|
88
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
89
|
+
:type count: int, optional
|
|
90
|
+
:param ndim: Number of dimensions per vector, defaults to 256
|
|
91
|
+
:type ndim: int, optional
|
|
92
|
+
"""
|
|
81
93
|
|
|
82
|
-
|
|
94
|
+
connectivity_options = range(10, 20)
|
|
83
95
|
results = []
|
|
84
96
|
|
|
85
97
|
try:
|
|
86
|
-
for connectivity in
|
|
98
|
+
for connectivity in connectivity_options:
|
|
87
99
|
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
88
100
|
|
|
89
101
|
print(f'USearch for {connectivity} connectivity')
|
|
@@ -107,6 +119,43 @@ class Main:
|
|
|
107
119
|
__file__), 'bench_connectivity.png'))
|
|
108
120
|
fig.show()
|
|
109
121
|
|
|
122
|
+
def robustness(
|
|
123
|
+
self,
|
|
124
|
+
experiments: int = 10,
|
|
125
|
+
count: int = 1_000_000,
|
|
126
|
+
ndim: int = 256,
|
|
127
|
+
connectivity: int = 16,
|
|
128
|
+
expansion_add: int = 128,
|
|
129
|
+
expansion_search: int = 64,
|
|
130
|
+
):
|
|
131
|
+
"""How much does accuracy fluctuate depending on the indexing order.
|
|
132
|
+
|
|
133
|
+
:param experiments: How many times to repeat, defaults to 10
|
|
134
|
+
:type experiments: int, optional
|
|
135
|
+
:param count: Number of vectors, defaults to 1_000_000
|
|
136
|
+
:type count: int, optional
|
|
137
|
+
:param ndim: Number of dimensions per vector, defaults to 256
|
|
138
|
+
:type ndim: int, optional
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
recall_levels = []
|
|
142
|
+
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
143
|
+
|
|
144
|
+
for _ in range(experiments):
|
|
145
|
+
|
|
146
|
+
index = Index(
|
|
147
|
+
ndim=ndim,
|
|
148
|
+
expansion_add=expansion_add,
|
|
149
|
+
expansion_search=expansion_search,
|
|
150
|
+
connectivity=connectivity,
|
|
151
|
+
)
|
|
152
|
+
bench_usearch(index, vectors_mat)
|
|
153
|
+
del index
|
|
154
|
+
|
|
155
|
+
min_ = min(recall_levels)
|
|
156
|
+
max_ = max(recall_levels)
|
|
157
|
+
print(f'Recall@1 varies between {min_:.3f} and {max_:.3f}')
|
|
158
|
+
|
|
110
159
|
|
|
111
160
|
if __name__ == '__main__':
|
|
112
161
|
fire.Fire(Main)
|
package/python/usearch/index.py
CHANGED
|
@@ -31,18 +31,43 @@ SetsIndex = _CompiledSetsIndex
|
|
|
31
31
|
class Matches(NamedTuple):
|
|
32
32
|
labels: np.ndarray
|
|
33
33
|
distances: np.ndarray
|
|
34
|
-
counts: np.ndarray
|
|
34
|
+
counts: Union[np.ndarray, int]
|
|
35
35
|
|
|
36
|
+
@property
|
|
37
|
+
def is_batch(self) -> bool:
|
|
38
|
+
return isinstance(self.counts, np.ndarray)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def batch_size(self) -> int:
|
|
42
|
+
return len(self.counts) if isinstance(self.counts, np.ndarray) else 1
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def total_matches(self) -> int:
|
|
46
|
+
return np.sum(self.counts)
|
|
47
|
+
|
|
48
|
+
def to_list(self, row: Optional[int] = None) -> Union[List[dict], List[List[dict]]]:
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
if not self.is_batch:
|
|
51
|
+
assert row is None, 'Exporting a single sequence is only for batch requests'
|
|
52
|
+
labels = self.labels
|
|
53
|
+
distances = self.distances
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
elif row is None:
|
|
56
|
+
return [self.to_list(i) for i in range(self.batch_size)]
|
|
57
|
+
|
|
58
|
+
else:
|
|
59
|
+
count = self.counts[row]
|
|
60
|
+
labels = self.labels[row, :count]
|
|
61
|
+
distances = self.distances[row, :count]
|
|
62
|
+
|
|
63
|
+
return [
|
|
64
|
+
{'label': int(label), 'distance': float(distance)}
|
|
65
|
+
for label, distance in zip(labels, distances)
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
def __repr__(self) -> str:
|
|
69
|
+
return f'usearch.Matches({self.total_matches})' if self.is_batch else \
|
|
70
|
+
f'usearch.Matches({self.total_matches} across {self.batch_size} queries)'
|
|
46
71
|
|
|
47
72
|
|
|
48
73
|
def jit_metric(ndim: int, metric: MetricKind, dtype: str = 'f32') -> Callable:
|
|
@@ -150,9 +175,9 @@ class Index:
|
|
|
150
175
|
:param metric: Distance function, defaults to MetricKind.IP
|
|
151
176
|
:type metric: Union[MetricKind, Callable], optional
|
|
152
177
|
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.
|
|
178
|
+
Possible `MetricKind` values: IP, Cos, L2sq, Haversine, Pearson,
|
|
179
|
+
BitwiseHamming, BitwiseTanimoto, BitwiseSorensen.
|
|
180
|
+
Not every kind is JIT-able. For Jaccard distance, use `SetsIndex`.
|
|
156
181
|
|
|
157
182
|
:param dtype: Scalar type for internal vector storage, defaults to None
|
|
158
183
|
:type dtype: str, optional
|
|
@@ -333,6 +358,10 @@ class Index:
|
|
|
333
358
|
def ndim(self) -> int:
|
|
334
359
|
return self._compiled.ndim
|
|
335
360
|
|
|
361
|
+
@property
|
|
362
|
+
def metric(self) -> MetricKind:
|
|
363
|
+
return self._metric_kind
|
|
364
|
+
|
|
336
365
|
@property
|
|
337
366
|
def dtype(self) -> str:
|
|
338
367
|
return self._compiled.dtype
|
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,32 @@
|
|
|
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_at_one(index: Index, vectors: np.ndarray) -> float:
|
|
26
|
+
empty_replica: Index = index.fork()
|
|
27
|
+
|
|
28
|
+
labels = np.arange(vectors.shape[0])
|
|
29
|
+
empty_replica.add(labels=labels, vectors=vectors)
|
|
30
|
+
matches: Matches = empty_replica.search(empty_replica, 1)
|
|
31
|
+
|
|
32
|
+
return np.sum(matches.labels.flatten() == labels) / len(labels)
|