usearch 0.2.3 → 0.4.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/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +64 -0
- package/.github/workflows/release.yml +45 -20
- package/.github/workflows/update_version.sh +6 -0
- package/.releaserc +77 -0
- package/.vscode/settings.json +14 -2
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +12 -0
- package/Cargo.toml +1 -1
- package/README.md +246 -220
- package/VERSION +1 -1
- package/binding.gyp +13 -9
- package/build.rs +1 -0
- package/conanfile.py +1 -1
- package/docs/Compilation.md +112 -0
- package/docs/benchmarks.md +182 -0
- package/docs/benchmarks.rst +5 -0
- package/docs/compilation.rst +5 -0
- package/docs/conf.dox +2668 -0
- package/docs/index.rst +7 -0
- package/include/usearch/usearch.hpp +454 -238
- package/javascript/lib.cpp +82 -8
- package/objc/USearchObjective.mm +2 -2
- package/objc/include/USearchObjective.h +10 -9
- package/package.json +7 -1
- package/python/bench.py +37 -7
- package/python/lib.cpp +70 -43
- package/python/test.py +78 -36
- package/rust/lib.cpp +6 -2
- package/simsimd/include/simsimd/simsimd.h +31 -41
- package/src/advanced.hpp +68 -40
- package/src/bench.cpp +24 -23
- package/swift/Index+Sugar.swift +1 -0
- package/swift/Test.swift +1 -0
- package/.github/workflows/package.json +0 -82
- package/CMakeListsInstructions.md +0 -185
package/README.md
CHANGED
|
@@ -33,123 +33,110 @@ Euclidean • Angular • Jaccard • Hamming • Haversine • User-Defined Met
|
|
|
33
33
|
Linux • MacOS • Windows
|
|
34
34
|
</p>
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- [x]
|
|
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.
|
|
39
42
|
- [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
|
|
40
|
-
- [x] [
|
|
41
|
-
- [x] Variable dimensionality vectors.
|
|
42
|
-
- [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].
|
|
43
45
|
- [x] [Bring your threads](#bring-your-threads), like OpenMP.
|
|
44
46
|
- [x] Multiple vectors per label.
|
|
45
47
|
- [ ] Thread-safe `reserve`.
|
|
46
|
-
- [x]
|
|
47
|
-
- [x] AI + Vector Search = Semantic Search.
|
|
48
|
+
- [x] AI + Vector Search = [Semantic Search](#ai--vector-search--semantic-search).
|
|
48
49
|
|
|
49
50
|
[usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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/blob/main/docs/benchmarks.md
|
|
86
|
+
|
|
87
|
+
## User-Defined Functions
|
|
54
88
|
|
|
55
89
|
Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing are hardly extendible to vectors/objects of variable length.
|
|
62
|
-
Modern NSW-like approaches, however, only require two objects to be comparable.
|
|
63
|
-
This means, the index can be extended to support fuzzy search with Levenstein distance over strings.
|
|
64
|
-
USearch implements HNSW, and provides some uncommon-to-vector-search "similarity measures" and "distances" out of the box: Haversine, Jaccard, Hamming, etc.
|
|
65
|
-
|
|
66
|
-
### Bring your Threads
|
|
67
|
-
|
|
68
|
-
Most AI, HPC, or Big Data packages use some form of a thread pool.
|
|
69
|
-
Instead of spawning additional threads within USearch, we focus on thread-safety of the `add` function.
|
|
70
|
-
|
|
71
|
-
```cpp
|
|
72
|
-
#pragma omp parallel for
|
|
73
|
-
for (std::size_t i = 0; i < n; ++i)
|
|
74
|
-
native.add(label, span_t{vector, dims}, omp_get_thread_num());
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
During initialization we allocate enough temporary memory for all the cores on the machine.
|
|
78
|
-
On call, the user can simply supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
|
|
79
|
-
|
|
80
|
-
### Go Beyond 4B Entries
|
|
81
|
-
|
|
82
|
-
Constructing a vector index is a memory-intensive task.
|
|
83
|
-
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.
|
|
84
|
-
Choosing the right data-structures and numeric types can have profound implications on both.
|
|
85
|
-
|
|
86
|
-

|
|
87
|
-
|
|
88
|
-
Internally, some point "label" is mapped into an integer identifier for every stored vector.
|
|
89
|
-
Most implementations hard-code it to `uint32_t`, as people rarely store more than 4 Billion vectors.
|
|
90
|
-
But when they do, software becomes unusable.
|
|
91
|
-
Others hard-code it to `uint64_t`, which hardly space-efficient.
|
|
92
|
-
We added a tiny class - `uint40_t`, that should be enough for collections up to 1 Trillion vectors.
|
|
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.
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+

|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
98
100
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
| Instance | u-24tb1.metal | is4gen.8xlarge |
|
|
102
|
-
| Price | ~ $200/h | ~$4.5/h |
|
|
103
|
-
| RAM | 24 TB | 192 GB |
|
|
104
|
-
| SSD | EBS | 30 TB |
|
|
101
|
+
[haversine]: https://ashvardanian.com/posts/abusing-vector-search#geo-spatial-indexing
|
|
102
|
+
[obscure]: https://ashvardanian.com/posts/abusing-vector-search
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
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.
|
|
104
|
+
## Memory Efficiency, Downcasting, and Quantization
|
|
108
105
|
|
|
109
|
-
|
|
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.
|
|
110
108
|
|
|
111
|
-
|
|
109
|
+

|
|
112
110
|
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
115
114
|
|
|
116
|
-
|
|
115
|
+
## View Larger Indexes from Disk
|
|
117
116
|
|
|
118
|
-
|
|
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.
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
|
|
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 |
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
| `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
|
|
126
|
-
| | | | | | | |
|
|
127
|
-
| `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
|
|
128
|
-
| `f32` x256 | 32 | 128 | 64 | 64'368 | 104'050 | 99.4% |
|
|
129
|
-
| | | | | | | |
|
|
130
|
-
| `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
131
|
-
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
132
|
-
| | | | | | | |
|
|
133
|
-
| `f16` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
134
|
-
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
135
|
-
|
|
136
|
-
The main columns are:
|
|
137
|
-
|
|
138
|
-
- Add: Number of insertion Queries Per Second.
|
|
139
|
-
- Search: Number search Queries Per Second.
|
|
140
|
-
- Recall @1: How often does approximate search yield the exact best match?
|
|
141
|
-
|
|
142
|
-
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.
|
|
143
128
|
|
|
144
129
|
## Usage
|
|
145
130
|
|
|
146
131
|
There are two usage patters:
|
|
147
132
|
|
|
148
|
-
|
|
149
|
-
|
|
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.
|
|
150
135
|
|
|
151
136
|
### C++
|
|
152
137
|
|
|
138
|
+
#### Installation
|
|
139
|
+
|
|
153
140
|
To use in a C++ project simply copy the `include/usearch/usearch.hpp` header into your project.
|
|
154
141
|
Alternatively fetch it with CMake:
|
|
155
142
|
|
|
@@ -158,19 +145,10 @@ FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearc
|
|
|
158
145
|
FetchContent_MakeAvailable(usearch)
|
|
159
146
|
```
|
|
160
147
|
|
|
161
|
-
|
|
162
|
-
That can be one of the following templates:
|
|
163
|
-
|
|
164
|
-
- `cos_gt<float>` for "Cosine" or "Angular" distance.
|
|
165
|
-
- `ip_gt<float>` for "Inner Product" or "Dot Product" distance.
|
|
166
|
-
- `l2sq_gt<float>` for the squared "L2" or "Euclidean" distance.
|
|
167
|
-
- `jaccard_gt<int>` for "Jaccard" distance between two ordered sets of unique elements.
|
|
168
|
-
- `bit_hamming_gt<uint>` for "Hamming" distance, as the number of shared bits in hashes.
|
|
169
|
-
- `pearson_correlation_gt<float>` for "Pearson" correlation between probability distributions.
|
|
170
|
-
- `haversine_gt<float>` for "Haversine" or "Great Circle" distance between coordinates.
|
|
148
|
+
#### Quickstart
|
|
171
149
|
|
|
172
|
-
|
|
173
|
-
|
|
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.
|
|
174
152
|
|
|
175
153
|
```c++
|
|
176
154
|
using namespace unum::usearch;
|
|
@@ -183,14 +161,21 @@ index.add(/* label: */ 42, /* vector: */ {&vec, 3});
|
|
|
183
161
|
index.search(
|
|
184
162
|
/* query: */ {&vec, 3}, /* top */ 5 /* results */,
|
|
185
163
|
/* with callback: */ [](std::size_t label, float distance) { });
|
|
164
|
+
```
|
|
186
165
|
|
|
187
|
-
|
|
188
|
-
|
|
166
|
+
The `add` is thread-safe for concurrent index construction.
|
|
167
|
+
|
|
168
|
+
#### Serialization
|
|
169
|
+
|
|
170
|
+
```c++
|
|
171
|
+
index.save("index.usearch");
|
|
172
|
+
index.load("index.usearch"); // Copying from disk
|
|
189
173
|
index.view("index.usearch"); // Memory-mapping from disk
|
|
190
174
|
```
|
|
191
175
|
|
|
192
|
-
|
|
193
|
-
|
|
176
|
+
#### User-Defined Metrics in C++
|
|
177
|
+
|
|
178
|
+
For advanced users, more compile-time abstractions are available.
|
|
194
179
|
|
|
195
180
|
```cpp
|
|
196
181
|
template <typename metric_at = ip_gt<float>, //
|
|
@@ -201,8 +186,8 @@ template <typename metric_at = ip_gt<float>, //
|
|
|
201
186
|
class index_gt;
|
|
202
187
|
```
|
|
203
188
|
|
|
204
|
-
|
|
205
|
-
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.
|
|
206
191
|
|
|
207
192
|
```cpp
|
|
208
193
|
struct custom_metric_t {
|
|
@@ -210,102 +195,184 @@ struct custom_metric_t {
|
|
|
210
195
|
};
|
|
211
196
|
```
|
|
212
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
|
+
#### Multi-Threading
|
|
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}, add_config_t { .thread = 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
|
+
|
|
213
222
|
### Python
|
|
214
223
|
|
|
224
|
+
#### Installation
|
|
225
|
+
|
|
215
226
|
```sh
|
|
216
227
|
pip install usearch
|
|
217
228
|
```
|
|
218
229
|
|
|
219
|
-
|
|
220
|
-
Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
|
|
230
|
+
#### Quickstart
|
|
221
231
|
|
|
222
232
|
```python
|
|
223
233
|
import numpy as np
|
|
224
234
|
from usearch.index import Index
|
|
225
235
|
|
|
226
236
|
index = Index(
|
|
227
|
-
ndim=
|
|
228
|
-
metric='cos', # Choose
|
|
229
|
-
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'
|
|
230
240
|
connectivity=16, # How frequent should the connections in the graph be, optional
|
|
231
241
|
expansion_add=128, # Control the recall of indexing, optional
|
|
232
242
|
expansion_search=64, # Control the quality of search, optional
|
|
233
243
|
)
|
|
234
244
|
|
|
235
|
-
vector = np.
|
|
245
|
+
vector = np.array([0.2, 0.6, 0.4], dtype=np.float32)
|
|
236
246
|
index.add(42, vector)
|
|
237
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
|
|
238
264
|
```
|
|
239
265
|
|
|
240
266
|
#### Batch Operations
|
|
241
267
|
|
|
242
|
-
|
|
243
|
-
|
|
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.
|
|
244
270
|
|
|
245
271
|
```py
|
|
246
272
|
n = 100
|
|
247
273
|
labels = np.array(range(n), dtype=np.longlong)
|
|
248
274
|
vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
249
275
|
|
|
250
|
-
index.add(labels, vectors)
|
|
251
|
-
matches, distances, counts = index.search(vectors, 10)
|
|
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
|
|
252
281
|
```
|
|
253
282
|
|
|
254
|
-
|
|
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.
|
|
255
287
|
|
|
256
|
-
|
|
288
|
+
#### User-Defined Metrics in Python
|
|
257
289
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
- JIT-compile your metrics for hardware and pass to `Index(matric_pointer=)`.
|
|
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.
|
|
261
292
|
|
|
262
|
-
|
|
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
|
|
263
314
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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)
|
|
324
|
+
```
|
|
267
325
|
|
|
268
326
|
### JavaScript
|
|
269
327
|
|
|
328
|
+
#### Installation
|
|
329
|
+
|
|
270
330
|
```sh
|
|
271
331
|
npm install usearch
|
|
272
332
|
```
|
|
273
333
|
|
|
334
|
+
#### Quickstart
|
|
335
|
+
|
|
274
336
|
```js
|
|
275
|
-
var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions:
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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)
|
|
340
|
+
|
|
341
|
+
assert.equal(index.size(), 1)
|
|
342
|
+
assert.deepEqual(results.labels, new Uint32Array([42]))
|
|
343
|
+
assert.deepEqual(results.distances, new Float32Array([0]))
|
|
344
|
+
```
|
|
279
345
|
|
|
280
|
-
|
|
281
|
-
assert.equal(index.size(), 2)
|
|
346
|
+
#### Serialization
|
|
282
347
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
348
|
+
```js
|
|
349
|
+
index.save('index.usearch')
|
|
350
|
+
index.load('index.usearch')
|
|
351
|
+
index.view('index.usearch')
|
|
286
352
|
```
|
|
287
353
|
|
|
288
354
|
### Rust
|
|
289
355
|
|
|
356
|
+
#### Installation
|
|
357
|
+
|
|
290
358
|
```sh
|
|
291
359
|
cargo add usearch
|
|
292
360
|
```
|
|
293
361
|
|
|
294
|
-
|
|
295
|
-
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.
|
|
362
|
+
#### Quickstart
|
|
296
363
|
|
|
297
364
|
```rust
|
|
298
365
|
let quant: &str = "f16";
|
|
299
|
-
let index = new_ip(
|
|
366
|
+
let index = new_ip(3, &quant, 0, 0, 0).unwrap();
|
|
300
367
|
|
|
301
368
|
assert!(index.reserve(10).is_ok());
|
|
302
369
|
assert!(index.capacity() >= 10);
|
|
303
370
|
assert!(index.connectivity() != 0);
|
|
304
|
-
assert_eq!(index.dimensions(),
|
|
371
|
+
assert_eq!(index.dimensions(), 3);
|
|
305
372
|
assert_eq!(index.size(), 0);
|
|
306
373
|
|
|
307
|
-
let first: [f32;
|
|
308
|
-
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];
|
|
309
376
|
|
|
310
377
|
assert!(index.add(42, &first).is_ok());
|
|
311
378
|
assert!(index.add(43, &second).is_ok());
|
|
@@ -314,20 +381,39 @@ assert_eq!(index.size(), 2);
|
|
|
314
381
|
// Read back the tags
|
|
315
382
|
let results = index.search(&first, 10).unwrap();
|
|
316
383
|
assert_eq!(results.count, 2);
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
#### Multi-Threading
|
|
317
387
|
|
|
318
|
-
|
|
388
|
+
```rust
|
|
389
|
+
assert!(index.add_in_thread(42, &first, 0).is_ok());
|
|
390
|
+
assert!(index.add_in_thread(43, &second, 0).is_ok());
|
|
391
|
+
let results = index.search_in_thread(&first, 10, 0).unwrap();
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
Being a systems-programming language, Rust has better control over memory management and concurrency but lacks function overloading.
|
|
395
|
+
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.
|
|
396
|
+
|
|
397
|
+
#### Serialization
|
|
398
|
+
|
|
399
|
+
```rust
|
|
319
400
|
assert!(index.save("index.usearch").is_ok());
|
|
320
401
|
assert!(index.load("index.usearch").is_ok());
|
|
321
402
|
assert!(index.view("index.usearch").is_ok());
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
#### Metrics
|
|
322
406
|
|
|
323
|
-
|
|
324
|
-
assert!(new_l2(
|
|
325
|
-
assert!(new_cos(
|
|
407
|
+
```rust
|
|
408
|
+
assert!(new_l2(3, &quant, 0, 0, 0).is_ok());
|
|
409
|
+
assert!(new_cos(3, &quant, 0, 0, 0).is_ok());
|
|
326
410
|
assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
327
411
|
```
|
|
328
412
|
|
|
329
413
|
### Java
|
|
330
414
|
|
|
415
|
+
#### Installation
|
|
416
|
+
|
|
331
417
|
```xml
|
|
332
418
|
<dependency>
|
|
333
419
|
<groupId>cloud.unum.usearch</groupId>
|
|
@@ -338,6 +424,8 @@ assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
|
338
424
|
|
|
339
425
|
Add that snippet to your `pom.xml` and hit `mvn install`.
|
|
340
426
|
|
|
427
|
+
#### Quickstart
|
|
428
|
+
|
|
341
429
|
```java
|
|
342
430
|
Index index = new Index.Config().metric("cos").dimensions(2).build();
|
|
343
431
|
float vec[] = {10, 20};
|
|
@@ -347,10 +435,18 @@ int[] labels = index.search(vec, 5);
|
|
|
347
435
|
|
|
348
436
|
### Swift
|
|
349
437
|
|
|
438
|
+
#### Installation
|
|
439
|
+
|
|
440
|
+
```txt
|
|
441
|
+
https://github.com/unum-cloud/usearch
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Quickstart
|
|
445
|
+
|
|
350
446
|
```swift
|
|
351
|
-
let index = Index.l2(dimensions:
|
|
352
|
-
let vectorA: [Float32] = [0.3, 0.5, 1.2
|
|
353
|
-
let vectorB: [Float32] = [0.4, 0.2, 1.2
|
|
447
|
+
let index = Index.l2(dimensions: 3, connectivity: 8)
|
|
448
|
+
let vectorA: [Float32] = [0.3, 0.5, 1.2]
|
|
449
|
+
let vectorB: [Float32] = [0.4, 0.2, 1.2]
|
|
354
450
|
index.add(label: 42, vector: vectorA[...])
|
|
355
451
|
index.add(label: 43, vector: vectorB[...])
|
|
356
452
|
|
|
@@ -362,81 +458,11 @@ assert(results.0[0] == 42)
|
|
|
362
458
|
|
|
363
459
|
### Wolfram
|
|
364
460
|
|
|
365
|
-
## Benchmarking
|
|
366
|
-
|
|
367
|
-
All major HNSW implementation share an identical list of hyper-parameters:
|
|
368
|
-
|
|
369
|
-
- connectivity (often called `M`),
|
|
370
|
-
- expansion on additions (often called `efConstruction`),
|
|
371
|
-
- expansion on search (often called `ef`).
|
|
372
|
-
|
|
373
|
-
The default values vary drastically.
|
|
374
|
-
|
|
375
|
-
| Library | Connectivity | EF @ A | EF @ S |
|
|
376
|
-
| :-------: | :----------: | :----: | :----: |
|
|
377
|
-
| `hnswlib` | 16 | 200 | 10 |
|
|
378
|
-
| `FAISS` | 32 | 40 | 16 |
|
|
379
|
-
| `USearch` | 16 | 128 | 64 |
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
To achieve best results, please compile locally and check out various configuration options.
|
|
383
|
-
|
|
384
|
-
```sh
|
|
385
|
-
cmake -B ./build_release \
|
|
386
|
-
-DCMAKE_BUILD_TYPE=Release \
|
|
387
|
-
-DUSEARCH_USE_OPENMP=1 \
|
|
388
|
-
-DUSEARCH_USE_JEMALLOC=1 && \
|
|
389
|
-
make -C ./build_release -j
|
|
390
|
-
|
|
391
|
-
./build_release/bench --help
|
|
392
|
-
```
|
|
393
|
-
|
|
394
|
-
Which would print the following instructions:
|
|
395
|
-
|
|
396
|
-
```txt
|
|
397
|
-
SYNOPSIS
|
|
398
|
-
./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
|
|
399
|
-
<integer>] [-c <integer>] [--expansion-add <integer>]
|
|
400
|
-
[--expansion-search <integer>] [--native|--f16quant|--i8quant]
|
|
401
|
-
[--ip|--l2|--cos|--haversine] [-h]
|
|
402
|
-
|
|
403
|
-
OPTIONS
|
|
404
|
-
--vectors <path>
|
|
405
|
-
.fbin file path to construct the index
|
|
406
|
-
|
|
407
|
-
--queries <path>
|
|
408
|
-
.fbin file path to query the index
|
|
409
|
-
|
|
410
|
-
--neighbors <path>
|
|
411
|
-
.ibin file path with ground truth
|
|
412
|
-
|
|
413
|
-
-b, --big Will switch to uint40_t for neighbors lists with over 4B entries
|
|
414
|
-
-j, --threads <integer>
|
|
415
|
-
Uses all available cores by default
|
|
416
|
-
|
|
417
|
-
-c, --connectivity <integer>
|
|
418
|
-
Index granularity
|
|
419
|
-
|
|
420
|
-
--expansion-add <integer>
|
|
421
|
-
Affects indexing depth
|
|
422
|
-
|
|
423
|
-
--expansion-search <integer>
|
|
424
|
-
Affects search depth
|
|
425
|
-
|
|
426
|
-
--native Use raw templates instead of type-punned classes
|
|
427
|
-
--f16quant Enable `f16_t` quantization
|
|
428
|
-
--i8quant Enable `int8_t` quantization
|
|
429
|
-
--ip Choose Inner Product metric
|
|
430
|
-
--l2 Choose L2 Euclidean metric
|
|
431
|
-
--cos Choose Angular metric
|
|
432
|
-
--haversine Choose Haversine metric
|
|
433
|
-
-h, --help Print this help information on this tool and exit
|
|
434
|
-
```
|
|
435
|
-
|
|
436
461
|
## TODO
|
|
437
462
|
|
|
438
463
|
- JavaScript: Allow calling from "worker threads".
|
|
439
464
|
- Rust: Allow passing a custom thread ID.
|
|
465
|
+
- C# .NET bindings.
|
|
440
466
|
|
|
441
467
|
## AI + Vector Search = Semantic Search
|
|
442
468
|
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|