usearch 0.18.6 → 0.19.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.
@@ -5,7 +5,16 @@ import numpy as np
5
5
  from usearch.io import load_matrix, save_matrix
6
6
  from usearch.eval import recall_members, random_vectors
7
7
 
8
- from usearch.index import Index, SparseIndex, MetricKind, ScalarKind, Matches, MetricKindBitwise
8
+ from usearch.index import (
9
+ Index,
10
+ SparseIndex,
11
+ MetricKind,
12
+ ScalarKind,
13
+ MetricSignature,
14
+ CompiledMetric,
15
+ Matches,
16
+ MetricKindBitwise,
17
+ )
9
18
  from usearch.index import _normalize_dtype
10
19
  from usearch.index import (
11
20
  DEFAULT_CONNECTIVITY,
@@ -17,8 +26,10 @@ from usearch.index import (
17
26
  dimensions = [3, 97, 256]
18
27
  batch_sizes = [1, 33]
19
28
  index_types = [
20
- ScalarKind.F32, ScalarKind.F64,
21
- ScalarKind.F16, ScalarKind.F8,
29
+ ScalarKind.F32,
30
+ ScalarKind.F64,
31
+ ScalarKind.F16,
32
+ ScalarKind.F8,
22
33
  ]
23
34
  numpy_types = [np.float32, np.float64, np.float16]
24
35
 
@@ -35,39 +46,40 @@ hash_metrics = [
35
46
  ]
36
47
 
37
48
 
38
- @pytest.mark.parametrize('rows', batch_sizes)
39
- @pytest.mark.parametrize('cols', dimensions)
49
+ @pytest.mark.parametrize("rows", batch_sizes)
50
+ @pytest.mark.parametrize("cols", dimensions)
40
51
  def test_serializing_fbin_matrix(rows: int, cols: int):
41
-
42
52
  original = np.random.rand(rows, cols).astype(np.float32)
43
- save_matrix(original, 'tmp.fbin')
44
- reconstructed = load_matrix('tmp.fbin')
53
+ save_matrix(original, "tmp.fbin")
54
+ reconstructed = load_matrix("tmp.fbin")
45
55
  assert np.allclose(original, reconstructed)
46
- os.remove('tmp.fbin')
56
+ os.remove("tmp.fbin")
47
57
 
48
58
 
49
- @pytest.mark.parametrize('rows', batch_sizes)
50
- @pytest.mark.parametrize('cols', dimensions)
59
+ @pytest.mark.parametrize("rows", batch_sizes)
60
+ @pytest.mark.parametrize("cols", dimensions)
51
61
  def test_serializing_ibin_matrix(rows: int, cols: int):
52
-
53
- original = np.random.randint(0, rows+1, size=(rows, cols)).astype(np.int32)
54
- save_matrix(original, 'tmp.ibin')
55
- reconstructed = load_matrix('tmp.ibin')
62
+ original = np.random.randint(0, rows + 1, size=(rows, cols)).astype(np.int32)
63
+ save_matrix(original, "tmp.ibin")
64
+ reconstructed = load_matrix("tmp.ibin")
56
65
  assert np.allclose(original, reconstructed)
57
- os.remove('tmp.ibin')
66
+ os.remove("tmp.ibin")
58
67
 
59
68
 
60
- @pytest.mark.parametrize('ndim', dimensions)
61
- @pytest.mark.parametrize('metric', continuous_metrics)
62
- @pytest.mark.parametrize('index_type', index_types)
63
- @pytest.mark.parametrize('numpy_type', numpy_types)
64
- @pytest.mark.parametrize('connectivity', connectivity_options)
65
- @pytest.mark.parametrize('jit', jit_options)
69
+ @pytest.mark.parametrize("ndim", dimensions)
70
+ @pytest.mark.parametrize("metric", continuous_metrics)
71
+ @pytest.mark.parametrize("index_type", index_types)
72
+ @pytest.mark.parametrize("numpy_type", numpy_types)
73
+ @pytest.mark.parametrize("connectivity", connectivity_options)
74
+ @pytest.mark.parametrize("jit", jit_options)
66
75
  def test_index(
67
- ndim: int, metric: MetricKind,
68
- index_type: str, numpy_type: str,
69
- connectivity: int, jit: bool):
70
-
76
+ ndim: int,
77
+ metric: MetricKind,
78
+ index_type: str,
79
+ numpy_type: str,
80
+ connectivity: int,
81
+ jit: bool,
82
+ ):
71
83
  index = Index(
72
84
  metric=metric,
73
85
  ndim=ndim,
@@ -89,6 +101,7 @@ def test_index(
89
101
  assert 43 not in index
90
102
  assert index[42] is not None
91
103
  assert index[43] is None
104
+ assert len(index[42]) == ndim
92
105
  if numpy_type != np.byte:
93
106
  assert np.allclose(index[42], vector, atol=0.1)
94
107
 
@@ -100,23 +113,30 @@ def test_index(
100
113
  assert matches[0] == 42
101
114
  assert distances[0] == pytest.approx(0, abs=1e-3)
102
115
 
103
- index.save('tmp.usearch')
116
+ index.save("tmp.usearch")
104
117
  index.clear()
105
118
  assert len(index) == 0
106
119
 
107
- index.load('tmp.usearch')
120
+ index.load("tmp.usearch")
108
121
  assert len(index) == 1
122
+ assert len(index[42]) == ndim
109
123
 
124
+ index = Index.restore("tmp.usearch")
125
+ assert len(index) == 1
126
+ assert len(index[42]) == ndim
110
127
 
111
- @pytest.mark.parametrize('ndim', dimensions)
112
- @pytest.mark.parametrize('metric', continuous_metrics)
113
- @pytest.mark.parametrize('batch_size', batch_sizes)
114
- @pytest.mark.parametrize('index_type', index_types)
115
- @pytest.mark.parametrize('numpy_type', numpy_types)
116
- def test_index_batch(
117
- ndim: int, metric: MetricKind,
118
- batch_size: int, index_type: str, numpy_type: str):
128
+ # Cleanup
129
+ os.remove("tmp.usearch")
119
130
 
131
+
132
+ @pytest.mark.parametrize("ndim", dimensions)
133
+ @pytest.mark.parametrize("metric", continuous_metrics)
134
+ @pytest.mark.parametrize("batch_size", batch_sizes)
135
+ @pytest.mark.parametrize("index_type", index_types)
136
+ @pytest.mark.parametrize("numpy_type", numpy_types)
137
+ def test_index_batch(
138
+ ndim: int, metric: MetricKind, batch_size: int, index_type: str, numpy_type: str
139
+ ):
120
140
  index = Index(ndim=ndim, metric=metric, dtype=index_type)
121
141
 
122
142
  labels = np.arange(batch_size)
@@ -124,8 +144,7 @@ def test_index_batch(
124
144
 
125
145
  index.add(labels, vectors)
126
146
  assert len(index) == batch_size
127
- assert np.allclose(index.get_vectors(
128
- labels).astype(numpy_type), vectors, atol=0.1)
147
+ assert np.allclose(index.get_vectors(labels).astype(numpy_type), vectors, atol=0.1)
129
148
 
130
149
  matches: Matches = index.search(vectors, 10)
131
150
  assert matches.labels.shape[0] == matches.distances.shape[0]
@@ -136,10 +155,9 @@ def test_index_batch(
136
155
  assert recall_members(index, exact=True) == 1
137
156
 
138
157
 
139
- @pytest.mark.parametrize('ndim', dimensions)
140
- @pytest.mark.parametrize('batch_size', batch_sizes)
158
+ @pytest.mark.parametrize("ndim", dimensions)
159
+ @pytest.mark.parametrize("batch_size", batch_sizes)
141
160
  def test_index_numba(ndim: int, batch_size: int):
142
-
143
161
  try:
144
162
  from numba import cfunc, types, carray
145
163
  except ImportError:
@@ -147,11 +165,24 @@ def test_index_numba(ndim: int, batch_size: int):
147
165
 
148
166
  # Showcases how to use Numba to JIT-compile similarity measures for USearch.
149
167
  # https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
150
- signature = types.float32(types.CPointer(
151
- types.float32), types.CPointer(types.float32))
168
+ signature_two_args = types.float32(
169
+ types.CPointer(types.float32),
170
+ types.CPointer(types.float32),
171
+ )
172
+ signature_three_args = types.float32(
173
+ types.CPointer(types.float32),
174
+ types.CPointer(types.float32),
175
+ types.uint64,
176
+ )
177
+ signature_four_args = types.float32(
178
+ types.CPointer(types.float32),
179
+ types.uint64,
180
+ types.CPointer(types.float32),
181
+ types.uint64,
182
+ )
152
183
 
153
- @cfunc(signature)
154
- def python_dot(a, b):
184
+ @cfunc(signature_two_args)
185
+ def python_inner_product_two_args(a, b):
155
186
  a_array = carray(a, ndim)
156
187
  b_array = carray(b, ndim)
157
188
  c = 0.0
@@ -159,7 +190,195 @@ def test_index_numba(ndim: int, batch_size: int):
159
190
  c += a_array[i] * b_array[i]
160
191
  return 1 - c
161
192
 
162
- index = Index(ndim=ndim, metric=python_dot)
193
+ @cfunc(signature_three_args)
194
+ def python_inner_product_three_args(a, b, ndim):
195
+ a_array = carray(a, ndim)
196
+ b_array = carray(b, ndim)
197
+ c = 0.0
198
+ for i in range(ndim):
199
+ c += a_array[i] * b_array[i]
200
+ return 1 - c
201
+
202
+ @cfunc(signature_four_args)
203
+ def python_inner_product_four_args(a, a_ndim, b, b_ndim):
204
+ a_array = carray(a, a_ndim)
205
+ b_array = carray(b, b_ndim)
206
+ c = 0.0
207
+ for i in range(a_ndim):
208
+ c += a_array[i] * b_array[i]
209
+ return 1 - c
210
+
211
+ functions = [
212
+ python_inner_product_two_args,
213
+ python_inner_product_three_args,
214
+ python_inner_product_four_args,
215
+ ]
216
+ signatures = [
217
+ MetricSignature.ArrayArray,
218
+ MetricSignature.ArrayArraySize,
219
+ MetricSignature.ArraySizeArraySize,
220
+ ]
221
+ for function, signature in zip(functions, signatures):
222
+ metric = CompiledMetric(
223
+ pointer=function.address,
224
+ kind=MetricKind.IP,
225
+ signature=signature,
226
+ )
227
+ index = Index(ndim=ndim, metric=metric)
228
+
229
+ labels = np.arange(batch_size)
230
+ vectors = random_vectors(count=batch_size, ndim=ndim)
231
+
232
+ index.add(labels, vectors)
233
+ matches, distances, count = index.search(vectors, 10)
234
+ assert matches.shape[0] == distances.shape[0]
235
+ assert count.shape[0] == batch_size
236
+
237
+ assert recall_members(index, exact=True) == 1
238
+
239
+
240
+ @pytest.mark.parametrize("ndim", dimensions[-1:])
241
+ @pytest.mark.parametrize("batch_size", batch_sizes[-1:])
242
+ def test_index_cppyy(ndim: int, batch_size: int):
243
+ try:
244
+ import cppyy
245
+ import cppyy.ll
246
+ except ImportError:
247
+ return
248
+
249
+ cppyy.cppdef(
250
+ """
251
+ float inner_product_two_args(float *a, float *b) {
252
+ float result = 0;
253
+ #pragma unroll
254
+ for (size_t i = 0; i != ndim; ++i)
255
+ result += a[i] * b[i];
256
+ return 1 - result;
257
+ }
258
+
259
+ float inner_product_three_args(float *a, float *b, size_t n) {
260
+ float result = 0;
261
+ for (size_t i = 0; i != n; ++i)
262
+ result += a[i] * b[i];
263
+ return 1 - result;
264
+ }
265
+
266
+ float inner_product_four_args(float *a, size_t an, float *b, size_t) {
267
+ float result = 0;
268
+ for (size_t i = 0; i != an; ++i)
269
+ result += a[i] * b[i];
270
+ return 1 - result;
271
+ }
272
+ """.replace(
273
+ "ndim", str(ndim)
274
+ )
275
+ )
276
+
277
+ functions = [
278
+ cppyy.gbl.inner_product_two_args,
279
+ cppyy.gbl.inner_product_three_args,
280
+ cppyy.gbl.inner_product_four_args,
281
+ ]
282
+ signatures = [
283
+ MetricSignature.ArrayArray,
284
+ MetricSignature.ArrayArraySize,
285
+ MetricSignature.ArraySizeArraySize,
286
+ ]
287
+ for function, signature in zip(functions, signatures):
288
+ metric = CompiledMetric(
289
+ pointer=cppyy.ll.addressof(function),
290
+ kind=MetricKind.IP,
291
+ signature=signature,
292
+ )
293
+ index = Index(ndim=ndim, metric=metric)
294
+
295
+ labels = np.arange(batch_size)
296
+ vectors = random_vectors(count=batch_size, ndim=ndim)
297
+
298
+ index.add(labels, vectors)
299
+ matches, distances, count = index.search(vectors, 10)
300
+ assert matches.shape[0] == distances.shape[0]
301
+ assert count.shape[0] == batch_size
302
+
303
+ assert recall_members(index, exact=True) == 1
304
+
305
+
306
+ @pytest.mark.parametrize("ndim", [8])
307
+ @pytest.mark.parametrize("batch_size", batch_sizes)
308
+ def test_index_peachpy(ndim: int, batch_size: int):
309
+ try:
310
+ from peachpy import (
311
+ Argument,
312
+ ptr,
313
+ float_,
314
+ const_float_,
315
+ )
316
+ from peachpy.x86_64 import (
317
+ abi,
318
+ Function,
319
+ uarch,
320
+ isa,
321
+ GeneralPurposeRegister64,
322
+ LOAD,
323
+ YMMRegister,
324
+ VSUBPS,
325
+ VADDPS,
326
+ VHADDPS,
327
+ VMOVUPS,
328
+ VFMADD231PS,
329
+ VPERM2F128,
330
+ VXORPS,
331
+ RETURN,
332
+ )
333
+ except ImportError:
334
+ return
335
+
336
+ a = Argument(ptr(const_float_), name="a")
337
+ b = Argument(ptr(const_float_), name="b")
338
+
339
+ with Function(
340
+ "InnerProduct", (a, b), float_, target=uarch.default + isa.avx + isa.avx2
341
+ ) as asm_function:
342
+ # Request two 64-bit general-purpose registers for addresses
343
+ reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
344
+ LOAD.ARGUMENT(reg_a, a)
345
+ LOAD.ARGUMENT(reg_b, b)
346
+
347
+ # Load the vectors
348
+ ymm_a = YMMRegister()
349
+ ymm_b = YMMRegister()
350
+ VMOVUPS(ymm_a, [reg_a])
351
+ VMOVUPS(ymm_b, [reg_b])
352
+
353
+ # Prepare the accumulator
354
+ ymm_c = YMMRegister()
355
+ ymm_one = YMMRegister()
356
+ VXORPS(ymm_c, ymm_c, ymm_c)
357
+ VXORPS(ymm_one, ymm_one, ymm_one)
358
+
359
+ # Accumulate A and B product into C
360
+ VFMADD231PS(ymm_c, ymm_a, ymm_b)
361
+
362
+ # Reduce the contents of a YMM register
363
+ ymm_c_permuted = YMMRegister()
364
+ VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
365
+ VADDPS(ymm_c, ymm_c, ymm_c_permuted)
366
+ VHADDPS(ymm_c, ymm_c, ymm_c)
367
+ VHADDPS(ymm_c, ymm_c, ymm_c)
368
+
369
+ # Negate the values, to go from "similarity" to "distance"
370
+ VSUBPS(ymm_c, ymm_one, ymm_c)
371
+
372
+ # A common convention is to return floats in XMM registers
373
+ RETURN(ymm_c.as_xmm)
374
+
375
+ python_function = asm_function.finalize(abi.detect()).encode().load()
376
+ metric = CompiledMetric(
377
+ pointer=python_function.loader.code_address,
378
+ kind=MetricKind.IP,
379
+ signature=MetricSignature.ArrayArray,
380
+ )
381
+ index = Index(ndim=ndim, metric=metric)
163
382
 
164
383
  labels = np.arange(batch_size)
165
384
  vectors = random_vectors(count=batch_size, ndim=ndim)
@@ -172,12 +391,13 @@ def test_index_numba(ndim: int, batch_size: int):
172
391
  assert recall_members(index, exact=True) == 1
173
392
 
174
393
 
175
- @pytest.mark.parametrize('bits', dimensions)
176
- @pytest.mark.parametrize('metric', hash_metrics)
177
- @pytest.mark.parametrize('connectivity', connectivity_options)
178
- @pytest.mark.parametrize('batch_size', batch_sizes)
179
- def test_bitwise_index(bits: int, metric: MetricKind, connectivity: int, batch_size: int):
180
-
394
+ @pytest.mark.parametrize("bits", dimensions)
395
+ @pytest.mark.parametrize("metric", hash_metrics)
396
+ @pytest.mark.parametrize("connectivity", connectivity_options)
397
+ @pytest.mark.parametrize("batch_size", batch_sizes)
398
+ def test_bitwise_index(
399
+ bits: int, metric: MetricKind, connectivity: int, batch_size: int
400
+ ):
181
401
  index = Index(ndim=bits, metric=metric, connectivity=connectivity)
182
402
 
183
403
  labels = np.arange(batch_size)
@@ -187,14 +407,13 @@ def test_bitwise_index(bits: int, metric: MetricKind, connectivity: int, batch_s
187
407
  index.add(labels, bit_vectors)
188
408
  assert np.allclose(index.get_vectors(labels), byte_vectors, atol=0.1)
189
409
  assert np.all(index.get_vectors(labels, ScalarKind.B1) == bit_vectors)
190
-
410
+
191
411
  index.search(bit_vectors, 10)
192
412
 
193
413
 
194
- @pytest.mark.parametrize('connectivity', connectivity_options)
195
- @pytest.mark.skipif(os.name == 'nt', reason='Spurious behaviour on windows')
414
+ @pytest.mark.parametrize("connectivity", connectivity_options)
415
+ @pytest.mark.skipif(os.name == "nt", reason="Spurious behaviour on windows")
196
416
  def test_sets_index(connectivity: int):
197
-
198
417
  index = SparseIndex(connectivity=connectivity)
199
418
  index.add(10, np.array([10, 12, 15], dtype=np.uint32))
200
419
  index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))