usearch 0.18.8 → 0.19.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.
@@ -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,
@@ -15,10 +24,12 @@ from usearch.index import (
15
24
 
16
25
 
17
26
  dimensions = [3, 97, 256]
18
- batch_sizes = [1, 33]
27
+ batch_sizes = [1, 77]
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: ScalarKind,
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,34 @@ 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")
121
+ assert len(index) == 1
122
+ assert len(index[42]) == ndim
123
+
124
+ index = Index.restore("tmp.usearch")
108
125
  assert len(index) == 1
126
+ assert len(index[42]) == ndim
109
127
 
128
+ # Cleanup
129
+ os.remove("tmp.usearch")
110
130
 
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):
119
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,
139
+ metric: MetricKind,
140
+ batch_size: int,
141
+ index_type: ScalarKind,
142
+ numpy_type: str,
143
+ ):
120
144
  index = Index(ndim=ndim, metric=metric, dtype=index_type)
121
145
 
122
146
  labels = np.arange(batch_size)
@@ -124,22 +148,62 @@ def test_index_batch(
124
148
 
125
149
  index.add(labels, vectors)
126
150
  assert len(index) == batch_size
127
- assert np.allclose(index.get_vectors(
128
- labels).astype(numpy_type), vectors, atol=0.1)
151
+ assert np.allclose(index.get_vectors(labels).astype(numpy_type), vectors, atol=0.1)
129
152
 
130
153
  matches: Matches = index.search(vectors, 10)
131
154
  assert matches.labels.shape[0] == matches.distances.shape[0]
132
155
  assert matches.counts.shape[0] == batch_size
133
156
  assert np.all(np.sort(index.labels) == np.sort(labels))
134
157
 
135
- if _normalize_dtype(numpy_type) == _normalize_dtype(index_type):
136
- assert recall_members(index, exact=True) == 1
158
+ index.save("tmp.usearch")
159
+ index.clear()
160
+ assert len(index) == 0
137
161
 
162
+ index.load("tmp.usearch")
163
+ assert len(index) == batch_size
164
+ assert len(index[0]) == ndim
138
165
 
139
- @pytest.mark.parametrize('ndim', dimensions)
140
- @pytest.mark.parametrize('batch_size', batch_sizes)
141
- def test_index_numba(ndim: int, batch_size: int):
166
+ index = Index.restore("tmp.usearch")
167
+ assert len(index) == batch_size
168
+ assert len(index[0]) == ndim
169
+
170
+ # Cleanup
171
+ os.remove("tmp.usearch")
142
172
 
173
+
174
+ @pytest.mark.parametrize("metric", [MetricKind.L2sq])
175
+ @pytest.mark.parametrize("batch_size", batch_sizes)
176
+ @pytest.mark.parametrize("index_type", index_types)
177
+ @pytest.mark.parametrize("numpy_type", numpy_types)
178
+ def test_exact_recall(
179
+ metric: MetricKind, batch_size: int, index_type: ScalarKind, numpy_type: str
180
+ ):
181
+ ndim: int = batch_size
182
+ index = Index(ndim=ndim, metric=metric, dtype=index_type)
183
+
184
+ vectors = np.zeros((batch_size, ndim), dtype=numpy_type)
185
+ for i in range(batch_size):
186
+ vectors[i, i] = 1
187
+ labels = np.arange(batch_size)
188
+ index.add(labels, vectors)
189
+ assert len(index) == batch_size
190
+
191
+ # Search one at a time
192
+ for i in range(batch_size):
193
+ matches: Matches = index.search(vectors[i], 10, exact=True)
194
+ found_labels = matches.labels
195
+ assert found_labels[0] == i
196
+
197
+ # Search the whole batch
198
+ matches: Matches = index.search(vectors, 10, exact=True)
199
+ found_labels = matches.labels
200
+ for i in range(batch_size):
201
+ assert found_labels[i, 0] == i
202
+
203
+
204
+ @pytest.mark.parametrize("ndim", dimensions)
205
+ @pytest.mark.parametrize("batch_size", batch_sizes)
206
+ def test_index_numba(ndim: int, batch_size: int):
143
207
  try:
144
208
  from numba import cfunc, types, carray
145
209
  except ImportError:
@@ -147,11 +211,24 @@ def test_index_numba(ndim: int, batch_size: int):
147
211
 
148
212
  # Showcases how to use Numba to JIT-compile similarity measures for USearch.
149
213
  # 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))
214
+ signature_two_args = types.float32(
215
+ types.CPointer(types.float32),
216
+ types.CPointer(types.float32),
217
+ )
218
+ signature_three_args = types.float32(
219
+ types.CPointer(types.float32),
220
+ types.CPointer(types.float32),
221
+ types.uint64,
222
+ )
223
+ signature_four_args = types.float32(
224
+ types.CPointer(types.float32),
225
+ types.uint64,
226
+ types.CPointer(types.float32),
227
+ types.uint64,
228
+ )
152
229
 
153
- @cfunc(signature)
154
- def python_dot(a, b):
230
+ @cfunc(signature_two_args)
231
+ def python_inner_product_two_args(a, b):
155
232
  a_array = carray(a, ndim)
156
233
  b_array = carray(b, ndim)
157
234
  c = 0.0
@@ -159,7 +236,191 @@ def test_index_numba(ndim: int, batch_size: int):
159
236
  c += a_array[i] * b_array[i]
160
237
  return 1 - c
161
238
 
162
- index = Index(ndim=ndim, metric=python_dot)
239
+ @cfunc(signature_three_args)
240
+ def python_inner_product_three_args(a, b, ndim):
241
+ a_array = carray(a, ndim)
242
+ b_array = carray(b, ndim)
243
+ c = 0.0
244
+ for i in range(ndim):
245
+ c += a_array[i] * b_array[i]
246
+ return 1 - c
247
+
248
+ @cfunc(signature_four_args)
249
+ def python_inner_product_four_args(a, a_ndim, b, b_ndim):
250
+ a_array = carray(a, a_ndim)
251
+ b_array = carray(b, b_ndim)
252
+ c = 0.0
253
+ for i in range(a_ndim):
254
+ c += a_array[i] * b_array[i]
255
+ return 1 - c
256
+
257
+ functions = [
258
+ python_inner_product_two_args,
259
+ python_inner_product_three_args,
260
+ python_inner_product_four_args,
261
+ ]
262
+ signatures = [
263
+ MetricSignature.ArrayArray,
264
+ MetricSignature.ArrayArraySize,
265
+ MetricSignature.ArraySizeArraySize,
266
+ ]
267
+ for function, signature in zip(functions, signatures):
268
+ metric = CompiledMetric(
269
+ pointer=function.address,
270
+ kind=MetricKind.IP,
271
+ signature=signature,
272
+ )
273
+ index = Index(ndim=ndim, metric=metric)
274
+
275
+ labels = np.arange(batch_size)
276
+ vectors = random_vectors(count=batch_size, ndim=ndim)
277
+
278
+ index.add(labels, vectors)
279
+ matches, distances, count = index.search(vectors, 10)
280
+ assert matches.shape[0] == distances.shape[0]
281
+ assert count.shape[0] == batch_size
282
+
283
+
284
+ @pytest.mark.parametrize("ndim", dimensions[-1:])
285
+ @pytest.mark.parametrize("batch_size", batch_sizes[-1:])
286
+ def test_index_cppyy(ndim: int, batch_size: int):
287
+ try:
288
+ import cppyy
289
+ import cppyy.ll
290
+ except ImportError:
291
+ return
292
+
293
+ cppyy.cppdef(
294
+ """
295
+ float inner_product_two_args(float *a, float *b) {
296
+ float result = 0;
297
+ #pragma unroll
298
+ for (size_t i = 0; i != ndim; ++i)
299
+ result += a[i] * b[i];
300
+ return 1 - result;
301
+ }
302
+
303
+ float inner_product_three_args(float *a, float *b, size_t n) {
304
+ float result = 0;
305
+ for (size_t i = 0; i != n; ++i)
306
+ result += a[i] * b[i];
307
+ return 1 - result;
308
+ }
309
+
310
+ float inner_product_four_args(float *a, size_t an, float *b, size_t) {
311
+ float result = 0;
312
+ for (size_t i = 0; i != an; ++i)
313
+ result += a[i] * b[i];
314
+ return 1 - result;
315
+ }
316
+ """.replace(
317
+ "ndim", str(ndim)
318
+ )
319
+ )
320
+
321
+ functions = [
322
+ cppyy.gbl.inner_product_two_args,
323
+ cppyy.gbl.inner_product_three_args,
324
+ cppyy.gbl.inner_product_four_args,
325
+ ]
326
+ signatures = [
327
+ MetricSignature.ArrayArray,
328
+ MetricSignature.ArrayArraySize,
329
+ MetricSignature.ArraySizeArraySize,
330
+ ]
331
+ for function, signature in zip(functions, signatures):
332
+ metric = CompiledMetric(
333
+ pointer=cppyy.ll.addressof(function),
334
+ kind=MetricKind.IP,
335
+ signature=signature,
336
+ )
337
+ index = Index(ndim=ndim, metric=metric)
338
+
339
+ labels = np.arange(batch_size)
340
+ vectors = random_vectors(count=batch_size, ndim=ndim)
341
+
342
+ index.add(labels, vectors)
343
+ matches, distances, count = index.search(vectors, 10)
344
+ assert matches.shape[0] == distances.shape[0]
345
+ assert count.shape[0] == batch_size
346
+
347
+
348
+ @pytest.mark.parametrize("ndim", [8])
349
+ @pytest.mark.parametrize("batch_size", batch_sizes)
350
+ def test_index_peachpy(ndim: int, batch_size: int):
351
+ try:
352
+ from peachpy import (
353
+ Argument,
354
+ ptr,
355
+ float_,
356
+ const_float_,
357
+ )
358
+ from peachpy.x86_64 import (
359
+ abi,
360
+ Function,
361
+ uarch,
362
+ isa,
363
+ GeneralPurposeRegister64,
364
+ LOAD,
365
+ YMMRegister,
366
+ VSUBPS,
367
+ VADDPS,
368
+ VHADDPS,
369
+ VMOVUPS,
370
+ VFMADD231PS,
371
+ VPERM2F128,
372
+ VXORPS,
373
+ RETURN,
374
+ )
375
+ except ImportError:
376
+ return
377
+
378
+ a = Argument(ptr(const_float_), name="a")
379
+ b = Argument(ptr(const_float_), name="b")
380
+
381
+ with Function(
382
+ "InnerProduct", (a, b), float_, target=uarch.default + isa.avx + isa.avx2
383
+ ) as asm_function:
384
+ # Request two 64-bit general-purpose registers for addresses
385
+ reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
386
+ LOAD.ARGUMENT(reg_a, a)
387
+ LOAD.ARGUMENT(reg_b, b)
388
+
389
+ # Load the vectors
390
+ ymm_a = YMMRegister()
391
+ ymm_b = YMMRegister()
392
+ VMOVUPS(ymm_a, [reg_a])
393
+ VMOVUPS(ymm_b, [reg_b])
394
+
395
+ # Prepare the accumulator
396
+ ymm_c = YMMRegister()
397
+ ymm_one = YMMRegister()
398
+ VXORPS(ymm_c, ymm_c, ymm_c)
399
+ VXORPS(ymm_one, ymm_one, ymm_one)
400
+
401
+ # Accumulate A and B product into C
402
+ VFMADD231PS(ymm_c, ymm_a, ymm_b)
403
+
404
+ # Reduce the contents of a YMM register
405
+ ymm_c_permuted = YMMRegister()
406
+ VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
407
+ VADDPS(ymm_c, ymm_c, ymm_c_permuted)
408
+ VHADDPS(ymm_c, ymm_c, ymm_c)
409
+ VHADDPS(ymm_c, ymm_c, ymm_c)
410
+
411
+ # Negate the values, to go from "similarity" to "distance"
412
+ VSUBPS(ymm_c, ymm_one, ymm_c)
413
+
414
+ # A common convention is to return floats in XMM registers
415
+ RETURN(ymm_c.as_xmm)
416
+
417
+ python_function = asm_function.finalize(abi.detect()).encode().load()
418
+ metric = CompiledMetric(
419
+ pointer=python_function.loader.code_address,
420
+ kind=MetricKind.IP,
421
+ signature=MetricSignature.ArrayArray,
422
+ )
423
+ index = Index(ndim=ndim, metric=metric)
163
424
 
164
425
  labels = np.arange(batch_size)
165
426
  vectors = random_vectors(count=batch_size, ndim=ndim)
@@ -169,15 +430,14 @@ def test_index_numba(ndim: int, batch_size: int):
169
430
  assert matches.shape[0] == distances.shape[0]
170
431
  assert count.shape[0] == batch_size
171
432
 
172
- assert recall_members(index, exact=True) == 1
173
-
174
-
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
433
 
434
+ @pytest.mark.parametrize("bits", dimensions)
435
+ @pytest.mark.parametrize("metric", hash_metrics)
436
+ @pytest.mark.parametrize("connectivity", connectivity_options)
437
+ @pytest.mark.parametrize("batch_size", batch_sizes)
438
+ def test_bitwise_index(
439
+ bits: int, metric: MetricKind, connectivity: int, batch_size: int
440
+ ):
181
441
  index = Index(ndim=bits, metric=metric, connectivity=connectivity)
182
442
 
183
443
  labels = np.arange(batch_size)
@@ -187,14 +447,13 @@ def test_bitwise_index(bits: int, metric: MetricKind, connectivity: int, batch_s
187
447
  index.add(labels, bit_vectors)
188
448
  assert np.allclose(index.get_vectors(labels), byte_vectors, atol=0.1)
189
449
  assert np.all(index.get_vectors(labels, ScalarKind.B1) == bit_vectors)
190
-
450
+
191
451
  index.search(bit_vectors, 10)
192
452
 
193
453
 
194
- @pytest.mark.parametrize('connectivity', connectivity_options)
195
- @pytest.mark.skipif(os.name == 'nt', reason='Spurious behaviour on windows')
454
+ @pytest.mark.parametrize("connectivity", connectivity_options)
455
+ @pytest.mark.skipif(os.name == "nt", reason="Spurious behaviour on windows")
196
456
  def test_sets_index(connectivity: int):
197
-
198
457
  index = SparseIndex(connectivity=connectivity)
199
458
  index.add(10, np.array([10, 12, 15], dtype=np.uint32))
200
459
  index.add(11, np.array([11, 12, 15, 16], dtype=np.uint32))