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.
Files changed (44) hide show
  1. package/.clang-tidy +60 -0
  2. package/.github/workflows/release.yml +42 -16
  3. package/.github/workflows/update_version.sh +1 -0
  4. package/.vscode/launch.json +22 -0
  5. package/.vscode/settings.json +5 -0
  6. package/.vscode/tasks.json +4 -4
  7. package/CITATION.cff +11 -0
  8. package/CMakeLists.txt +108 -95
  9. package/Cargo.toml +1 -1
  10. package/Dockerfile +14 -0
  11. package/README.md +8 -7
  12. package/VERSION +1 -1
  13. package/binding.gyp +2 -2
  14. package/build.rs +2 -2
  15. package/cmake/config.cmake.in +15 -0
  16. package/cmake/pkg-config.pc.in +4 -0
  17. package/cmake/usearchConfigVersion.cmake.in +20 -0
  18. package/conanfile.py +1 -1
  19. package/docs/benchmarks.md +11 -2
  20. package/docs/{Compilation.md → compilation.md} +54 -27
  21. package/include/usearch/usearch.hpp +664 -390
  22. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +17 -16
  23. package/javascript/lib.cpp +13 -13
  24. package/javascript/test.js +1 -1
  25. package/objc/USearchObjective.mm +46 -29
  26. package/objc/include/USearchObjective.h +18 -26
  27. package/package.json +1 -1
  28. package/pyproject.toml +3 -2
  29. package/python/lib.cpp +179 -120
  30. package/python/{bench.py → scripts/bench.py} +6 -1
  31. package/python/scripts/bench_params.py +112 -0
  32. package/python/{test.py → scripts/test.py} +32 -13
  33. package/python/usearch/client.py +100 -14
  34. package/python/usearch/server.py +104 -23
  35. package/rust/lib.cpp +15 -14
  36. package/rust/lib.hpp +5 -5
  37. package/rust/lib.rs +3 -3
  38. package/setup.py +7 -0
  39. package/src/CMakeLists.txt +107 -0
  40. package/src/bench.cpp +45 -15
  41. package/src/{advanced.hpp → punned.hpp} +227 -199
  42. package/src/test.cpp +62 -0
  43. package/swift/Test.swift +1 -1
  44. 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 "advanced.hpp"
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 native_index_t = auto_index_gt<label_t>;
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 hash_index_w_meta_t : public hash_index_t {
43
- using hash_index_t::add;
44
- using hash_index_t::capacity;
45
- using hash_index_t::reserve;
46
- using hash_index_t::search;
47
- using hash_index_t::size;
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
- hash_index_w_meta_t(config_t config, std::size_t bits) : hash_index_t(config) {
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 native_index_t make_index( //
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 native_index_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
132
+ return punned_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
103
133
  else
104
- return index_from_name<native_index_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
134
+ return index_from_name<punned_t>(metric.c_str(), metric.size(), dimensions, accuracy, config);
105
135
  }
106
136
 
107
- static std::unique_ptr<sets_index_t> make_sets_index( //
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<sets_index_t>(new sets_index_t(config));
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<hash_index_w_meta_t> make_hash_index( //
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<hash_index_w_meta_t>(new hash_index_w_meta_t(config, bits));
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(native_index_t& index, label_t label, py::buffer vector, bool copy) {
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 == "e")
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
- native_index_t& index, py::buffer labels, py::buffer vectors, //
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 == "e")
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(native_index_t& index, py::buffer vector, std::size_t wanted, bool exact) {
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 == "e")
253
- count = index.search( //
254
- reinterpret_cast<f16_bits_t const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
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.search( //
257
- reinterpret_cast<float const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
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.search( //
260
- reinterpret_cast<double const*>(vector_data), wanted, &labels_py1d(0), &distances_py1d(0), config);
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(native_index_t& index, py::buffer vectors, std::size_t wanted, bool exact) {
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 == "e")
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), config));
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), config));
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), config));
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(hash_index_w_meta_t& index, py::buffer_info const& vector_info) noexcept {
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(hash_index_w_meta_t& index, py::buffer vector) {
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_<native_index_t>(m, "Index");
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") = config_t::connectivity_default_k, //
430
- py::arg("expansion_add") = config_t::expansion_add_default_k, //
431
- py::arg("expansion_search") = config_t::expansion_search_default_k, //
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__", &native_index_t::size);
460
- i.def_property_readonly("size", &native_index_t::size);
461
- i.def_property_readonly("ndim", &native_index_t::dimensions);
462
- i.def_property_readonly("connectivity", &native_index_t::connectivity);
463
- i.def_property_readonly("capacity", &native_index_t::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", [](native_index_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
466
-
467
- i.def("save", &save_index<native_index_t>, py::arg("path"));
468
- i.def("load", &load_index<native_index_t>, py::arg("path"));
469
- i.def("view", &view_index<native_index_t>, py::arg("path"));
470
- i.def("clear", &clear_index<native_index_t>);
471
-
472
- auto si = py::class_<sets_index_t>(m, "SetsIndex");
473
-
474
- si.def( //
475
- py::init(&make_sets_index), //
476
- py::kw_only(), //
477
- py::arg("capacity") = 0, //
478
- py::arg("connectivity") = config_t::connectivity_default_k, //
479
- py::arg("expansion_add") = config_t::expansion_add_default_k, //
480
- py::arg("expansion_search") = config_t::expansion_search_default_k //
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
- [](sets_index_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
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
- [](sets_index_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
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, &labels_proxy(0), nullptr);
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__", &sets_index_t::size);
518
- si.def_property_readonly("size", &sets_index_t::size);
519
- si.def_property_readonly("connectivity", &sets_index_t::connectivity);
520
- si.def_property_readonly("capacity", &sets_index_t::capacity);
521
-
522
- si.def("save", &save_index<sets_index_t>, py::arg("path"));
523
- si.def("load", &load_index<sets_index_t>, py::arg("path"));
524
- si.def("view", &view_index<sets_index_t>, py::arg("path"));
525
- si.def("clear", &clear_index<sets_index_t>);
526
-
527
- auto hi = py::class_<hash_index_w_meta_t>(m, "HashIndex");
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") = config_t::connectivity_default_k, //
535
- py::arg("expansion_add") = config_t::expansion_add_default_k, //
536
- py::arg("expansion_search") = config_t::expansion_search_default_k //
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
- [](hash_index_w_meta_t& index, label_t label, py::buffer array) {
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
- [](hash_index_w_meta_t& index, py::buffer array, std::size_t count) -> py::array_t<label_t> {
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, &labels_proxy(0), nullptr);
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__", &hash_index_w_meta_t::size);
568
- hi.def_property_readonly("size", &hash_index_w_meta_t::size);
569
- hi.def_property_readonly("connectivity", &hash_index_w_meta_t::connectivity);
570
- hi.def_property_readonly("capacity", &hash_index_w_meta_t::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<hash_index_w_meta_t>, py::arg("path"));
573
- hi.def("load", &load_index<hash_index_w_meta_t>, py::arg("path"));
574
- hi.def("view", &view_index<hash_index_w_meta_t>, py::arg("path"));
575
- hi.def("clear", &clear_index<hash_index_w_meta_t>);
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)