usearch 0.18.8 → 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.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.18.8"
3
+ version = "0.19.0"
4
4
  authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -251,7 +251,7 @@ index = Index(
251
251
  expansion_search=64, # Control the quality of search, optional
252
252
  )
253
253
 
254
- vector = np.array([0.2, 0.6, 0.4], dtype=np.float32)
254
+ vector = np.array([0.2, 0.6, 0.4])
255
255
  index.add(42, vector)
256
256
  matches, distances, count = index.search(vector, 10)
257
257
 
@@ -259,6 +259,7 @@ assert len(index) == 1
259
259
  assert count == 1
260
260
  assert matches[0] == 42
261
261
  assert distances[0] <= 0.001
262
+ assert np.allclose(index[42], vector)
262
263
  ```
263
264
 
264
265
  Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
@@ -272,6 +273,13 @@ index.load('index.usearch') # Copy the whole index into memory
272
273
  index.view('index.usearch') # View from disk without loading in memory
273
274
  ```
274
275
 
276
+ If you don't know anything about the index except its path, there are two more endpoints to know:
277
+
278
+ ```py
279
+ Index.metadata('index.usearch') -> IndexMetadata
280
+ Index.restore('index.usearch', view=False) -> Index
281
+ ```
282
+
275
283
  #### Batch Operations
276
284
 
277
285
  Adding or querying a batch of entries is identical to adding a single vector.
@@ -294,7 +302,9 @@ The first controls the number of threads spawned for the task.
294
302
  The second controls whether the vector itself will be persisted inside the index.
295
303
  If you can preserve the lifetime of the vector somewhere else, you can avoid the copy.
296
304
 
297
- #### User-Defined Metrics in Python
305
+ #### User-Defined Metrics and JIT in Python
306
+
307
+ ##### [Numba][numba]
298
308
 
299
309
  Assuming the language boundary exists between Python user code and C++ implementation, there are more efficient solutions than passing a Python callable to the engine.
300
310
  Luckily, with the help of [Numba][numba], we can JIT compile a function with a matching signature and pass it down to the engine.
@@ -308,7 +318,7 @@ signature = types.float32(
308
318
  types.CPointer(types.float32))
309
319
 
310
320
  @cfunc(signature)
311
- def python_dot(a, b):
321
+ def inner_product(a, b):
312
322
  a_array = carray(a, ndim)
313
323
  b_array = carray(b, ndim)
314
324
  c = 0.0
@@ -316,19 +326,157 @@ def python_dot(a, b):
316
326
  c += a_array[i] * b_array[i]
317
327
  return 1 - c
318
328
 
319
- index = Index(ndim=ndim, metric=python_dot.address)
329
+ index = Index(ndim=ndim, metric=CompiledMetric(
330
+ pointer=inner_product.address,
331
+ kind=MetricKind.IP,
332
+ signature=MetricSignature.ArrayArray,
333
+ ))
320
334
  ```
321
335
 
322
- To use Numba JIT, install USearch with extras:
336
+ Alternatively, you can avoid pre-defining the number of dimensions, and pass it separately:
337
+
338
+ ```py
339
+ signature = types.float32(
340
+ types.CPointer(types.float32),
341
+ types.CPointer(types.float32),
342
+ types.uint64)
343
+
344
+ @cfunc(signature)
345
+ def inner_product(a, b, ndim):
346
+ a_array = carray(a, ndim)
347
+ b_array = carray(b, ndim)
348
+ c = 0.0
349
+ for i in range(ndim):
350
+ c += a_array[i] * b_array[i]
351
+ return 1 - c
352
+
353
+ index = Index(ndim=ndim, metric=CompiledMetric(
354
+ pointer=inner_product.address,
355
+ kind=MetricKind.IP,
356
+ signature=MetricSignature.ArrayArraySize,
357
+ ))
358
+ ```
323
359
 
324
360
  ```sh
325
- pip install usearch[jit]
361
+ pip install numba
362
+ ```
363
+
364
+ ##### [Cppyy][cppyy]
365
+
366
+ Similarly, you can use Cppyy with Cling to JIT-compile native C or C++ code and pass it to USearch, which may be a good idea, if you want to explicitly request loop-unrolling or other low-level optimizations!
367
+
368
+ ```py
369
+ import cppyy
370
+ import cppyy.ll
371
+
372
+ cppyy.cppdef("""
373
+ float inner_product(float *a, float *b) {
374
+ float result = 0;
375
+ #pragma unroll
376
+ for (size_t i = 0; i != ndim; ++i)
377
+ result += a[i] * b[i];
378
+ return 1 - result;
379
+ }
380
+ """.replace("ndim", str(ndim)))
381
+
382
+ function = cppyy.gbl.inner_product
383
+ index = Index(ndim=ndim, metric=CompiledMetric(
384
+ pointer=cppyy.ll.addressof(function),
385
+ kind=MetricKind.IP,
386
+ signature=MetricSignature.ArrayArraySize,
387
+ ))
388
+ ```
389
+
390
+ ##### [PeachPy][peachpy]
391
+
392
+ We have covered JIT-ing Python with Numba and C++ with Cppyy and Cling.
393
+ How about writing Assembly directly?
394
+ That is also possible.
395
+ Below is an example of constructing the "Inner Product" distance for 8-dimensional `f32` vectors for x86 using [PeachPy][peachpy].
396
+
397
+ ```py
398
+ from peachpy import (
399
+ Argument,
400
+ ptr,
401
+ float_,
402
+ const_float_,
403
+ )
404
+ from peachpy.x86_64 import (
405
+ abi,
406
+ Function,
407
+ uarch,
408
+ isa,
409
+ GeneralPurposeRegister64,
410
+ LOAD,
411
+ YMMRegister,
412
+ VSUBPS,
413
+ VADDPS,
414
+ VHADDPS,
415
+ VMOVUPS,
416
+ VFMADD231PS,
417
+ VPERM2F128,
418
+ VXORPS,
419
+ RETURN,
420
+ )
421
+
422
+ a = Argument(ptr(const_float_), name="a")
423
+ b = Argument(ptr(const_float_), name="b")
424
+
425
+ with Function(
426
+ "inner_product", (a, b), float_, target=uarch.default + isa.avx2
427
+ ) as asm_function:
428
+
429
+ # Request two 64-bit general-purpose registers for addresses
430
+ reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
431
+ LOAD.ARGUMENT(reg_a, a)
432
+ LOAD.ARGUMENT(reg_b, b)
433
+
434
+ # Load the vectors
435
+ ymm_a = YMMRegister()
436
+ ymm_b = YMMRegister()
437
+ VMOVUPS(ymm_a, [reg_a])
438
+ VMOVUPS(ymm_b, [reg_b])
439
+
440
+ # Prepare the accumulator
441
+ ymm_c = YMMRegister()
442
+ ymm_one = YMMRegister()
443
+ VXORPS(ymm_c, ymm_c, ymm_c)
444
+ VXORPS(ymm_one, ymm_one, ymm_one)
445
+
446
+ # Accumulate A and B product into C
447
+ VFMADD231PS(ymm_c, ymm_a, ymm_b)
448
+
449
+ # Reduce the contents of a YMM register
450
+ ymm_c_permuted = YMMRegister()
451
+ VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
452
+ VADDPS(ymm_c, ymm_c, ymm_c_permuted)
453
+ VHADDPS(ymm_c, ymm_c, ymm_c)
454
+ VHADDPS(ymm_c, ymm_c, ymm_c)
455
+
456
+ # Negate the values, to go from "similarity" to "distance"
457
+ VSUBPS(ymm_c, ymm_one, ymm_c)
458
+
459
+ # A common convention is to return floats in XMM registers
460
+ RETURN(ymm_c.as_xmm)
461
+
462
+ python_function = asm_function.finalize(abi.detect()).encode().load()
463
+ metric = CompiledMetric(
464
+ pointer=python_function.loader.code_address,
465
+ kind=MetricKind.IP,
466
+ signature=MetricSignature.ArrayArray,
467
+ )
468
+ index = Index(ndim=ndim, metric=metric)
326
469
  ```
327
470
 
328
471
  [numba]: https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
472
+ [cppyy]: https://cppyy.readthedocs.io/en/latest/
473
+ [peachpy]: https://github.com/Maratyszcza/PeachPy
329
474
 
330
475
  #### Tooling
331
476
 
477
+ To work with `bbin`, `fbin`, `ibin`, `hbin` matrix files USearch provides `load_matrix` and `save_matrix`.
478
+ Such files are standard in k-ANN tasks and represent a binary object with all the scalars, prepended by two 32-bit integers - the number of rows and columns in the matrix.
479
+
332
480
  ```py
333
481
  from usearch.index import Index
334
482
  from usearch.io import load_matrix, save_matrix
@@ -338,6 +486,28 @@ index = Index(ndim=vectors.shape[1])
338
486
  index.add(labels, vectors)
339
487
  ```
340
488
 
489
+ One may often want to evaluate the quality of the constructed index before running in production.
490
+ The trivial way is to measure `recall@1` on the entries already present in the index.
491
+
492
+ ```py
493
+ from usearch.eval import recall_members
494
+
495
+ assert recall_members(index, exact=True) == 1
496
+ print(recall_members(index, exact=False))
497
+ ```
498
+
499
+ In case you have some ground-truth data for more than one entry, you compare search results against expected values:
500
+
501
+ ```py
502
+ from usearch.eval import relevance, dcg, ndcg, random_vectors
503
+
504
+ vectors = random_vectors(index=index)
505
+ matches_approximate = index.search(vectors)
506
+ matches_exact = index.search(vectors, exact=True)
507
+ relevance_scores = relevance(matches_exact, matches_approximate)
508
+ print(dcg(relevance_scores), ndcg(relevance_scores))
509
+ ```
510
+
341
511
  ### JavaScript
342
512
 
343
513
  #### Installation
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '0.18.8'
7
+ version = '0.19.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/cpp/bench.cpp CHANGED
@@ -483,7 +483,7 @@ void run_punned(dataset_at& dataset, args_t const& args, index_config_t config,
483
483
 
484
484
  index_at index = index_at::make(dataset.dimensions(), kind, config, accuracy);
485
485
  index.reserve(limits);
486
- std::printf("-- Hardware acceleration: %s\n", isa_name(index.isa()));
486
+ std::printf("-- Hardware acceleration: %s\n", isa_name(index.metric().isa_));
487
487
  std::printf("Will benchmark in-memory\n");
488
488
 
489
489
  single_shot(dataset, index, true);
@@ -535,7 +535,7 @@ void run_big_or_small(dataset_at& dataset, args_t const& args, index_config_t co
535
535
 
536
536
  void report_alternative_setups() {
537
537
  using set_member_t = std::uint32_t;
538
- using sets_index_t = index_gt<jaccard_gt<set_member_t>, std::size_t, std::uint32_t, set_member_t>;
538
+ using sets_index_t = index_gt<jaccard_gt<set_member_t>, std::size_t, std::uint32_t>;
539
539
  set_member_t set_a[] = {10, 12, 15};
540
540
  set_member_t set_b[] = {11, 12, 15, 16};
541
541
  sets_index_t index;
package/cpp/test.cpp CHANGED
@@ -20,7 +20,8 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
20
20
 
21
21
  using scalar_t = scalar_at;
22
22
  using view_t = span_gt<scalar_t const>;
23
- using distance_t = typename index_at::distance_t;
23
+ using index_t = typename std::remove_reference<index_at>::type;
24
+ using distance_t = typename index_t::distance_t;
24
25
 
25
26
  scalar_t vec[3] = {10, 20, 15};
26
27
  scalar_t vec_a[3] = {15, 16, 17};
@@ -67,6 +68,7 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
67
68
  expect(std::abs(matched_distances[0]) < 0.01);
68
69
 
69
70
  // Search again over mapped index
71
+ index_metadata("tmp.usearch");
70
72
  index.view("tmp.usearch");
71
73
  matched_count = index.search(view_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
72
74
  expect(matched_count == 3);
@@ -144,11 +146,11 @@ int main(int, char**) {
144
146
  // static_assert(!std::is_same<index_gt<pearson_correlation_gt<>>::value_type, std::true_type>());
145
147
  // static_assert(!std::is_same<index_gt<haversine_gt<>>::value_type, std::true_type>());
146
148
 
147
- test3d<float>(index_gt<cos_gt<float>, point_id_t, std::uint32_t, float>{});
148
- test3d<float>(index_gt<l2sq_gt<float>, point_id_t, std::uint32_t, float>{});
149
+ test3d<float>(index_gt<cos_gt<float>, point_id_t, std::uint32_t>{});
150
+ test3d<float>(index_gt<l2sq_gt<float>, point_id_t, std::uint32_t>{});
149
151
 
150
- test3d<double>(index_gt<cos_gt<double>, point_id_t, std::uint32_t, double>{});
151
- test3d<double>(index_gt<l2sq_gt<double>, point_id_t, std::uint32_t, double>{});
152
+ test3d<double>(index_gt<cos_gt<double>, point_id_t, std::uint32_t>{});
153
+ test3d<double>(index_gt<l2sq_gt<double>, point_id_t, std::uint32_t>{});
152
154
 
153
155
  test3d<float>(punned_small_t::make(3, metric_kind_t::cos_k));
154
156
  test3d<float>(punned_small_t::make(3, metric_kind_t::l2sq_k));
@@ -159,11 +161,11 @@ int main(int, char**) {
159
161
  test3d_punned<float>(punned_small_t::make(3, metric_kind_t::cos_k));
160
162
  test3d_punned<float>(punned_small_t::make(3, metric_kind_t::l2sq_k));
161
163
 
162
- test_sets(index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t, std::int32_t>{});
163
- test_sets(index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t, std::int64_t>{});
164
+ test_sets(index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t>{});
165
+ test_sets(index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t>{});
164
166
 
165
- test_sets_moved<index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t, std::int32_t>>();
166
- test_sets_moved<index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t, std::int64_t>>();
167
+ test_sets_moved<index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t>>();
168
+ test_sets_moved<index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t>>();
167
169
 
168
170
  return 0;
169
171
  }