usearch 0.1.8 → 0.1.10
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/.vscode/settings.json +1 -0
- package/CMakeLists.txt +26 -8
- package/CMakeListsInstructions.md +49 -33
- package/Cargo.toml +2 -2
- package/LICENSE +201 -0
- package/README.md +306 -20
- package/VERSION +1 -1
- package/assets/usearch-approaches-transparent.png +0 -0
- package/assets/usearch-approaches-white.png +0 -0
- package/assets/usearch-neighbor-types.png +0 -0
- package/build.gradle +1 -1
- package/include/usearch/usearch.hpp +24 -0
- package/java/test/IndexTest.java +6 -1
- package/package.json +2 -2
- package/python/bench.py +42 -69
- package/python/lib.cpp +17 -11
- package/python/test.py +31 -6
- package/python/usearch/client.py +34 -0
- package/python/usearch/io.py +67 -0
- package/python/usearch/server.py +51 -0
- package/setup.py +1 -1
- package/simsimd/.vscode/settings.json +3 -1
- package/simsimd/include/simsimd/simsimd.h +14 -7
- package/src/advanced.hpp +86 -20
- package/src/bench.cpp +38 -3
- package/wolfram/CMakeLists.txt +17 -0
- package/wolfram/lib.cpp +139 -4
- package/wolfram/test.wls +27 -0
- package/docs/Makefile +0 -20
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<h1 align="center">USearch</h1>
|
|
2
2
|
<h3 align="center">
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Smaller & Faster Single-File<br/>
|
|
4
|
+
Vector Search Engine<br/>
|
|
5
5
|
</h3>
|
|
6
6
|
<br/>
|
|
7
7
|
|
|
@@ -17,30 +17,126 @@ Compact, yet Powerful<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
|
-
- [x] Single C++11 header implementation, easily extendible.
|
|
21
|
-
- [x] 4B+ sized space efficient point-clouds with `uint40_t`.
|
|
22
|
-
- [x] Half-precision support with [`maratyszcza/fp16`](https://github.com/maratyszcza/fp16).
|
|
23
|
-
- [x] View from disk, without loading into RAM.
|
|
24
|
-
- [x] Any metric, includes:
|
|
20
|
+
- [x] [Single C++11 header][usearch-header] implementation, easily extendible.
|
|
21
|
+
- [x] [4B+](#go-beyond-4b-entries) sized space efficient point-clouds with `uint40_t`.
|
|
22
|
+
- [x] [Half-precision](#quantize-on-the-fly) support with [`maratyszcza/fp16`](https://github.com/maratyszcza/fp16).
|
|
23
|
+
- [x] [View from disk](#view-larger-indexes-from-disk), without loading into RAM.
|
|
24
|
+
- [x] [Any metric](#define-custom-metrics), includes:
|
|
25
25
|
- [x] Euclidean, Dot-product, Cosine,
|
|
26
26
|
- [x] Jaccard, Hamming, Haversine.
|
|
27
27
|
- [x] Hardware-accelerated [`ashvardanian/simsimd`](https://github.com/ashvardanian/simsimd).
|
|
28
28
|
- [x] Variable dimensionality vectors.
|
|
29
29
|
- [x] Don't copy vectors if not needed.
|
|
30
|
-
- [x] Bring your threads.
|
|
30
|
+
- [x] [Bring your threads](#bring-your-threads), like OpenMP.
|
|
31
31
|
- [x] Multiple vectors per label.
|
|
32
|
-
- [x] Python bindings: `pip install usearch`.
|
|
33
|
-
- [x] JavaScript bindings: `npm install usearch`.
|
|
34
|
-
- [x] Rust bindings: `cargo add usearch`.
|
|
35
|
-
- [x] Java bindings: `cloud.unum:usearch` on GitHub.
|
|
32
|
+
- [x] [Python](#python) bindings: `pip install usearch`.
|
|
33
|
+
- [x] [JavaScript](#java) bindings: `npm install usearch`.
|
|
34
|
+
- [x] [Rust](#rust) bindings: `cargo add usearch`.
|
|
35
|
+
- [x] [Java](#java) bindings: `cloud.unum:usearch` on GitHub.
|
|
36
36
|
- [ ] GoLang bindings.
|
|
37
|
-
- [
|
|
37
|
+
- [x] Wolfram language bindings.
|
|
38
38
|
- [x] For Linux: GCC, Clang.
|
|
39
39
|
- [x] For MacOS: Apple Clang.
|
|
40
40
|
- [ ] For Windows.
|
|
41
41
|
- [ ] Multi-index lookups in Python.
|
|
42
42
|
- [ ] Thread-safe `reserve`.
|
|
43
|
-
- [ ] Distributed construction.
|
|
43
|
+
- [ ] Distributed construction with MPI.
|
|
44
|
+
- [x] AI + Vector Search = Semantic Search.
|
|
45
|
+
|
|
46
|
+
[usearch-header]: https://github.com/unum-cloud/usearch/blob/main/include/usearch/usearch.hpp
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
### Define Custom Metrics
|
|
51
|
+
|
|
52
|
+
Most vector-search packages focus on just 2 metrics - "Inner Product distance" and "Euclidean distance".
|
|
53
|
+
The lack of dedicated "Cosine distance" can be justified with the simplicity of normalizing such vectors on the fly.
|
|
54
|
+
But that hardly exhausts the list of possible metrics.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
Older approaches indexing high-dimensional spaces, like KD-Trees and Locality Sensitive Hashing are hardly extendible to vectors/objects of variable length.
|
|
59
|
+
Modern NSW-like approaches, however, only require two objects to be comparable.
|
|
60
|
+
This means, the index can be extended to support fuzzy search with Levenstein distance over strings.
|
|
61
|
+
USearch implements HNSW, and provides some uncommon-to-vector-search "similarity measures" and "distances" out of the box: Haversine, Jaccard, Hamming, etc.
|
|
62
|
+
|
|
63
|
+
### Bring your Threads
|
|
64
|
+
|
|
65
|
+
Most AI, HPC, or Big Data packages use some form of a thread pool.
|
|
66
|
+
Instead of spawning additional threads within USearch, we focus on thread-safety of the `add` function.
|
|
67
|
+
|
|
68
|
+
```cpp
|
|
69
|
+
#pragma omp parallel for
|
|
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.
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
Internally, some point "label" is mapped into an integer identifier for every stored vector.
|
|
86
|
+
Most implementations hard-code it to `uint32_t`, as people rarely store more than 4 Billion vectors.
|
|
87
|
+
But when they do, software becomes unusable.
|
|
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.
|
|
107
|
+
|
|
108
|
+
### Quantize on the Fly
|
|
109
|
+
|
|
110
|
+
Most modern CPU have at least partial support for `half`-precision `f16_t` arithmetic.
|
|
111
|
+
USearch supports automatic down-casting and up-casting between `f32_t`, `f16_t`, `f64_t`, and `i8q100_t` representations.
|
|
112
|
+
|
|
113
|
+
> Making vectors smaller will help pack more of them in-memory, while also increasing performance on CPUs implementing native support for target type.
|
|
114
|
+
|
|
115
|
+
## Performance
|
|
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).
|
|
44
140
|
|
|
45
141
|
## Usage
|
|
46
142
|
|
|
@@ -91,22 +187,41 @@ index.view("index.usearch"); // Memory-mapping from disk
|
|
|
91
187
|
```
|
|
92
188
|
|
|
93
189
|
The `add` is thread-safe for concurrent index construction.
|
|
190
|
+
For advanced users, more compile-time abstraction are available.
|
|
191
|
+
|
|
192
|
+
```cpp
|
|
193
|
+
template <typename metric_at = ip_gt<float>, //
|
|
194
|
+
typename label_at = std::size_t, // `uint32_t`, `uuid_t`...
|
|
195
|
+
typename id_at = std::uint32_t, // `uint40_t`, `uint64_t`...
|
|
196
|
+
typename scalar_at = float, // `double`, `half`, `char`...
|
|
197
|
+
typename allocator_at = std::allocator<char>> //
|
|
198
|
+
class index_gt;
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
One may also define a custom metric, such as Damerau–Levenshtein distance, to compute the similarity between variable length strings.
|
|
202
|
+
The only constraint is the function signature:
|
|
203
|
+
|
|
204
|
+
```cpp
|
|
205
|
+
struct custom_metric_t {
|
|
206
|
+
T operator()(T const* a, T const* b, std::size_t a_length, std::size_t b_length) const;
|
|
207
|
+
};
|
|
208
|
+
```
|
|
94
209
|
|
|
95
210
|
### Python
|
|
96
211
|
|
|
97
212
|
Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
|
|
98
213
|
Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
|
|
99
214
|
|
|
100
|
-
```
|
|
215
|
+
```python
|
|
101
216
|
$ pip install usearch
|
|
102
217
|
|
|
103
218
|
import numpy as np
|
|
104
219
|
import usearch
|
|
105
220
|
|
|
106
221
|
index = usearch.Index(
|
|
107
|
-
|
|
222
|
+
ndim=256, # Define the number of dimensions in input vectors
|
|
108
223
|
metric='cos', # Choose the "metric" or "distance", default = 'ip', optional
|
|
109
|
-
dtype='
|
|
224
|
+
dtype='f32', # Quantize to 'f16' or 'i8q100' if needed, default = 'f32', optional
|
|
110
225
|
connectivity=16, # How frequent should the connections in the graph be, optional
|
|
111
226
|
expansion_add=128, # Control the recall of indexing, optional
|
|
112
227
|
expansion_search=64, # Control the quality of search, optional
|
|
@@ -125,13 +240,184 @@ assert len(index) == n
|
|
|
125
240
|
matches, distances, counts = index.search(vectors, 10)
|
|
126
241
|
```
|
|
127
242
|
|
|
128
|
-
|
|
243
|
+
### JavaScript
|
|
129
244
|
|
|
130
|
-
|
|
245
|
+
```js
|
|
246
|
+
// npm install usearch
|
|
131
247
|
|
|
132
|
-
|
|
248
|
+
var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 2 })
|
|
249
|
+
assert.equal(index.connectivity(), 16)
|
|
250
|
+
assert.equal(index.dimensions(), 2)
|
|
251
|
+
assert.equal(index.size(), 0)
|
|
252
|
+
|
|
253
|
+
index.add(15, new Float32Array([10, 20]))
|
|
254
|
+
assert.equal(index.size(), 2)
|
|
255
|
+
|
|
256
|
+
var results = index.search(new Float32Array([13, 14]), 2)
|
|
257
|
+
assert.deepEqual(results.labels, new Uint32Array([15, 16]))
|
|
258
|
+
assert.deepEqual(results.distances, new Float32Array([45, 130]))
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Rust
|
|
262
|
+
|
|
263
|
+
Being a systems-programming language, Rust has better control over memory management and concurrency, but lacks function overloading.
|
|
264
|
+
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.
|
|
265
|
+
|
|
266
|
+
```rust
|
|
267
|
+
// cargo add usearch
|
|
268
|
+
|
|
269
|
+
let quant: &str = "f16";
|
|
270
|
+
let index = new_ip(5, &quant, 0, 0, 0).unwrap();
|
|
271
|
+
|
|
272
|
+
assert!(index.reserve(10).is_ok());
|
|
273
|
+
assert!(index.capacity() >= 10);
|
|
274
|
+
assert!(index.connectivity() != 0);
|
|
275
|
+
assert_eq!(index.dimensions(), 5);
|
|
276
|
+
assert_eq!(index.size(), 0);
|
|
277
|
+
|
|
278
|
+
let first: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
|
|
279
|
+
let second: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
|
|
280
|
+
|
|
281
|
+
assert!(index.add(42, &first).is_ok());
|
|
282
|
+
assert!(index.add(43, &second).is_ok());
|
|
283
|
+
assert_eq!(index.size(), 2);
|
|
284
|
+
|
|
285
|
+
// Read back the tags
|
|
286
|
+
let results = index.search(&first, 10).unwrap();
|
|
287
|
+
assert_eq!(results.count, 2);
|
|
288
|
+
|
|
289
|
+
// Validate serialization
|
|
290
|
+
assert!(index.save("index.usearch").is_ok());
|
|
291
|
+
assert!(index.load("index.usearch").is_ok());
|
|
292
|
+
assert!(index.view("index.usearch").is_ok());
|
|
293
|
+
|
|
294
|
+
// There are more "metrics" available
|
|
295
|
+
assert!(new_l2(5, &quant, 0, 0, 0).is_ok());
|
|
296
|
+
assert!(new_cos(5, &quant, 0, 0, 0).is_ok());
|
|
297
|
+
assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Java
|
|
301
|
+
|
|
302
|
+
```java
|
|
303
|
+
Index index = new Index.Config().metric("cos").dimensions(2).build();
|
|
304
|
+
float vec[] = {10, 20};
|
|
305
|
+
index.add(42, vec);
|
|
306
|
+
int[] labels = index.search(vec, 5);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### GoLang
|
|
310
|
+
|
|
311
|
+
### Wolfram
|
|
312
|
+
|
|
313
|
+
## Benchmarking
|
|
314
|
+
|
|
315
|
+
All major HNSW implementation share an identical list of hyper-parameters:
|
|
316
|
+
|
|
317
|
+
- connectivity (often called `M`),
|
|
318
|
+
- expansion on additions (often called `efConstruction`),
|
|
319
|
+
- expansion on search (often called `ef`).
|
|
320
|
+
|
|
321
|
+
The default values vary drastically.
|
|
322
|
+
|
|
323
|
+
| Library | Connectivity | EF @ A | EF @ S |
|
|
324
|
+
| :-------: | :----------: | :----: | :----: |
|
|
325
|
+
| `hnswlib` | 16 | 200 | 10 |
|
|
326
|
+
| `FAISS` | 32 | 40 | 16 |
|
|
327
|
+
| `USearch` | 16 | 128 | 64 |
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
To achieve best results, please compile locally and check out various configuration options.
|
|
331
|
+
|
|
332
|
+
```sh
|
|
333
|
+
cmake -B ./build_release \
|
|
334
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
335
|
+
-DUSEARCH_USE_OPENMP=1 \
|
|
336
|
+
-DUSEARCH_USE_JEMALLOC=1 && \
|
|
337
|
+
make -C ./build_release -j
|
|
338
|
+
|
|
339
|
+
./build_release/bench --help
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Which would print the following instructions:
|
|
343
|
+
|
|
344
|
+
```txt
|
|
345
|
+
SYNOPSIS
|
|
346
|
+
./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
|
|
347
|
+
<integer>] [-c <integer>] [--expansion-add <integer>]
|
|
348
|
+
[--expansion-search <integer>] [--native|--f16quant|--i8quant]
|
|
349
|
+
[--ip|--l2|--cos|--haversine] [-h]
|
|
350
|
+
|
|
351
|
+
OPTIONS
|
|
352
|
+
--vectors <path>
|
|
353
|
+
.fbin file path to construct the index
|
|
354
|
+
|
|
355
|
+
--queries <path>
|
|
356
|
+
.fbin file path to query the index
|
|
357
|
+
|
|
358
|
+
--neighbors <path>
|
|
359
|
+
.ibin file path with ground truth
|
|
360
|
+
|
|
361
|
+
-b, --big Will switch to uint40_t for neighbors lists with over 4B entries
|
|
362
|
+
-j, --threads <integer>
|
|
363
|
+
Uses all available cores by default
|
|
364
|
+
|
|
365
|
+
-c, --connectivity <integer>
|
|
366
|
+
Index granularity
|
|
367
|
+
|
|
368
|
+
--expansion-add <integer>
|
|
369
|
+
Affects indexing depth
|
|
370
|
+
|
|
371
|
+
--expansion-search <integer>
|
|
372
|
+
Affects search depth
|
|
373
|
+
|
|
374
|
+
--native Use raw templates instead of type-punned classes
|
|
375
|
+
--f16quant Enable `f16_t` quantization
|
|
376
|
+
--i8quant Enable `int8_t` quantization
|
|
377
|
+
--ip Choose Inner Product metric
|
|
378
|
+
--l2 Choose L2 Euclidean metric
|
|
379
|
+
--cos Choose Angular metric
|
|
380
|
+
--haversine Choose Haversine metric
|
|
381
|
+
-h, --help Print this help information on this tool and exit
|
|
382
|
+
```
|
|
133
383
|
|
|
134
384
|
## TODO
|
|
135
385
|
|
|
136
386
|
- JavaScript: Allow calling from "worker threads".
|
|
137
387
|
- Rust: Allow passing a custom thread ID.
|
|
388
|
+
|
|
389
|
+
## AI + Vector Search = Semantic Search
|
|
390
|
+
|
|
391
|
+
AI has a growing number of applications, but one of the coolest classic ideas is to use it for Semantic Search.
|
|
392
|
+
One can take an encoder model, like the multi-modal UForm, and a web-programming framework, like UCall, and build an image search platform in just 20 lines of Python.
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
import ucall.rich_posix as ucall
|
|
396
|
+
import uform
|
|
397
|
+
import usearch
|
|
398
|
+
|
|
399
|
+
import numpy as np
|
|
400
|
+
from PIL import Image
|
|
401
|
+
|
|
402
|
+
server = ucall.Server()
|
|
403
|
+
model = uform.get_model('unum-cloud/uform-vl-multilingual')
|
|
404
|
+
index = usearch.Index(ndim=256)
|
|
405
|
+
|
|
406
|
+
@server
|
|
407
|
+
def add(label: int, photo: Image.Image):
|
|
408
|
+
image = model.preprocess_image(photo)
|
|
409
|
+
vector = model.encode_image(image).detach().numpy()
|
|
410
|
+
labels = np.array([label], dtype=np.longlong)
|
|
411
|
+
index.add(labels, vector, copy=True)
|
|
412
|
+
|
|
413
|
+
@server
|
|
414
|
+
def search(query: str) -> np.ndarray:
|
|
415
|
+
tokens = model.preprocess_text(query)
|
|
416
|
+
vector = model.encode_text(tokens).detach().numpy()
|
|
417
|
+
neighbors = index.search(vector, 3)
|
|
418
|
+
return neighbors[0][:neighbors[2][0]]
|
|
419
|
+
|
|
420
|
+
server.run()
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Check [that](https://github.com/ashvardanian/image-search) and [other](https://github.com/unum-cloud/examples) examples on our corporate GitHub 🤗
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.10
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/build.gradle
CHANGED
|
@@ -90,7 +90,7 @@ publishing {
|
|
|
90
90
|
|
|
91
91
|
pom {
|
|
92
92
|
name = "USearch"
|
|
93
|
-
description = "
|
|
93
|
+
description = "Smaller & Faster Single-File Vector Search Engine from Unum"
|
|
94
94
|
url = "https://github.com/unum-cloud/usearch"
|
|
95
95
|
licenses {
|
|
96
96
|
license {
|
|
@@ -136,6 +136,30 @@ template <typename scalar_at> struct l2_squared_gt {
|
|
|
136
136
|
}
|
|
137
137
|
};
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @brief Hamming distance computes the number of differing elements in
|
|
141
|
+
* two arrays. An example would be a chess board.
|
|
142
|
+
*/
|
|
143
|
+
template <typename scalar_at> struct hamming_gt {
|
|
144
|
+
using scalar_t = scalar_at;
|
|
145
|
+
using result_type = std::size_t;
|
|
146
|
+
|
|
147
|
+
inline std::size_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
|
|
148
|
+
std::size_t = 0) const noexcept {
|
|
149
|
+
std::size_t matches{};
|
|
150
|
+
#if defined(__GNUC__)
|
|
151
|
+
#pragma GCC ivdep
|
|
152
|
+
#elif defined(__clang__)
|
|
153
|
+
#pragma clang loop vectorize(enable)
|
|
154
|
+
#elif defined(_OPENMP)
|
|
155
|
+
#pragma omp simd reduction(+ : matches)
|
|
156
|
+
#endif
|
|
157
|
+
for (std::size_t i = 0; i != elements; ++i)
|
|
158
|
+
matches += a[i] != b[i];
|
|
159
|
+
return matches;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
139
163
|
/**
|
|
140
164
|
* @brief Hamming distance computes the number of differing bits in
|
|
141
165
|
* two arrays of integers. An example would be a textual document,
|
package/java/test/IndexTest.java
CHANGED
|
@@ -19,7 +19,12 @@ public class IndexTest {
|
|
|
19
19
|
public void test() {
|
|
20
20
|
String path = "./tmp/";
|
|
21
21
|
deleteDirectoryFiles(path);
|
|
22
|
-
|
|
22
|
+
|
|
23
|
+
// Index index = new Index.Config().metric("cos").dimensions(2).build();
|
|
24
|
+
// float vec[] = {10, 20};
|
|
25
|
+
// index.add(42, vec);
|
|
26
|
+
// int[] labels = index.search(vec, 5);
|
|
27
|
+
|
|
23
28
|
System.out.println("Java Tests Passed!");
|
|
24
29
|
}
|
|
25
30
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usearch",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"description": "Smaller & Faster Single-File Vector Search Engine from Unum",
|
|
5
5
|
"author": "Ashot Vardanian",
|
|
6
6
|
"license": "Apache 2.0",
|
|
7
7
|
"repository": {
|
package/python/bench.py
CHANGED
|
@@ -1,89 +1,62 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import numpy as np
|
|
3
|
+
import fire
|
|
4
|
+
|
|
5
|
+
from usearchh.io import load_matrix
|
|
3
6
|
|
|
4
7
|
import faiss
|
|
5
8
|
import usearch
|
|
6
9
|
|
|
7
10
|
|
|
8
|
-
print('Welcome to basic kANN benchmark!')
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def read_matrix(filename: str, start_row: int = 0, count_rows: int = None):
|
|
12
|
-
"""
|
|
13
|
-
Read *.ibin, *.hbin, *.fbin, *.dbin files with matrixes.
|
|
14
|
-
Args:
|
|
15
|
-
:param filename (str): path to the matrix file
|
|
16
|
-
:param start_row (int): start reading vectors from this index
|
|
17
|
-
:param count_rows (int): number of vectors to read. If None, read all vectors
|
|
18
|
-
Returns:
|
|
19
|
-
Parsed matrix (numpy.ndarray)
|
|
20
|
-
"""
|
|
21
|
-
dtype = np.float32
|
|
22
|
-
scalar_size = 4
|
|
23
|
-
if filename.endswith('.fbin'):
|
|
24
|
-
dtype = np.float32
|
|
25
|
-
scalar_size = 4
|
|
26
|
-
elif filename.endswith('.dbin'):
|
|
27
|
-
dtype = np.float64
|
|
28
|
-
scalar_size = 8
|
|
29
|
-
elif filename.endswith('.hbin'):
|
|
30
|
-
dtype = np.float16
|
|
31
|
-
scalar_size = 2
|
|
32
|
-
elif filename.endswith('.ibin'):
|
|
33
|
-
dtype = np.int32
|
|
34
|
-
scalar_size = 4
|
|
35
|
-
else:
|
|
36
|
-
raise Exception('Unknown file type')
|
|
37
|
-
with open(filename, 'rb') as f:
|
|
38
|
-
rows, cols = np.fromfile(f, count=2, dtype=np.int32)
|
|
39
|
-
rows = (rows - start_row) if count_rows is None else count_rows
|
|
40
|
-
arr = np.fromfile(
|
|
41
|
-
f, count=rows * cols, dtype=dtype,
|
|
42
|
-
offset=start_row * scalar_size * cols)
|
|
43
|
-
return arr.reshape(rows, cols)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
print('Will read datasets!')
|
|
47
|
-
xb = read_matrix('datasets/wiki_1M/base.1M.fbin')
|
|
48
|
-
xq = read_matrix('datasets/wiki_1M/query.public.100K.fbin')
|
|
49
|
-
|
|
50
|
-
d = 256 # vector size
|
|
51
|
-
M = 16
|
|
52
|
-
efSearch = 32 # number of entry points (neighbors) we use on each layer
|
|
53
|
-
efConstruction = 128 # number of entry points used on each layer
|
|
54
|
-
labels = np.arange(xb.shape[0], dtype=np.longlong)
|
|
55
|
-
print('Will benchmark algorithms!')
|
|
56
|
-
|
|
57
|
-
|
|
58
11
|
def measure(f) -> float:
|
|
59
12
|
a = datetime.datetime.now()
|
|
60
13
|
f()
|
|
61
14
|
b = datetime.datetime.now()
|
|
62
15
|
c = b - a
|
|
63
|
-
print(f'Took: {c.seconds} seconds')
|
|
16
|
+
print(f'Took: {c.seconds:.2f} seconds')
|
|
64
17
|
return c.seconds
|
|
65
18
|
|
|
66
19
|
|
|
67
|
-
def
|
|
20
|
+
def bench_faiss(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
21
|
+
dt = measure(lambda: index.add(vectors))
|
|
22
|
+
print(f'- FAISS: {vectors.shape[0]/dt:.2f} vectors/s')
|
|
68
23
|
|
|
69
|
-
index_meta = faiss.IndexHNSWFlat(d, M)
|
|
70
|
-
index_meta.hnsw.efSearch = efSearch
|
|
71
|
-
index_meta.hnsw.efConstruction = efConstruction
|
|
72
|
-
dt_meta = measure(lambda: index_meta.add(xb))
|
|
73
24
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
|
|
25
|
+
def bench_usearch(index, vectors: np.array, queries: np.array, neighbors: np.array):
|
|
26
|
+
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
27
|
+
assert len(index) == 0
|
|
28
|
+
dt = measure(lambda: index.add(labels, vectors))
|
|
29
|
+
assert len(index) == vectors.shape[0]
|
|
30
|
+
print(f'- USearch: {vectors.shape[0]/dt:.2f} vectors/s')
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main(
|
|
34
|
+
vectors: str,
|
|
35
|
+
queries: str,
|
|
36
|
+
neighbors: str,
|
|
37
|
+
connectivity: int = 16,
|
|
38
|
+
expansion_add: int = 128,
|
|
39
|
+
expansion_search: int = 64,
|
|
40
|
+
):
|
|
80
41
|
|
|
81
|
-
|
|
42
|
+
vectors_mat = load_matrix(vectors)
|
|
43
|
+
queries_mat = load_matrix(queries)
|
|
44
|
+
neighbors_mat = load_matrix(neighbors)
|
|
45
|
+
dim = vectors_mat.shape[1]
|
|
46
|
+
|
|
47
|
+
index = faiss.IndexHNSWFlat(dim, connectivity)
|
|
48
|
+
index.hnsw.efSearch = expansion_search
|
|
49
|
+
index.hnsw.efConstruction = expansion_add
|
|
50
|
+
bench_faiss(index, vectors_mat, queries_mat, neighbors_mat)
|
|
51
|
+
|
|
52
|
+
index = usearch.Index(
|
|
53
|
+
ndim=dim,
|
|
54
|
+
expansion_add=expansion_add,
|
|
55
|
+
expansion_search=expansion_search,
|
|
56
|
+
connectivity=connectivity,
|
|
57
|
+
)
|
|
58
|
+
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
82
59
|
|
|
83
60
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
dt_meta = sum(x[0] for x in durations)
|
|
87
|
-
dt_unum = sum(x[1] for x in durations)
|
|
88
|
-
print(f'- FAISS vectors per second: {xb.shape[0]*experiments/dt_meta:.2f}')
|
|
89
|
-
print(f'- USearch vectors per second: {xb.shape[0]*experiments/dt_unum:.2f}')
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
fire.Fire(main)
|
package/python/lib.cpp
CHANGED
|
@@ -36,7 +36,8 @@ static native_index_t make_index( //
|
|
|
36
36
|
std::string const& metric, //
|
|
37
37
|
std::size_t connectivity, //
|
|
38
38
|
std::size_t expansion_add, //
|
|
39
|
-
std::size_t expansion_search
|
|
39
|
+
std::size_t expansion_search, //
|
|
40
|
+
std::size_t metric_uintptr //
|
|
40
41
|
) {
|
|
41
42
|
|
|
42
43
|
config_t config;
|
|
@@ -48,7 +49,11 @@ static native_index_t make_index( //
|
|
|
48
49
|
config.max_threads_search = std::thread::hardware_concurrency();
|
|
49
50
|
|
|
50
51
|
accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
|
|
51
|
-
|
|
52
|
+
punned_metric_t metric_ptr = reinterpret_cast<punned_metric_t>(metric_uintptr);
|
|
53
|
+
if (metric_ptr)
|
|
54
|
+
return native_index_t::udf(dimensions, metric_ptr, accuracy, config);
|
|
55
|
+
else
|
|
56
|
+
return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
59
|
static void add_to_index(native_index_t& index, py::buffer labels, py::buffer vectors, bool copy) {
|
|
@@ -173,15 +178,16 @@ PYBIND11_MODULE(usearch, m) {
|
|
|
173
178
|
|
|
174
179
|
auto i = py::class_<native_index_t>(m, "Index");
|
|
175
180
|
|
|
176
|
-
i.def(py::init(&make_index),
|
|
177
|
-
py::kw_only(),
|
|
178
|
-
py::arg("
|
|
179
|
-
py::arg("capacity") = 0,
|
|
180
|
-
py::arg("dtype") = std::string("f32"),
|
|
181
|
-
py::arg("metric") = std::string("ip"),
|
|
182
|
-
py::arg("connectivity") = config_t::connectivity_default_k,
|
|
183
|
-
py::arg("expansion_add") = config_t::expansion_add_default_k,
|
|
184
|
-
py::arg("expansion_search") = config_t::expansion_search_default_k //
|
|
181
|
+
i.def(py::init(&make_index), //
|
|
182
|
+
py::kw_only(), //
|
|
183
|
+
py::arg("ndim"), //
|
|
184
|
+
py::arg("capacity") = 0, //
|
|
185
|
+
py::arg("dtype") = std::string("f32"), //
|
|
186
|
+
py::arg("metric") = std::string("ip"), //
|
|
187
|
+
py::arg("connectivity") = config_t::connectivity_default_k, //
|
|
188
|
+
py::arg("expansion_add") = config_t::expansion_add_default_k, //
|
|
189
|
+
py::arg("expansion_search") = config_t::expansion_search_default_k, //
|
|
190
|
+
py::arg("metric_pointer") = 0 //
|
|
185
191
|
);
|
|
186
192
|
|
|
187
193
|
i.def( //
|