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.
@@ -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.size_t, types.size_t)
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.size_t, types.size_t)
119
+ types.uint64, types.uint64)
120
120
 
121
121
  @cfunc(signature_f8)
122
122
  def cos_f8(a, b, n, m):
@@ -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
@@ -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.size_t, types.size_t)
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.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)
@@ -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 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
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, metric_name: str, accuracy: str = 'f32') -> Callable:
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 accuracy == 'f32':
60
+ if dtype == 'f32':
50
61
  signature = types.float32(
51
62
  types.CPointer(types.float32),
52
63
  types.CPointer(types.float32),
53
- types.size_t, types.size_t)
64
+ types.uint64, types.uint64)
54
65
 
55
- if metric_name == 'ip':
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 cfunc(numba_ip, signature)
77
+ return numba_ip
66
78
 
67
- if metric_name == 'cos':
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
- 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)
80
100
 
81
- return cfunc(numba_cos, signature)
101
+ return numba_cos
82
102
 
83
- if metric_name == 'l2sq':
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 cfunc(numba_l2sq, signature)
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
- dtype: str = 'f32',
113
- metric: Union[str, int] = 'ip',
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
- :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
129
158
  :type dtype: str, optional
130
- 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,
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
- if jit:
171
- assert isinstance(metric, str), 'Name the metric to JIT'
172
- self._metric_name = metric
173
- 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(
174
235
  ndim=ndim,
175
- metric_name=metric,
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::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();
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::shared_ptr<punned_t> index);
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::shared_ptr<punned_t> index_;
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 orhigher is needed for `aligned_alloc` support.
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
  )
@@ -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)