usearch 0.20.0 → 0.22.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/README.md CHANGED
@@ -35,210 +35,45 @@ Linux • MacOS • Windows • Docker • WebAssembly 🔜
35
35
 
36
36
  ---
37
37
 
38
- - [x] Industry-leading [performance](#performance).
39
- - [x] Easily-extendible [single C++11 header][usearch-header] implementation.
40
- - [x] SIMD-accelerated and [User-defined](#define-custom-metrics) metrics with JIT-compilation.
41
- - [x] Variable dimensionality vectors - for [obscure use-cases, like GIS and Chess][obscure-use-cases].
42
- - [x] Bitwise Tanimoto and Sorensen coefficients for Genomics and Chemistry.
43
- - [x] [Half-precision `f16` and Quarter-precision `f8`](#quantize-on-the-fly) support on any hardware.
44
- - [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
45
- - [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
46
- - [x] [Bring your threads](#bring-your-threads), like OpenMP or C++23 executors.
47
- - [x] Multiple vectors per label.
48
- - [ ] Thread-safe `reserve`.
38
+ - [x] Benchmark-topping performance.
39
+ - [x] Simple and extensible [single C++11 header][usearch-header] implementation.
40
+ - [x] SIMD-optimized and [user-defined metrics](#user-defined-functions) with JIT-compilation.
41
+ - [x] Variable dimensionality vectors for unique applications, including GIS and Chess.
42
+ - [x] Advanced bitwise Tanimoto and Sorensen coefficients ideal for Genomics and Chemistry.
43
+ - [x] On-the-fly support for [half-precision `f16` and quarter-precision `f8`](#memory-efficiency-downcasting-and-quantization), compatible with any hardware.
44
+ - [x] [View large indexes from disk](#disk-based-indexes) without loading into RAM.
45
+ - [x] Space-efficient point-clouds with `uint40_t`, accommodating 4B+ size.
46
+ - [x] Optimized for concurrent threading, like OpenMP or C++23 executors with bring your threads.
47
+ - [x] Support for multiple vectors per label.
49
48
  - [ ] On-the-fly deletions.
50
- - [x] USearch + UForm Transformers = [Semantic Search](#ai--vector-search--semantic-search).
51
- - [x] USearch + RDKit = [Molecule Search](#ai--vector-search--semantic-search).
49
+ - [x] Utilize [USearch and UForm Transformers for Semantic Search](#usearch--ai--multi-modal-semantic-search).
50
+ - [x] Combine [USearch and RDKit for Molecule Search](#usearch--rdkit--molecular-search).
52
51
 
53
52
  [usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/index.hpp
54
53
  [obscure-use-cases]: https://ashvardanian.com/posts/abusing-vector-search
55
54
 
56
- ---
55
+ ## Comparison with FAISS
56
+
57
+ FAISS is a widely recognized standard for high-performance vector search engines.
58
+ USearch and FAISS both employ the same HNSW algorithm, but they differ significantly in their design principles.
59
+ USearch is compact and broadly compatible without sacrificing performance, with a primary focus on user-defined metrics and fewer dependencies.
57
60
 
58
61
  | | FAISS | USearch |
59
62
  | :----------------- | :---------------------------- | :--------------------------------- |
60
- | Implementation | 84 K [SLOC][sloc] in `faiss/` | 1 K [SLOC][sloc] in `usearch/` |
63
+ | Implementation | 84 K [SLOC][sloc] in `faiss/` | 3 K [SLOC][sloc] in `usearch/` |
61
64
  | Supported metrics | 9 fixed metrics | Any User-Defined metrics |
62
65
  | Supported ID types | `uint32_t`, `uint64_t` | `uint32_t`, `uint40_t`, `uint64_t` |
63
66
  | Dependencies | BLAS, OpenMP | None |
64
67
  | Bindings | SWIG | Native |
65
68
  | Acceleration | Learned Quantization | Downcasting |
66
69
 
67
- FAISS is the industry standard for a high-performance batteries-included vector search engine.
68
- Both USearch and FAISS implement the same HNSW algorithm.
69
- But they differ in a lot of design decisions.
70
- USearch is designed to be compact and broadly compatible without sacrificing performance.
71
-
72
- | | FAISS, `f32` | USearch, `f32` | USearch, `f16` | USearch, `f8` |
73
- | :----------- | -----------: | -------------: | -------------: | ------------: |
74
- | Batch Insert | 16 K/s | 73 K/s | 100 K/s | **104 K/s** |
75
- | Batch Search | 82 K/s | 103 K/s | 113 K/s | **134 K/s** |
76
- | Bulk Insert | 76 K/s | 105 K/s | 115 K/s | **202 K/s** |
77
- | Bulk Search | 118 K/s | 174 K/s | 173 K/s | **304 K/s** |
78
- | Recall @1 | 99% | 99.2% | 99.1% | **99.2%** |
79
-
80
- > Dataset: 1M vectors sample of the Deep1B dataset.
81
- > Hardware: `c7g.metal` AWS instance with 64 cores and DDR5 memory.
82
- > HNSW was configured with identical hyper-parameters:
83
- > connectivity `M=16`,
84
- > expansion @ construction `efConstruction=128`,
85
- > and expansion @ search `ef=64`.
86
- > Batch size is 256.
87
- > Both libraries were compiled for the target architecture.
88
- > Jump to the [Performance Tuning][benchmarking] section to read about the effects of those hyper-parameters.
89
-
90
70
  [sloc]: https://en.wikipedia.org/wiki/Source_lines_of_code
91
- [benchmarking]: https://github.com/unum-cloud/usearch/blob/main/docs/benchmarks.md
92
-
93
- ## User-Defined Functions
94
-
95
- Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
96
- That only partially exhausts the list of possible metrics.
97
- A good example would be the rare [Haversine][haversine] distance, used to compute the distance between geo-spatial coordinates, extending Vector Search into the GIS domain.
98
- Another example would be designing a custom metric for **composite embeddings** concatenated from multiple AI models in real-world applications.
99
- USearch supports that: [Python](#user-defined-functions-in-python) and [C++](#user-defined-functions-in-c) examples.
100
-
101
- ![USearch: Vector Search Approaches](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-approaches-white.png?raw=true)
102
-
103
- Unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
104
- They only have to be comparable.
105
- So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching.
106
-
107
- [haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
108
- [obscure]: https://ashvardanian.com/posts/abusing-vector-search
109
-
110
- ## Memory Efficiency, Downcasting, and Quantization
111
-
112
- Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
113
- Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
114
-
115
- ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
116
-
117
- Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
118
- The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f32_t`, `f16_t`, `f64_t`, and `f8_t` representations, even if the hardware doesn't natively support it.
119
- Continuing the topic of memory-efficiency, we provide a `uint40_t` to allow collection with over 4B+ vectors without allocating 8 bytes for every neighbor reference in the proximity graph.
120
-
121
- ## View Larger Indexes from Disk
122
-
123
- Modern search systems often suggest using different servers to maximize indexing speed and minimize serving costs.
124
- Memory-optimized for the first task, and storage-optimized for the second, if the index can be served from external memory, which USearch can.
125
-
126
- | | To Build | To Serve |
127
- | :------- | :-------------: | :--------------------: |
128
- | Instance | u-24tb1.metal | is4gen.8xlarge |
129
- | Price | ~ $200/h | ~$4.5/h |
130
- | Memory | 24 TB RAM + EBS | 192 GB RAM + 30 TB SSD |
131
-
132
- There is a 50x difference between the cost of such instances for identical capacity.
133
- Of course, the latency of external memory access will be higher, but it is in part compensated with an excellent prefetching mechanism.
134
-
135
- ## Usage
136
-
137
- There are two usage patters:
138
-
139
- 1. Bare-bones with `usearch/index.hpp`, only available in C++.
140
- 2. Full-fat version with it's own threads, mutexes, type-punning, quantization, that is available both in C++ and is wrapped for higher-level bindings.
141
-
142
- ### C++
143
-
144
- #### Installation
145
-
146
- To use in a C++ project simply copy the `include/usearch/index.hpp` header into your project.
147
- Alternatively fetch it with CMake:
148
-
149
- ```cmake
150
- FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearch.git)
151
- FetchContent_MakeAvailable(usearch)
152
- ```
153
-
154
- #### Quickstart
155
-
156
- Once included, the low-level C++11 interface is as simple as it gets: `reserve()`, `add()`, `search()`, `size()`, `capacity()`, `save()`, `load()`, `view()`.
157
- This covers 90% of use-cases.
158
71
 
159
- ```c++
160
- using namespace unum::usearch;
72
+ Base functionality is identical to FAISS, and the interface must be familiar if you have ever investigated Approximate Nearest Neigbors search:
161
73
 
162
- index_gt<cos_gt<float>> index;
163
- float vec[3] = {0.1, 0.3, 0.2};
164
-
165
- index.reserve(10);
166
- index.add(/* label: */ 42, /* vector: */ {&vec[0], 3});
167
- auto results = index.search(/* query: */ {&vec[0], 3}, 5 /* neighbors */);
168
-
169
- for (std::size_t i = 0; i != results.size(); ++i)
170
- results[i].member.label, results[i].member.vector, results[i].member.id, results[i].distance;
171
- ```
172
-
173
- The `add` is thread-safe for concurrent index construction.
174
-
175
- #### Serialization
176
-
177
- ```c++
178
- index.save("index.usearch");
179
- index.load("index.usearch"); // Copying from disk
180
- index.view("index.usearch"); // Memory-mapping from disk
181
- ```
182
-
183
- #### User-Defined Metrics in C++
184
-
185
- For advanced users, more compile-time abstractions are available.
186
-
187
- ```cpp
188
- template <typename metric_at = ip_gt<float>, //
189
- typename label_at = std::size_t, // `uint32_t`, `uuid_t`...
190
- typename id_at = std::uint32_t, // `uint40_t`, `uint64_t`...
191
- typename scalar_at = float, // `double`, `half`, `char`...
192
- typename allocator_at = std::allocator<char>> //
193
- class index_gt;
194
- ```
195
-
196
- You may want to use a custom memory allocator or a rare scalar type, but most often, you would start by defining a custom similarity measure.
197
- The function object should have the following signature to support different-length vectors.
198
-
199
- ```cpp
200
- struct custom_metric_t {
201
- T operator()(T const* a, T const* b, std::size_t a_length, std::size_t b_length) const;
202
- };
203
- ```
204
-
205
- The following distances are pre-packaged:
206
-
207
- - `cos_gt<scalar_t>` for "Cosine" or "Angular" distance.
208
- - `ip_gt<scalar_t>` for "Inner Product" or "Dot Product" distance.
209
- - `l2sq_gt<scalar_t>` for the squared "L2" or "Euclidean" distance.
210
- - `jaccard_gt<scalar_t>` for "Jaccard" distance between two ordered sets of unique elements.
211
- - `hamming_gt<scalar_t>` for "Hamming" distance, as the number of shared bits in hashes.
212
- - `tanimoto_gt<scalar_t>` for "Tanimoto" coefficient for bit-strings.
213
- - `sorensen_gt<scalar_t>` for "Dice-Sorensen" coefficient for bit-strings.
214
- - `pearson_correlation_gt<scalar_t>` for "Pearson" correlation between probability distributions.
215
- - `haversine_gt<scalar_t>` for "Haversine" or "Great Circle" distance between coordinates used in GIS applications.
216
-
217
- #### Multi-Threading
218
-
219
- Most AI, HPC, or Big Data packages use some form of a thread pool.
220
- Instead of spawning additional threads within USearch, we focus on the thread safety of `add()` function, simplifying resource management.
221
-
222
- ```cpp
223
- #pragma omp parallel for
224
- for (std::size_t i = 0; i < n; ++i)
225
- native.add(label, span_t{vector, dims}, add_config_t { .thread = omp_get_thread_num() });
226
- ```
227
-
228
- During initialization, we allocate enough temporary memory for all the cores on the machine.
229
- On the call, the user can supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
230
-
231
- ### Python
232
-
233
- #### Installation
234
-
235
- ```sh
236
- pip install usearch
237
- ```
238
-
239
- #### Quickstart
74
+ ```py
75
+ $ pip install usearch numpy
240
76
 
241
- ```python
242
77
  import numpy as np
243
78
  from usearch.index import Index
244
79
 
@@ -246,9 +81,9 @@ index = Index(
246
81
  ndim=3, # Define the number of dimensions in input vectors
247
82
  metric='cos', # Choose 'l2sq', 'haversine' or other metric, default = 'ip'
248
83
  dtype='f32', # Quantize to 'f16' or 'f8' if needed, default = 'f32'
249
- connectivity=16, # How frequent should the connections in the graph be, optional
250
- expansion_add=128, # Control the recall of indexing, optional
251
- expansion_search=64, # Control the quality of search, optional
84
+ connectivity=16, # Optional: How frequent should the connections in the graph be
85
+ expansion_add=128, # Optional: Control the recall of indexing
86
+ expansion_search=64, # Optional: Control the quality of search
252
87
  )
253
88
 
254
89
  vector = np.array([0.2, 0.6, 0.4])
@@ -262,428 +97,105 @@ assert distances[0] <= 0.001
262
97
  assert np.allclose(index[42], vector)
263
98
  ```
264
99
 
265
- Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
266
- Assuming the presence of Global Interpreter Lock in Python, we spawn threads in the C++ layer on large insertions.
267
-
268
- #### Serialization
269
-
270
- ```py
271
- index.save('index.usearch')
272
- index.load('index.usearch') # Copy the whole index into memory
273
- index.view('index.usearch') # View from disk without loading in memory
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
-
283
- #### Batch Operations
284
-
285
- Adding or querying a batch of entries is identical to adding a single vector.
286
- The difference would be in the shape of the tensors.
287
-
288
- ```py
289
- n = 100
290
- labels = np.arange(n)
291
- vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
292
-
293
- index.add(labels, vectors, threads=..., copy=...)
294
- matches, distances, counts = index.search(vectors, 10, threads=...)
295
-
296
- assert matches.shape[0] == vectors.shape[0]
297
- assert counts[0] <= 10
298
- ```
100
+ ## User-Defined Functions
299
101
 
300
- You can also override the default `threads` and `copy` arguments in bulk workloads.
301
- The first controls the number of threads spawned for the task.
302
- The second controls whether the vector itself will be persisted inside the index.
303
- If you can preserve the lifetime of the vector somewhere else, you can avoid the copy.
102
+ While most vector search packages concentrate on just a couple of metrics - "Inner Product distance" and "Euclidean distance," USearch extends this list to include any user-defined metrics.
103
+ This flexibility allows you to customize your search for a myriad of applications, from computing geo-spatial coordinates with the rare [Haversine][haversine] distance to creating custom metrics for composite embeddings from multiple AI models.
304
104
 
305
- #### User-Defined Metrics and JIT in Python
105
+ ![USearch: Vector Search Approaches](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-approaches-white.png?raw=true)
306
106
 
307
- ##### [Numba][numba]
107
+ Unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
108
+ They only have to be comparable.
109
+ So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching, using [GZip][gzip-similarity] as a distance function.
308
110
 
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.
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.
111
+ > Read more about [JIT and UDF in USearch Python SDK]().
311
112
 
312
- ```py
313
- from numba import cfunc, types, carray
314
-
315
- ndim = 256
316
- signature = types.float32(
317
- types.CPointer(types.float32),
318
- types.CPointer(types.float32))
319
-
320
- @cfunc(signature)
321
- def inner_product(a, b):
322
- a_array = carray(a, ndim)
323
- b_array = carray(b, ndim)
324
- c = 0.0
325
- for i in range(ndim):
326
- c += a_array[i] * b_array[i]
327
- return 1 - c
328
-
329
- index = Index(ndim=ndim, metric=CompiledMetric(
330
- pointer=inner_product.address,
331
- kind=MetricKind.IP,
332
- signature=MetricSignature.ArrayArray,
333
- ))
334
- ```
113
+ [haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
114
+ [obscure]: https://ashvardanian.com/posts/abusing-vector-search
115
+ [gzip-similarity]: https://twitter.com/LukeGessler/status/1679211291292889100?s=20
335
116
 
336
- Alternatively, you can avoid pre-defining the number of dimensions, and pass it separately:
117
+ ## Memory Efficiency, Downcasting, and Quantization
337
118
 
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
- ```
119
+ Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
120
+ Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
359
121
 
360
- ```sh
361
- pip install numba
362
- ```
122
+ ![USearch uint40_t support](https://github.com/unum-cloud/usearch/blob/main/assets/usearch-neighbor-types.png?raw=true)
363
123
 
364
- ##### [Cppyy][cppyy]
124
+ Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
125
+ The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f32_t`, `f16_t`, `f64_t`, and `f8_t` representations, even if the hardware doesn't natively support it.
126
+ Continuing the topic of memory-efficiency, we provide a `uint40_t` to allow collection with over 4B+ vectors without allocating 8 bytes for every neighbor reference in the proximity graph.
365
127
 
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!
128
+ | | FAISS, `f32` | USearch, `f32` | USearch, `f16` | USearch, `f8` |
129
+ | :----------- | -----------: | -------------: | -------------: | ----------------: |
130
+ | Batch Insert | 16 K/s | 73 K/s | 100 K/s | 104 K/s **+550%** |
131
+ | Batch Search | 82 K/s | 103 K/s | 113 K/s | 134 K/s **+63%** |
132
+ | Bulk Insert | 76 K/s | 105 K/s | 115 K/s | 202 K/s **+165%** |
133
+ | Bulk Search | 118 K/s | 174 K/s | 173 K/s | 304 K/s **+157%** |
134
+ | Recall @ 10 | 99% | 99.2% | 99.1% | 99.2% |
367
135
 
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
- ```
136
+ > Dataset: 1M vectors sample of the Deep1B dataset.
137
+ > Hardware: `c7g.metal` AWS instance with 64 cores and DDR5 memory.
138
+ > HNSW was configured with identical hyper-parameters:
139
+ > connectivity `M=16`,
140
+ > expansion @ construction `efConstruction=128`,
141
+ > and expansion @ search `ef=64`.
142
+ > Batch size is 256.
143
+ > Both libraries were compiled for the target architecture.
144
+ > Jump to the [Performance Tuning][benchmarking] section to read about the effects of those hyper-parameters.
389
145
 
390
- ```sh
391
- conda install -c conda-forge cppyy
392
- ```
146
+ [benchmarking]: https://github.com/unum-cloud/usearch/blob/main/docs/benchmarks.md
393
147
 
394
- ##### [PeachPy][peachpy]
148
+ ## Disk-based Indexes
395
149
 
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].
150
+ With USearch, you can serve indexes from external memory, enabling you to optimize your server choices for indexing speed and serving costs.
151
+ This can result in **20x costs reduction** on AWS and other public clouds.
400
152
 
401
153
  ```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)
473
- ```
154
+ index.save("index.usearch")
474
155
 
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
156
+ direct_copy = index.copy()
157
+ loaded_copy = index.load("index.usearch")
478
158
 
479
- #### Tooling
159
+ view = Index.restore("index.usearch", view=True)
480
160
 
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
-
484
- ```py
485
- from usearch.index import Index
486
- from usearch.io import load_matrix, save_matrix
487
-
488
- vectors = load_matrix('deep1B.fbin')
489
- index = Index(ndim=vectors.shape[1])
490
- index.add(labels, vectors)
161
+ other_view = Index(ndim=..., metric=CompiledMetric(...))
162
+ other_view.view("index.usearch")
491
163
  ```
492
164
 
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.
165
+ ## Joins
495
166
 
496
- ```py
497
- from usearch.eval import recall_members
167
+ One of the big questions these days is how will AI change the world of databases and data-management?
168
+ Most databases are still struggling to implement high-quality fuzzy search, and the only kind of joins they know are deterministic.
169
+ A `join` is different from searching for every entry, as it requires a one-to-one mapping, banning collisions among separate search results.
498
170
 
499
- assert recall_members(index, exact=True) == 1
500
- print(recall_members(index, exact=False))
501
- ```
171
+ | Exact Search | Fuzzy Search | Semantic Search ? |
172
+ | :----------: | :----------: | :---------------: |
173
+ | Exact Join | Fuzzy Join ? | Semantic Join ?? |
502
174
 
503
- In case you have some ground-truth data for more than one entry, you compare search results against expected values:
175
+ Using USearch one can implement sub-quadratic complexity approximate, fuzzy, and semantic joins.
176
+ This can come handy in any fuzzy-matching tasks, common to Database Management Software.
504
177
 
505
178
  ```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))
179
+ men = Index(...)
180
+ women = Index(...)
181
+ pairs: dict = men.join(women, max_proposals=0, exact=False)
513
182
  ```
514
183
 
515
- ### JavaScript
516
-
517
- #### Installation
518
-
519
- ```sh
520
- npm install usearch
521
- ```
522
-
523
- #### Quickstart
524
-
525
- ```js
526
- var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 3 })
527
- index.add(42, new Float32Array([0.2, 0.6, 0.4]))
528
- var results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10)
529
-
530
- assert.equal(index.size(), 1)
531
- assert.deepEqual(results.labels, new Uint32Array([42]))
532
- assert.deepEqual(results.distances, new Float32Array([0]))
533
- ```
534
-
535
- #### Serialization
536
-
537
- ```js
538
- index.save('index.usearch')
539
- index.load('index.usearch')
540
- index.view('index.usearch')
541
- ```
542
-
543
- ### Rust
544
-
545
- #### Installation
546
-
547
- ```sh
548
- cargo add usearch
549
- ```
184
+ > Read more in post: [From Dating to Vector Search - "Stable Marriages" on a Planetary Scale 👩‍❤️‍👨](https://ashvardanian.com/posts/searching-stable-marriages)
550
185
 
551
- #### Quickstart
186
+ ## Functionality
552
187
 
553
- ```rust
554
-
555
- let options = IndexOptions {
556
- dimensions: 5,
557
- metric: MetricKind::IP,
558
- quantization: ScalarKind::F16,
559
- connectivity: 0,
560
- expansion_add: 0,
561
- expansion_search: 0
562
- };
563
-
564
- let index = new_index(&options).unwrap();
565
-
566
- assert!(index.reserve(10).is_ok());
567
- assert!(index.capacity() >= 10);
568
- assert!(index.connectivity() != 0);
569
- assert_eq!(index.dimensions(), 3);
570
- assert_eq!(index.size(), 0);
571
-
572
- let first: [f32; 3] = [0.2, 0.1, 0.2];
573
- let second: [f32; 3] = [0.2, 0.1, 0.2];
574
-
575
- assert!(index.add(42, &first).is_ok());
576
- assert!(index.add(43, &second).is_ok());
577
- assert_eq!(index.size(), 2);
578
-
579
- // Read back the tags
580
- let results = index.search(&first, 10).unwrap();
581
- assert_eq!(results.count, 2);
582
- ```
583
-
584
- #### Multi-Threading
585
-
586
- ```rust
587
- assert!(index.add_in_thread(42, &first, 0).is_ok());
588
- assert!(index.add_in_thread(43, &second, 0).is_ok());
589
- let results = index.search_in_thread(&first, 10, 0).unwrap();
590
- ```
591
-
592
- Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
593
- Aside from the `add` and `search`, USearch Rust binding also provides `add_in_thread` and `search_in_thread`, which let users identify the calling thread to use underlying temporary memory more efficiently.
594
-
595
- #### Serialization
596
-
597
- ```rust
598
- assert!(index.save("index.usearch").is_ok());
599
- assert!(index.load("index.usearch").is_ok());
600
- assert!(index.view("index.usearch").is_ok());
601
- ```
602
-
603
- #### Metrics
604
-
605
- ```rust
606
- assert!(new_l2sq(3, &quant, 0, 0, 0).is_ok());
607
- assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
608
- assert!(new_haversine(&quant, 0, 0, 0).is_ok());
609
- ```
610
-
611
- ### Java
612
-
613
- #### Installation
614
-
615
- ```xml
616
- <dependency>
617
- <groupId>cloud.unum</groupId>
618
- <artifactId>usearch</artifactId>
619
- <version>0.2.3</version>
620
- </dependency>
621
- ```
622
-
623
- Add that snippet to your `pom.xml` and hit `mvn install`.
624
-
625
- #### Quickstart
626
-
627
- ```java
628
- Index index = new Index.Config().metric("cos").dimensions(2).build();
629
- float vec[] = {10, 20};
630
- index.add(42, vec);
631
- int[] labels = index.search(vec, 5);
632
- ```
633
-
634
- ### Swift
635
-
636
- #### Installation
637
-
638
- ```txt
639
- https://github.com/unum-cloud/usearch
640
- ```
641
-
642
- #### Quickstart
643
-
644
- ```swift
645
- let index = Index.l2sq(dimensions: 3, connectivity: 8)
646
- let vectorA: [Float32] = [0.3, 0.5, 1.2]
647
- let vectorB: [Float32] = [0.4, 0.2, 1.2]
648
- index.reserve(2)
649
- index.add(label: 42, vector: vectorA[...])
650
- index.add(label: 43, vector: vectorB[...])
651
-
652
- let results = index.search(vector: vectorA[...], count: 10)
653
- assert(results.0[0] == 42)
654
- ```
655
-
656
- ### GoLang
657
-
658
- #### Installation
659
-
660
- ```golang
661
- import (
662
- "github.com/unum-cloud/usearch/golang"
663
- )
664
- ```
665
-
666
- #### Quickstart
667
-
668
- ```golang
669
- package main
670
-
671
- import (
672
- "fmt"
673
- "github.com/unum-cloud/usearch/golang"
674
- )
675
-
676
- func main() {
677
- conf := usearch.DefaultConfig(128)
678
- index := usearch.NewIndex(conf)
679
- v := make([]float32, 128)
680
- index.Add(42, v)
681
- results := index.Search(v, 1)
682
- }
683
- ```
684
-
685
- ### Wolfram
188
+ By now, core functionality is supported across all bindings.
189
+ Broader functionality is ported per request.
686
190
 
191
+ | | C++ | Python | Java | JavaScript | Rust | GoLang | Swift |
192
+ | :---------------------- | :---: | :----: | :---: | :--------: | :---: | :----: | :---: |
193
+ | add/search/remove | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
194
+ | save/load/view | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
195
+ | join | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
196
+ | user-defiend metrics | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
197
+ | variable-length vectors | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
198
+ | 4B+ capacities | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
687
199
 
688
200
  ## Application Examples
689
201
 
@@ -755,7 +267,7 @@ encoder = AllChem.GetRDKitFPGenerator()
755
267
  fingerprints = np.vstack([encoder.GetFingerprint(x) for x in molecules])
756
268
  fingerprints = np.packbits(fingerprints, axis=1)
757
269
 
758
- index = Index(ndim=2048, metric=MetricKind.BitwiseTanimoto)
270
+ index = Index(ndim=2048, metric=MetricKind.Tanimoto)
759
271
  labels = np.arange(len(molecules))
760
272
 
761
273
  index.add(labels, fingerprints)
@@ -791,7 +303,3 @@ year = {2022}
791
303
  month = jun,
792
304
  }
793
305
  ```
794
-
795
- ---
796
-
797
- Check [that](https://github.com/ashvardanian/image-search) and [other](https://github.com/unum-cloud/examples) examples on our corporate GitHub 🤗