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 +1 -1
- package/README.md +187 -40
- package/c/lib.cpp +71 -51
- package/c/test.c +182 -0
- package/c/usearch.h +11 -5
- package/conanfile.py +1 -1
- package/cpp/bench.cpp +6 -6
- package/cpp/test.cpp +11 -9
- package/docs/compilation.md +1 -0
- package/include/usearch/index.hpp +290 -54
- package/include/usearch/index_punned_dense.hpp +125 -108
- package/include/usearch/index_punned_helpers.hpp +2 -12
- package/objc/USearchObjective.mm +4 -0
- package/objc/include/USearchObjective.h +5 -0
- package/package.json +2 -2
- package/python/lib.cpp +234 -199
- package/python/scripts/bench.ipynb +13 -2113
- package/python/scripts/bench.py +45 -29
- package/python/scripts/index_faiss.py +19 -19
- package/python/scripts/test.py +320 -61
- package/python/usearch/eval.py +100 -49
- package/python/usearch/index.py +245 -129
- package/python/usearch/io.py +25 -25
- package/python/usearch/join.py +2 -4
- package/python/usearch/numba.py +22 -18
- package/python/usearch/server.py +38 -26
- package/setup.py +56 -59
- package/simsimd/.vscode/settings.json +2 -1
- package/simsimd/CMakeLists.txt +1 -1
- package/simsimd/README.md +14 -2
- package/simsimd/bench.cxx +44 -27
- package/simsimd/include/simsimd/simsimd.h +2 -1
- package/swift/Test.swift +13 -9
- package/simsimd/include/simsimd/simsimd.hpp +0 -443
package/Cargo.toml
CHANGED
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]
|
|
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
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
551
|
-
|
|
|
552
|
-
| [Unsplash 25K][unsplash-25k-origin]
|
|
553
|
-
| [
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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
|
|
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
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
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
|
|
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
|
+
}
|