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.
@@ -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
- sizes = [1, 2, 100]
9
- index_types = ['f64', 'f32', 'f16', 'f8']
10
- numpy_types = [np.float64, np.float32, np.float16, np.byte]
11
- connectivities = [3, 13, 50]
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', connectivities)
18
- def test_l2sq(ndim: int, index_type: str, numpy_type: str, connectivity: int):
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='l2sq',
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
- vector = np.random.uniform(0, 0.7, (index.ndim)).astype(numpy_type)
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('size', sizes)
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 test_l2sq_batch(ndim: int, size: int, index_type: str, numpy_type: str):
52
- index = Index(ndim=ndim, metric='l2sq', dtype=index_type)
92
+ def test_index_batch(
93
+ ndim: int, metric: MetricKind,
94
+ batch_size: int, index_type: str, numpy_type: str):
53
95
 
54
- vectors = np.random.uniform(0, 0.7, (size, index.ndim)).astype(numpy_type)
55
- labels = np.array(range(size), dtype=np.longlong)
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] == size
107
+ assert count.shape[0] == batch_size
62
108
 
63
109
 
64
110
  @pytest.mark.parametrize('ndim', dimensions)
65
- @pytest.mark.parametrize('size', sizes)
66
- def test_user_defined_function(ndim: int, size: int):
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.address)
135
+ index = Index(ndim=ndim, metric=python_dot)
90
136
 
91
137
  vectors = np.random.uniform(
92
- 0, 0.3, (size, index.ndim)).astype(np.float32)
93
- labels = np.array(range(size), dtype=np.longlong)
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] == size
145
+ assert count.shape[0] == batch_size
100
146
 
101
147
 
102
- def test_sets():
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
- # results = index.search(np.array([12, 15], dtype=np.uint32), 10)
108
- # assert list(results) == [10, 11]
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
- if __name__ == '__main__':
112
- pass
170
+ index.add(labels, bit_vectors)
171
+ index.search(bit_vectors, 10)
@@ -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) -> Triplet:
57
- matches: list[dict] = []
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) -> Triplet:
77
+ def search_many(self, vectors: np.ndarray, count: int) -> Matches:
79
78
  batch_size: int = vectors.shape[0]
80
- list_of_matches: list[list[dict]] = self.client.search_many(
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) -> Triplet:
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:
@@ -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 HashIndex as _CompiledHashIndex
14
- from usearch.compiled import DEFAULT_CONNECTIVITY, DEFAULT_EXPANSION_ADD, DEFAULT_EXPANSION_SEARCH
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
- def results_to_list(results: Triplet, row: int) -> list[dict]:
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, metric_name: str, accuracy: str = 'f32') -> Callable:
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 accuracy == 'f32':
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 metric_name == 'ip':
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 cfunc(numba_ip, signature)
77
+ return numba_ip
61
78
 
62
- if metric_name == 'cos':
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
- return ab / (sqrt(a_sq) * sqrt(b_sq))
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 cfunc(numba_cos, signature)
101
+ return numba_cos
77
102
 
78
- if metric_name == 'l2sq':
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 cfunc(numba_l2sq, signature)
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
- dtype: str = 'f32',
108
- metric: Union[str, int] = 'ip',
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
- :param dtype: Scalar type for vectors, defaults to 'f32'
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
- Example: you can use the `f16` index with `f32` vectors,
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
- if jit:
166
- assert isinstance(metric, str), 'Name the metric to JIT'
167
- self._metric_name = metric
168
- self._metric_jit = jit_metric(
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
- metric_name=metric,
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) -> Triplet:
240
- return self._compiled.search(vectors, k, exact=exact, threads=threads)
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
@@ -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) -> list[dict]:
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 _results_to_json(results, 0)
75
+ return list_matches(results, 0)
86
76
 
87
77
  @server
88
- def search_many(vectors: np.ndarray, count: int) -> list[list[dict]]:
78
+ def search_many(vectors: np.ndarray, count: int) -> List[List[dict]]:
89
79
  results = index.search(vectors, count)
90
- return [_results_to_json(results, i) for i in range(vectors.shape[0])]
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::shared_ptr<punned_t> index) : index_(index) {}
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 results;
19
- results.labels.reserve(count);
20
- results.distances.reserve(count);
21
+ Matches matches;
22
+ matches.labels.reserve(count);
23
+ matches.distances.reserve(count);
21
24
  for (size_t i = 0; i != count; ++i)
22
- results.labels.push_back(0), results.distances.push_back(0);
25
+ matches.labels.push_back(0), matches.distances.push_back(0);
23
26
  search_config_t config;
24
27
  config.thread = thread;
25
- results.count =
26
- index_->search(vector.data(), count, config).dump_to(results.labels.data(), results.distances.data());
27
- results.labels.truncate(results.count);
28
- results.distances.truncate(results.count);
29
- return results;
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 results;
38
- results.labels.reserve(count);
39
- results.distances.reserve(count);
41
+ Matches matches;
42
+ matches.labels.reserve(count);
43
+ matches.distances.reserve(count);
40
44
  for (size_t i = 0; i != count; ++i)
41
- results.labels.push_back(0), results.distances.push_back(0);
42
- results.count = index_->search(vector.data(), count).dump_to(results.labels.data(), results.distances.data());
43
- results.labels.truncate(results.count);
44
- results.distances.truncate(results.count);
45
- return results;
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::shared_ptr<punned_t> native = std::make_shared<punned_t>(std::move(index));
63
- return std::unique_ptr<Index>(new Index(native));
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
- config_t config(size_t connectivity, size_t exp_add, size_t exp_search) {
67
- config_t result;
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();