usearch 0.4.0 → 0.8.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/.clang-tidy +60 -0
- package/.github/workflows/release.yml +42 -16
- package/.github/workflows/update_version.sh +1 -0
- package/.vscode/launch.json +22 -0
- package/.vscode/settings.json +5 -0
- package/.vscode/tasks.json +4 -4
- package/CITATION.cff +11 -0
- package/CMakeLists.txt +108 -95
- package/Cargo.toml +1 -1
- package/Dockerfile +14 -0
- package/README.md +8 -7
- package/VERSION +1 -1
- package/binding.gyp +2 -2
- package/build.rs +2 -2
- package/cmake/config.cmake.in +15 -0
- package/cmake/pkg-config.pc.in +4 -0
- package/cmake/usearchConfigVersion.cmake.in +20 -0
- package/conanfile.py +1 -1
- package/docs/benchmarks.md +11 -2
- package/docs/{Compilation.md → compilation.md} +54 -27
- package/include/usearch/usearch.hpp +664 -390
- package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +17 -16
- package/javascript/lib.cpp +13 -13
- package/javascript/test.js +1 -1
- package/objc/USearchObjective.mm +46 -29
- package/objc/include/USearchObjective.h +18 -26
- package/package.json +1 -1
- package/pyproject.toml +3 -2
- package/python/lib.cpp +179 -120
- package/python/{bench.py → scripts/bench.py} +6 -1
- package/python/scripts/bench_params.py +112 -0
- package/python/{test.py → scripts/test.py} +32 -13
- package/python/usearch/client.py +100 -14
- package/python/usearch/server.py +104 -23
- package/rust/lib.cpp +15 -14
- package/rust/lib.hpp +5 -5
- package/rust/lib.rs +3 -3
- package/setup.py +7 -0
- package/src/CMakeLists.txt +107 -0
- package/src/bench.cpp +45 -15
- package/src/{advanced.hpp → punned.hpp} +227 -199
- package/src/test.cpp +62 -0
- package/swift/Test.swift +1 -1
- package/wolfram/lib.cpp +14 -14
package/python/lib.cpp
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
#include <pybind11/numpy.h>
|
|
19
19
|
#include <pybind11/pybind11.h>
|
|
20
20
|
|
|
21
|
-
#include "
|
|
21
|
+
#include "punned.hpp"
|
|
22
22
|
|
|
23
23
|
using namespace unum::usearch;
|
|
24
24
|
using namespace unum;
|
|
@@ -29,28 +29,55 @@ using py_shape_t = py::array::ShapeContainer;
|
|
|
29
29
|
using label_t = Py_ssize_t;
|
|
30
30
|
using distance_t = punned_distance_t;
|
|
31
31
|
using id_t = std::uint32_t;
|
|
32
|
-
using
|
|
32
|
+
using punned_t = punned_gt<label_t, id_t>;
|
|
33
|
+
|
|
34
|
+
struct punned_py_t : public punned_t {
|
|
35
|
+
using native_t = punned_t;
|
|
36
|
+
using native_t::add;
|
|
37
|
+
using native_t::capacity;
|
|
38
|
+
using native_t::reserve;
|
|
39
|
+
using native_t::search;
|
|
40
|
+
using native_t::size;
|
|
41
|
+
|
|
42
|
+
std::vector<char> ascii_normalization_buffer_;
|
|
43
|
+
|
|
44
|
+
punned_py_t(native_t&& base) : native_t(std::move(base)) {}
|
|
45
|
+
};
|
|
33
46
|
|
|
34
47
|
using set_member_t = std::uint32_t;
|
|
35
48
|
using set_view_t = span_gt<set_member_t const>;
|
|
36
49
|
using sets_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
|
|
37
50
|
|
|
51
|
+
struct sets_index_py_t : public sets_index_t {
|
|
52
|
+
using native_t = sets_index_t;
|
|
53
|
+
using native_t::add;
|
|
54
|
+
using native_t::capacity;
|
|
55
|
+
using native_t::reserve;
|
|
56
|
+
using native_t::search;
|
|
57
|
+
using native_t::size;
|
|
58
|
+
|
|
59
|
+
sets_index_py_t(native_t&& base) : native_t(std::move(base)) {}
|
|
60
|
+
};
|
|
61
|
+
|
|
38
62
|
using hash_word_t = std::uint64_t;
|
|
39
63
|
using hash_index_t = index_gt<bit_hamming_gt<hash_word_t>, label_t, id_t, hash_word_t>;
|
|
40
64
|
static constexpr std::size_t bits_per_hash_word_k = sizeof(hash_word_t) * CHAR_BIT;
|
|
41
65
|
|
|
42
|
-
struct
|
|
43
|
-
using hash_index_t
|
|
44
|
-
using
|
|
45
|
-
using
|
|
46
|
-
using
|
|
47
|
-
using
|
|
66
|
+
struct hash_index_py_t : public hash_index_t {
|
|
67
|
+
using native_t = hash_index_t;
|
|
68
|
+
using native_t::add;
|
|
69
|
+
using native_t::capacity;
|
|
70
|
+
using native_t::reserve;
|
|
71
|
+
using native_t::search;
|
|
72
|
+
using native_t::size;
|
|
48
73
|
|
|
49
74
|
std::vector<hash_word_t> buffer_;
|
|
50
75
|
std::size_t words_;
|
|
51
76
|
std::size_t bits_;
|
|
52
77
|
|
|
53
|
-
|
|
78
|
+
hash_index_py_t(native_t&& base) : native_t(std::move(base)) {}
|
|
79
|
+
|
|
80
|
+
hash_index_py_t(config_t config, std::size_t bits) : hash_index_t(config) {
|
|
54
81
|
words_ = divide_round_up<bits_per_hash_word_k>(bits);
|
|
55
82
|
bits_ = words_ * bits_per_hash_word_k;
|
|
56
83
|
buffer_.resize(words_);
|
|
@@ -78,7 +105,7 @@ punned_stateful_metric_t udf(std::size_t metric_uintptr, accuracy_t accuracy) {
|
|
|
78
105
|
}
|
|
79
106
|
}
|
|
80
107
|
|
|
81
|
-
static
|
|
108
|
+
static punned_py_t make_index( //
|
|
82
109
|
std::size_t dimensions, //
|
|
83
110
|
std::size_t capacity, //
|
|
84
111
|
std::string const& scalar_type, //
|
|
@@ -86,8 +113,8 @@ static native_index_t make_index( //
|
|
|
86
113
|
std::size_t connectivity, //
|
|
87
114
|
std::size_t expansion_add, //
|
|
88
115
|
std::size_t expansion_search, //
|
|
89
|
-
std::size_t metric_uintptr
|
|
90
|
-
) {
|
|
116
|
+
std::size_t metric_uintptr, //
|
|
117
|
+
bool tune) {
|
|
91
118
|
|
|
92
119
|
config_t config;
|
|
93
120
|
config.expansion_add = expansion_add;
|
|
@@ -97,18 +124,21 @@ static native_index_t make_index( //
|
|
|
97
124
|
config.max_threads_add = std::thread::hardware_concurrency();
|
|
98
125
|
config.max_threads_search = std::thread::hardware_concurrency();
|
|
99
126
|
|
|
127
|
+
if (tune)
|
|
128
|
+
config = punned_t::optimize(config);
|
|
129
|
+
|
|
100
130
|
accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
|
|
101
131
|
if (metric_uintptr)
|
|
102
|
-
return
|
|
132
|
+
return punned_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
|
|
103
133
|
else
|
|
104
|
-
return index_from_name<
|
|
134
|
+
return index_from_name<punned_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
|
|
105
135
|
}
|
|
106
136
|
|
|
107
|
-
static std::unique_ptr<
|
|
108
|
-
std::size_t capacity,
|
|
109
|
-
std::size_t connectivity,
|
|
110
|
-
std::size_t expansion_add,
|
|
111
|
-
std::size_t expansion_search
|
|
137
|
+
static std::unique_ptr<sets_index_py_t> make_sets_index( //
|
|
138
|
+
std::size_t capacity, //
|
|
139
|
+
std::size_t connectivity, //
|
|
140
|
+
std::size_t expansion_add, //
|
|
141
|
+
std::size_t expansion_search //
|
|
112
142
|
) {
|
|
113
143
|
config_t config;
|
|
114
144
|
config.expansion_add = expansion_add;
|
|
@@ -118,15 +148,15 @@ static std::unique_ptr<sets_index_t> make_sets_index( //
|
|
|
118
148
|
config.max_threads_add = 1;
|
|
119
149
|
config.max_threads_search = 1;
|
|
120
150
|
|
|
121
|
-
return std::unique_ptr<
|
|
151
|
+
return std::unique_ptr<sets_index_py_t>(new sets_index_py_t(sets_index_t(config)));
|
|
122
152
|
}
|
|
123
153
|
|
|
124
|
-
static std::unique_ptr<
|
|
125
|
-
std::size_t bits,
|
|
126
|
-
std::size_t capacity,
|
|
127
|
-
std::size_t connectivity,
|
|
128
|
-
std::size_t expansion_add,
|
|
129
|
-
std::size_t expansion_search
|
|
154
|
+
static std::unique_ptr<hash_index_py_t> make_hash_index( //
|
|
155
|
+
std::size_t bits, //
|
|
156
|
+
std::size_t capacity, //
|
|
157
|
+
std::size_t connectivity, //
|
|
158
|
+
std::size_t expansion_add, //
|
|
159
|
+
std::size_t expansion_search //
|
|
130
160
|
) {
|
|
131
161
|
config_t config;
|
|
132
162
|
config.expansion_add = expansion_add;
|
|
@@ -136,10 +166,10 @@ static std::unique_ptr<hash_index_w_meta_t> make_hash_index( //
|
|
|
136
166
|
config.max_threads_add = 1;
|
|
137
167
|
config.max_threads_search = 1;
|
|
138
168
|
|
|
139
|
-
return std::unique_ptr<
|
|
169
|
+
return std::unique_ptr<hash_index_py_t>(new hash_index_py_t(config, bits));
|
|
140
170
|
}
|
|
141
171
|
|
|
142
|
-
static void add_one_to_index(
|
|
172
|
+
static void add_one_to_index(punned_py_t& index, label_t label, py::buffer vector, bool copy) {
|
|
143
173
|
|
|
144
174
|
py::buffer_info vector_info = vector.request();
|
|
145
175
|
if (vector_info.ndim != 1)
|
|
@@ -157,7 +187,9 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
|
|
|
157
187
|
config.store_vector = copy;
|
|
158
188
|
|
|
159
189
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
160
|
-
if (vector_info.format == "
|
|
190
|
+
if (vector_info.format == "c" || vector_info.format == "b")
|
|
191
|
+
index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config);
|
|
192
|
+
else if (vector_info.format == "e")
|
|
161
193
|
index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config);
|
|
162
194
|
else if (vector_info.format == "f")
|
|
163
195
|
index.add(label, reinterpret_cast<float const*>(vector_data), config);
|
|
@@ -167,8 +199,8 @@ static void add_one_to_index(native_index_t& index, label_t label, py::buffer ve
|
|
|
167
199
|
throw std::invalid_argument("Incompatible scalars in the vector!");
|
|
168
200
|
}
|
|
169
201
|
|
|
170
|
-
static void add_many_to_index(
|
|
171
|
-
|
|
202
|
+
static void add_many_to_index( //
|
|
203
|
+
punned_py_t& index, py::buffer labels, py::buffer vectors, //
|
|
172
204
|
bool copy, std::size_t threads = 0) {
|
|
173
205
|
|
|
174
206
|
py::buffer_info labels_info = labels.request();
|
|
@@ -199,8 +231,18 @@ static void add_many_to_index( //
|
|
|
199
231
|
char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
|
|
200
232
|
|
|
201
233
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
202
|
-
if (vectors_info.format == "
|
|
203
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
234
|
+
if (vectors_info.format == "c" || vectors_info.format == "b")
|
|
235
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
236
|
+
add_config_t config;
|
|
237
|
+
config.store_vector = copy;
|
|
238
|
+
config.thread = thread_idx;
|
|
239
|
+
label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
|
|
240
|
+
f8_bits_t const* vector =
|
|
241
|
+
reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
|
|
242
|
+
index.add(label, vector, config);
|
|
243
|
+
});
|
|
244
|
+
else if (vectors_info.format == "e")
|
|
245
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
204
246
|
add_config_t config;
|
|
205
247
|
config.store_vector = copy;
|
|
206
248
|
config.thread = thread_idx;
|
|
@@ -210,7 +252,7 @@ static void add_many_to_index( //
|
|
|
210
252
|
index.add(label, vector, config);
|
|
211
253
|
});
|
|
212
254
|
else if (vectors_info.format == "f")
|
|
213
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
255
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
214
256
|
add_config_t config;
|
|
215
257
|
config.store_vector = copy;
|
|
216
258
|
config.thread = thread_idx;
|
|
@@ -219,7 +261,7 @@ static void add_many_to_index( //
|
|
|
219
261
|
index.add(label, vector, config);
|
|
220
262
|
});
|
|
221
263
|
else if (vectors_info.format == "d")
|
|
222
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
264
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
223
265
|
add_config_t config;
|
|
224
266
|
config.store_vector = copy;
|
|
225
267
|
config.thread = thread_idx;
|
|
@@ -231,7 +273,7 @@ static void add_many_to_index( //
|
|
|
231
273
|
throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
|
|
232
274
|
}
|
|
233
275
|
|
|
234
|
-
static py::tuple search_one_in_index(
|
|
276
|
+
static py::tuple search_one_in_index(punned_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
|
|
235
277
|
|
|
236
278
|
py::buffer_info vector_info = vector.request();
|
|
237
279
|
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
@@ -249,15 +291,22 @@ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, s
|
|
|
249
291
|
config.exact = exact;
|
|
250
292
|
|
|
251
293
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
252
|
-
if (vector_info.format == "
|
|
253
|
-
count = index
|
|
254
|
-
|
|
294
|
+
if (vector_info.format == "c" || vector_info.format == "b")
|
|
295
|
+
count = index //
|
|
296
|
+
.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config)
|
|
297
|
+
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
298
|
+
else if (vector_info.format == "e")
|
|
299
|
+
count = index //
|
|
300
|
+
.search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config)
|
|
301
|
+
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
255
302
|
else if (vector_info.format == "f")
|
|
256
|
-
count = index
|
|
257
|
-
|
|
303
|
+
count = index //
|
|
304
|
+
.search(reinterpret_cast<float const*>(vector_data), wanted, config)
|
|
305
|
+
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
258
306
|
else if (vector_info.format == "d")
|
|
259
|
-
count = index
|
|
260
|
-
|
|
307
|
+
count = index //
|
|
308
|
+
.search(reinterpret_cast<double const*>(vector_data), wanted, config)
|
|
309
|
+
.dump_to(&labels_py1d(0), &distances_py1d(0));
|
|
261
310
|
else
|
|
262
311
|
throw std::invalid_argument("Incompatible scalars in the query vector!");
|
|
263
312
|
|
|
@@ -280,7 +329,7 @@ static py::tuple search_one_in_index(native_index_t& index, py::buffer vector, s
|
|
|
280
329
|
* 2. matrix of distances,
|
|
281
330
|
* 3. array with match counts.
|
|
282
331
|
*/
|
|
283
|
-
static py::tuple search_many_in_index(
|
|
332
|
+
static py::tuple search_many_in_index(punned_py_t& index, py::buffer vectors, std::size_t wanted, bool exact) {
|
|
284
333
|
|
|
285
334
|
if (wanted == 0)
|
|
286
335
|
return py::tuple(3);
|
|
@@ -305,32 +354,41 @@ static py::tuple search_many_in_index(native_index_t& index, py::buffer vectors,
|
|
|
305
354
|
auto counts_py1d = counts_py.mutable_unchecked<1>();
|
|
306
355
|
|
|
307
356
|
// https://docs.python.org/3/library/struct.html#format-characters
|
|
308
|
-
if (vectors_info.format == "
|
|
309
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
357
|
+
if (vectors_info.format == "c" || vectors_info.format == "b")
|
|
358
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
359
|
+
search_config_t config;
|
|
360
|
+
config.thread = thread_idx;
|
|
361
|
+
config.exact = exact;
|
|
362
|
+
f8_bits_t const* vector = (f8_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
363
|
+
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
364
|
+
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
365
|
+
});
|
|
366
|
+
else if (vectors_info.format == "e")
|
|
367
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
310
368
|
search_config_t config;
|
|
311
369
|
config.thread = thread_idx;
|
|
312
370
|
config.exact = exact;
|
|
313
371
|
f16_bits_t const* vector = (f16_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
314
372
|
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
315
|
-
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)
|
|
373
|
+
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
316
374
|
});
|
|
317
375
|
else if (vectors_info.format == "f")
|
|
318
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
376
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
319
377
|
search_config_t config;
|
|
320
378
|
config.thread = thread_idx;
|
|
321
379
|
config.exact = exact;
|
|
322
380
|
float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
323
381
|
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
324
|
-
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)
|
|
382
|
+
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
325
383
|
});
|
|
326
384
|
else if (vectors_info.format == "d")
|
|
327
|
-
multithreaded(index.concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
385
|
+
multithreaded(index.config().concurrency(), vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
|
|
328
386
|
search_config_t config;
|
|
329
387
|
config.thread = thread_idx;
|
|
330
388
|
config.exact = exact;
|
|
331
389
|
double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
|
|
332
390
|
counts_py1d(task_idx) = static_cast<Py_ssize_t>(
|
|
333
|
-
index.search(vector, wanted, &labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)
|
|
391
|
+
index.search(vector, wanted, config).dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
|
|
334
392
|
});
|
|
335
393
|
else
|
|
336
394
|
throw std::invalid_argument("Incompatible scalars in the query matrix!");
|
|
@@ -386,7 +444,7 @@ inline std::uint64_t hash(std::uint64_t v) noexcept {
|
|
|
386
444
|
}
|
|
387
445
|
|
|
388
446
|
template <typename scalar_at>
|
|
389
|
-
inline void hash_typed_buffer(
|
|
447
|
+
inline void hash_typed_buffer(hash_index_py_t& index, py::buffer_info const& vector_info) noexcept {
|
|
390
448
|
char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
|
|
391
449
|
Py_ssize_t vector_dimensions = vector_info.shape[0];
|
|
392
450
|
Py_ssize_t vector_stride = vector_info.strides[0];
|
|
@@ -399,7 +457,7 @@ inline void hash_typed_buffer(hash_index_w_meta_t& index, py::buffer_info const&
|
|
|
399
457
|
}
|
|
400
458
|
}
|
|
401
459
|
|
|
402
|
-
void hash_buffer(
|
|
460
|
+
void hash_buffer(hash_index_py_t& index, py::buffer vector) {
|
|
403
461
|
py::buffer_info info = vector.request();
|
|
404
462
|
if (info.ndim != 1)
|
|
405
463
|
throw std::invalid_argument("Array can't be multi-dimensional!");
|
|
@@ -418,18 +476,19 @@ void hash_buffer(hash_index_w_meta_t& index, py::buffer vector) {
|
|
|
418
476
|
PYBIND11_MODULE(index, m) {
|
|
419
477
|
m.doc() = "Unum USearch Python bindings";
|
|
420
478
|
|
|
421
|
-
auto i = py::class_<
|
|
422
|
-
|
|
423
|
-
i.def(py::init(&make_index),
|
|
424
|
-
py::kw_only(),
|
|
425
|
-
py::arg("ndim") = 0,
|
|
426
|
-
py::arg("capacity") = 0,
|
|
427
|
-
py::arg("dtype") = std::string("f32"),
|
|
428
|
-
py::arg("metric") = std::string("ip"),
|
|
429
|
-
py::arg("connectivity") =
|
|
430
|
-
py::arg("expansion_add") =
|
|
431
|
-
py::arg("expansion_search") =
|
|
432
|
-
py::arg("metric_pointer") = 0
|
|
479
|
+
auto i = py::class_<punned_py_t>(m, "Index");
|
|
480
|
+
|
|
481
|
+
i.def(py::init(&make_index), //
|
|
482
|
+
py::kw_only(), //
|
|
483
|
+
py::arg("ndim") = 0, //
|
|
484
|
+
py::arg("capacity") = 0, //
|
|
485
|
+
py::arg("dtype") = std::string("f32"), //
|
|
486
|
+
py::arg("metric") = std::string("ip"), //
|
|
487
|
+
py::arg("connectivity") = default_connectivity(), //
|
|
488
|
+
py::arg("expansion_add") = default_expansion_add(), //
|
|
489
|
+
py::arg("expansion_search") = default_expansion_search(), //
|
|
490
|
+
py::arg("metric_pointer") = 0, //
|
|
491
|
+
py::arg("tune") = false //
|
|
433
492
|
);
|
|
434
493
|
|
|
435
494
|
i.def( //
|
|
@@ -456,33 +515,33 @@ PYBIND11_MODULE(index, m) {
|
|
|
456
515
|
py::arg("exact") = false //
|
|
457
516
|
);
|
|
458
517
|
|
|
459
|
-
i.def("__len__", &
|
|
460
|
-
i.def_property_readonly("size", &
|
|
461
|
-
i.def_property_readonly("ndim", &
|
|
462
|
-
i.def_property_readonly("connectivity", &
|
|
463
|
-
i.def_property_readonly("capacity", &
|
|
518
|
+
i.def("__len__", &punned_py_t::size);
|
|
519
|
+
i.def_property_readonly("size", &punned_py_t::size);
|
|
520
|
+
i.def_property_readonly("ndim", &punned_py_t::dimensions);
|
|
521
|
+
i.def_property_readonly("connectivity", &punned_py_t::connectivity);
|
|
522
|
+
i.def_property_readonly("capacity", &punned_py_t::capacity);
|
|
464
523
|
i.def_property_readonly( //
|
|
465
|
-
"dtype", [](
|
|
466
|
-
|
|
467
|
-
i.def("save", &save_index<
|
|
468
|
-
i.def("load", &load_index<
|
|
469
|
-
i.def("view", &view_index<
|
|
470
|
-
i.def("clear", &clear_index<
|
|
471
|
-
|
|
472
|
-
auto si = py::class_<
|
|
473
|
-
|
|
474
|
-
si.def(
|
|
475
|
-
py::init(&make_sets_index),
|
|
476
|
-
py::kw_only(),
|
|
477
|
-
py::arg("capacity") = 0,
|
|
478
|
-
py::arg("connectivity") =
|
|
479
|
-
py::arg("expansion_add") =
|
|
480
|
-
py::arg("expansion_search") =
|
|
524
|
+
"dtype", [](punned_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
|
|
525
|
+
|
|
526
|
+
i.def("save", &save_index<punned_py_t>, py::arg("path"));
|
|
527
|
+
i.def("load", &load_index<punned_py_t>, py::arg("path"));
|
|
528
|
+
i.def("view", &view_index<punned_py_t>, py::arg("path"));
|
|
529
|
+
i.def("clear", &clear_index<punned_py_t>);
|
|
530
|
+
|
|
531
|
+
auto si = py::class_<sets_index_py_t>(m, "SetsIndex");
|
|
532
|
+
|
|
533
|
+
si.def( //
|
|
534
|
+
py::init(&make_sets_index), //
|
|
535
|
+
py::kw_only(), //
|
|
536
|
+
py::arg("capacity") = 0, //
|
|
537
|
+
py::arg("connectivity") = default_connectivity(), //
|
|
538
|
+
py::arg("expansion_add") = default_expansion_add(), //
|
|
539
|
+
py::arg("expansion_search") = default_expansion_search() //
|
|
481
540
|
);
|
|
482
541
|
|
|
483
542
|
si.def( //
|
|
484
543
|
"add",
|
|
485
|
-
[](
|
|
544
|
+
[](sets_index_py_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
|
|
486
545
|
validate_set(set);
|
|
487
546
|
if (index.size() + 1 >= index.capacity())
|
|
488
547
|
index.reserve(ceil2(index.size() + 1));
|
|
@@ -500,13 +559,13 @@ PYBIND11_MODULE(index, m) {
|
|
|
500
559
|
|
|
501
560
|
si.def( //
|
|
502
561
|
"search",
|
|
503
|
-
[](
|
|
562
|
+
[](sets_index_py_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
|
|
504
563
|
validate_set(set);
|
|
505
564
|
auto proxy = set.unchecked<1>();
|
|
506
565
|
auto view = set_view_t{proxy.data(0), static_cast<std::size_t>(proxy.shape(0))};
|
|
507
566
|
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
508
567
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
509
|
-
auto found = index.search(view, count
|
|
568
|
+
auto found = index.search(view, count).dump_to(&labels_proxy(0), nullptr);
|
|
510
569
|
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
511
570
|
return labels_py;
|
|
512
571
|
},
|
|
@@ -514,31 +573,31 @@ PYBIND11_MODULE(index, m) {
|
|
|
514
573
|
py::arg("count") = 10 //
|
|
515
574
|
);
|
|
516
575
|
|
|
517
|
-
si.def("__len__", &
|
|
518
|
-
si.def_property_readonly("size", &
|
|
519
|
-
si.def_property_readonly("connectivity", &
|
|
520
|
-
si.def_property_readonly("capacity", &
|
|
521
|
-
|
|
522
|
-
si.def("save", &save_index<
|
|
523
|
-
si.def("load", &load_index<
|
|
524
|
-
si.def("view", &view_index<
|
|
525
|
-
si.def("clear", &clear_index<
|
|
526
|
-
|
|
527
|
-
auto hi = py::class_<
|
|
528
|
-
|
|
529
|
-
hi.def(
|
|
530
|
-
py::init(&make_hash_index),
|
|
531
|
-
py::kw_only(),
|
|
532
|
-
py::arg("bits"),
|
|
533
|
-
py::arg("capacity") = 0,
|
|
534
|
-
py::arg("connectivity") =
|
|
535
|
-
py::arg("expansion_add") =
|
|
536
|
-
py::arg("expansion_search") =
|
|
576
|
+
si.def("__len__", &sets_index_py_t::size);
|
|
577
|
+
si.def_property_readonly("size", &sets_index_py_t::size);
|
|
578
|
+
si.def_property_readonly("connectivity", &sets_index_py_t::connectivity);
|
|
579
|
+
si.def_property_readonly("capacity", &sets_index_py_t::capacity);
|
|
580
|
+
|
|
581
|
+
si.def("save", &save_index<sets_index_py_t>, py::arg("path"));
|
|
582
|
+
si.def("load", &load_index<sets_index_py_t>, py::arg("path"));
|
|
583
|
+
si.def("view", &view_index<sets_index_py_t>, py::arg("path"));
|
|
584
|
+
si.def("clear", &clear_index<sets_index_py_t>);
|
|
585
|
+
|
|
586
|
+
auto hi = py::class_<hash_index_py_t>(m, "HashIndex");
|
|
587
|
+
|
|
588
|
+
hi.def( //
|
|
589
|
+
py::init(&make_hash_index), //
|
|
590
|
+
py::kw_only(), //
|
|
591
|
+
py::arg("bits"), //
|
|
592
|
+
py::arg("capacity") = 0, //
|
|
593
|
+
py::arg("connectivity") = default_connectivity(), //
|
|
594
|
+
py::arg("expansion_add") = default_expansion_add(), //
|
|
595
|
+
py::arg("expansion_search") = default_expansion_search() //
|
|
537
596
|
);
|
|
538
597
|
|
|
539
598
|
hi.def( //
|
|
540
599
|
"add",
|
|
541
|
-
[](
|
|
600
|
+
[](hash_index_py_t& index, label_t label, py::buffer array) {
|
|
542
601
|
if (index.size() + 1 >= index.capacity())
|
|
543
602
|
index.reserve(ceil2(index.size() + 1));
|
|
544
603
|
hash_buffer(index, array);
|
|
@@ -550,13 +609,13 @@ PYBIND11_MODULE(index, m) {
|
|
|
550
609
|
|
|
551
610
|
hi.def( //
|
|
552
611
|
"search",
|
|
553
|
-
[](
|
|
612
|
+
[](hash_index_py_t& index, py::buffer array, std::size_t count) -> py::array_t<label_t> {
|
|
554
613
|
if (index.size() + 1 >= index.capacity())
|
|
555
614
|
index.reserve(ceil2(index.size() + 1));
|
|
556
615
|
hash_buffer(index, array);
|
|
557
616
|
auto labels_py = py::array_t<label_t>(py_shape_t{static_cast<Py_ssize_t>(count)});
|
|
558
617
|
auto labels_proxy = labels_py.mutable_unchecked<1>();
|
|
559
|
-
auto found = index.search(index.buffer(), count
|
|
618
|
+
auto found = index.search(index.buffer(), count).dump_to(&labels_proxy(0), nullptr);
|
|
560
619
|
labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(found)});
|
|
561
620
|
return labels_py;
|
|
562
621
|
},
|
|
@@ -564,13 +623,13 @@ PYBIND11_MODULE(index, m) {
|
|
|
564
623
|
py::arg("count") = 10 //
|
|
565
624
|
);
|
|
566
625
|
|
|
567
|
-
hi.def("__len__", &
|
|
568
|
-
hi.def_property_readonly("size", &
|
|
569
|
-
hi.def_property_readonly("connectivity", &
|
|
570
|
-
hi.def_property_readonly("capacity", &
|
|
626
|
+
hi.def("__len__", &hash_index_py_t::size);
|
|
627
|
+
hi.def_property_readonly("size", &hash_index_py_t::size);
|
|
628
|
+
hi.def_property_readonly("connectivity", &hash_index_py_t::connectivity);
|
|
629
|
+
hi.def_property_readonly("capacity", &hash_index_py_t::capacity);
|
|
571
630
|
|
|
572
|
-
hi.def("save", &save_index<
|
|
573
|
-
hi.def("load", &load_index<
|
|
574
|
-
hi.def("view", &view_index<
|
|
575
|
-
hi.def("clear", &clear_index<
|
|
631
|
+
hi.def("save", &save_index<hash_index_py_t>, py::arg("path"));
|
|
632
|
+
hi.def("load", &load_index<hash_index_py_t>, py::arg("path"));
|
|
633
|
+
hi.def("view", &view_index<hash_index_py_t>, py::arg("path"));
|
|
634
|
+
hi.def("clear", &clear_index<hash_index_py_t>);
|
|
576
635
|
}
|
|
@@ -5,7 +5,6 @@ import fire
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from numba import cfunc, types, carray
|
|
7
7
|
|
|
8
|
-
from faiss import IndexHNSWFlat, IndexIVFPQ
|
|
9
8
|
from usearch.index import Index
|
|
10
9
|
from usearch.io import load_matrix
|
|
11
10
|
|
|
@@ -125,6 +124,12 @@ def main(
|
|
|
125
124
|
)
|
|
126
125
|
bench_usearch(index, vectors_mat, queries_mat, neighbors_mat)
|
|
127
126
|
|
|
127
|
+
# Don't depend on the FAISS installation for benchmarks
|
|
128
|
+
try:
|
|
129
|
+
from faiss import IndexHNSWFlat, IndexIVFPQ
|
|
130
|
+
except ImportError:
|
|
131
|
+
return
|
|
132
|
+
|
|
128
133
|
print('FAISS: HNSW')
|
|
129
134
|
index = IndexHNSWFlat(dim, connectivity)
|
|
130
135
|
index.hnsw.efSearch = expansion_search
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import numpy as np
|
|
4
|
+
import fire
|
|
5
|
+
import plotly.express as px
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from usearch.index import Index
|
|
11
|
+
from usearch.io import load_matrix
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def measure(f) -> float:
|
|
15
|
+
a = time.time_ns()
|
|
16
|
+
result = f()
|
|
17
|
+
b = time.time_ns()
|
|
18
|
+
c = b - a
|
|
19
|
+
secs = c / (10 ** 9)
|
|
20
|
+
print(f'- Took: {secs:.2f} seconds')
|
|
21
|
+
return secs, result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def bench_usearch(index, vectors: np.ndarray) -> float:
|
|
25
|
+
labels = np.arange(vectors.shape[0], dtype=np.longlong)
|
|
26
|
+
assert len(index) == 0
|
|
27
|
+
dt, _ = measure(lambda: index.add(labels, vectors))
|
|
28
|
+
assert len(index) == vectors.shape[0]
|
|
29
|
+
insertions_per_second = vectors.shape[0]/dt
|
|
30
|
+
print(f'- Performance: {insertions_per_second:.2f} insertions/s')
|
|
31
|
+
return insertions_per_second
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Main:
|
|
35
|
+
|
|
36
|
+
def dimensions(
|
|
37
|
+
self,
|
|
38
|
+
count: int = 1_000_000,
|
|
39
|
+
connectivity: int = 16,
|
|
40
|
+
expansion_add: int = 128,
|
|
41
|
+
expansion_search: int = 64,
|
|
42
|
+
):
|
|
43
|
+
|
|
44
|
+
dimensions = [
|
|
45
|
+
2, 3, 4, 8, 16, 32, 96, 100,
|
|
46
|
+
256, 384, 512, 768, 1024, 1536]
|
|
47
|
+
results = []
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
for ndim in dimensions:
|
|
51
|
+
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
52
|
+
|
|
53
|
+
print(f'USearch for {ndim}-dimensional f32 vectors')
|
|
54
|
+
index = Index(
|
|
55
|
+
ndim=ndim,
|
|
56
|
+
expansion_add=expansion_add,
|
|
57
|
+
expansion_search=expansion_search,
|
|
58
|
+
connectivity=connectivity,
|
|
59
|
+
)
|
|
60
|
+
speed = bench_usearch(index, vectors_mat)
|
|
61
|
+
results.append({'ndim': ndim, 'speed': speed})
|
|
62
|
+
del index
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
df = pd.DataFrame(results)
|
|
67
|
+
fig = px.line(
|
|
68
|
+
df, x='ndim', y='speed', log_x=True,
|
|
69
|
+
title='HNSW Indexing Speed vs Vector Dimensions')
|
|
70
|
+
fig.write_image(os.path.join(os.path.dirname(
|
|
71
|
+
__file__), 'bench_dimensions.png'))
|
|
72
|
+
fig.show()
|
|
73
|
+
|
|
74
|
+
def connectivity(
|
|
75
|
+
self,
|
|
76
|
+
count: int = 1_000_000,
|
|
77
|
+
ndim: int = 256,
|
|
78
|
+
expansion_add: int = 128,
|
|
79
|
+
expansion_search: int = 64,
|
|
80
|
+
):
|
|
81
|
+
|
|
82
|
+
connectivities = range(10, 20)
|
|
83
|
+
results = []
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
for connectivity in connectivities:
|
|
87
|
+
vectors_mat = np.random.rand(count, ndim).astype(np.float32)
|
|
88
|
+
|
|
89
|
+
print(f'USearch for {connectivity} connectivity')
|
|
90
|
+
index = Index(
|
|
91
|
+
ndim=ndim,
|
|
92
|
+
expansion_add=expansion_add,
|
|
93
|
+
expansion_search=expansion_search,
|
|
94
|
+
connectivity=connectivity,
|
|
95
|
+
)
|
|
96
|
+
speed = bench_usearch(index, vectors_mat)
|
|
97
|
+
results.append({'connectivity': connectivity, 'speed': speed})
|
|
98
|
+
del index
|
|
99
|
+
except KeyboardInterrupt:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
df = pd.DataFrame(results)
|
|
103
|
+
fig = px.line(
|
|
104
|
+
df, x='connectivity', y='speed', log_x=True,
|
|
105
|
+
title='HNSW Indexing Speed vs Connectivity')
|
|
106
|
+
fig.write_image(os.path.join(os.path.dirname(
|
|
107
|
+
__file__), 'bench_connectivity.png'))
|
|
108
|
+
fig.show()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
fire.Fire(Main)
|