usearch 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +1 -1
- package/README.md +350 -18
- package/VERSION +1 -1
- package/assets/usearch-approaches-transparent.png +0 -0
- package/assets/usearch-approaches-white.png +0 -0
- package/java/test/IndexTest.java +6 -1
- package/package.json +1 -1
- package/python/bench.py +6 -3
- package/python/lib.cpp +6 -0
- package/python/test.py +3 -4
- package/src/advanced.hpp +20 -13
- package/src/bench.cpp +8 -1
package/Cargo.toml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "usearch"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.9"
|
|
4
4
|
authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
|
|
5
5
|
description = "The Mighty Tiny Vector Search Engine with Automatic Quantization and Hardware Acceleration"
|
|
6
6
|
edition = "2021"
|
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ 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).
|
|
@@ -29,10 +29,10 @@ Compact, yet Powerful<br/>
|
|
|
29
29
|
- [x] Don't copy vectors if not needed.
|
|
30
30
|
- [x] Bring your threads.
|
|
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
|
- [ ] Wolfram language bindings.
|
|
38
38
|
- [x] For Linux: GCC, Clang.
|
|
@@ -41,45 +41,377 @@ Compact, yet Powerful<br/>
|
|
|
41
41
|
- [ ] Multi-index lookups in Python.
|
|
42
42
|
- [ ] Thread-safe `reserve`.
|
|
43
43
|
- [ ] Distributed construction.
|
|
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
|
+
|
|
89
|
+
Others hard-code it to `uint64_t`, which hardly space-efficient.
|
|
90
|
+
We added a tiny class - `uint40_t`, that should be enough for collections up to 1 Trillion vectors.
|
|
91
|
+
|
|
92
|
+
### View Larger Indexes from Disk
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
Those are, however, pricy, and would cost you over $200/hour.
|
|
96
|
+
|
|
97
|
+
You wouldn't want to have them running all the time.
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
Construct on large machines, deploy on the cheap ones.
|
|
101
|
+
|
|
102
|
+
### Quantize on the Fly
|
|
103
|
+
|
|
104
|
+
Most modern CPU have at least partial support for `half`-precision `f16_t` arithmetic.
|
|
105
|
+
USearch supports automatic down-casting and up-casting between `f32_t`, `f16_t`, `f64_t`, and `i8q100_t` representations.
|
|
106
|
+
|
|
107
|
+
Making vectors smaller will help pack more of them in-memory, while also increasing performance on CPUs implementing native support for target type.
|
|
108
|
+
|
|
109
|
+
## Performance
|
|
110
|
+
|
|
111
|
+
Below are the performance numbers for a benchmark running on the 64 cores of AWS `c7g.metal` "Graviton 3"-based instances.
|
|
112
|
+
We fix the default configuration in the top line and show the affects of various parameters by changing one parameter at a time.
|
|
113
|
+
|
|
114
|
+
| Vectors | Connectivity | EF @ A | EF @ S | **Add**, QPS | **Search**, QPS | **Recall @1** |
|
|
115
|
+
| :--------: | :----------: | :----: | :----: | :----------: | :-------------: | ------------: |
|
|
116
|
+
| `f32` x256 | 16 | 128 | 64 | 75'640 | 131'654 | 99.3% |
|
|
117
|
+
| | | | | | | |
|
|
118
|
+
| `f32` x256 | 12 | 128 | 64 | 81'747 | 149'728 | 99.0% |
|
|
119
|
+
| `f32` x256 | 32 | 128 | 64 | 64'368 | 104'050 | 99.4% |
|
|
120
|
+
| | | | | | | |
|
|
121
|
+
| `f32` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
122
|
+
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
123
|
+
| | | | | | | |
|
|
124
|
+
| `f16` x256 | 16 | 64 | 32 | 128'644 | 228'422 | 97.2% |
|
|
125
|
+
| `f32` x256 | 16 | 256 | 128 | 39'981 | 69'065 | 99.2% |
|
|
126
|
+
|
|
127
|
+
The main columns are:
|
|
128
|
+
|
|
129
|
+
- Add: Number of insertion Queries Per Second.
|
|
130
|
+
- Search: Number search Queries Per Second.
|
|
131
|
+
- Recall @1: How often does approximate search yield the exact best match?
|
|
132
|
+
|
|
133
|
+
To read more and reproduce, jump to [benchmarking section](#benchmarking).
|
|
44
134
|
|
|
45
135
|
## Usage
|
|
46
136
|
|
|
137
|
+
There are two usage patters:
|
|
138
|
+
|
|
139
|
+
- Bare-bones with `usearch/usearch.hpp`, only available in C++.
|
|
140
|
+
- 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
|
+
|
|
47
142
|
### C++
|
|
48
143
|
|
|
49
144
|
To use in a C++ project simply copy the `include/usearch/usearch.hpp` header into your project.
|
|
145
|
+
Alternatively fetch it with CMake:
|
|
146
|
+
|
|
147
|
+
```cmake
|
|
148
|
+
FetchContent_Declare(usearch GIT_REPOSITORY https://github.com/unum-cloud/usearch.git)
|
|
149
|
+
FetchContent_MakeAvailable(usearch)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The simple usage example would require including the `unum::usearch` namespace and choosing the right "distance" function.
|
|
153
|
+
That can be one of the following templates:
|
|
154
|
+
|
|
155
|
+
- `cos_gt<float>` for "Cosine" or "Angular" distance.
|
|
156
|
+
- `ip_gt<float>` for "Inner Product" or "Dot Product" distance.
|
|
157
|
+
- `l2_squared_gt<float>` for the squared "L2" or "Euclidean" distance.
|
|
158
|
+
- `jaccard_gt<int>` for "Jaccard" distance between two ordered sets of unique elements.
|
|
159
|
+
- `bit_hamming_gt<uint>` for "Hamming" distance, as the number of shared bits in hashes.
|
|
160
|
+
- `pearson_correlation_gt<float>` for "Pearson" correlation between probability distributions.
|
|
161
|
+
- `haversine_gt<float>` for "Haversine" or "Great Circle" distance between coordinates.
|
|
162
|
+
|
|
163
|
+
That list is easily extendible, and can include similarity measures for vectors that have a different number of elements/dimensions.
|
|
164
|
+
The minimal example would be.
|
|
50
165
|
|
|
51
166
|
```c++
|
|
52
167
|
using namespace unum::usearch;
|
|
53
168
|
|
|
54
169
|
index_gt<cos_gt<float>> index;
|
|
55
170
|
float vec[3] = {0.1, 0.3, 0.2};
|
|
171
|
+
|
|
172
|
+
index.reserve(10);
|
|
56
173
|
index.add(/* label: */ 42, /* vector: */ {&vec, 3});
|
|
57
174
|
index.search(
|
|
58
|
-
/* query: */ {&vec, 3},
|
|
59
|
-
/* callback: */ [](std::size_t label, float distance) { });
|
|
175
|
+
/* query: */ {&vec, 3}, /* top */ 5 /* results */,
|
|
176
|
+
/* with callback: */ [](std::size_t label, float distance) { });
|
|
60
177
|
|
|
61
178
|
index.save("index.usearch"); // Serializing to disk
|
|
62
179
|
index.load("index.usearch"); // Reconstructing from disk
|
|
63
180
|
index.view("index.usearch"); // Memory-mapping from disk
|
|
64
181
|
```
|
|
65
182
|
|
|
66
|
-
|
|
183
|
+
The `add` is thread-safe for concurrent index construction.
|
|
184
|
+
For advanced users, more compile-time abstraction are available.
|
|
67
185
|
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
|
|
186
|
+
```cpp
|
|
187
|
+
template <typename metric_at = ip_gt<float>, //
|
|
188
|
+
typename label_at = std::size_t, // `uint32_t`, `uuid_t`...
|
|
189
|
+
typename id_at = std::uint32_t, // `uint40_t`, `uint64_t`...
|
|
190
|
+
typename scalar_at = float, // `double`, `half`, `char`...
|
|
191
|
+
typename allocator_at = std::allocator<char>> //
|
|
192
|
+
class index_gt;
|
|
71
193
|
```
|
|
72
194
|
|
|
195
|
+
One may also define a custom metric, such as Damerau–Levenshtein distance, to compute the similarity between variable length strings.
|
|
196
|
+
The only constraint is the function signature:
|
|
73
197
|
|
|
198
|
+
```cpp
|
|
199
|
+
struct custom_metric_t {
|
|
200
|
+
T operator()(T const* a, T const* b, std::size_t a_length, std::size_t b_length) const;
|
|
201
|
+
};
|
|
202
|
+
```
|
|
74
203
|
|
|
75
|
-
|
|
204
|
+
### Python
|
|
76
205
|
|
|
77
|
-
|
|
206
|
+
Python bindings are implemented with [`pybind/pybind11`](https://github.com/pybind/pybind11).
|
|
207
|
+
Assuming the presence of Global Interpreter Lock in Python, on large insertions we spawn threads in the C++ layer.
|
|
78
208
|
|
|
79
|
-
|
|
209
|
+
```python
|
|
210
|
+
$ pip install usearch
|
|
211
|
+
|
|
212
|
+
import numpy as np
|
|
213
|
+
import usearch
|
|
214
|
+
|
|
215
|
+
index = usearch.Index(
|
|
216
|
+
dim=256, # Define the number of dimensions in input vectors
|
|
217
|
+
metric='cos', # Choose the "metric" or "distance", default = 'ip', optional
|
|
218
|
+
dtype='f16', # Quantize to 'f16' or 'i8q100' if needed, default = 'f32', optional
|
|
219
|
+
connectivity=16, # How frequent should the connections in the graph be, optional
|
|
220
|
+
expansion_add=128, # Control the recall of indexing, optional
|
|
221
|
+
expansion_search=64, # Control the quality of search, optional
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
n = 100
|
|
225
|
+
labels = np.array(range(n), dtype=np.longlong)
|
|
226
|
+
vectors = np.random.uniform(0, 0.3, (n, index.ndim)).astype(np.float32)
|
|
80
227
|
|
|
228
|
+
# You can avoid copying the data
|
|
229
|
+
# Handy when build 1B+ indexes of memory-mapped files
|
|
230
|
+
index.add(labels, vectors, copy=True)
|
|
231
|
+
assert len(index) == n
|
|
232
|
+
|
|
233
|
+
# You can search a batch at once
|
|
234
|
+
matches, distances, counts = index.search(vectors, 10)
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### JavaScript
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
// npm install usearch
|
|
241
|
+
|
|
242
|
+
var index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 2 })
|
|
243
|
+
assert.equal(index.connectivity(), 16)
|
|
244
|
+
assert.equal(index.dimensions(), 2)
|
|
245
|
+
assert.equal(index.size(), 0)
|
|
246
|
+
|
|
247
|
+
index.add(15, new Float32Array([10, 20]))
|
|
248
|
+
assert.equal(index.size(), 2)
|
|
249
|
+
|
|
250
|
+
var results = index.search(new Float32Array([13, 14]), 2)
|
|
251
|
+
assert.deepEqual(results.labels, new Uint32Array([15, 16]))
|
|
252
|
+
assert.deepEqual(results.distances, new Float32Array([45, 130]))
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Rust
|
|
256
|
+
|
|
257
|
+
Being a systems-programming language, Rust has better control over memory management and concurrency, but lacks function overloading.
|
|
258
|
+
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.
|
|
259
|
+
|
|
260
|
+
```rust
|
|
261
|
+
// cargo add usearch
|
|
262
|
+
|
|
263
|
+
let quant: &str = "f16";
|
|
264
|
+
let index = new_ip(5, &quant, 0, 0, 0).unwrap();
|
|
265
|
+
|
|
266
|
+
assert!(index.reserve(10).is_ok());
|
|
267
|
+
assert!(index.capacity() >= 10);
|
|
268
|
+
assert!(index.connectivity() != 0);
|
|
269
|
+
assert_eq!(index.dimensions(), 5);
|
|
270
|
+
assert_eq!(index.size(), 0);
|
|
271
|
+
|
|
272
|
+
let first: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
|
|
273
|
+
let second: [f32; 5] = [0.2, 0.1, 0.2, 0.1, 0.3];
|
|
274
|
+
|
|
275
|
+
assert!(index.add(42, &first).is_ok());
|
|
276
|
+
assert!(index.add(43, &second).is_ok());
|
|
277
|
+
assert_eq!(index.size(), 2);
|
|
278
|
+
|
|
279
|
+
// Read back the tags
|
|
280
|
+
let results = index.search(&first, 10).unwrap();
|
|
281
|
+
assert_eq!(results.count, 2);
|
|
282
|
+
|
|
283
|
+
// Validate serialization
|
|
284
|
+
assert!(index.save("index.usearch").is_ok());
|
|
285
|
+
assert!(index.load("index.usearch").is_ok());
|
|
286
|
+
assert!(index.view("index.usearch").is_ok());
|
|
287
|
+
|
|
288
|
+
// There are more "metrics" available
|
|
289
|
+
assert!(new_l2(5, &quant, 0, 0, 0).is_ok());
|
|
290
|
+
assert!(new_cos(5, &quant, 0, 0, 0).is_ok());
|
|
291
|
+
assert!(new_haversine(&quant, 0, 0, 0).is_ok());
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Java
|
|
295
|
+
|
|
296
|
+
```java
|
|
297
|
+
Index index = new Index.Config().metric("cos").dimensions(2).build();
|
|
298
|
+
float vec[] = {10, 20};
|
|
299
|
+
index.add(42, vec);
|
|
300
|
+
int[] labels = index.search(vec, 5);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### GoLang
|
|
304
|
+
|
|
305
|
+
### Wolfram
|
|
306
|
+
|
|
307
|
+
## Benchmarking
|
|
308
|
+
|
|
309
|
+
All major HNSW implementation share an identical list of hyper-parameters:
|
|
310
|
+
|
|
311
|
+
- connectivity (often called `M`),
|
|
312
|
+
- expansion on additions (often called `efConstruction`),
|
|
313
|
+
- expansion on search (often called `ef`).
|
|
314
|
+
|
|
315
|
+
The default values vary drastically.
|
|
316
|
+
|
|
317
|
+
| Library | Connectivity | EF @ A | EF @ S |
|
|
318
|
+
| :-------: | :----------: | :----: | :----: |
|
|
319
|
+
| `hnswlib` | 16 | 200 | 10 |
|
|
320
|
+
| `FAISS` | 32 | 40 | 16 |
|
|
321
|
+
| `USearch` | 16 | 128 | 64 |
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
To achieve best results, please compile locally and check out various configuration options.
|
|
325
|
+
|
|
326
|
+
```sh
|
|
327
|
+
cmake -B ./build_release \
|
|
328
|
+
-DCMAKE_BUILD_TYPE=Release \
|
|
329
|
+
-DUSEARCH_USE_OPENMP=1 \
|
|
330
|
+
-DUSEARCH_USE_JEMALLOC=1 && \
|
|
331
|
+
make -C ./build_release -j
|
|
332
|
+
|
|
333
|
+
./build_release/bench --help
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Which would print the following instructions:
|
|
337
|
+
|
|
338
|
+
```txt
|
|
339
|
+
SYNOPSIS
|
|
340
|
+
./build_release/bench [--vectors <path>] [--queries <path>] [--neighbors <path>] [-b] [-j
|
|
341
|
+
<integer>] [-c <integer>] [--expansion-add <integer>]
|
|
342
|
+
[--expansion-search <integer>] [--native|--f16quant|--i8quant]
|
|
343
|
+
[--ip|--l2|--cos|--haversine] [-h]
|
|
344
|
+
|
|
345
|
+
OPTIONS
|
|
346
|
+
--vectors <path>
|
|
347
|
+
.fbin file path to construct the index
|
|
348
|
+
|
|
349
|
+
--queries <path>
|
|
350
|
+
.fbin file path to query the index
|
|
351
|
+
|
|
352
|
+
--neighbors <path>
|
|
353
|
+
.ibin file path with ground truth
|
|
354
|
+
|
|
355
|
+
-b, --big Will switch to uint40_t for neighbors lists with over 4B entries
|
|
356
|
+
-j, --threads <integer>
|
|
357
|
+
Uses all available cores by default
|
|
358
|
+
|
|
359
|
+
-c, --connectivity <integer>
|
|
360
|
+
Index granularity
|
|
361
|
+
|
|
362
|
+
--expansion-add <integer>
|
|
363
|
+
Affects indexing depth
|
|
364
|
+
|
|
365
|
+
--expansion-search <integer>
|
|
366
|
+
Affects search depth
|
|
367
|
+
|
|
368
|
+
--native Use raw templates instead of type-punned classes
|
|
369
|
+
--f16quant Enable `f16_t` quantization
|
|
370
|
+
--i8quant Enable `int8_t` quantization
|
|
371
|
+
--ip Choose Inner Product metric
|
|
372
|
+
--l2 Choose L2 Euclidean metric
|
|
373
|
+
--cos Choose Angular metric
|
|
374
|
+
--haversine Choose Haversine metric
|
|
375
|
+
-h, --help Print this help information on this tool and exit
|
|
376
|
+
```
|
|
81
377
|
|
|
82
378
|
## TODO
|
|
83
379
|
|
|
84
380
|
- JavaScript: Allow calling from "worker threads".
|
|
85
381
|
- Rust: Allow passing a custom thread ID.
|
|
382
|
+
|
|
383
|
+
## AI + Vector Search = Semantic Search
|
|
384
|
+
|
|
385
|
+
AI has a growing number of applications, but one of the coolest classic ideas is to use it for Semantic Search.
|
|
386
|
+
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.
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
import ucall.rich_posix as ucall
|
|
390
|
+
import uform
|
|
391
|
+
import usearch
|
|
392
|
+
|
|
393
|
+
import numpy as np
|
|
394
|
+
from PIL import Image
|
|
395
|
+
|
|
396
|
+
server = ucall.Server()
|
|
397
|
+
model = uform.get_model('unum-cloud/uform-vl-multilingual')
|
|
398
|
+
index = usearch.Index(dim=256)
|
|
399
|
+
|
|
400
|
+
@server
|
|
401
|
+
def add(label: int, photo: Image.Image):
|
|
402
|
+
image = model.preprocess_image(photo)
|
|
403
|
+
vector = model.encode_image(image).detach().numpy()
|
|
404
|
+
labels = np.array([label], dtype=np.longlong)
|
|
405
|
+
index.add(labels, vector, copy=True)
|
|
406
|
+
|
|
407
|
+
@server
|
|
408
|
+
def search(query: str) -> np.ndarray:
|
|
409
|
+
tokens = model.preprocess_text(query)
|
|
410
|
+
vector = model.encode_text(tokens).detach().numpy()
|
|
411
|
+
neighbors = index.search(vector, 3)
|
|
412
|
+
return neighbors[0][:neighbors[2][0]]
|
|
413
|
+
|
|
414
|
+
server.run()
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
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.9
|
|
Binary file
|
|
Binary file
|
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
package/python/bench.py
CHANGED
|
@@ -60,7 +60,7 @@ def measure(f) -> float:
|
|
|
60
60
|
f()
|
|
61
61
|
b = datetime.datetime.now()
|
|
62
62
|
c = b - a
|
|
63
|
-
print(f'Took: {c.seconds} seconds')
|
|
63
|
+
print(f'Took: {c.seconds:.2f} seconds')
|
|
64
64
|
return c.seconds
|
|
65
65
|
|
|
66
66
|
|
|
@@ -71,12 +71,15 @@ def construct_both() -> tuple:
|
|
|
71
71
|
index_meta.hnsw.efConstruction = efConstruction
|
|
72
72
|
dt_meta = measure(lambda: index_meta.add(xb))
|
|
73
73
|
|
|
74
|
-
index_unum = usearch.
|
|
74
|
+
index_unum = usearch.Index(
|
|
75
|
+
dim=d,
|
|
75
76
|
expansion_add=efConstruction,
|
|
76
77
|
expansion_search=efSearch,
|
|
77
78
|
connectivity=M,
|
|
78
79
|
)
|
|
79
|
-
|
|
80
|
+
assert len(index_unum) == 0
|
|
81
|
+
dt_unum = measure(lambda: index_unum.add(labels, xb))
|
|
82
|
+
assert len(index_unum) == xb.shape[0]
|
|
80
83
|
|
|
81
84
|
return dt_meta, dt_unum
|
|
82
85
|
|
package/python/lib.cpp
CHANGED
|
@@ -198,6 +198,12 @@ PYBIND11_MODULE(usearch, m) {
|
|
|
198
198
|
py::arg("count") = 10 //
|
|
199
199
|
);
|
|
200
200
|
|
|
201
|
+
i.def("__len__", &native_index_t::size);
|
|
202
|
+
i.def_property_readonly("size", &native_index_t::size);
|
|
203
|
+
i.def_property_readonly("ndim", &native_index_t::dimensions);
|
|
204
|
+
i.def_property_readonly("connectivity", &native_index_t::connectivity);
|
|
205
|
+
i.def_property_readonly("capacity", &native_index_t::capacity);
|
|
206
|
+
|
|
201
207
|
i.def("save", &save_index, py::arg("path"));
|
|
202
208
|
i.def("load", &load_index, py::arg("path"));
|
|
203
209
|
i.def("view", &view_index, py::arg("path"));
|
package/python/test.py
CHANGED
|
@@ -6,15 +6,14 @@ count_vectors = 100
|
|
|
6
6
|
count_dimensions = 96
|
|
7
7
|
|
|
8
8
|
index = usearch.Index(dim=96)
|
|
9
|
-
print(index)
|
|
10
|
-
|
|
11
9
|
vectors = np.random.uniform(
|
|
12
10
|
0, 0.3, (count_vectors, count_dimensions)).astype(np.float32)
|
|
13
11
|
labels = np.array(range(count_vectors), dtype=np.longlong)
|
|
14
|
-
print('will insert', labels.shape, vectors.shape)
|
|
15
12
|
|
|
16
13
|
index.add(labels, vectors, copy=True)
|
|
14
|
+
assert index.ndim == count_dimensions
|
|
15
|
+
assert index.capacity >= count_vectors
|
|
16
|
+
assert len(index) == count_vectors
|
|
17
17
|
|
|
18
|
-
print('will search')
|
|
19
18
|
results = index.search(vectors, 10)
|
|
20
19
|
print('found', results[0].shape, results[1].shape, results[2].shape)
|
package/src/advanced.hpp
CHANGED
|
@@ -127,17 +127,17 @@ void multithreaded(std::size_t threads, std::size_t tasks, callback_at&& callbac
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
std::vector<std::thread> threads_pool;
|
|
130
|
-
std::size_t tasks_per_thread =
|
|
131
|
-
for (std::size_t
|
|
130
|
+
std::size_t tasks_per_thread = tasks / threads + (tasks % threads) != 0;
|
|
131
|
+
for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx) {
|
|
132
132
|
threads_pool.emplace_back([=]() {
|
|
133
|
-
for (std::size_t task_idx =
|
|
134
|
-
task_idx < std::min(tasks,
|
|
135
|
-
callback(
|
|
133
|
+
for (std::size_t task_idx = thread_idx * tasks_per_thread;
|
|
134
|
+
task_idx < std::min(tasks, thread_idx * tasks_per_thread + tasks_per_thread); ++task_idx)
|
|
135
|
+
callback(thread_idx, task_idx);
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
for (std::size_t
|
|
140
|
-
threads_pool[
|
|
139
|
+
for (std::size_t thread_idx = 0; thread_idx != threads; ++thread_idx)
|
|
140
|
+
threads_pool[thread_idx].join();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/**
|
|
@@ -172,6 +172,16 @@ enum class accuracy_t {
|
|
|
172
172
|
i8q100_k,
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
inline char const* accuracy_name(accuracy_t accuracy) noexcept {
|
|
176
|
+
switch (accuracy) {
|
|
177
|
+
case accuracy_t::f32_k: return "f32";
|
|
178
|
+
case accuracy_t::f16_k: return "f16";
|
|
179
|
+
case accuracy_t::f64_k: return "f64";
|
|
180
|
+
case accuracy_t::i8q100_k: return "i8q100";
|
|
181
|
+
default: return "";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
inline bool str_equals(char const* begin, std::size_t len, char const* other_begin) noexcept {
|
|
176
186
|
std::size_t other_len = strlen(other_begin);
|
|
177
187
|
return len == other_len && strncmp(begin, other_begin, len) == 0;
|
|
@@ -217,13 +227,14 @@ enum class isa_t {
|
|
|
217
227
|
avx512_k,
|
|
218
228
|
};
|
|
219
229
|
|
|
220
|
-
inline char const*
|
|
230
|
+
inline char const* isa_name(isa_t isa) noexcept {
|
|
221
231
|
switch (isa) {
|
|
222
232
|
case isa_t::auto_k: return "auto";
|
|
223
233
|
case isa_t::neon_k: return "neon";
|
|
224
234
|
case isa_t::sve_k: return "sve";
|
|
225
235
|
case isa_t::avx2_k: return "avx2";
|
|
226
236
|
case isa_t::avx512_k: return "avx512";
|
|
237
|
+
default: return "";
|
|
227
238
|
}
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -445,7 +456,6 @@ class auto_index_gt {
|
|
|
445
456
|
}
|
|
446
457
|
|
|
447
458
|
static metric_and_meta_t ip_metric_f32(std::size_t dimensions) {
|
|
448
|
-
#if 0
|
|
449
459
|
#if defined(__x86_64__)
|
|
450
460
|
if (dimensions % 4 == 0)
|
|
451
461
|
return {
|
|
@@ -458,7 +468,7 @@ class auto_index_gt {
|
|
|
458
468
|
if (supports_arm_sve())
|
|
459
469
|
return {
|
|
460
470
|
pun_metric<simsimd_f32_t>([](simsimd_f32_t const* a, simsimd_f32_t const* b, size_t d) noexcept {
|
|
461
|
-
|
|
471
|
+
return 1 - simsimd_dot_f32sve(a, b, d);
|
|
462
472
|
}),
|
|
463
473
|
isa_t::sve_k,
|
|
464
474
|
};
|
|
@@ -469,13 +479,11 @@ class auto_index_gt {
|
|
|
469
479
|
}),
|
|
470
480
|
isa_t::neon_k,
|
|
471
481
|
};
|
|
472
|
-
#endif
|
|
473
482
|
#endif
|
|
474
483
|
return {pun_metric<f32_t>(ip_gt<f32_t>{}), isa_t::auto_k};
|
|
475
484
|
}
|
|
476
485
|
|
|
477
486
|
static metric_and_meta_t ip_metric_f16(std::size_t dimensions) {
|
|
478
|
-
#if 0
|
|
479
487
|
#if defined(__x86_64__)
|
|
480
488
|
#elif defined(__aarch64__)
|
|
481
489
|
if (supports_arm_sve())
|
|
@@ -492,7 +500,6 @@ class auto_index_gt {
|
|
|
492
500
|
}),
|
|
493
501
|
isa_t::neon_k,
|
|
494
502
|
};
|
|
495
|
-
#endif
|
|
496
503
|
#endif
|
|
497
504
|
return {pun_metric<f16_converted_t>(ip_gt<f16_converted_t>{}), isa_t::auto_k};
|
|
498
505
|
}
|
package/src/bench.cpp
CHANGED
|
@@ -295,8 +295,10 @@ void run_type_punned(dataset_at& dataset, args_t const& args, config_t config) {
|
|
|
295
295
|
if (args.quantize_i8)
|
|
296
296
|
accuracy = accuracy_t::i8q100_k;
|
|
297
297
|
|
|
298
|
+
std::printf("-- Accuracy: %s\n", accuracy_name(accuracy));
|
|
299
|
+
|
|
298
300
|
index_at index{type_punned_index_for_metric<index_at>(dataset, args, config, accuracy)};
|
|
299
|
-
std::printf("- Hardware acceleration: %s\n",
|
|
301
|
+
std::printf("- Hardware acceleration: %s\n", isa_name(index.acceleration()));
|
|
300
302
|
std::printf("-- Will benchmark in-memory\n");
|
|
301
303
|
|
|
302
304
|
single_shot(dataset, index, true);
|
|
@@ -410,6 +412,11 @@ int main(int argc, char** argv) {
|
|
|
410
412
|
config.max_threads_add = config.max_threads_search = args.threads;
|
|
411
413
|
config.max_elements = dataset.vectors_count();
|
|
412
414
|
|
|
415
|
+
std::printf("- Index: \n");
|
|
416
|
+
std::printf("-- Connectivity: %zu\n", config.connectivity);
|
|
417
|
+
std::printf("-- Expansion @ Add: %zu\n", config.expansion_add);
|
|
418
|
+
std::printf("-- Expansion @ Search: %zu\n", config.expansion_search);
|
|
419
|
+
|
|
413
420
|
if (args.big)
|
|
414
421
|
run_big_or_small<uint40_t>(dataset, args, config);
|
|
415
422
|
else
|