usearch 0.11.0 → 0.12.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/prerelease.yml +1 -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 +42 -7
- 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/pyproject.toml +1 -1
- package/python/lib.cpp +354 -181
- package/python/scripts/test.py +87 -28
- package/python/usearch/client.py +8 -9
- package/python/usearch/index.py +143 -60
- package/python/usearch/server.py +6 -16
- 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/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
|
|
@@ -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/client.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
from typing import Union, Optional
|
|
1
|
+
from typing import Union, Optional, List
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
from ucall.client import Client
|
|
5
5
|
|
|
6
|
+
from usearch.index import Matches
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
|
|
8
10
|
if vector.dtype != np.int8 and vector.dtype != np.uint8 and vector.dtype != np.byte:
|
|
@@ -18,9 +20,6 @@ def _vector_to_ascii(vector: np.ndarray) -> Optional[str]:
|
|
|
18
20
|
return ascii
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
22
|
-
|
|
23
|
-
|
|
24
23
|
class IndexClient:
|
|
25
24
|
|
|
26
25
|
def __init__(
|
|
@@ -53,8 +52,8 @@ class IndexClient:
|
|
|
53
52
|
else:
|
|
54
53
|
return self.add_many(labels, vectors)
|
|
55
54
|
|
|
56
|
-
def search_one(self, vector: np.ndarray, count: int) ->
|
|
57
|
-
matches:
|
|
55
|
+
def search_one(self, vector: np.ndarray, count: int) -> Matches:
|
|
56
|
+
matches: List[dict] = []
|
|
58
57
|
vector = vector.flatten()
|
|
59
58
|
ascii = _vector_to_ascii(vector)
|
|
60
59
|
if ascii:
|
|
@@ -75,9 +74,9 @@ class IndexClient:
|
|
|
75
74
|
|
|
76
75
|
return labels, distances, counts
|
|
77
76
|
|
|
78
|
-
def search_many(self, vectors: np.ndarray, count: int) ->
|
|
77
|
+
def search_many(self, vectors: np.ndarray, count: int) -> Matches:
|
|
79
78
|
batch_size: int = vectors.shape[0]
|
|
80
|
-
list_of_matches:
|
|
79
|
+
list_of_matches: List[List[dict]] = self.client.search_many(
|
|
81
80
|
vectors=vectors, count=count)
|
|
82
81
|
|
|
83
82
|
labels = np.array((batch_size, count), dtype=np.uint32)
|
|
@@ -91,7 +90,7 @@ class IndexClient:
|
|
|
91
90
|
|
|
92
91
|
return labels, distances, counts
|
|
93
92
|
|
|
94
|
-
def search(self, vectors: np.ndarray, count: int) ->
|
|
93
|
+
def search(self, vectors: np.ndarray, count: int) -> Matches:
|
|
95
94
|
if vectors.ndim == 1 or (vectors.ndim == 2 and vectors.shape[0] == 1):
|
|
96
95
|
return self.search_one(vectors, count)
|
|
97
96
|
else:
|
package/python/usearch/index.py
CHANGED
|
@@ -4,21 +4,37 @@
|
|
|
4
4
|
# into the primary `Index` class, connecting USearch with Numba.
|
|
5
5
|
import os
|
|
6
6
|
from math import sqrt
|
|
7
|
-
from typing import Optional, Callable, Union
|
|
7
|
+
from typing import Optional, Callable, Union, NamedTuple, List
|
|
8
8
|
|
|
9
9
|
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
|
-
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
17
28
|
SetsIndex = _CompiledSetsIndex
|
|
18
|
-
HashIndex = _CompiledHashIndex
|
|
19
29
|
|
|
20
30
|
|
|
21
|
-
|
|
31
|
+
class Matches(NamedTuple):
|
|
32
|
+
labels: np.ndarray
|
|
33
|
+
distances: np.ndarray
|
|
34
|
+
counts: np.ndarray
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def list_matches(results: Matches, row: int) -> List[dict]:
|
|
22
38
|
|
|
23
39
|
count = results[2][row]
|
|
24
40
|
labels = results[0][row, :count]
|
|
@@ -29,7 +45,7 @@ def results_to_list(results: Triplet, row: int) -> list[dict]:
|
|
|
29
45
|
]
|
|
30
46
|
|
|
31
47
|
|
|
32
|
-
def jit_metric(ndim: int,
|
|
48
|
+
def jit_metric(ndim: int, metric: MetricKind, dtype: str = 'f32') -> Callable:
|
|
33
49
|
|
|
34
50
|
try:
|
|
35
51
|
from numba import cfunc, types, carray
|
|
@@ -41,26 +57,28 @@ def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
|
|
|
41
57
|
# Showcases how to use Numba to JIT-compile similarity measures for USearch.
|
|
42
58
|
# https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
43
59
|
|
|
44
|
-
if
|
|
60
|
+
if dtype == 'f32':
|
|
45
61
|
signature = types.float32(
|
|
46
62
|
types.CPointer(types.float32),
|
|
47
63
|
types.CPointer(types.float32),
|
|
48
64
|
types.size_t, types.size_t)
|
|
49
65
|
|
|
50
|
-
if
|
|
66
|
+
if metric == MetricKind.IP:
|
|
51
67
|
|
|
68
|
+
@cfunc(signature)
|
|
52
69
|
def numba_ip(a, b, _n, _m):
|
|
53
70
|
a_array = carray(a, ndim)
|
|
54
71
|
b_array = carray(b, ndim)
|
|
55
72
|
ab = 0.0
|
|
56
73
|
for i in range(ndim):
|
|
57
74
|
ab += a_array[i] * b_array[i]
|
|
58
|
-
return ab
|
|
75
|
+
return 1 - ab
|
|
59
76
|
|
|
60
|
-
return
|
|
77
|
+
return numba_ip
|
|
61
78
|
|
|
62
|
-
if
|
|
79
|
+
if metric == MetricKind.Cos:
|
|
63
80
|
|
|
81
|
+
@cfunc(signature)
|
|
64
82
|
def numba_cos(a, b, _n, _m):
|
|
65
83
|
a_array = carray(a, ndim)
|
|
66
84
|
b_array = carray(b, ndim)
|
|
@@ -71,28 +89,36 @@ def jit_metric(ndim: int, metric_name: str, accuracy: str = 'f32') -> Callable:
|
|
|
71
89
|
ab += a_array[i] * b_array[i]
|
|
72
90
|
a_sq += a_array[i] * a_array[i]
|
|
73
91
|
b_sq += b_array[i] * b_array[i]
|
|
74
|
-
|
|
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)
|
|
75
100
|
|
|
76
|
-
return
|
|
101
|
+
return numba_cos
|
|
77
102
|
|
|
78
|
-
if
|
|
103
|
+
if metric == MetricKind.L2sq:
|
|
79
104
|
|
|
105
|
+
@cfunc(signature)
|
|
80
106
|
def numba_l2sq(a, b, _n, _m):
|
|
81
107
|
a_array = carray(a, ndim)
|
|
82
108
|
b_array = carray(b, ndim)
|
|
83
|
-
ab_delta_sq = 0.0
|
|
109
|
+
ab_delta_sq = types.float32(0.0)
|
|
84
110
|
for i in range(ndim):
|
|
85
111
|
ab_delta_sq += (a_array[i] - b_array[i]) * \
|
|
86
112
|
(a_array[i] - b_array[i])
|
|
87
113
|
return ab_delta_sq
|
|
88
114
|
|
|
89
|
-
return
|
|
115
|
+
return numba_l2sq
|
|
90
116
|
|
|
91
117
|
return None
|
|
92
118
|
|
|
93
119
|
|
|
94
120
|
class Index:
|
|
95
|
-
"""Fast JIT-compiled index for equi-dimensional embeddings.
|
|
121
|
+
"""Fast JIT-compiled vector-search index for dense equi-dimensional embeddings.
|
|
96
122
|
|
|
97
123
|
Vector labels must be integers.
|
|
98
124
|
Vectors must have the same number of dimensions within the index.
|
|
@@ -104,8 +130,8 @@ class Index:
|
|
|
104
130
|
def __init__(
|
|
105
131
|
self,
|
|
106
132
|
ndim: int,
|
|
107
|
-
|
|
108
|
-
|
|
133
|
+
metric: Union[MetricKind, Callable] = MetricKind.IP,
|
|
134
|
+
dtype: Optional[str] = None,
|
|
109
135
|
jit: bool = False,
|
|
110
136
|
|
|
111
137
|
connectivity: int = DEFAULT_CONNECTIVITY,
|
|
@@ -120,16 +146,21 @@ class Index:
|
|
|
120
146
|
|
|
121
147
|
:param ndim: Number of vector dimensions
|
|
122
148
|
:type ndim: int
|
|
123
|
-
|
|
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
|
|
124
158
|
:type dtype: str, optional
|
|
125
|
-
|
|
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,
|
|
126
162
|
which will be automatically downcasted.
|
|
127
163
|
|
|
128
|
-
:param metric: Distance function, defaults to 'ip'
|
|
129
|
-
:type metric: Union[str, int], optional
|
|
130
|
-
Name of distance function, or the address of the
|
|
131
|
-
Numba `cfunc` JIT-compiled object.
|
|
132
|
-
|
|
133
164
|
:param jit: Enable Numba to JIT compile the metric, defaults to False
|
|
134
165
|
:type jit: bool, optional
|
|
135
166
|
This can result in up-to 3x performance difference on very large vectors
|
|
@@ -162,40 +193,54 @@ class Index:
|
|
|
162
193
|
:param view: Are we simply viewing an immutable index, defaults to False
|
|
163
194
|
:type view: bool, optional
|
|
164
195
|
"""
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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(
|
|
169
235
|
ndim=ndim,
|
|
170
|
-
|
|
236
|
+
metric=self._metric_kind,
|
|
237
|
+
metric_pointer=self._metric_pointer,
|
|
171
238
|
dtype=dtype,
|
|
239
|
+
connectivity=connectivity,
|
|
240
|
+
expansion_add=expansion_add,
|
|
241
|
+
expansion_search=expansion_search,
|
|
242
|
+
tune=tune,
|
|
172
243
|
)
|
|
173
|
-
self._metric_pointer = self._metric_jit.address if \
|
|
174
|
-
self._metric_jit else 0
|
|
175
|
-
|
|
176
|
-
elif isinstance(metric, int):
|
|
177
|
-
self._metric_name = ''
|
|
178
|
-
self._metric_jit = None
|
|
179
|
-
self._metric_pointer = metric
|
|
180
|
-
|
|
181
|
-
else:
|
|
182
|
-
if metric is None:
|
|
183
|
-
metric = 'ip'
|
|
184
|
-
assert isinstance(metric, str)
|
|
185
|
-
self._metric_name = metric
|
|
186
|
-
self._metric_jit = None
|
|
187
|
-
self._metric_pointer = 0
|
|
188
|
-
|
|
189
|
-
self._compiled = _CompiledIndex(
|
|
190
|
-
ndim=ndim,
|
|
191
|
-
metric=self._metric_name,
|
|
192
|
-
metric_pointer=self._metric_pointer,
|
|
193
|
-
dtype=dtype,
|
|
194
|
-
connectivity=connectivity,
|
|
195
|
-
expansion_add=expansion_add,
|
|
196
|
-
expansion_search=expansion_search,
|
|
197
|
-
tune=tune,
|
|
198
|
-
)
|
|
199
244
|
|
|
200
245
|
self.path = path
|
|
201
246
|
if path and os.path.exists(path):
|
|
@@ -230,18 +275,56 @@ class Index:
|
|
|
230
275
|
:param threads: Optimal number of cores to use, defaults to 0
|
|
231
276
|
:type threads: int, optional
|
|
232
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
|
|
233
290
|
if isinstance(labels, np.ndarray):
|
|
234
291
|
labels = labels.astype(np.longlong)
|
|
292
|
+
|
|
235
293
|
self._compiled.add(labels, vectors, copy=copy, threads=threads)
|
|
236
294
|
|
|
295
|
+
if generate_labels:
|
|
296
|
+
return labels
|
|
297
|
+
|
|
237
298
|
def search(
|
|
238
299
|
self, vectors, k: int = 10, *,
|
|
239
|
-
threads: int = 0, exact: bool = False) ->
|
|
240
|
-
|
|
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
|
+
"""
|
|
315
|
+
tuple_ = self._compiled.search(
|
|
316
|
+
vectors, k,
|
|
317
|
+
exact=exact, threads=threads,
|
|
318
|
+
)
|
|
319
|
+
return Matches(*tuple_)
|
|
241
320
|
|
|
242
321
|
def __len__(self) -> int:
|
|
243
322
|
return len(self._compiled)
|
|
244
323
|
|
|
324
|
+
@property
|
|
325
|
+
def jit(self) -> bool:
|
|
326
|
+
return self._metric_jit is not None
|
|
327
|
+
|
|
245
328
|
@property
|
|
246
329
|
def size(self) -> int:
|
|
247
330
|
return self._compiled.size
|
package/python/usearch/server.py
CHANGED
|
@@ -4,20 +4,10 @@
|
|
|
4
4
|
import os
|
|
5
5
|
import argparse
|
|
6
6
|
import numpy as np
|
|
7
|
+
from typing import List
|
|
7
8
|
|
|
8
9
|
from ucall.rich_posix import Server
|
|
9
|
-
from usearch.index import Index
|
|
10
|
-
|
|
11
|
-
Triplet = tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _results_to_json(results: Triplet, row: int) -> list[dict]:
|
|
15
|
-
|
|
16
|
-
count = results[2][row]
|
|
17
|
-
labels = results[0][row, :count]
|
|
18
|
-
distances = results[1][row, :count]
|
|
19
|
-
return [{'label': int(label), 'distance': float(distance)}
|
|
20
|
-
for label, distance in zip(labels, distances)]
|
|
10
|
+
from usearch.index import Index, list_matches
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
def _ascii_to_vector(string: str) -> np.ndarray:
|
|
@@ -78,16 +68,16 @@ def serve(
|
|
|
78
68
|
index.add(labels, vectors, threads=threads)
|
|
79
69
|
|
|
80
70
|
@server
|
|
81
|
-
def search_one(vector: np.ndarray, count: int) ->
|
|
71
|
+
def search_one(vector: np.ndarray, count: int) -> List[dict]:
|
|
82
72
|
print('search', vector, count)
|
|
83
73
|
vectors = vector.reshape(vector.shape[0], 1)
|
|
84
74
|
results = index.search(vectors, count)
|
|
85
|
-
return
|
|
75
|
+
return list_matches(results, 0)
|
|
86
76
|
|
|
87
77
|
@server
|
|
88
|
-
def search_many(vectors: np.ndarray, count: int) ->
|
|
78
|
+
def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
|
|
89
79
|
results = index.search(vectors, count)
|
|
90
|
-
return [
|
|
80
|
+
return [list_matches(results, i) for i in range(vectors.shape[0])]
|
|
91
81
|
|
|
92
82
|
@server
|
|
93
83
|
def add_ascii(label: int, string: str):
|
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();
|