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