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.
package/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.18.8"
3
+ version = "0.19.1"
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,161 @@ 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
+ ```
359
+
360
+ ```sh
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
+ ```
323
389
 
324
390
  ```sh
325
- pip install usearch[jit]
391
+ conda install -c conda-forge cppyy
392
+ ```
393
+
394
+ ##### [PeachPy][peachpy]
395
+
396
+ We have covered JIT-ing Python with Numba and C++ with Cppyy and Cling.
397
+ How about writing Assembly directly?
398
+ That is also possible.
399
+ Below is an example of constructing the "Inner Product" distance for 8-dimensional `f32` vectors for x86 using [PeachPy][peachpy].
400
+
401
+ ```py
402
+ from peachpy import (
403
+ Argument,
404
+ ptr,
405
+ float_,
406
+ const_float_,
407
+ )
408
+ from peachpy.x86_64 import (
409
+ abi,
410
+ Function,
411
+ uarch,
412
+ isa,
413
+ GeneralPurposeRegister64,
414
+ LOAD,
415
+ YMMRegister,
416
+ VSUBPS,
417
+ VADDPS,
418
+ VHADDPS,
419
+ VMOVUPS,
420
+ VFMADD231PS,
421
+ VPERM2F128,
422
+ VXORPS,
423
+ RETURN,
424
+ )
425
+
426
+ a = Argument(ptr(const_float_), name="a")
427
+ b = Argument(ptr(const_float_), name="b")
428
+
429
+ with Function(
430
+ "inner_product", (a, b), float_, target=uarch.default + isa.avx2
431
+ ) as asm_function:
432
+
433
+ # Request two 64-bit general-purpose registers for addresses
434
+ reg_a, reg_b = GeneralPurposeRegister64(), GeneralPurposeRegister64()
435
+ LOAD.ARGUMENT(reg_a, a)
436
+ LOAD.ARGUMENT(reg_b, b)
437
+
438
+ # Load the vectors
439
+ ymm_a = YMMRegister()
440
+ ymm_b = YMMRegister()
441
+ VMOVUPS(ymm_a, [reg_a])
442
+ VMOVUPS(ymm_b, [reg_b])
443
+
444
+ # Prepare the accumulator
445
+ ymm_c = YMMRegister()
446
+ ymm_one = YMMRegister()
447
+ VXORPS(ymm_c, ymm_c, ymm_c)
448
+ VXORPS(ymm_one, ymm_one, ymm_one)
449
+
450
+ # Accumulate A and B product into C
451
+ VFMADD231PS(ymm_c, ymm_a, ymm_b)
452
+
453
+ # Reduce the contents of a YMM register
454
+ ymm_c_permuted = YMMRegister()
455
+ VPERM2F128(ymm_c_permuted, ymm_c, ymm_c, 1)
456
+ VADDPS(ymm_c, ymm_c, ymm_c_permuted)
457
+ VHADDPS(ymm_c, ymm_c, ymm_c)
458
+ VHADDPS(ymm_c, ymm_c, ymm_c)
459
+
460
+ # Negate the values, to go from "similarity" to "distance"
461
+ VSUBPS(ymm_c, ymm_one, ymm_c)
462
+
463
+ # A common convention is to return floats in XMM registers
464
+ RETURN(ymm_c.as_xmm)
465
+
466
+ python_function = asm_function.finalize(abi.detect()).encode().load()
467
+ metric = CompiledMetric(
468
+ pointer=python_function.loader.code_address,
469
+ kind=MetricKind.IP,
470
+ signature=MetricSignature.ArrayArray,
471
+ )
472
+ index = Index(ndim=ndim, metric=metric)
326
473
  ```
327
474
 
328
475
  [numba]: https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
476
+ [cppyy]: https://cppyy.readthedocs.io/en/latest/
477
+ [peachpy]: https://github.com/Maratyszcza/PeachPy
329
478
 
330
479
  #### Tooling
331
480
 
481
+ To work with `bbin`, `fbin`, `ibin`, `hbin` matrix files USearch provides `load_matrix` and `save_matrix`.
482
+ 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.
483
+
332
484
  ```py
333
485
  from usearch.index import Index
334
486
  from usearch.io import load_matrix, save_matrix
@@ -338,6 +490,28 @@ index = Index(ndim=vectors.shape[1])
338
490
  index.add(labels, vectors)
339
491
  ```
340
492
 
493
+ One may often want to evaluate the quality of the constructed index before running in production.
494
+ The trivial way is to measure `recall@1` on the entries already present in the index.
495
+
496
+ ```py
497
+ from usearch.eval import recall_members
498
+
499
+ assert recall_members(index, exact=True) == 1
500
+ print(recall_members(index, exact=False))
501
+ ```
502
+
503
+ In case you have some ground-truth data for more than one entry, you compare search results against expected values:
504
+
505
+ ```py
506
+ from usearch.eval import relevance, dcg, ndcg, random_vectors
507
+
508
+ vectors = random_vectors(index=index)
509
+ matches_approximate = index.search(vectors)
510
+ matches_exact = index.search(vectors, exact=True)
511
+ relevance_scores = relevance(matches_exact, matches_approximate)
512
+ print(dcg(relevance_scores), ndcg(relevance_scores))
513
+ ```
514
+
341
515
  ### JavaScript
342
516
 
343
517
  #### Installation
@@ -471,6 +645,7 @@ https://github.com/unum-cloud/usearch
471
645
  let index = Index.l2sq(dimensions: 3, connectivity: 8)
472
646
  let vectorA: [Float32] = [0.3, 0.5, 1.2]
473
647
  let vectorB: [Float32] = [0.4, 0.2, 1.2]
648
+ index.reserve(2)
474
649
  index.add(label: 42, vector: vectorA[...])
475
650
  index.add(label: 43, vector: vectorB[...])
476
651
 
@@ -547,22 +722,16 @@ server.run()
547
722
 
548
723
  We have pre-processed some commonly used datasets, cleaning the images, producing the vectors, and pre-building the index.
549
724
 
550
- | Dataset | Size | Images | Preprocessed |
551
- | :---------------------------------- | ---: | -----: | --------------------: |
552
- | [Unsplash 25K][unsplash-25k-origin] | - | 25 K | [HF][unsplash-25k-hf] |
553
- | [Unsplash 2M][unsplash-2m-origin] | - | 2 M | [HF][unsplash-2m-hf] |
554
- | [LAION 400M][laion-400m-origin] | - | 400 M | [HF][laion-400m-hf] |
555
- | [LAION 5B][laion-5b-origin] | - | 5 B | [HF][laion-5b-hf] |
725
+ | Dataset | Size | Images | Preprocessed |
726
+ | :----------------------------------- | ---: | -----: | --------------------: |
727
+ | [Unsplash 25K][unsplash-25k-origin] | - | 25 K | [HF][unsplash-25k-hf] |
728
+ | [Createve Captions 3M][cc-3m-origin] | - | 3 M | [HF][cc-3m-hf] |
556
729
 
557
730
 
558
731
  [unsplash-25k-origin]: https://unum.cloud
559
- [unsplash-2m-origin]: https://unum.cloud
560
- [laion-400m-origin]: https://unum.cloud
561
- [laion-5b-origin]: https://unum.cloud
732
+ [cc-3m-origin]: https://unum.cloud
562
733
  [unsplash-25k-hf]: https://unum.cloud
563
- [unsplash-2m-hf]: https://unum.cloud
564
- [laion-400m-hf]: https://unum.cloud
565
- [laion-5b-hf]: https://unum.cloud
734
+ [cc-3m-hf]: https://unum.cloud
566
735
 
567
736
  ### USearch + RDKit = Molecular Search
568
737
 
@@ -593,31 +762,9 @@ index.add(labels, fingerprints)
593
762
  matches = index.search(fingerprints, 10)
594
763
  ```
595
764
 
596
- RDKit [provides][rdkit-fingerprints] following fingerprinting techniques:
597
-
598
- - Atom-Pair,
599
- - Topological Torsion,
600
- - Morgan,
601
- - Layered Fingerprints.
602
-
603
- We have preprocessed some of the most commonly used datasets, and made it available for free on the HuggingFace portal, together with visual interface.
604
-
605
- | Dataset | Size | Molecules | Preprocessed |
606
- | :------------------------ | -------: | ----------: | ---------------: |
607
- | [PubChem][pubchem-origin] | 8 GB | 115'034'339 | [HF][pubchem-hf] |
608
- | [GDB 13][gdb13-origin] | 2.3 GB | 977'468'301 | [HF][gdb13-hf] |
609
- | [REAL][real-origin] | > 100 GB | 6 B | [HF][real-hf] |
610
-
611
765
  [smiles]: https://en.wikipedia.org/wiki/Simplified_molecular-input_line-entry_system
612
766
  [rdkit-fingerprints]: https://www.rdkit.org/docs/RDKit_Book.html#additional-information-about-the-fingerprints
613
767
 
614
- [gdb13-origin]: https://zenodo.org/record/5172018/files/gdb13.tgz?download=1
615
- [pubchem-origin]: ftp://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Extras/CID-SMILES.gz
616
- [real-origin]: https://enamine.net/compound-collections/real-compounds/real-database
617
- [pubchem-hf]: https://unum.cloud
618
- [gdb13-hf]: https://unum.cloud
619
- [real-hf]: https://unum.cloud
620
-
621
768
  ## TODO
622
769
 
623
770
  - JavaScript: Allow calling from "worker threads".
package/c/lib.cpp CHANGED
@@ -1,4 +1,6 @@
1
- #include "../src/index_punned_dense.hpp"
1
+ #include <cassert>
2
+
3
+ #include <usearch/index_punned_dense.hpp>
2
4
 
3
5
  extern "C" {
4
6
  #include "usearch.h"
@@ -12,10 +14,11 @@ using distance_t = usearch_distance_t;
12
14
  using index_t = index_punned_dense_gt<label_t>;
13
15
  using add_result_t = index_t::add_result_t;
14
16
  using search_result_t = index_t::search_result_t;
15
- using seerialization_result_t = index_t::seerialization_result_t;
17
+ using serialization_result_t = index_t::serialization_result_t;
16
18
 
17
19
  using span_t = span_gt<float>;
18
20
 
21
+ // helper functions that are not part of the C ABI
19
22
  metric_kind_t to_native_metric(usearch_metric_kind_t kind) {
20
23
  switch (kind) {
21
24
  case usearch_metric_ip_k: return metric_kind_t::ip_k;
@@ -37,30 +40,76 @@ scalar_kind_t to_native_scalar(usearch_scalar_kind_t kind) {
37
40
  case usearch_scalar_f64_k: return scalar_kind_t::f64_k;
38
41
  case usearch_scalar_f16_k: return scalar_kind_t::f16_k;
39
42
  case usearch_scalar_f8_k: return scalar_kind_t::f8_k;
40
- case usearch_scalar_b1_k: return scalar_kind_t::b1_k;
43
+ case usearch_scalar_b1_k: return scalar_kind_t::b1x8_k;
41
44
  default: return scalar_kind_t::unknown_k;
42
45
  }
43
46
  }
44
47
 
45
- void usearch_init(usearch_init_options_t* options, usearch_index_t* index, usearch_error_t* error) {
48
+ add_result_t add_(index_t* index, usearch_label_t label, void const* vector, scalar_kind_t kind) {
49
+ switch (kind) {
50
+ case scalar_kind_t::f32_k: return index->add(label, (f32_t const*)vector);
51
+ case scalar_kind_t::f64_k: return index->add(label, (f64_t const*)vector);
52
+ case scalar_kind_t::f16_k: return index->add(label, (f16_t const*)vector);
53
+ case scalar_kind_t::f8_k: return index->add(label, (f8_bits_t const*)vector);
54
+ case scalar_kind_t::b1x8_k: return index->add(label, (b1x8_t const*)vector);
55
+ default: return add_result_t{}.failed("Unknown scalar kind!");
56
+ }
57
+ }
58
+
59
+ bool get_(index_t* index, label_t label, void* vector, scalar_kind_t kind) {
60
+ switch (kind) {
61
+ case scalar_kind_t::f32_k: return index->get(label, (f32_t*)vector);
62
+ case scalar_kind_t::f64_k: return index->get(label, (f64_t*)vector);
63
+ case scalar_kind_t::f16_k: return index->get(label, (f16_t*)vector);
64
+ case scalar_kind_t::f8_k: return index->get(label, (f8_bits_t*)vector);
65
+ case scalar_kind_t::b1x8_k: return index->get(label, (b1x8_t*)vector);
66
+ default: return index->empty_search_result().failed("Unknown scalar kind!");
67
+ }
68
+ }
69
+
70
+ search_result_t search_(index_t* index, void const* vector, scalar_kind_t kind, size_t n) {
71
+ switch (kind) {
72
+ case scalar_kind_t::f32_k: return index->search((f32_t const*)vector, n);
73
+ case scalar_kind_t::f64_k: return index->search((f64_t const*)vector, n);
74
+ case scalar_kind_t::f16_k: return index->search((f16_t const*)vector, n);
75
+ case scalar_kind_t::f8_k: return index->search((f8_bits_t const*)vector, n);
76
+ case scalar_kind_t::b1x8_k: return index->search((b1x8_t const*)vector, n);
77
+ default: return index->empty_search_result().failed("Unknown scalar kind!");
78
+ }
79
+ }
80
+
81
+ index_punned_dense_metric_t udf(metric_kind_t kind, usearch_metric_t raw_ptr) {
82
+ index_punned_dense_metric_t result;
83
+ result.kind_ = kind;
84
+ result.func_ = [raw_ptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
85
+ return raw_ptr((void const*)a.data(), (void const*)b.data());
86
+ };
87
+ return result;
88
+ }
89
+
90
+ extern "C" {
46
91
 
47
- config_t config;
48
- config.expansion_add = static_cast<std::size_t>(expansion_add);
49
- config.expansion_search = static_cast<std::size_t>(expansion_search);
50
- config.connectivity = static_cast<std::size_t>(connectivity);
51
- config.max_elements = static_cast<std::size_t>(capacity);
52
- config.max_threads_add = std::thread::hardware_concurrency();
53
- config.max_threads_search = std::thread::hardware_concurrency();
92
+ usearch_index_t usearch_init(usearch_init_options_t* options, usearch_error_t* error) {
54
93
 
55
- scalar_kind_t accuracy = scalar_kind_from_name(accuracy_str, accuracy_len);
56
- metric_kind_t metric_kind = metric_from_name(metric_str, metric_len);
57
- index_t index = index_t::make(metric_kind, static_cast<std::size_t>(dimensions), accuracy, config);
94
+ assert(options && error);
95
+
96
+ index_config_t config;
97
+ config.connectivity = options->connectivity;
98
+ index_t index = //
99
+ options->metric ? //
100
+ index_t::make( //
101
+ options->dimensions, udf(to_native_metric(options->metric_kind), options->metric), config,
102
+ to_native_scalar(options->quantization), options->expansion_add, options->expansion_search)
103
+ : //
104
+ index_t::make( //
105
+ options->dimensions, to_native_metric(options->metric_kind), config,
106
+ to_native_scalar(options->quantization), options->expansion_add, options->expansion_search);
58
107
 
59
108
  index_t* result_ptr = new index_t(std::move(index));
60
109
  return result_ptr;
61
110
  }
62
111
 
63
- void usearch_free(index_t* index) { delete index; }
112
+ void usearch_free(usearch_index_t index, usearch_error_t*) { delete reinterpret_cast<index_t*>(index); }
64
113
 
65
114
  void usearch_save(usearch_index_t index, char const* path, usearch_error_t* error) {
66
115
  serialization_result_t result = reinterpret_cast<index_t*>(index)->save(path);
@@ -101,17 +150,6 @@ void usearch_reserve(usearch_index_t index, size_t capacity, usearch_error_t*) {
101
150
  reinterpret_cast<index_t*>(index)->reserve(capacity);
102
151
  }
103
152
 
104
- add_result_t add_(index_t* index, usearch_label_t label, void const* vector, scalar_kind_t kind) {
105
- switch (kind) {
106
- case scalar_kind_t::f32_k: return index->add(label, (f32_t const*)vector);
107
- case scalar_kind_t::f64_k: return index->add(label, (f64_t const*)vector);
108
- case scalar_kind_t::f16_k: return index->add(label, (f16_t const*)vector);
109
- case scalar_kind_t::f8_k: return index->add(label, (f8_bits_t const*)vector);
110
- case scalar_kind_t::b1x8_k: return index->add(label, (b1x8_t const*)vector);
111
- default: return add_result_t{}.failed("Unknown scalar kind!");
112
- }
113
- }
114
-
115
153
  void usearch_add( //
116
154
  usearch_index_t index, usearch_label_t label, void const* vector, usearch_scalar_kind_t kind, //
117
155
  usearch_error_t* error) {
@@ -121,18 +159,7 @@ void usearch_add(
121
159
  }
122
160
 
123
161
  bool usearch_contains(usearch_index_t index, usearch_label_t label, usearch_error_t*) {
124
- reinterpret_cast<index_t*>(index)->contains(label);
125
- }
126
-
127
- search_result_t search_(index_t* index, void const* vector, scalar_kind_t kind, size_t n) {
128
- switch (kind) {
129
- case scalar_kind_t::f32_k: return index->search((f32_t const*)vector, n);
130
- case scalar_kind_t::f64_k: return index->search((f64_t const*)vector, n);
131
- case scalar_kind_t::f16_k: return index->search((f16_t const*)vector, n);
132
- case scalar_kind_t::f8_k: return index->search((f8_bits_t const*)vector, n);
133
- case scalar_kind_t::b1x8_k: return index->search((b1x8_t const*)vector, n);
134
- default: return search_result_t{}.failed("Unknown scalar kind!");
135
- }
162
+ return reinterpret_cast<index_t*>(index)->contains(label);
136
163
  }
137
164
 
138
165
  size_t usearch_search( //
@@ -147,21 +174,14 @@ size_t usearch_search(
147
174
  return result.dump_to(found_labels, found_distances);
148
175
  }
149
176
 
150
- bool get_(index_t* index, void const* vector, scalar_kind_t kind) {
151
- switch (kind) {
152
- case scalar_kind_t::f32_k: return index->get((f32_t const*)vector, n);
153
- case scalar_kind_t::f64_k: return index->get((f64_t const*)vector, n);
154
- case scalar_kind_t::f16_k: return index->get((f16_t const*)vector, n);
155
- case scalar_kind_t::f8_k: return index->get((f8_bits_t const*)vector, n);
156
- case scalar_kind_t::b1x8_k: return index->get((b1x8_t const*)vector, n);
157
- default: return search_result_t{}.failed("Unknown scalar kind!");
158
- }
159
- }
160
-
161
177
  bool usearch_get( //
162
178
  usearch_index_t index, usearch_label_t label, //
163
179
  void* vector, usearch_scalar_kind_t kind, usearch_error_t*) {
164
- return search_(reinterpret_cast<index_t*>(index), vector, to_native_scalar(kind));
180
+ return get_(reinterpret_cast<index_t*>(index), label, vector, to_native_scalar(kind));
165
181
  }
166
182
 
167
- void usearch_remove(usearch_index_t, usearch_label_t, usearch_error_t*) {}
183
+ void usearch_remove(usearch_index_t, usearch_label_t, usearch_error_t* error) {
184
+ if (error != nullptr)
185
+ *error = "USearch does not support removal of elements yet.";
186
+ }
187
+ }
package/c/test.c ADDED
@@ -0,0 +1,182 @@
1
+ #include <assert.h>
2
+ #include <errno.h>
3
+ #include <stdio.h>
4
+ #include <stdlib.h>
5
+ #include <string.h>
6
+ #include <sys/stat.h>
7
+ #include <time.h>
8
+
9
+ #include "usearch.h"
10
+
11
+ float* read_fvecs_file(const char* filename, size_t* num_vectors, size_t* vector_dim) {
12
+ struct stat st;
13
+ FILE* file;
14
+ size_t cnt;
15
+ float skip;
16
+
17
+ ;
18
+ if (stat(filename, &st) == -1) {
19
+ printf("Failed to read file: %s err: %d: %s\n", filename, errno, strerror(errno));
20
+ return NULL;
21
+ }
22
+
23
+ file = fopen(filename, "rb");
24
+ if (file == NULL) {
25
+ printf("Failed to open file: %s\n", filename);
26
+ return NULL;
27
+ }
28
+
29
+ // Read the number of vectors and vector dimension from the file
30
+ cnt = fread(vector_dim, sizeof(int), 1, file);
31
+ assert(cnt == 1);
32
+ if (st.st_size % ((*vector_dim + 1) * sizeof(float)) != 0) {
33
+ printf("File does not contain a whole number of vectors.\n Vector dim: %ld, file size: %ld\n", *vector_dim,
34
+ st.st_size);
35
+ fclose(file);
36
+ return NULL;
37
+ }
38
+
39
+ *num_vectors = st.st_size / ((*vector_dim + 1) * sizeof(float));
40
+
41
+ // Allocate memory for the array
42
+ float* data = (float*)malloc(*num_vectors * *vector_dim * sizeof(float));
43
+ if (data == NULL) {
44
+ printf("Memory allocation failed.\n");
45
+ fclose(file);
46
+ return NULL;
47
+ }
48
+
49
+ // Read the data from the file into the array
50
+ for (size_t i = 0; i < *num_vectors; i++) {
51
+ cnt = fread(data + i * *vector_dim, sizeof(float), *vector_dim, file);
52
+ assert(cnt == *vector_dim);
53
+ if (i == *num_vectors - 1) {
54
+ break;
55
+ }
56
+ cnt = fread(&skip, sizeof(float), 1, file);
57
+ assert(cnt == 1);
58
+ }
59
+
60
+ fclose(file);
61
+ return data;
62
+ }
63
+
64
+ // log every this many iterations in test loops
65
+ const int LOG_GRANULARITY = 10000;
66
+
67
+ void test_index(float* data, size_t num_vectors, size_t vector_dim) {
68
+ usearch_init_options_t opts;
69
+ usearch_index_t idx;
70
+ usearch_error_t error = NULL;
71
+ clock_t start;
72
+ opts.connectivity = 2; // 32 in faiss
73
+ opts.dimensions = vector_dim;
74
+ opts.expansion_add = 40; // 40 in faiss
75
+ opts.expansion_search = 16; // 10 in faiss
76
+ opts.metric_kind = usearch_metric_ip_k;
77
+ opts.metric = NULL;
78
+ opts.quantization = usearch_scalar_f32_k;
79
+
80
+ // results arrays
81
+ int k = 10;
82
+ num_vectors /= 10;
83
+ usearch_label_t* labels = (usearch_label_t*)calloc(k, sizeof(usearch_label_t));
84
+ float* distances = (float*)calloc(k, sizeof(float));
85
+
86
+ fprintf(stderr, "read %ld vectors of dimension %ld\n", num_vectors, vector_dim);
87
+ fprintf(stderr, "initializing usearch...\n");
88
+ idx = usearch_init(&opts, &error);
89
+ if (error != NULL) {
90
+ fprintf(stderr, "usearch error: %s\n", error);
91
+ assert(false);
92
+ }
93
+
94
+ fprintf(stderr, "reserving capacity...\n");
95
+ usearch_reserve(idx, num_vectors, &error);
96
+ assert(!error);
97
+
98
+ fprintf(stderr, "adding vectors...\n");
99
+ for (size_t i = 0; i < num_vectors; i++) {
100
+ if (i % LOG_GRANULARITY == 0) {
101
+ fprintf(stderr, "added %ld vectors...\n", i);
102
+ }
103
+ usearch_label_t label = i;
104
+ usearch_add(idx, label, data + i * vector_dim, usearch_scalar_f32_k, &error);
105
+ assert(!error);
106
+ }
107
+
108
+ fprintf(stderr, "checking containment of vectors...\n");
109
+ for (size_t i = 0; i < num_vectors; i++) {
110
+ if (i % LOG_GRANULARITY == 0) {
111
+ fprintf(stderr, "checked %ld vectors...\n", i);
112
+ }
113
+ usearch_label_t label = i;
114
+ assert(usearch_contains(idx, label, &error));
115
+ assert(!error);
116
+ }
117
+
118
+ assert(!usearch_contains(idx, -1, &error));
119
+ assert(!error);
120
+
121
+ fprintf(stderr, "searching vectors...\n");
122
+ for (size_t i = 0; i < num_vectors; i++) {
123
+ if (i % LOG_GRANULARITY == 0) {
124
+ fprintf(stderr, "searched %ld vectors...\n", i);
125
+ }
126
+ const void *query_vector = data + i * vector_dim;
127
+ usearch_search(idx, query_vector, usearch_scalar_f32_k, k, labels, distances, &error);
128
+ assert(!error);
129
+ }
130
+
131
+ fprintf(stderr, "saving the index...\n");
132
+ start = clock();
133
+ usearch_save(idx, "usearch_index.bin", &error);
134
+ assert(!error);
135
+ fprintf(stderr, "saving took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
136
+
137
+ fprintf(stderr, "destroying the index...\n");
138
+ start = clock();
139
+ usearch_free(idx, &error);
140
+ assert(!error);
141
+ fprintf(stderr, "destroying the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
142
+
143
+ fprintf(stderr, "loading the index...\n");
144
+ start = clock();
145
+ idx = usearch_init(&opts, &error);
146
+ assert(!error);
147
+ assert(usearch_size(idx, &error) == 0);
148
+ assert(!error);
149
+ usearch_load(idx, "usearch_index.bin", &error);
150
+ assert(!error);
151
+ fprintf(stderr, "loading the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
152
+ assert(usearch_size(idx, &error) == num_vectors);
153
+ assert(!error);
154
+ usearch_free(idx, &error);
155
+
156
+ fprintf(stderr, "viewing the index...\n");
157
+ start = clock();
158
+ idx = usearch_init(&opts, &error);
159
+ assert(!error);
160
+ assert(usearch_size(idx, &error) == 0);
161
+ assert(!error);
162
+ usearch_view(idx, "usearch_index.bin", &error);
163
+ assert(!error);
164
+ fprintf(stderr, "viewing the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
165
+ assert(usearch_size(idx, &error) == num_vectors);
166
+ assert(!error);
167
+ usearch_free(idx, &error);
168
+
169
+ }
170
+
171
+ int main() {
172
+ const char* filename = "sift/sift_base.fvecs";
173
+ size_t num_vectors = 0, vector_dim = 0;
174
+ float* data = read_fvecs_file(filename, &num_vectors, &vector_dim);
175
+ if (data != NULL) {
176
+
177
+ test_index(data, num_vectors, vector_dim);
178
+
179
+ free(data); // Remember to free the dynamically allocated memory
180
+ }
181
+ return 0;
182
+ }