usearch 0.11.1 → 0.12.1
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 +2 -5
- package/.github/workflows/release.yml +22 -2
- package/.gitignore +84 -0
- package/.vscode/launch.json +7 -1
- package/.vscode/settings.json +15 -1
- package/Cargo.toml +1 -1
- package/README.md +43 -8
- package/binding.gyp +2 -2
- package/c/lib.cpp +141 -0
- package/c/usearch.h +43 -0
- package/conanfile.py +1 -1
- package/docs/compilation.md +14 -3
- package/golang/go.mod +3 -0
- package/golang/lib.go +238 -0
- package/golang/usearch.h +9 -0
- package/golang/usearch_par_test.go +40 -0
- package/golang/usearch_test.go +82 -0
- package/include/usearch/usearch.hpp +595 -248
- package/java/cloud/unum/usearch/Index.java +1 -1
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +36 -44
- package/javascript/lib.cpp +10 -6
- package/javascript/usearch.js +3 -0
- package/objc/USearchObjective.mm +3 -3
- package/package.json +4 -3
- package/python/lib.cpp +354 -181
- package/python/scripts/bench.py +2 -2
- package/python/scripts/test.py +88 -29
- package/python/usearch/index.py +132 -57
- package/rust/lib.cpp +33 -24
- package/rust/lib.hpp +2 -2
- package/setup.py +4 -1
- package/src/CMakeLists.txt +4 -4
- package/src/bench.cpp +22 -19
- package/src/punned.hpp +108 -79
- package/src/test.cpp +77 -27
- package/wolfram/lib.cpp +7 -8
- package/Dockerfile +0 -16
- package/VERSION +0 -1
package/python/scripts/bench.py
CHANGED
|
@@ -102,7 +102,7 @@ def main(
|
|
|
102
102
|
signature_f32 = types.float32(
|
|
103
103
|
types.CPointer(types.float32),
|
|
104
104
|
types.CPointer(types.float32),
|
|
105
|
-
types.
|
|
105
|
+
types.uint64, types.uint64)
|
|
106
106
|
|
|
107
107
|
@cfunc(signature_f32)
|
|
108
108
|
def inner_product_f32(a, b, n, m):
|
|
@@ -116,7 +116,7 @@ def main(
|
|
|
116
116
|
signature_f8 = types.float32(
|
|
117
117
|
types.CPointer(types.int8),
|
|
118
118
|
types.CPointer(types.int8),
|
|
119
|
-
types.
|
|
119
|
+
types.uint64, types.uint64)
|
|
120
120
|
|
|
121
121
|
@cfunc(signature_f8)
|
|
122
122
|
def cos_f8(a, b, n, m):
|
package/python/scripts/test.py
CHANGED
|
@@ -1,31 +1,71 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import pytest
|
|
2
3
|
import numpy as np
|
|
3
4
|
|
|
4
|
-
from usearch.io import load_matrix
|
|
5
|
-
from usearch.index import Index, SetsIndex
|
|
5
|
+
from usearch.io import load_matrix, save_matrix
|
|
6
|
+
from usearch.index import Index, SetsIndex, MetricKind
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
dimensions = [13, 99, 100, 256]
|
|
8
|
-
|
|
9
|
-
index_types = ['
|
|
10
|
-
numpy_types = [np.
|
|
11
|
-
|
|
10
|
+
batch_sizes = [1, 2, 100]
|
|
11
|
+
index_types = ['f32', 'f64', 'f16', 'f8']
|
|
12
|
+
numpy_types = [np.float32, np.float64, np.float16, np.byte]
|
|
13
|
+
connectivity_options = [3, 13, 50]
|
|
14
|
+
jit_options = [False]
|
|
15
|
+
continuous_metrics = [
|
|
16
|
+
MetricKind.Cos,
|
|
17
|
+
MetricKind.L2sq,
|
|
18
|
+
]
|
|
19
|
+
hash_metrics = [
|
|
20
|
+
MetricKind.BitwiseHamming,
|
|
21
|
+
MetricKind.BitwiseTanimoto,
|
|
22
|
+
MetricKind.BitwiseSorensen,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize('rows', batch_sizes)
|
|
27
|
+
@pytest.mark.parametrize('cols', dimensions)
|
|
28
|
+
def test_serializing_fbin_matrix(rows: int, cols: int):
|
|
29
|
+
original = np.random.rand(rows, cols).astype(np.float32)
|
|
30
|
+
save_matrix(original, 'tmp.fbin')
|
|
31
|
+
reconstructed = load_matrix('tmp.fbin')
|
|
32
|
+
assert np.allclose(original, reconstructed)
|
|
33
|
+
os.remove('tmp.fbin')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize('rows', batch_sizes)
|
|
37
|
+
@pytest.mark.parametrize('cols', dimensions)
|
|
38
|
+
def test_serializing_ibin_matrix(rows: int, cols: int):
|
|
39
|
+
original = np.random.randint(0, rows+1, size=(rows, cols)).astype(np.int32)
|
|
40
|
+
save_matrix(original, 'tmp.ibin')
|
|
41
|
+
reconstructed = load_matrix('tmp.ibin')
|
|
42
|
+
assert np.allclose(original, reconstructed)
|
|
43
|
+
os.remove('tmp.ibin')
|
|
12
44
|
|
|
13
45
|
|
|
14
46
|
@pytest.mark.parametrize('ndim', dimensions)
|
|
47
|
+
@pytest.mark.parametrize('metric', continuous_metrics)
|
|
15
48
|
@pytest.mark.parametrize('index_type', index_types)
|
|
16
49
|
@pytest.mark.parametrize('numpy_type', numpy_types)
|
|
17
|
-
@pytest.mark.parametrize('connectivity',
|
|
18
|
-
|
|
50
|
+
@pytest.mark.parametrize('connectivity', connectivity_options)
|
|
51
|
+
@pytest.mark.parametrize('jit', jit_options)
|
|
52
|
+
def test_index(
|
|
53
|
+
ndim: int, metric: MetricKind,
|
|
54
|
+
index_type: str, numpy_type: str,
|
|
55
|
+
connectivity: int, jit: bool):
|
|
56
|
+
|
|
19
57
|
index = Index(
|
|
20
|
-
metric=
|
|
58
|
+
metric=metric,
|
|
21
59
|
ndim=ndim,
|
|
22
60
|
dtype=index_type,
|
|
23
61
|
connectivity=connectivity,
|
|
62
|
+
jit=jit,
|
|
24
63
|
)
|
|
25
64
|
assert index.ndim == ndim
|
|
26
65
|
assert index.connectivity == connectivity
|
|
27
66
|
|
|
28
|
-
|
|
67
|
+
limit = 0.7 if numpy_type != np.byte else 70
|
|
68
|
+
vector = np.random.uniform(0, limit, (index.ndim)).astype(numpy_type)
|
|
29
69
|
index.add(42, vector)
|
|
30
70
|
matches, distances, count = index.search(vector, 10)
|
|
31
71
|
|
|
@@ -45,25 +85,31 @@ def test_l2sq(ndim: int, index_type: str, numpy_type: str, connectivity: int):
|
|
|
45
85
|
|
|
46
86
|
|
|
47
87
|
@pytest.mark.parametrize('ndim', dimensions)
|
|
48
|
-
@pytest.mark.parametrize('
|
|
88
|
+
@pytest.mark.parametrize('metric', continuous_metrics)
|
|
89
|
+
@pytest.mark.parametrize('batch_size', batch_sizes)
|
|
49
90
|
@pytest.mark.parametrize('index_type', index_types)
|
|
50
91
|
@pytest.mark.parametrize('numpy_type', numpy_types)
|
|
51
|
-
def
|
|
52
|
-
|
|
92
|
+
def test_index_batch(
|
|
93
|
+
ndim: int, metric: MetricKind,
|
|
94
|
+
batch_size: int, index_type: str, numpy_type: str):
|
|
53
95
|
|
|
54
|
-
|
|
55
|
-
|
|
96
|
+
index = Index(ndim=ndim, metric=metric, dtype=index_type)
|
|
97
|
+
|
|
98
|
+
limit = 0.7 if numpy_type != np.byte else 70
|
|
99
|
+
vectors = np.random.uniform(
|
|
100
|
+
0, limit, (batch_size, index.ndim)).astype(numpy_type)
|
|
101
|
+
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
56
102
|
|
|
57
103
|
index.add(labels, vectors)
|
|
58
104
|
matches, distances, count = index.search(vectors, 10)
|
|
59
105
|
|
|
60
106
|
assert matches.shape[0] == distances.shape[0]
|
|
61
|
-
assert count.shape[0] ==
|
|
107
|
+
assert count.shape[0] == batch_size
|
|
62
108
|
|
|
63
109
|
|
|
64
110
|
@pytest.mark.parametrize('ndim', dimensions)
|
|
65
|
-
@pytest.mark.parametrize('
|
|
66
|
-
def
|
|
111
|
+
@pytest.mark.parametrize('batch_size', batch_sizes)
|
|
112
|
+
def test_index_udf(ndim: int, batch_size: int):
|
|
67
113
|
|
|
68
114
|
try:
|
|
69
115
|
from numba import cfunc, types, carray
|
|
@@ -75,7 +121,7 @@ def test_user_defined_function(ndim: int, size: int):
|
|
|
75
121
|
signature = types.float32(
|
|
76
122
|
types.CPointer(types.float32),
|
|
77
123
|
types.CPointer(types.float32),
|
|
78
|
-
types.
|
|
124
|
+
types.uint64, types.uint64)
|
|
79
125
|
|
|
80
126
|
@cfunc(signature)
|
|
81
127
|
def python_dot(a, b, n, m):
|
|
@@ -86,27 +132,40 @@ def test_user_defined_function(ndim: int, size: int):
|
|
|
86
132
|
c += a_array[i] * b_array[i]
|
|
87
133
|
return c
|
|
88
134
|
|
|
89
|
-
index = Index(ndim=ndim, metric=python_dot
|
|
135
|
+
index = Index(ndim=ndim, metric=python_dot)
|
|
90
136
|
|
|
91
137
|
vectors = np.random.uniform(
|
|
92
|
-
0, 0.3, (
|
|
93
|
-
labels = np.array(range(
|
|
138
|
+
0, 0.3, (batch_size, index.ndim)).astype(np.float32)
|
|
139
|
+
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
94
140
|
|
|
95
141
|
index.add(labels, vectors)
|
|
96
142
|
matches, distances, count = index.search(vectors, 10)
|
|
97
143
|
|
|
98
144
|
assert matches.shape[0] == distances.shape[0]
|
|
99
|
-
assert count.shape[0] ==
|
|
145
|
+
assert count.shape[0] == batch_size
|
|
100
146
|
|
|
101
147
|
|
|
102
|
-
|
|
148
|
+
@pytest.mark.parametrize('connectivity', connectivity_options)
|
|
149
|
+
def test_sets_index(connectivity: int):
|
|
103
150
|
|
|
104
|
-
index = SetsIndex()
|
|
151
|
+
index = SetsIndex(connectivity=connectivity)
|
|
105
152
|
index.add(10, np.array([10, 12, 15], dtype=np.uint32))
|
|
106
153
|
index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))
|
|
107
|
-
|
|
108
|
-
|
|
154
|
+
results = index.search(np.array([12, 15], dtype=np.uint32), 10)
|
|
155
|
+
assert list(results) == [10, 11]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@pytest.mark.parametrize('bits', dimensions)
|
|
159
|
+
@pytest.mark.parametrize('metric', hash_metrics)
|
|
160
|
+
@pytest.mark.parametrize('connectivity', connectivity_options)
|
|
161
|
+
@pytest.mark.parametrize('batch_size', batch_sizes)
|
|
162
|
+
def test_hash_index(bits: int, metric: MetricKind, connectivity: int, batch_size: int):
|
|
163
|
+
|
|
164
|
+
index = Index(ndim=bits, metric=metric, connectivity=connectivity)
|
|
109
165
|
|
|
166
|
+
bit_vectors = np.random.randint(2, size=(batch_size, bits))
|
|
167
|
+
bit_vectors = np.packbits(bit_vectors, axis=1)
|
|
168
|
+
labels = np.array(range(batch_size), dtype=np.longlong)
|
|
110
169
|
|
|
111
|
-
|
|
112
|
-
|
|
170
|
+
index.add(labels, bit_vectors)
|
|
171
|
+
index.search(bit_vectors, 10)
|
package/python/usearch/index.py
CHANGED
|
@@ -10,11 +10,22 @@ import numpy as np
|
|
|
10
10
|
|
|
11
11
|
from usearch.compiled import Index as _CompiledIndex
|
|
12
12
|
from usearch.compiled import SetsIndex as _CompiledSetsIndex
|
|
13
|
-
from usearch.compiled import
|
|
14
|
-
|
|
13
|
+
from usearch.compiled import BitsIndex as _CompiledBitsIndex
|
|
14
|
+
|
|
15
|
+
from usearch.compiled import MetricKind
|
|
16
|
+
from usearch.compiled import (
|
|
17
|
+
DEFAULT_CONNECTIVITY,
|
|
18
|
+
DEFAULT_EXPANSION_ADD,
|
|
19
|
+
DEFAULT_EXPANSION_SEARCH,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
BitwiseMetricKind = (
|
|
23
|
+
MetricKind.BitwiseHamming,
|
|
24
|
+
MetricKind.BitwiseTanimoto,
|
|
25
|
+
MetricKind.BitwiseSorensen,
|
|
26
|
+
)
|
|
15
27
|
|
|
16
28
|
SetsIndex = _CompiledSetsIndex
|
|
17
|
-
HashIndex = _CompiledHashIndex
|
|
18
29
|
|
|
19
30
|
|
|
20
31
|
class Matches(NamedTuple):
|
|
@@ -34,7 +45,7 @@ def list_matches(results: Matches, row: int) -> List[dict]:
|
|
|
34
45
|
]
|
|
35
46
|
|
|
36
47
|
|
|
37
|
-
def jit_metric(ndim: int,
|
|
48
|
+
def jit_metric(ndim: int, metric: MetricKind, dtype: str = 'f32') -> Callable:
|
|
38
49
|
|
|
39
50
|
try:
|
|
40
51
|
from numba import cfunc, types, carray
|
|
@@ -46,26 +57,28 @@ def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
|
|
|
46
57
|
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
47
58
|
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
48
59
|
|
|
49
|
-
if
|
|
60
|
+
if dtype == 'f32':
|
|
50
61
|
signature = types.float32(
|
|
51
62
|
types.CPointer(types.float32),
|
|
52
63
|
types.CPointer(types.float32),
|
|
53
|
-
types.
|
|
64
|
+
types.uint64, types.uint64)
|
|
54
65
|
|
|
55
|
-
if
|
|
66
|
+
if metric == MetricKind.IP:
|
|
56
67
|
|
|
68
|
+
@cfunc(signature)
|
|
57
69
|
def numba_ip(a, b, _n, _m):
|
|
58
70
|
a_array = carray(a, ndim)
|
|
59
71
|
b_array = carray(b, ndim)
|
|
60
72
|
ab = 0.0
|
|
61
73
|
for i in range(ndim):
|
|
62
74
|
ab += a_array[i] * b_array[i]
|
|
63
|
-
return ab
|
|
75
|
+
return 1 - ab
|
|
64
76
|
|
|
65
|
-
return
|
|
77
|
+
return numba_ip
|
|
66
78
|
|
|
67
|
-
if
|
|
79
|
+
if metric == MetricKind.Cos:
|
|
68
80
|
|
|
81
|
+
@cfunc(signature)
|
|
69
82
|
def numba_cos(a, b, _n, _m):
|
|
70
83
|
a_array = carray(a, ndim)
|
|
71
84
|
b_array = carray(b, ndim)
|
|
@@ -76,28 +89,36 @@ def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
|
|
|
76
89
|
ab += a_array[i] * b_array[i]
|
|
77
90
|
a_sq += a_array[i] * a_array[i]
|
|
78
91
|
b_sq += b_array[i] * b_array[i]
|
|
79
|
-
|
|
92
|
+
a_norm = sqrt(a_sq)
|
|
93
|
+
b_norm = sqrt(b_sq)
|
|
94
|
+
if a_norm == 0 and b_norm == 0:
|
|
95
|
+
return 0
|
|
96
|
+
elif a_norm == 0 or b_norm == 0 or ab == 0:
|
|
97
|
+
return 1
|
|
98
|
+
else:
|
|
99
|
+
return 1 - ab / (a_norm * b_norm)
|
|
80
100
|
|
|
81
|
-
return
|
|
101
|
+
return numba_cos
|
|
82
102
|
|
|
83
|
-
if
|
|
103
|
+
if metric == MetricKind.L2sq:
|
|
84
104
|
|
|
105
|
+
@cfunc(signature)
|
|
85
106
|
def numba_l2sq(a, b, _n, _m):
|
|
86
107
|
a_array = carray(a, ndim)
|
|
87
108
|
b_array = carray(b, ndim)
|
|
88
|
-
ab_delta_sq = 0.0
|
|
109
|
+
ab_delta_sq = types.float32(0.0)
|
|
89
110
|
for i in range(ndim):
|
|
90
111
|
ab_delta_sq += (a_array[i] - b_array[i]) * \
|
|
91
112
|
(a_array[i] - b_array[i])
|
|
92
113
|
return ab_delta_sq
|
|
93
114
|
|
|
94
|
-
return
|
|
115
|
+
return numba_l2sq
|
|
95
116
|
|
|
96
117
|
return None
|
|
97
118
|
|
|
98
119
|
|
|
99
120
|
class Index:
|
|
100
|
-
"""Fast JIT-compiled index for equi-dimensional embeddings.
|
|
121
|
+
"""Fast JIT-compiled vector-search index for dense equi-dimensional embeddings.
|
|
101
122
|
|
|
102
123
|
Vector labels must be integers.
|
|
103
124
|
Vectors must have the same number of dimensions within the index.
|
|
@@ -109,8 +130,8 @@ class Index:
|
|
|
109
130
|
def __init__(
|
|
110
131
|
self,
|
|
111
132
|
ndim: int,
|
|
112
|
-
|
|
113
|
-
|
|
133
|
+
metric: Union[MetricKind, Callable] = MetricKind.IP,
|
|
134
|
+
dtype: Optional[str] = None,
|
|
114
135
|
jit: bool = False,
|
|
115
136
|
|
|
116
137
|
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
@@ -125,16 +146,21 @@ class Index:
|
|
|
125
146
|
|
|
126
147
|
:param ndim: Number of vector dimensions
|
|
127
148
|
:type ndim: int
|
|
128
|
-
|
|
149
|
+
|
|
150
|
+
:param metric: Distance function, defaults to MetricKind.IP
|
|
151
|
+
:type metric: Union[MetricKind, Callable], optional
|
|
152
|
+
Kind of the distance function, or the Numba `cfunc` JIT-compiled object.
|
|
153
|
+
Possible `MetricKind` values: IP, Cosine, L2sq, Haversine,
|
|
154
|
+
Hamming, Tanimoto, Sorensen.
|
|
155
|
+
Not every kind is JIT-able.
|
|
156
|
+
|
|
157
|
+
:param dtype: Scalar type for internal vector storage, defaults to None
|
|
129
158
|
:type dtype: str, optional
|
|
130
|
-
|
|
159
|
+
For continuous metrics can be: f16, f32, f64, or f8.
|
|
160
|
+
For bitwise metrics it's implementation-defined, and can't change.
|
|
161
|
+
Example: you can use the `f16` index with `f32` vectors in Euclidean space,
|
|
131
162
|
which will be automatically downcasted.
|
|
132
163
|
|
|
133
|
-
:param metric: Distance function, defaults to 'ip'
|
|
134
|
-
:type metric: Union[str, int], optional
|
|
135
|
-
Name of distance function, or the address of the
|
|
136
|
-
Numba `cfunc` JIT-compiled object.
|
|
137
|
-
|
|
138
164
|
:param jit: Enable Numba to JIT compile the metric, defaults to False
|
|
139
165
|
:type jit: bool, optional
|
|
140
166
|
This can result in up-to 3x performance difference on very large vectors
|
|
@@ -167,40 +193,54 @@ class Index:
|
|
|
167
193
|
:param view: Are we simply viewing an immutable index, defaults to False
|
|
168
194
|
:type view: bool, optional
|
|
169
195
|
"""
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
196
|
+
|
|
197
|
+
if metric is None:
|
|
198
|
+
metric = MetricKind.IP
|
|
199
|
+
|
|
200
|
+
if isinstance(metric, Callable):
|
|
201
|
+
self._metric_kind = MetricKind.Unknown
|
|
202
|
+
self._metric_jit = metric
|
|
203
|
+
self._metric_pointer = int(metric.address)
|
|
204
|
+
|
|
205
|
+
elif isinstance(metric, MetricKind):
|
|
206
|
+
if jit:
|
|
207
|
+
self._metric_kind = metric
|
|
208
|
+
self._metric_jit = jit_metric(
|
|
209
|
+
ndim=ndim,
|
|
210
|
+
metric_kind=metric,
|
|
211
|
+
dtype=dtype,
|
|
212
|
+
)
|
|
213
|
+
self._metric_pointer = self._metric_jit.address if \
|
|
214
|
+
self._metric_jit else 0
|
|
215
|
+
else:
|
|
216
|
+
self._metric_kind = metric
|
|
217
|
+
self._metric_jit = None
|
|
218
|
+
self._metric_pointer = 0
|
|
219
|
+
else:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
'The `metric` must be Numba callback or a `MetricKind`')
|
|
222
|
+
|
|
223
|
+
if metric in BitwiseMetricKind:
|
|
224
|
+
self._compiled = _CompiledBitsIndex(
|
|
225
|
+
bits=ndim,
|
|
226
|
+
metric=self._metric_kind,
|
|
227
|
+
connectivity=connectivity,
|
|
228
|
+
expansion_add=expansion_add,
|
|
229
|
+
expansion_search=expansion_search,
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
if dtype is None:
|
|
233
|
+
dtype = 'f32'
|
|
234
|
+
self._compiled = _CompiledIndex(
|
|
174
235
|
ndim=ndim,
|
|
175
|
-
|
|
236
|
+
metric=self._metric_kind,
|
|
237
|
+
metric_pointer=self._metric_pointer,
|
|
176
238
|
dtype=dtype,
|
|
239
|
+
connectivity=connectivity,
|
|
240
|
+
expansion_add=expansion_add,
|
|
241
|
+
expansion_search=expansion_search,
|
|
242
|
+
tune=tune,
|
|
177
243
|
)
|
|
178
|
-
self._metric_pointer = self._metric_jit.address if \
|
|
179
|
-
self._metric_jit else 0
|
|
180
|
-
|
|
181
|
-
elif isinstance(metric, int):
|
|
182
|
-
self._metric_name = ''
|
|
183
|
-
self._metric_jit = None
|
|
184
|
-
self._metric_pointer = metric
|
|
185
|
-
|
|
186
|
-
else:
|
|
187
|
-
if metric is None:
|
|
188
|
-
metric = 'ip'
|
|
189
|
-
assert isinstance(metric, str)
|
|
190
|
-
self._metric_name = metric
|
|
191
|
-
self._metric_jit = None
|
|
192
|
-
self._metric_pointer = 0
|
|
193
|
-
|
|
194
|
-
self._compiled = _CompiledIndex(
|
|
195
|
-
ndim=ndim,
|
|
196
|
-
metric=self._metric_name,
|
|
197
|
-
metric_pointer=self._metric_pointer,
|
|
198
|
-
dtype=dtype,
|
|
199
|
-
connectivity=connectivity,
|
|
200
|
-
expansion_add=expansion_add,
|
|
201
|
-
expansion_search=expansion_search,
|
|
202
|
-
tune=tune,
|
|
203
|
-
)
|
|
204
244
|
|
|
205
245
|
self.path = path
|
|
206
246
|
if path and os.path.exists(path):
|
|
@@ -235,21 +275,56 @@ class Index:
|
|
|
235
275
|
:param threads: Optimal number of cores to use, defaults to 0
|
|
236
276
|
:type threads: int, optional
|
|
237
277
|
"""
|
|
278
|
+
assert isinstance(vectors, np.ndarray), 'Expects a NumPy array'
|
|
279
|
+
assert vectors.ndim == 1 or vectors.ndim == 2, 'Expects a matrix or vector'
|
|
280
|
+
is_batch = vectors.ndim == 2
|
|
281
|
+
generate_labels = labels is None
|
|
282
|
+
|
|
283
|
+
# If no `labels` were provided, generate some
|
|
284
|
+
if generate_labels:
|
|
285
|
+
start_id = len(self._compiled)
|
|
286
|
+
if is_batch:
|
|
287
|
+
labels = np.arange(start_id, start_id + vectors.shape[0])
|
|
288
|
+
else:
|
|
289
|
+
labels = start_id
|
|
238
290
|
if isinstance(labels, np.ndarray):
|
|
239
291
|
labels = labels.astype(np.longlong)
|
|
292
|
+
|
|
240
293
|
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
241
294
|
|
|
295
|
+
if generate_labels:
|
|
296
|
+
return labels
|
|
297
|
+
|
|
242
298
|
def search(
|
|
243
299
|
self, vectors, k: int = 10, *,
|
|
244
300
|
threads: int = 0, exact: bool = False) -> Matches:
|
|
301
|
+
"""Performs approximate nearest neighbors search for one or more queries.
|
|
302
|
+
|
|
303
|
+
:param vectors: Query vector or vectors.
|
|
304
|
+
:type vectors: Buffer
|
|
305
|
+
:param k: Upper limit on the number of matches to find, defaults to 10
|
|
306
|
+
:type k: int, optional
|
|
307
|
+
|
|
308
|
+
:param threads: Optimal number of cores to use, defaults to 0
|
|
309
|
+
:type threads: int, optional
|
|
310
|
+
:param exact: Perform exhaustive linear-time exact search, defaults to False
|
|
311
|
+
:type exact: bool, optional
|
|
312
|
+
:return: Approximate matches for one or more queries
|
|
313
|
+
:rtype: Matches
|
|
314
|
+
"""
|
|
245
315
|
tuple_ = self._compiled.search(
|
|
246
316
|
vectors, k,
|
|
247
|
-
exact=exact, threads=threads
|
|
317
|
+
exact=exact, threads=threads,
|
|
318
|
+
)
|
|
248
319
|
return Matches(*tuple_)
|
|
249
320
|
|
|
250
321
|
def __len__(self) -> int:
|
|
251
322
|
return len(self._compiled)
|
|
252
323
|
|
|
324
|
+
@property
|
|
325
|
+
def jit(self) -> bool:
|
|
326
|
+
return self._metric_jit is not None
|
|
327
|
+
|
|
253
328
|
@property
|
|
254
329
|
def size(self) -> int:
|
|
255
330
|
return self._compiled.size
|
package/rust/lib.cpp
CHANGED
|
@@ -5,44 +5,50 @@ using namespace unum::usearch;
|
|
|
5
5
|
using namespace unum;
|
|
6
6
|
|
|
7
7
|
using punned_t = typename Index::punned_t;
|
|
8
|
+
using add_result_t = typename punned_t::add_result_t;
|
|
9
|
+
using search_result_t = typename punned_t::search_result_t;
|
|
10
|
+
using serialization_result_t = typename punned_t::serialization_result_t;
|
|
8
11
|
|
|
9
|
-
Index::Index(std::
|
|
12
|
+
Index::Index(std::unique_ptr<punned_t> index) : index_(std::move(index)) {}
|
|
10
13
|
|
|
11
14
|
void Index::add_in_thread(uint32_t label, rust::Slice<float const> vector, size_t thread) const {
|
|
12
15
|
add_config_t config;
|
|
13
16
|
config.thread = thread;
|
|
14
|
-
index_->add(label, vector.data(), config);
|
|
17
|
+
index_->add(label, vector.data(), config).error.raise();
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
Matches Index::search_in_thread(rust::Slice<float const> vector, size_t count, size_t thread) const {
|
|
18
|
-
Matches
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
Matches matches;
|
|
22
|
+
matches.labels.reserve(count);
|
|
23
|
+
matches.distances.reserve(count);
|
|
21
24
|
for (size_t i = 0; i != count; ++i)
|
|
22
|
-
|
|
25
|
+
matches.labels.push_back(0), matches.distances.push_back(0);
|
|
23
26
|
search_config_t config;
|
|
24
27
|
config.thread = thread;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
search_result_t result = index_->search(vector.data(), count, config);
|
|
29
|
+
result.error.raise();
|
|
30
|
+
matches.count = result.dump_to(matches.labels.data(), matches.distances.data());
|
|
31
|
+
matches.labels.truncate(matches.count);
|
|
32
|
+
matches.distances.truncate(matches.count);
|
|
33
|
+
return matches;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
void Index::add(uint32_t label, rust::Slice<float const> vector) const {
|
|
33
|
-
index_->add(label, vector.data());
|
|
37
|
+
index_->add(label, vector.data()).error.raise();
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
Matches Index::search(rust::Slice<float const> vector, size_t count) const {
|
|
37
|
-
Matches
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
Matches matches;
|
|
42
|
+
matches.labels.reserve(count);
|
|
43
|
+
matches.distances.reserve(count);
|
|
40
44
|
for (size_t i = 0; i != count; ++i)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
matches.labels.push_back(0), matches.distances.push_back(0);
|
|
46
|
+
search_result_t result = index_->search(vector.data(), count);
|
|
47
|
+
result.error.raise();
|
|
48
|
+
matches.count = result.dump_to(matches.labels.data(), matches.distances.data());
|
|
49
|
+
matches.labels.truncate(matches.count);
|
|
50
|
+
matches.distances.truncate(matches.count);
|
|
51
|
+
return matches;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
void Index::reserve(size_t capacity) const { index_->reserve(capacity); }
|
|
@@ -59,12 +65,15 @@ void Index::view(rust::Str path) const { index_->view(std::string(path).c_str())
|
|
|
59
65
|
accuracy_t accuracy(rust::Str quant) { return accuracy_from_name(quant.data(), quant.size()); }
|
|
60
66
|
|
|
61
67
|
std::unique_ptr<Index> wrap(punned_t&& index) {
|
|
62
|
-
std::
|
|
63
|
-
|
|
68
|
+
std::unique_ptr<punned_t> punned_ptr;
|
|
69
|
+
punned_ptr.reset(new punned_t(std::move(index)));
|
|
70
|
+
std::unique_ptr<Index> result;
|
|
71
|
+
result.reset(new Index(std::move(punned_ptr)));
|
|
72
|
+
return result;
|
|
64
73
|
}
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
index_config_t config(size_t connectivity, size_t exp_add, size_t exp_search) {
|
|
76
|
+
index_config_t result;
|
|
68
77
|
result.connectivity = connectivity ? connectivity : default_connectivity();
|
|
69
78
|
result.expansion_add = exp_add ? exp_add : default_expansion_add();
|
|
70
79
|
result.expansion_search = exp_search ? exp_search : default_expansion_search();
|
package/rust/lib.hpp
CHANGED
|
@@ -17,7 +17,7 @@ class Index {
|
|
|
17
17
|
using distance_t = unum::usearch::punned_distance_t;
|
|
18
18
|
using punned_t = unum::usearch::punned_gt<label_t>;
|
|
19
19
|
|
|
20
|
-
Index(std::
|
|
20
|
+
Index(std::unique_ptr<punned_t> index);
|
|
21
21
|
|
|
22
22
|
void reserve(size_t) const;
|
|
23
23
|
|
|
@@ -37,7 +37,7 @@ class Index {
|
|
|
37
37
|
void view(rust::Str path) const;
|
|
38
38
|
|
|
39
39
|
private:
|
|
40
|
-
std::
|
|
40
|
+
std::unique_ptr<punned_t> index_;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
std::unique_ptr<Index> new_ip(size_t dims, rust::Str quant, size_t connectivity, size_t exp_add, size_t exp_search);
|
package/setup.py
CHANGED
|
@@ -14,7 +14,7 @@ if sys.platform == 'linux':
|
|
|
14
14
|
compile_args.append('-Wno-unknown-pragmas')
|
|
15
15
|
|
|
16
16
|
if sys.platform == 'darwin':
|
|
17
|
-
# MacOS 10.15
|
|
17
|
+
# MacOS 10.15 or higher is needed for `aligned_alloc` support.
|
|
18
18
|
# https://github.com/unum-cloud/usearch/actions/runs/4975434891/jobs/8902603392
|
|
19
19
|
compile_args.append('-mmacosx-version-min=10.15')
|
|
20
20
|
compile_args.append('-Wno-unknown-pragmas')
|
|
@@ -82,4 +82,7 @@ setup(
|
|
|
82
82
|
'include', 'src',
|
|
83
83
|
'fp16/include', 'robin-map/include', 'simsimd/include'],
|
|
84
84
|
ext_modules=ext_modules,
|
|
85
|
+
extras_require={
|
|
86
|
+
'jit': ['numba'],
|
|
87
|
+
},
|
|
85
88
|
)
|
package/src/CMakeLists.txt
CHANGED
|
@@ -12,6 +12,7 @@ endif()
|
|
|
12
12
|
# List of all possible compiler IDs:
|
|
13
13
|
# https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html
|
|
14
14
|
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
15
|
+
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
|
15
16
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address")
|
|
16
17
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wall -Wextra -Wno-conversion -Wno-unknown-pragmas")
|
|
17
18
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native")
|
|
@@ -20,11 +21,14 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
|
|
20
21
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic")
|
|
21
22
|
|
|
22
23
|
elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
|
24
|
+
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
|
25
|
+
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address")
|
|
23
26
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Wfatal-errors")
|
|
24
27
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fopenmp")
|
|
25
28
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pedantic")
|
|
26
29
|
|
|
27
30
|
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "NVIDIA" OR CMAKE_CXX_COMPILER_ID STREQUAL "NVHPC")
|
|
31
|
+
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
|
28
32
|
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} --expt-relaxed-constexpr --extended-lambda")
|
|
29
33
|
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -gencode arch=compute_86,code=sm_86")
|
|
30
34
|
set_property(SOURCE bench.cpp PROPERTY LANGUAGE CUDA)
|
|
@@ -39,10 +43,6 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
|
|
|
39
43
|
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} /DEBUG")
|
|
40
44
|
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2")
|
|
41
45
|
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} /O2 /DEBUG")
|
|
42
|
-
else()
|
|
43
|
-
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")
|
|
44
|
-
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O3")
|
|
45
|
-
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -O3 -pg -g")
|
|
46
46
|
endif()
|
|
47
47
|
|
|
48
48
|
if(APPLE)
|