usearch 0.2.2 → 0.2.4
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/.github/workflows/release.yml +39 -15
- package/.github/workflows/update_version.sh +6 -0
- package/.releaserc +77 -0
- package/.vscode/settings.json +10 -1
- package/Cargo.toml +1 -1
- package/README.md +281 -230
- package/VERSION +1 -1
- package/conanfile.py +3 -2
- package/docs/Benchmarking.md +182 -0
- package/docs/Compilation.md +112 -0
- package/include/usearch/usearch.hpp +212 -116
- package/objc/include/USearchObjective.h +10 -9
- package/package.json +7 -1
- package/python/bench.py +100 -28
- package/python/lib.cpp +51 -28
- package/python/test.py +78 -36
- package/setup.py +11 -3
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +58 -23
- package/src/bench.cpp +11 -15
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
package/README.md
CHANGED
|
@@ -17,136 +17,126 @@ Vector Search Engine<br/>
|
|
|
17
17
|
<a href="https://github.com/unum-cloud/usearch"><img height="25" src="https://github.com/unum-cloud/.github/raw/main/assets/github.svg" alt="GitHub"></a>
|
|
18
18
|
</p>
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
<p align="center">
|
|
21
|
+
Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Metrics
|
|
22
|
+
<br/>
|
|
23
|
+
<a href="#c++">C++11</a> •
|
|
24
|
+
<a href="#python">Python</a> •
|
|
25
|
+
<a href="#javascript">JavaScript</a> •
|
|
26
|
+
<a href="#java">Java</a> •
|
|
27
|
+
<a href="#rust">Rust</a> •
|
|
28
|
+
<a href="#objective-c">Objective-C</a> •
|
|
29
|
+
<a href="#swift">Swift</a> •
|
|
30
|
+
<a href="#golang">GoLang</a> •
|
|
31
|
+
<a href="#wolfram">Wolfram</a>
|
|
32
|
+
<br/>
|
|
33
|
+
Linux • MacOS • Windows
|
|
34
|
+
</p>
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
- [x] Industry-leading [performance](#performance).
|
|
39
|
+
- [x] Easily-extendible [single C++11 header][usearch-header] implementation.
|
|
40
|
+
- [x] [User-defined](#define-custom-metrics) and pre-packaged SIMD-accelerated metrics.
|
|
41
|
+
- [x] [Half-precision `f16` and Quarter-precision `f8`](#quantize-on-the-fly) support on any hardware.
|
|
23
42
|
- [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
|
|
24
|
-
- [x] [
|
|
25
|
-
|
|
26
|
-
- [x] Jaccard, Hamming, Haversine.
|
|
27
|
-
- [x] Hardware-accelerated [`ashvardanian/simsimd`](https://github.com/ashvardanian/simsimd).
|
|
28
|
-
- [x] Variable dimensionality vectors.
|
|
29
|
-
- [x] Don't copy vectors if not needed.
|
|
43
|
+
- [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
|
|
44
|
+
- [x] Variable dimensionality vectors - for [obscure use-cases][obscure-use-cases].
|
|
30
45
|
- [x] [Bring your threads](#bring-your-threads), like OpenMP.
|
|
31
46
|
- [x] Multiple vectors per label.
|
|
32
|
-
- [x] [Python](#python) bindings: `pip install usearch`.
|
|
33
|
-
- [x] [JavaScript](#javascript) bindings: `npm install usearch`.
|
|
34
|
-
- [x] [Rust](#rust) bindings: `cargo add usearch`.
|
|
35
|
-
- [x] [Java](#java) bindings: `cloud.unum:usearch` on GitHub.
|
|
36
|
-
- [ ] GoLang bindings.
|
|
37
|
-
- [x] Wolfram language bindings.
|
|
38
|
-
- [x] For Linux: GCC, Clang.
|
|
39
|
-
- [x] For MacOS: Apple Clang.
|
|
40
|
-
- [ ] For Windows.
|
|
41
|
-
- [ ] Multi-index lookups in Python.
|
|
42
47
|
- [ ] Thread-safe `reserve`.
|
|
43
|
-
- [
|
|
44
|
-
- [x] AI + Vector Search = Semantic Search.
|
|
48
|
+
- [x] AI + Vector Search = [Semantic Search](#ai--vector-search--semantic-search).
|
|
45
49
|
|
|
46
50
|
[usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
[obscure-use-cases]: https://ashvardanian.com/posts/abusing-vector-search
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
| | FAISS | USearch |
|
|
56
|
+
| :----------------- | :---------------------------- | :--------------------------------- |
|
|
57
|
+
| Implementation | 84 K [SLOC][sloc] in `faiss/` | 1 K [SLOC][sloc] in `usearch/` |
|
|
58
|
+
| Supported metrics | 9 fixed metrics | Any User-Defined metrics |
|
|
59
|
+
| Supported ID types | `uint32_t`, `uint64_t` | `uint32_t`, `uint40_t`, `uint64_t` |
|
|
60
|
+
| Dependencies | BLAS, OpenMP | None |
|
|
61
|
+
| Bindings | SWIG | Native |
|
|
62
|
+
| Acceleration | Learned Quantization | Downcasting |
|
|
63
|
+
|
|
64
|
+
FAISS is the industry standard for a high-performance batteries-included vector search engine.
|
|
65
|
+
Both USearch and FAISS implement the same HNSW algorithm.
|
|
66
|
+
But they differ in a lot of design decisions.
|
|
67
|
+
USearch is designed to be compact and broadly compatible without sacrificing performance.
|
|
68
|
+
|
|
69
|
+
| | FAISS, `f32` | USearch, `f32` | USearch, `f16` | USearch, `f8` |
|
|
70
|
+
| :--------- | -----------: | -------------: | -------------: | ------------: |
|
|
71
|
+
| Insertions | 76 K/s | 89 K/s | 73 K/s | **137 K/s** |
|
|
72
|
+
| Queries | 118 K/s | 167 K/s | 140 K/s | **281 K/s** |
|
|
73
|
+
| Recall @1 | 99% | 99.2% | 98% | **99.2%** |
|
|
74
|
+
|
|
75
|
+
> Dataset: 1M vectors sample of the Deep1B dataset.
|
|
76
|
+
> Hardware: `c7g.metal` AWS instance with 64 cores and DDR5 memory.
|
|
77
|
+
> HNSW was configured with identical hyper-parameters:
|
|
78
|
+
> connectivity `M=16`,
|
|
79
|
+
> expansion @ construction `efConstruction=128`,
|
|
80
|
+
> and expansion @ search `ef=64`.
|
|
81
|
+
> Both libraries were compiled for the target architecture.
|
|
82
|
+
> Jump to the [Performance Tuning][benchmarking] section to read about the effects of those hyper-parameters.
|
|
83
|
+
|
|
84
|
+
[sloc]: https://en.wikipedia.org/wiki/Source_lines_of_code
|
|
85
|
+
[benchmarking]: https://github.com/unum-cloud/usearch
|
|
86
|
+
|
|
87
|
+
## User-Defined Functions
|
|
51
88
|
|
|
52
89
|
Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
That only partially exhausts the list of possible metrics.
|
|
91
|
+
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.
|
|
92
|
+
Another example would be designing a custom metric for **composite embeddings** concatenated from multiple AI models in real-world applications.
|
|
93
|
+
USearch supports that: [Python](#user-defined-functions-in-python) and [C++](#user-defined-functions-in-c) examples.
|
|
55
94
|
|
|
56
95
|

|
|
57
96
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
USearch implements HNSW, and provides some uncommon-to-vector-search "similarity measures" and "distances" out of the box: Haversine, Jaccard, Hamming, etc.
|
|
97
|
+
Unlike older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing, HNSW doesn't require vectors to be identical in length.
|
|
98
|
+
They only have to be comparable.
|
|
99
|
+
So you can apply it in [obscure][obscure] applications, like searching for similar sets or fuzzy text matching.
|
|
62
100
|
|
|
63
|
-
|
|
101
|
+
[haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
|
|
102
|
+
[obscure]: https://ashvardanian.com/posts/abusing-vector-search
|
|
64
103
|
|
|
65
|
-
|
|
66
|
-
Instead of spawning additional threads within USearch, we focus on thread-safety of the `add` function.
|
|
104
|
+
## Memory Efficiency, Downcasting, and Quantization
|
|
67
105
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for (std::size_t i = 0; i < n; ++i)
|
|
71
|
-
native.add(label, span_t{vector, dims}, omp_get_thread_num());
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
During initialization we allocate enough temporary memory for all the cores on the machine.
|
|
75
|
-
On call, the user can simply supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
|
|
76
|
-
|
|
77
|
-
### Go Beyond 4B Entries
|
|
78
|
-
|
|
79
|
-
Constructing a vector index is a memory-intensive task.
|
|
80
|
-
Depending on your configuration parameters, part of the memory will be allocated towards copies of vectors, and another part to the Small World Graph, common to NSW-like algorithms.
|
|
81
|
-
Choosing the right data-structures and numeric types can have profound implications on both.
|
|
106
|
+
Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
|
|
107
|
+
Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
|
|
82
108
|
|
|
83
109
|

|
|
84
110
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Others hard-code it to `uint64_t`, which hardly space-efficient.
|
|
89
|
-
We added a tiny class - `uint40_t`, that should be enough for collections up to 1 Trillion vectors.
|
|
90
|
-
|
|
91
|
-
### View Larger Indexes from Disk
|
|
92
|
-
|
|
93
|
-
To construct a large index one may use a beefy RAM-optimized machine, like the AWS `u-24tb1.metal` instances with 24 TB of RAM.
|
|
94
|
-
Those are, however, pricy, and would cost you over $200/hour.
|
|
95
|
-
|
|
96
|
-
| | To Build | To Serve |
|
|
97
|
-
| :------- | :-----------: | :------------: |
|
|
98
|
-
| Instance | u-24tb1.metal | is4gen.8xlarge |
|
|
99
|
-
| Price | ~ $200/h | ~$4.5/h |
|
|
100
|
-
| RAM | 24 TB | 192 GB |
|
|
101
|
-
| SSD | EBS | 30 TB |
|
|
102
|
-
|
|
103
|
-
You wouldn't want to have them running all the time.
|
|
104
|
-
So, if you are working on large datasets, but don't need the in-RAM throughput, you can simply view an existing dataset from disk, without ever loading it fully into memory.
|
|
105
|
-
|
|
106
|
-
> Construct on large machines, deploy on the cheap ones.
|
|
111
|
+
Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
|
|
112
|
+
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.
|
|
113
|
+
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.
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
## View Larger Indexes from Disk
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
Modern search systems often suggest using different servers to maximize indexing speed and minimize serving costs.
|
|
118
|
+
Memory-optimized for the first task, and storage-optimized for the second, if the index can be served from external memory, which USearch can.
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
| | To Build | To Serve |
|
|
121
|
+
| :------- | :-------------: | :--------------------: |
|
|
122
|
+
| Instance | u-24tb1.metal | is4gen.8xlarge |
|
|
123
|
+
| Price | ~ $200/h | ~$4.5/h |
|
|
124
|
+
| Memory | 24 TB RAM + EBS | 192 GB RAM + 30 TB SSD |
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Below are the performance numbers for a benchmark running on the 64 cores of AWS `c7g.metal` "Graviton 3"-based instances.
|
|
118
|
-
We fix the default configuration in the top line and show the affects of various parameters by changing one parameter at a time.
|
|
119
|
-
|
|
120
|
-
| Vectors | Connectivity | EF @ A | EF @ S | **Add**, QPS | **Search**, QPS | **Recall @1** |
|
|
121
|
-
| :--------: | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
|
|
122
|
-
| `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
|
|
123
|
-
| | | | | | | |
|
|
124
|
-
| `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
|
|
125
|
-
| `f32` x256 | 32 | 128 | 64 | 64'368 | 104'050 | 99.4% |
|
|
126
|
-
| | | | | | | |
|
|
127
|
-
| `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
128
|
-
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
129
|
-
| | | | | | | |
|
|
130
|
-
| `f16` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
131
|
-
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
132
|
-
|
|
133
|
-
The main columns are:
|
|
134
|
-
|
|
135
|
-
- Add: Number of insertion Queries Per Second.
|
|
136
|
-
- Search: Number search Queries Per Second.
|
|
137
|
-
- Recall @1: How often does approximate search yield the exact best match?
|
|
138
|
-
|
|
139
|
-
To read more and reproduce, jump to [benchmarking section](#benchmarking).
|
|
126
|
+
There is a 50x difference between the cost of such instances for identical capacity.
|
|
127
|
+
Of course, the latency of external memory access will be higher, but it is in part compensated with an excellent prefetching mechanism.
|
|
140
128
|
|
|
141
129
|
## Usage
|
|
142
130
|
|
|
143
131
|
There are two usage patters:
|
|
144
132
|
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
1. Bare-bones with `usearch/usearch.hpp`, only available in C++.
|
|
134
|
+
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.
|
|
147
135
|
|
|
148
136
|
### C++
|
|
149
137
|
|
|
138
|
+
#### Installation
|
|
139
|
+
|
|
150
140
|
To use in a C++ project simply copy the `include/usearch/usearch.hpp` header into your project.
|
|
151
141
|
Alternatively fetch it with CMake:
|
|
152
142
|
|
|
@@ -155,19 +145,10 @@ FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearc
|
|
|
155
145
|
FetchContent_MakeAvailable(usearch)
|
|
156
146
|
```
|
|
157
147
|
|
|
158
|
-
|
|
159
|
-
That can be one of the following templates:
|
|
160
|
-
|
|
161
|
-
- `cos_gt<float>` for "Cosine" or "Angular" distance.
|
|
162
|
-
- `ip_gt<float>` for "Inner Product" or "Dot Product" distance.
|
|
163
|
-
- `l2sq_gt<float>` for the squared "L2" or "Euclidean" distance.
|
|
164
|
-
- `jaccard_gt<int>` for "Jaccard" distance between two ordered sets of unique elements.
|
|
165
|
-
- `bit_hamming_gt<uint>` for "Hamming" distance, as the number of shared bits in hashes.
|
|
166
|
-
- `pearson_correlation_gt<float>` for "Pearson" correlation between probability distributions.
|
|
167
|
-
- `haversine_gt<float>` for "Haversine" or "Great Circle" distance between coordinates.
|
|
148
|
+
#### Quickstart
|
|
168
149
|
|
|
169
|
-
|
|
170
|
-
|
|
150
|
+
Once included, the low-level C++11 interface is as simple as it gets: `reserve()`, `add()`, `search()`, `size()`, `capacity()`, `save()`, `load()`, `view()`.
|
|
151
|
+
This covers 90% of use-cases.
|
|
171
152
|
|
|
172
153
|
```c++
|
|
173
154
|
using namespace unum::usearch;
|
|
@@ -180,14 +161,21 @@ index.add(/* label: */ 42, /* vector: */ {&vec, 3});
|
|
|
180
161
|
index.search(
|
|
181
162
|
/* query: */ {&vec, 3}, /* top */ 5 /* results */,
|
|
182
163
|
/* with callback: */ [](std::size_t label, float distance) { });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The `add` is thread-safe for concurrent index construction.
|
|
167
|
+
|
|
168
|
+
#### Serialization
|
|
183
169
|
|
|
184
|
-
|
|
185
|
-
index.
|
|
170
|
+
```c++
|
|
171
|
+
index.save("index.usearch");
|
|
172
|
+
index.load("index.usearch"); // Copying from disk
|
|
186
173
|
index.view("index.usearch"); // Memory-mapping from disk
|
|
187
174
|
```
|
|
188
175
|
|
|
189
|
-
|
|
190
|
-
|
|
176
|
+
#### User-Defined Metrics in C++
|
|
177
|
+
|
|
178
|
+
For advanced users, more compile-time abstractions are available.
|
|
191
179
|
|
|
192
180
|
```cpp
|
|
193
181
|
template <typename metric_at = ip_gt<float>, //
|
|
@@ -198,8 +186,8 @@ template <typename metric_at = ip_gt<float>, //
|
|
|
198
186
|
class index_gt;
|
|
199
187
|
```
|
|
200
188
|
|
|
201
|
-
|
|
202
|
-
The
|
|
189
|
+
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.
|
|
190
|
+
The function object should have the following signature to support different-length vectors.
|
|
203
191
|
|
|
204
192
|
```cpp
|
|
205
193
|
struct custom_metric_t {
|
|
@@ -207,82 +195,184 @@ struct custom_metric_t {
|
|
|
207
195
|
};
|
|
208
196
|
```
|
|
209
197
|
|
|
198
|
+
The following distances are pre-packaged:
|
|
199
|
+
|
|
200
|
+
- `cos_gt<scalar_t>` for "Cosine" or "Angular" distance.
|
|
201
|
+
- `ip_gt<scalar_t>` for "Inner Product" or "Dot Product" distance.
|
|
202
|
+
- `l2sq_gt<scalar_t>` for the squared "L2" or "Euclidean" distance.
|
|
203
|
+
- `jaccard_gt<scalar_t>` for "Jaccard" distance between two ordered sets of unique elements.
|
|
204
|
+
- `bit_hamming_gt<scalar_t>` for "Hamming" distance, as the number of shared bits in hashes.
|
|
205
|
+
- `pearson_correlation_gt<scalar_t>` for "Pearson" correlation between probability distributions.
|
|
206
|
+
- `haversine_gt<scalar_t>` for "Haversine" or "Great Circle" distance between coordinates.
|
|
207
|
+
|
|
208
|
+
#### Bring your Threads
|
|
209
|
+
|
|
210
|
+
Most AI, HPC, or Big Data packages use some form of a thread pool.
|
|
211
|
+
Instead of spawning additional threads within USearch, we focus on the thread safety of `add()` function, simplifying resource management.
|
|
212
|
+
|
|
213
|
+
```cpp
|
|
214
|
+
#pragma omp parallel for
|
|
215
|
+
for (std::size_t i = 0; i < n; ++i)
|
|
216
|
+
native.add(label, span_t{vector, dims}, omp_get_thread_num());
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
During initialization, we allocate enough temporary memory for all the cores on the machine.
|
|
220
|
+
On the call, the user can supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
|
|
221
|
+
|
|
210
222
|
### Python
|
|
211
223
|
|
|
212
|
-
|
|
213
|
-
Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
|
|
224
|
+
#### Installation
|
|
214
225
|
|
|
215
|
-
```
|
|
216
|
-
|
|
226
|
+
```sh
|
|
227
|
+
pip install usearch
|
|
228
|
+
```
|
|
217
229
|
|
|
230
|
+
#### Quickstart
|
|
231
|
+
|
|
232
|
+
```python
|
|
218
233
|
import numpy as np
|
|
219
234
|
from usearch.index import Index
|
|
220
235
|
|
|
221
236
|
index = Index(
|
|
222
|
-
ndim=
|
|
223
|
-
metric='cos', # Choose
|
|
224
|
-
dtype='f32', # Quantize to 'f16' or 'f8' if needed, default = 'f32'
|
|
237
|
+
ndim=3, # Define the number of dimensions in input vectors
|
|
238
|
+
metric='cos', # Choose 'l2', 'haversine' or other metric, default = 'ip'
|
|
239
|
+
dtype='f32', # Quantize to 'f16' or 'f8' if needed, default = 'f32'
|
|
225
240
|
connectivity=16, # How frequent should the connections in the graph be, optional
|
|
226
241
|
expansion_add=128, # Control the recall of indexing, optional
|
|
227
242
|
expansion_search=64, # Control the quality of search, optional
|
|
228
243
|
)
|
|
229
244
|
|
|
230
|
-
vector = np.
|
|
245
|
+
vector = np.array([0.2, 0.6, 0.4], dtype=np.float32)
|
|
231
246
|
index.add(42, vector)
|
|
232
247
|
matches, distances, count = index.search(vector, 10)
|
|
248
|
+
|
|
249
|
+
assert len(index) == 1
|
|
250
|
+
assert count == 1
|
|
251
|
+
assert matches[0] == 42
|
|
252
|
+
assert distances[0] <= 0.001
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
|
|
256
|
+
Assuming the presence of Global Interpreter Lock in Python, we spawn threads in the C++ layer on large insertions.
|
|
257
|
+
|
|
258
|
+
#### Serialization
|
|
259
|
+
|
|
260
|
+
```py
|
|
261
|
+
index.save('index.usearch')
|
|
262
|
+
index.load('index.usearch') # Copy the whole index into memory
|
|
263
|
+
index.view('index.usearch') # View from disk without loading in memory
|
|
233
264
|
```
|
|
234
265
|
|
|
235
|
-
|
|
236
|
-
|
|
266
|
+
#### Batch Operations
|
|
267
|
+
|
|
268
|
+
Adding or querying a batch of entries is identical to adding a single vector.
|
|
269
|
+
The difference would be in the shape of the tensors.
|
|
237
270
|
|
|
238
271
|
```py
|
|
239
272
|
n = 100
|
|
240
273
|
labels = np.array(range(n), dtype=np.longlong)
|
|
241
274
|
vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
matches
|
|
276
|
+
index.add(labels, vectors, threads=..., copy=...)
|
|
277
|
+
matches, distances, counts = index.search(vectors, 10, threads=...)
|
|
278
|
+
|
|
279
|
+
assert matches.shape[0] == vectors.shape[0]
|
|
280
|
+
assert counts[0] <= 10
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
You can also override the default `threads` and `copy` arguments in bulk workloads.
|
|
284
|
+
The first controls the number of threads spawned for the task.
|
|
285
|
+
The second controls whether the vector itself will be persisted inside the index.
|
|
286
|
+
If you can preserve the lifetime of the vector somewhere else, you can avoid the copy.
|
|
287
|
+
|
|
288
|
+
#### User-Defined Metrics in Python
|
|
289
|
+
|
|
290
|
+
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.
|
|
291
|
+
Luckily, with the help of [Numba][numba], we can JIT compile a function with a matching signature and pass it down to the engine.
|
|
292
|
+
|
|
293
|
+
```py
|
|
294
|
+
from numba import cfunc, types, carray
|
|
295
|
+
|
|
296
|
+
signature = types.float32(
|
|
297
|
+
types.CPointer(types.float32),
|
|
298
|
+
types.CPointer(types.float32),
|
|
299
|
+
types.size_t, types.size_t)
|
|
300
|
+
|
|
301
|
+
@cfunc(signature)
|
|
302
|
+
def python_dot(a, b, n, m):
|
|
303
|
+
a_array = carray(a, n)
|
|
304
|
+
b_array = carray(b, n)
|
|
305
|
+
c = 0.0
|
|
306
|
+
for i in range(n):
|
|
307
|
+
c += a_array[i] * b_array[i]
|
|
308
|
+
return c
|
|
309
|
+
|
|
310
|
+
index = Index(ndim=ndim, metric_pointer=python_dot.address)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
[numba]: https://numba.readthedocs.io/en/stable/reference/jit-compilation.html#c-callbacks
|
|
314
|
+
|
|
315
|
+
#### Tooling
|
|
316
|
+
|
|
317
|
+
```py
|
|
318
|
+
from usearch.index import Index
|
|
319
|
+
from usearch.io import load_matrix, save_matrix
|
|
320
|
+
|
|
321
|
+
vectors = load_matrix('deep1B.fbin')
|
|
322
|
+
index = Index(ndim=vectors.shape[1])
|
|
323
|
+
index.add(labels, vectors)
|
|
247
324
|
```
|
|
248
325
|
|
|
249
326
|
### JavaScript
|
|
250
327
|
|
|
328
|
+
#### Installation
|
|
329
|
+
|
|
330
|
+
```sh
|
|
331
|
+
npm install usearch
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### Quickstart
|
|
335
|
+
|
|
251
336
|
```js
|
|
252
|
-
|
|
337
|
+
var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 3 })
|
|
338
|
+
index.add(42, new Float32Array([0.2, 0.6, 0.4]))
|
|
339
|
+
var results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10)
|
|
253
340
|
|
|
254
|
-
|
|
255
|
-
assert.
|
|
256
|
-
assert.
|
|
257
|
-
|
|
341
|
+
assert.equal(index.size(), 1)
|
|
342
|
+
assert.deepEqual(results.labels, new Uint32Array([42]))
|
|
343
|
+
assert.deepEqual(results.distances, new Float32Array([0]))
|
|
344
|
+
```
|
|
258
345
|
|
|
259
|
-
|
|
260
|
-
assert.equal(index.size(), 2)
|
|
346
|
+
#### Serialization
|
|
261
347
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
348
|
+
```js
|
|
349
|
+
index.save('index.usearch')
|
|
350
|
+
index.load('index.usearch')
|
|
351
|
+
index.view('index.usearch')
|
|
265
352
|
```
|
|
266
353
|
|
|
267
354
|
### Rust
|
|
268
355
|
|
|
269
|
-
|
|
270
|
-
Aside from the `add` and `search`, it also provides `add_in_thread` and `search_in_thread` which let users identify the calling thread to use underlying temporary memory more efficiently.
|
|
356
|
+
#### Installation
|
|
271
357
|
|
|
272
|
-
```
|
|
273
|
-
|
|
358
|
+
```sh
|
|
359
|
+
cargo add usearch
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### Quickstart
|
|
274
363
|
|
|
364
|
+
```rust
|
|
275
365
|
let quant: &str = "f16";
|
|
276
|
-
let index = new_ip(
|
|
366
|
+
let index = new_ip(3, &quant, 0, 0, 0).unwrap();
|
|
277
367
|
|
|
278
368
|
assert!(index.reserve(10).is_ok());
|
|
279
369
|
assert!(index.capacity() >= 10);
|
|
280
370
|
assert!(index.connectivity() != 0);
|
|
281
|
-
assert_eq!(index.dimensions(),
|
|
371
|
+
assert_eq!(index.dimensions(), 3);
|
|
282
372
|
assert_eq!(index.size(), 0);
|
|
283
373
|
|
|
284
|
-
let first: [f32;
|
|
285
|
-
let second: [f32;
|
|
374
|
+
let first: [f32; 3] = [0.2, 0.1, 0.2];
|
|
375
|
+
let second: [f32; 3] = [0.2, 0.1, 0.2];
|
|
286
376
|
|
|
287
377
|
assert!(index.add(42, &first).is_ok());
|
|
288
378
|
assert!(index.add(43, &second).is_ok());
|
|
@@ -291,20 +381,43 @@ assert_eq!(index.size(), 2);
|
|
|
291
381
|
// Read back the tags
|
|
292
382
|
let results = index.search(&first, 10).unwrap();
|
|
293
383
|
assert_eq!(results.count, 2);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
|
|
387
|
+
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.
|
|
294
388
|
|
|
295
|
-
|
|
389
|
+
#### Serialization
|
|
390
|
+
|
|
391
|
+
```rust
|
|
296
392
|
assert!(index.save("index.usearch").is_ok());
|
|
297
393
|
assert!(index.load("index.usearch").is_ok());
|
|
298
394
|
assert!(index.view("index.usearch").is_ok());
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
#### Metrics
|
|
299
398
|
|
|
300
|
-
|
|
301
|
-
assert!(new_l2(
|
|
302
|
-
assert!(new_cos(
|
|
399
|
+
```rust
|
|
400
|
+
assert!(new_l2(3, &quant, 0, 0, 0).is_ok());
|
|
401
|
+
assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
|
|
303
402
|
assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
304
403
|
```
|
|
305
404
|
|
|
306
405
|
### Java
|
|
307
406
|
|
|
407
|
+
#### Installation
|
|
408
|
+
|
|
409
|
+
```xml
|
|
410
|
+
<dependency>
|
|
411
|
+
<groupId>cloud.unum.usearch</groupId>
|
|
412
|
+
<artifactId>usearch</artifactId>
|
|
413
|
+
<version>0.2.3</version>
|
|
414
|
+
</dependency>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Add that snippet to your `pom.xml` and hit `mvn install`.
|
|
418
|
+
|
|
419
|
+
#### Quickstart
|
|
420
|
+
|
|
308
421
|
```java
|
|
309
422
|
Index index = new Index.Config().metric("cos").dimensions(2).build();
|
|
310
423
|
float vec[] = {10, 20};
|
|
@@ -312,12 +425,20 @@ index.add(42, vec);
|
|
|
312
425
|
int[] labels = index.search(vec, 5);
|
|
313
426
|
```
|
|
314
427
|
|
|
315
|
-
###
|
|
428
|
+
### Swift
|
|
429
|
+
|
|
430
|
+
#### Installation
|
|
431
|
+
|
|
432
|
+
```txt
|
|
433
|
+
https://github.com/unum-cloud/usearch
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
#### Quickstart
|
|
316
437
|
|
|
317
438
|
```swift
|
|
318
|
-
let index = Index.l2(dimensions:
|
|
319
|
-
let vectorA: [Float32] = [0.3, 0.5, 1.2
|
|
320
|
-
let vectorB: [Float32] = [0.4, 0.2, 1.2
|
|
439
|
+
let index = Index.l2(dimensions: 3, connectivity: 8)
|
|
440
|
+
let vectorA: [Float32] = [0.3, 0.5, 1.2]
|
|
441
|
+
let vectorB: [Float32] = [0.4, 0.2, 1.2]
|
|
321
442
|
index.add(label: 42, vector: vectorA[...])
|
|
322
443
|
index.add(label: 43, vector: vectorB[...])
|
|
323
444
|
|
|
@@ -329,81 +450,11 @@ assert(results.0[0] == 42)
|
|
|
329
450
|
|
|
330
451
|
### Wolfram
|
|
331
452
|
|
|
332
|
-
## Benchmarking
|
|
333
|
-
|
|
334
|
-
All major HNSW implementation share an identical list of hyper-parameters:
|
|
335
|
-
|
|
336
|
-
- connectivity (often called `M`),
|
|
337
|
-
- expansion on additions (often called `efConstruction`),
|
|
338
|
-
- expansion on search (often called `ef`).
|
|
339
|
-
|
|
340
|
-
The default values vary drastically.
|
|
341
|
-
|
|
342
|
-
| Library | Connectivity | EF @ A | EF @ S |
|
|
343
|
-
| :-------: | :----------: | :----: | :----: |
|
|
344
|
-
| `hnswlib` | 16 | 200 | 10 |
|
|
345
|
-
| `FAISS` | 32 | 40 | 16 |
|
|
346
|
-
| `USearch` | 16 | 128 | 64 |
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
To achieve best results, please compile locally and check out various configuration options.
|
|
350
|
-
|
|
351
|
-
```sh
|
|
352
|
-
cmake -B ./build_release \
|
|
353
|
-
-DCMAKE_BUILD_TYPE=Release \
|
|
354
|
-
-DUSEARCH_USE_OPENMP=1 \
|
|
355
|
-
-DUSEARCH_USE_JEMALLOC=1 && \
|
|
356
|
-
make -C ./build_release -j
|
|
357
|
-
|
|
358
|
-
./build_release/bench --help
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
Which would print the following instructions:
|
|
362
|
-
|
|
363
|
-
```txt
|
|
364
|
-
SYNOPSIS
|
|
365
|
-
./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
|
|
366
|
-
<integer>] [-c <integer>] [--expansion-add <integer>]
|
|
367
|
-
[--expansion-search <integer>] [--native|--f16quant|--i8quant]
|
|
368
|
-
[--ip|--l2|--cos|--haversine] [-h]
|
|
369
|
-
|
|
370
|
-
OPTIONS
|
|
371
|
-
--vectors <path>
|
|
372
|
-
.fbin file path to construct the index
|
|
373
|
-
|
|
374
|
-
--queries <path>
|
|
375
|
-
.fbin file path to query the index
|
|
376
|
-
|
|
377
|
-
--neighbors <path>
|
|
378
|
-
.ibin file path with ground truth
|
|
379
|
-
|
|
380
|
-
-b, --big Will switch to uint40_t for neighbors lists with over 4B entries
|
|
381
|
-
-j, --threads <integer>
|
|
382
|
-
Uses all available cores by default
|
|
383
|
-
|
|
384
|
-
-c, --connectivity <integer>
|
|
385
|
-
Index granularity
|
|
386
|
-
|
|
387
|
-
--expansion-add <integer>
|
|
388
|
-
Affects indexing depth
|
|
389
|
-
|
|
390
|
-
--expansion-search <integer>
|
|
391
|
-
Affects search depth
|
|
392
|
-
|
|
393
|
-
--native Use raw templates instead of type-punned classes
|
|
394
|
-
--f16quant Enable `f16_t` quantization
|
|
395
|
-
--i8quant Enable `int8_t` quantization
|
|
396
|
-
--ip Choose Inner Product metric
|
|
397
|
-
--l2 Choose L2 Euclidean metric
|
|
398
|
-
--cos Choose Angular metric
|
|
399
|
-
--haversine Choose Haversine metric
|
|
400
|
-
-h, --help Print this help information on this tool and exit
|
|
401
|
-
```
|
|
402
|
-
|
|
403
453
|
## TODO
|
|
404
454
|
|
|
405
455
|
- JavaScript: Allow calling from "worker threads".
|
|
406
456
|
- Rust: Allow passing a custom thread ID.
|
|
457
|
+
- C# .NET bindings.
|
|
407
458
|
|
|
408
459
|
## AI + Vector Search = Semantic Search
|
|
409
460
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.4
|