usearch 0.14.0 → 0.15.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 (64) hide show
  1. package/.github/workflows/release.yml +56 -4
  2. package/.github/workflows/update_version.sh +6 -1
  3. package/.vscode/launch.json +21 -22
  4. package/.vscode/settings.json +7 -1
  5. package/CITATION.cff +4 -4
  6. package/CMakeLists.txt +1 -1
  7. package/Cargo.toml +1 -1
  8. package/Package.swift +0 -1
  9. package/README.md +100 -27
  10. package/binding.gyp +0 -1
  11. package/build.gradle +1 -1
  12. package/build.rs +3 -3
  13. package/c/lib.cpp +32 -9
  14. package/c/usearch.h +67 -36
  15. package/conanfile.py +1 -1
  16. package/{src → cpp}/bench.cpp +52 -36
  17. package/{src → cpp}/test.cpp +17 -7
  18. package/docs/benchmarks.md +33 -10
  19. package/docs/c/index.rst +11 -0
  20. package/docs/c/reference.rst +7 -0
  21. package/docs/compilation.md +24 -9
  22. package/docs/conf.dox +6 -6
  23. package/docs/conf.py +3 -1
  24. package/docs/cpp/index.rst +11 -0
  25. package/docs/cpp/reference.rst +7 -0
  26. package/docs/index.rst +10 -0
  27. package/docs/java/index.rst +11 -0
  28. package/docs/java/reference.rst +7 -0
  29. package/docs/javascript/index.rst +11 -0
  30. package/docs/javascript/reference.rst +9 -0
  31. package/docs/python/index.rst +11 -0
  32. package/docs/python/reference.rst +35 -0
  33. package/golang/lib.go +1 -1
  34. package/include/usearch/{usearch.hpp → index.hpp} +152 -148
  35. package/include/usearch/index_punned_dense.hpp +633 -0
  36. package/include/usearch/index_punned_helpers.hpp +528 -0
  37. package/java/cloud/unum/usearch/cloud_unum_usearch_Index.cpp +8 -7
  38. package/javascript/lib.cpp +26 -50
  39. package/javascript/usearch.js +117 -2
  40. package/objc/USearchObjective.mm +145 -49
  41. package/objc/include/USearchObjective.h +106 -30
  42. package/package.json +2 -2
  43. package/python/lib.cpp +134 -331
  44. package/python/scripts/bench.ipynb +170 -0
  45. package/python/scripts/bench.py +87 -175
  46. package/python/scripts/index_faiss.py +100 -0
  47. package/python/scripts/test.py +41 -37
  48. package/python/usearch/eval.py +353 -0
  49. package/python/usearch/index.py +117 -113
  50. package/python/usearch/io.py +7 -2
  51. package/python/usearch/join.py +100 -0
  52. package/python/usearch/numba.py +101 -0
  53. package/rust/lib.cpp +27 -14
  54. package/rust/lib.hpp +4 -10
  55. package/rust/lib.rs +50 -17
  56. package/setup.py +12 -2
  57. package/simsimd/include/simsimd/simsimd.h +32 -3
  58. package/swift/Index+Sugar.swift +55 -29
  59. package/swift/Test.swift +1 -1
  60. package/wolfram/lib.cpp +4 -4
  61. package/python/scripts/bench_params.py +0 -201
  62. package/python/usearch/synthetic.py +0 -31
  63. package/src/punned.hpp +0 -906
  64. /package/{src → cpp}/CMakeLists.txt +0 -0
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 "punned.hpp"
21
+ #include <usearch/index_punned_dense.hpp>
22
22
 
23
23
  using namespace unum::usearch;
24
24
  using namespace unum;
@@ -28,9 +28,11 @@ using py_shape_t = py::array::ShapeContainer;
28
28
 
29
29
  using label_t = Py_ssize_t;
30
30
  using distance_t = punned_distance_t;
31
+ using metric_t = punned_metric_t;
31
32
  using id_t = std::uint32_t;
33
+ using big_id_t = std::uint64_t;
32
34
 
33
- using punned_index_t = punned_gt<label_t, id_t>;
35
+ using punned_index_t = index_punned_dense_gt<label_t, id_t>;
34
36
  using punned_add_result_t = typename punned_index_t::add_result_t;
35
37
  using punned_search_result_t = typename punned_index_t::search_result_t;
36
38
 
@@ -47,89 +49,41 @@ struct punned_index_py_t : public punned_index_t {
47
49
 
48
50
  using set_member_t = std::uint32_t;
49
51
  using set_view_t = span_gt<set_member_t const>;
50
- using sets_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
52
+ using sparse_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
51
53
 
52
- struct sets_index_py_t : public sets_index_t {
53
- using native_t = sets_index_t;
54
+ struct sparse_index_py_t : public sparse_index_t {
55
+ using native_t = sparse_index_t;
54
56
  using native_t::add;
55
57
  using native_t::capacity;
56
58
  using native_t::reserve;
57
59
  using native_t::search;
58
60
  using native_t::size;
59
61
 
60
- sets_index_py_t(native_t&& base) : native_t(std::move(base)) {}
62
+ sparse_index_py_t(native_t&& base) : native_t(std::move(base)) {}
61
63
  };
62
64
 
63
- using bits_numpy_word_t = std::uint8_t;
64
- using bits_native_word_t = std::uint64_t;
65
- using bits_distance_t = float;
66
- using bits_metric_t =
67
- std::function<bits_distance_t(bits_native_word_t const*, bits_native_word_t const*, std::size_t, std::size_t)>;
68
- using bits_index_t = index_gt<bits_metric_t, label_t, id_t, bits_native_word_t>;
69
- using bits_add_result_t = typename bits_index_t::add_result_t;
70
- using bits_search_result_t = typename bits_index_t::search_result_t;
71
-
72
- static constexpr std::size_t bits_per_hash_numpy_word_k = sizeof(bits_numpy_word_t) * CHAR_BIT;
73
- static constexpr std::size_t bits_per_hash_native_word_k = sizeof(bits_native_word_t) * CHAR_BIT;
74
-
75
- struct bits_index_py_t : public bits_index_t {
76
- using native_t = bits_index_t;
77
- using native_t::add;
78
- using native_t::capacity;
79
- using native_t::limits;
80
- using native_t::reserve;
81
- using native_t::search;
82
- using native_t::size;
83
-
84
- std::size_t bits_;
85
- std::size_t numpy_words_;
86
- std::size_t native_words_;
87
- std::vector<bits_native_word_t> word_aligned_buffer_;
88
-
89
- bits_index_py_t(native_t&& base) : native_t(std::move(base)) {}
90
-
91
- bits_index_py_t(index_config_t config, std::size_t bits, bits_metric_t metric)
92
- : bits_index_t(config, metric), bits_(bits), //
93
- numpy_words_(divide_round_up<bits_per_hash_numpy_word_k>(bits)), //
94
- native_words_(divide_round_up<bits_per_hash_native_word_k>(bits)), //
95
- word_aligned_buffer_(native_words_* index_limits_t{}.threads()) {}
96
- };
97
-
98
- bits_metric_t bits_metric(common_metric_kind_t kind) {
99
- switch (kind) {
100
- case common_metric_kind_t::bitwise_hamming_k:
101
- return bits_metric_t(bitwise_hamming_gt<bits_native_word_t, bits_distance_t>{});
102
- case common_metric_kind_t::bitwise_tanimoto_k:
103
- return bits_metric_t(bitwise_tanimoto_gt<bits_native_word_t, bits_distance_t>{});
104
- case common_metric_kind_t::bitwise_sorensen_k:
105
- return bits_metric_t(bitwise_sorensen_gt<bits_native_word_t, bits_distance_t>{});
106
- default: throw std::invalid_argument("Not a bitwise metric!"); return {};
107
- }
108
- }
109
-
110
- template <typename scalar_at> punned_stateful_metric_t udf(std::size_t metric_uintptr) {
111
- return [metric_uintptr](byte_t const* a_bytes, byte_t const* b_bytes, std::size_t n, std::size_t m) -> distance_t {
112
- using metric_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, std::size_t, std::size_t);
113
- metric_t metric_ptr = reinterpret_cast<metric_t>(metric_uintptr);
114
- scalar_at const* a = reinterpret_cast<scalar_at const*>(a_bytes);
115
- scalar_at const* b = reinterpret_cast<scalar_at const*>(b_bytes);
116
- return metric_ptr(a, b, n / sizeof(scalar_at), m / sizeof(scalar_at));
65
+ template <typename scalar_at> metric_t udf(std::size_t metric_uintptr) {
66
+ return [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
67
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*);
68
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
69
+ return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data());
117
70
  };
118
71
  }
119
72
 
120
- punned_stateful_metric_t udf(std::size_t metric_uintptr, accuracy_t accuracy) {
73
+ metric_t udf(std::size_t metric_uintptr, scalar_kind_t accuracy) {
121
74
  switch (accuracy) {
122
- case accuracy_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
123
- case accuracy_t::f16_k: return udf<f16_bits_t>(metric_uintptr);
124
- case accuracy_t::f32_k: return udf<f32_t>(metric_uintptr);
125
- case accuracy_t::f64_k: return udf<f64_t>(metric_uintptr);
75
+ case scalar_kind_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
76
+ case scalar_kind_t::f16_k: return udf<f16_t>(metric_uintptr);
77
+ case scalar_kind_t::f32_k: return udf<f32_t>(metric_uintptr);
78
+ case scalar_kind_t::f64_k: return udf<f64_t>(metric_uintptr);
79
+ default: return {};
126
80
  }
127
81
  }
128
82
 
129
83
  static punned_index_py_t make_index( //
130
84
  std::size_t dimensions, //
131
- std::string const& scalar_type, //
132
- common_metric_kind_t metric, //
85
+ scalar_kind_t scalar_kind, //
86
+ metric_kind_t metric_kind, //
133
87
  std::size_t connectivity, //
134
88
  std::size_t expansion_add, //
135
89
  std::size_t expansion_search, //
@@ -137,45 +91,27 @@ static punned_index_py_t make_index( //
137
91
  bool tune) {
138
92
 
139
93
  index_config_t config;
140
- config.expansion_add = expansion_add;
141
- config.expansion_search = expansion_search;
142
94
  config.connectivity = connectivity;
143
95
 
144
96
  if (tune)
145
97
  config = punned_index_t::optimize(config);
146
98
 
147
- accuracy_t accuracy = accuracy_from_name(scalar_type.c_str(), scalar_type.size());
148
99
  if (metric_uintptr)
149
- return punned_index_t::udf(dimensions, udf(metric_uintptr, accuracy), accuracy, config);
100
+ return punned_index_t::make( //
101
+ dimensions, udf(metric_uintptr, scalar_kind), config, scalar_kind, expansion_add, expansion_search);
150
102
  else
151
- return punned_index_t(make_punned<punned_index_t>(metric, dimensions, accuracy, config));
103
+ return punned_index_t::make(dimensions, metric_kind, config, scalar_kind, expansion_add, expansion_search);
152
104
  }
153
105
 
154
- static std::unique_ptr<sets_index_py_t> make_sets_index( //
155
- std::size_t connectivity, //
156
- std::size_t expansion_add, //
157
- std::size_t expansion_search //
106
+ static std::unique_ptr<sparse_index_py_t> make_sparse_index( //
107
+ std::size_t connectivity, //
108
+ std::size_t expansion_add, //
109
+ std::size_t expansion_search //
158
110
  ) {
159
111
  index_config_t config;
160
- config.expansion_add = expansion_add;
161
- config.expansion_search = expansion_search;
162
112
  config.connectivity = connectivity;
163
113
 
164
- return std::unique_ptr<sets_index_py_t>(new sets_index_py_t(sets_index_t(config)));
165
- }
166
-
167
- static std::unique_ptr<bits_index_py_t> make_hash_index( //
168
- std::size_t bits, //
169
- common_metric_kind_t metric, //
170
- std::size_t connectivity, //
171
- std::size_t expansion_add, //
172
- std::size_t expansion_search //
173
- ) {
174
- index_config_t config;
175
- config.expansion_add = expansion_add;
176
- config.expansion_search = expansion_search;
177
- config.connectivity = connectivity;
178
- return std::unique_ptr<bits_index_py_t>(new bits_index_py_t(config, bits, bits_metric(metric)));
114
+ return std::unique_ptr<sparse_index_py_t>(new sparse_index_py_t(sparse_index_t(config)));
179
115
  }
180
116
 
181
117
  static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
@@ -186,7 +122,7 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
186
122
 
187
123
  Py_ssize_t vector_dimensions = vector_info.shape[0];
188
124
  char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
189
- if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
125
+ if (vector_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
190
126
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
191
127
 
192
128
  if (index.size() + 1 >= index.capacity())
@@ -196,16 +132,18 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
196
132
  config.store_vector = copy;
197
133
 
198
134
  // https://docs.python.org/3/library/struct.html#format-characters
199
- if (vector_info.format == "c" || vector_info.format == "b")
135
+ if (vector_info.format == "B")
136
+ index.add(label, reinterpret_cast<b1x8_t const*>(vector_data), config).error.raise();
137
+ else if (vector_info.format == "c" || vector_info.format == "b")
200
138
  index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
201
139
  else if (vector_info.format == "e")
202
- index.add(label, reinterpret_cast<f16_bits_t const*>(vector_data), config).error.raise();
140
+ index.add(label, reinterpret_cast<f16_t const*>(vector_data), config).error.raise();
203
141
  else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
204
142
  index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
205
143
  else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
206
144
  index.add(label, reinterpret_cast<double const*>(vector_data), config).error.raise();
207
145
  else
208
- throw std::invalid_argument("Incompatible scalars in the vector!");
146
+ throw std::invalid_argument("Incompatible scalars in the vector: " + vector_info.format);
209
147
  }
210
148
 
211
149
  static void add_many_to_index( //
@@ -230,7 +168,7 @@ static void add_many_to_index( //
230
168
  Py_ssize_t labels_count = labels_info.shape[0];
231
169
  Py_ssize_t vectors_count = vectors_info.shape[0];
232
170
  Py_ssize_t vectors_dimensions = vectors_info.shape[1];
233
- if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
171
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
234
172
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
235
173
 
236
174
  if (labels_count != vectors_count)
@@ -243,8 +181,17 @@ static void add_many_to_index( //
243
181
  char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
244
182
 
245
183
  // https://docs.python.org/3/library/struct.html#format-characters
246
- if (vectors_info.format == "c" || vectors_info.format == "b")
247
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
184
+ if (vectors_info.format == "B")
185
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
186
+ add_config_t config;
187
+ config.store_vector = copy;
188
+ config.thread = thread_idx;
189
+ label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
190
+ b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
191
+ index.add(label, vector, config).error.raise();
192
+ });
193
+ else if (vectors_info.format == "c" || vectors_info.format == "b")
194
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
248
195
  add_config_t config;
249
196
  config.store_vector = copy;
250
197
  config.thread = thread_idx;
@@ -254,17 +201,16 @@ static void add_many_to_index( //
254
201
  index.add(label, vector, config).error.raise();
255
202
  });
256
203
  else if (vectors_info.format == "e")
257
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
204
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
258
205
  add_config_t config;
259
206
  config.store_vector = copy;
260
207
  config.thread = thread_idx;
261
208
  label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
262
- f16_bits_t const* vector =
263
- reinterpret_cast<f16_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
209
+ f16_t const* vector = reinterpret_cast<f16_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
264
210
  index.add(label, vector, config).error.raise();
265
211
  });
266
212
  else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
267
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
213
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
268
214
  add_config_t config;
269
215
  config.store_vector = copy;
270
216
  config.thread = thread_idx;
@@ -273,7 +219,7 @@ static void add_many_to_index( //
273
219
  index.add(label, vector, config).error.raise();
274
220
  });
275
221
  else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
276
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
222
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
277
223
  add_config_t config;
278
224
  config.store_vector = copy;
279
225
  config.thread = thread_idx;
@@ -282,58 +228,7 @@ static void add_many_to_index( //
282
228
  index.add(label, vector, config).error.raise();
283
229
  });
284
230
  else
285
- throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
286
- }
287
-
288
- static void add_many_to_bits_index( //
289
- bits_index_py_t& index, py::buffer labels, py::buffer vectors, //
290
- bool copy, std::size_t threads) {
291
-
292
- if (index.limits().threads_add < threads)
293
- throw std::invalid_argument("Can't use that many threads!");
294
-
295
- py::buffer_info labels_info = labels.request();
296
- py::buffer_info vectors_info = vectors.request();
297
-
298
- if (labels_info.format != py::format_descriptor<label_t>::format())
299
- throw std::invalid_argument("Incompatible label type!");
300
-
301
- if (labels_info.ndim != 1)
302
- throw std::invalid_argument("Labels must be placed in a single-dimensional array!");
303
-
304
- if (vectors_info.ndim != 2)
305
- throw std::invalid_argument("Expects a matrix of vectors to add!");
306
-
307
- Py_ssize_t labels_count = labels_info.shape[0];
308
- Py_ssize_t vectors_count = vectors_info.shape[0];
309
- Py_ssize_t vectors_dimensions = vectors_info.shape[1];
310
- if (vectors_dimensions != static_cast<Py_ssize_t>(index.numpy_words_))
311
- throw std::invalid_argument("The number of vector dimensions doesn't match!");
312
-
313
- if (labels_count != vectors_count)
314
- throw std::invalid_argument("Number of labels and vectors must match!");
315
-
316
- if (index.size() + vectors_count >= index.capacity())
317
- index.reserve(ceil2(index.size() + vectors_count));
318
-
319
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
320
- char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
321
-
322
- // https://docs.python.org/3/library/struct.html#format-characters
323
- if (vectors_info.format != "B")
324
- throw std::invalid_argument("Incompatible scalars in the vectors matrix!");
325
-
326
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
327
- add_config_t config;
328
- config.store_vector = copy;
329
- config.thread = thread_idx;
330
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
331
- bits_numpy_word_t const* vector =
332
- reinterpret_cast<bits_numpy_word_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
333
- bits_native_word_t* vector_words = index.word_aligned_buffer_.data() + index.native_words_ * thread_idx;
334
- std::memcpy(vector_words, vector, vectors_dimensions);
335
- index.add(label, {vector_words, index.native_words_}, config).error.raise();
336
- });
231
+ throw std::invalid_argument("Incompatible scalars in the vectors matrix: " + vectors_info.format);
337
232
  }
338
233
 
339
234
  static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
@@ -341,7 +236,7 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
341
236
  py::buffer_info vector_info = vector.request();
342
237
  Py_ssize_t vector_dimensions = vector_info.shape[0];
343
238
  char const* vector_data = reinterpret_cast<char const*>(vector_info.ptr);
344
- if (vector_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
239
+ if (vector_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
345
240
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
346
241
 
347
242
  py::array_t<label_t> labels_py(static_cast<Py_ssize_t>(wanted));
@@ -354,12 +249,16 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
354
249
  config.exact = exact;
355
250
 
356
251
  // https://docs.python.org/3/library/struct.html#format-characters
357
- if (vector_info.format == "c" || vector_info.format == "b") {
252
+ if (vector_info.format == "B") {
253
+ punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
254
+ result.error.raise();
255
+ count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
256
+ } else if (vector_info.format == "c" || vector_info.format == "b") {
358
257
  punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
359
258
  result.error.raise();
360
259
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
361
260
  } else if (vector_info.format == "e") {
362
- punned_search_result_t result = index.search(reinterpret_cast<f16_bits_t const*>(vector_data), wanted, config);
261
+ punned_search_result_t result = index.search(reinterpret_cast<f16_t const*>(vector_data), wanted, config);
363
262
  result.error.raise();
364
263
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
365
264
  } else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4") {
@@ -371,7 +270,7 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
371
270
  result.error.raise();
372
271
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
373
272
  } else
374
- throw std::invalid_argument("Incompatible scalars in the query vector!");
273
+ throw std::invalid_argument("Incompatible scalars in the query vector: " + vector_info.format);
375
274
 
376
275
  labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
377
276
  distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
@@ -410,7 +309,7 @@ static py::tuple search_many_in_index( //
410
309
  Py_ssize_t vectors_count = vectors_info.shape[0];
411
310
  Py_ssize_t vectors_dimensions = vectors_info.shape[1];
412
311
  char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
413
- if (vectors_dimensions != static_cast<Py_ssize_t>(index.dimensions()))
312
+ if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
414
313
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
415
314
 
416
315
  py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
@@ -421,8 +320,19 @@ static py::tuple search_many_in_index( //
421
320
  auto counts_py1d = counts_py.template mutable_unchecked<1>();
422
321
 
423
322
  // https://docs.python.org/3/library/struct.html#format-characters
424
- if (vectors_info.format == "c" || vectors_info.format == "b")
425
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
323
+ if (vectors_info.format == "B")
324
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
325
+ search_config_t config;
326
+ config.thread = thread_idx;
327
+ config.exact = exact;
328
+ b1x8_t const* vector = (b1x8_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
329
+ punned_search_result_t result = index.search(vector, wanted, config);
330
+ result.error.raise();
331
+ counts_py1d(task_idx) =
332
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
333
+ });
334
+ else if (vectors_info.format == "c" || vectors_info.format == "b")
335
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
426
336
  search_config_t config;
427
337
  config.thread = thread_idx;
428
338
  config.exact = exact;
@@ -433,18 +343,18 @@ static py::tuple search_many_in_index( //
433
343
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
434
344
  });
435
345
  else if (vectors_info.format == "e")
436
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
346
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
437
347
  search_config_t config;
438
348
  config.thread = thread_idx;
439
349
  config.exact = exact;
440
- f16_bits_t const* vector = (f16_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
350
+ f16_t const* vector = (f16_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
441
351
  punned_search_result_t result = index.search(vector, wanted, config);
442
352
  result.error.raise();
443
353
  counts_py1d(task_idx) =
444
354
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
445
355
  });
446
356
  else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
447
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
357
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
448
358
  search_config_t config;
449
359
  config.thread = thread_idx;
450
360
  config.exact = exact;
@@ -455,7 +365,7 @@ static py::tuple search_many_in_index( //
455
365
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
456
366
  });
457
367
  else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
458
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
368
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
459
369
  search_config_t config;
460
370
  config.thread = thread_idx;
461
371
  config.exact = exact;
@@ -466,71 +376,7 @@ static py::tuple search_many_in_index( //
466
376
  static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
467
377
  });
468
378
  else
469
- throw std::invalid_argument("Incompatible scalars in the query matrix!");
470
-
471
- py::tuple results(3);
472
- results[0] = labels_py;
473
- results[1] = distances_py;
474
- results[2] = counts_py;
475
- return results;
476
- }
477
-
478
- /**
479
- * @param vectors Matrix of vectors to search for.
480
- * @param wanted Number of matches per request.
481
- *
482
- * @return Tuple with:
483
- * 1. matrix of neighbors,
484
- * 2. matrix of distances,
485
- * 3. array with match counts.
486
- */
487
- static py::tuple search_many_in_bits_index( //
488
- bits_index_py_t& index, py::buffer vectors, std::size_t wanted, bool exact, std::size_t threads) {
489
-
490
- if (wanted == 0)
491
- return py::tuple(3);
492
-
493
- if (index.limits().threads_add < threads)
494
- throw std::invalid_argument("Can't use that many threads!");
495
-
496
- py::buffer_info vectors_info = vectors.request();
497
- // if (vectors_info.ndim == 1)
498
- // return search_one_in_index(index, vectors, wanted, exact);
499
- if (vectors_info.ndim != 2)
500
- throw std::invalid_argument("Expects a matrix of vectors to add!");
501
-
502
- Py_ssize_t vectors_count = vectors_info.shape[0];
503
- Py_ssize_t vectors_dimensions = vectors_info.shape[1];
504
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
505
- if (vectors_dimensions != static_cast<Py_ssize_t>(index.numpy_words_))
506
- throw std::invalid_argument("The number of vector dimensions doesn't match!");
507
-
508
- py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
509
- py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
510
- py::array_t<Py_ssize_t> counts_py(vectors_count);
511
- auto labels_py2d = labels_py.template mutable_unchecked<2>();
512
- auto distances_py2d = distances_py.template mutable_unchecked<2>();
513
- auto counts_py1d = counts_py.template mutable_unchecked<1>();
514
-
515
- // https://docs.python.org/3/library/struct.html#format-characters
516
- if (vectors_info.format != "B")
517
- throw std::invalid_argument("Incompatible scalars in the query matrix!");
518
-
519
- multithreaded(threads, vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
520
- search_config_t config;
521
- config.thread = thread_idx;
522
- config.exact = exact;
523
-
524
- bits_numpy_word_t const* vector =
525
- reinterpret_cast<bits_numpy_word_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
526
- bits_native_word_t* vector_words = index.word_aligned_buffer_.data() + index.native_words_ * thread_idx;
527
- std::memcpy(vector_words, vector, vectors_dimensions);
528
-
529
- bits_search_result_t result = index.search({vector_words, index.native_words_}, wanted, config);
530
- result.error.raise();
531
- counts_py1d(task_idx) =
532
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
533
- });
379
+ throw std::invalid_argument("Incompatible scalars in the query matrix: " + vectors_info.format);
534
380
 
535
381
  py::tuple results(3);
536
382
  results[0] = labels_py;
@@ -543,29 +389,27 @@ static py::tuple search_many_in_bits_index( //
543
389
  template <typename index_at> void save_index(index_at const& index, std::string const& path) { index.save(path.c_str()).error.raise(); }
544
390
  template <typename index_at> void load_index(index_at& index, std::string const& path) { index.load(path.c_str()).error.raise(); }
545
391
  template <typename index_at> void view_index(index_at& index, std::string const& path) { index.view(path.c_str()).error.raise(); }
546
- template <typename index_at> void clear_index(index_at& index) { index.clear(); }
547
- template <typename index_at> std::size_t get_expansion_add(index_at const &index) { return index.config().expansion_add; }
548
- template <typename index_at> std::size_t get_expansion_search(index_at const &index) { return index.config().expansion_search; }
549
392
  // clang-format on
550
393
 
551
- template <typename scalar_at, typename index_at>
552
- py::array_t<scalar_at> get_typed_member(index_at const& index, label_t label) {
553
- py::array_t<scalar_at> result_py(static_cast<Py_ssize_t>(index.dimensions()));
394
+ template <typename internal_at, typename external_at = internal_at, typename index_at = void>
395
+ py::array_t<external_at> get_typed_member(index_at const& index, label_t label) {
396
+ py::array_t<external_at> result_py(static_cast<Py_ssize_t>(index.scalar_words()));
554
397
  auto result_py1d = result_py.template mutable_unchecked<1>();
555
- index.reconstruct(label, &result_py1d(0));
398
+ index.reconstruct(label, (internal_at*)&result_py1d(0));
556
399
  return result_py;
557
400
  }
558
401
 
559
- template <typename index_at>
560
- py::array get_member(index_at const& index, label_t label, common_scalar_kind_t scalar_kind) {
561
- if (scalar_kind == common_scalar_kind_t::f32_k)
402
+ template <typename index_at> py::array get_member(index_at const& index, label_t label, scalar_kind_t scalar_kind) {
403
+ if (scalar_kind == scalar_kind_t::f32_k)
562
404
  return get_typed_member<f32_t>(index, label);
563
- else if (scalar_kind == common_scalar_kind_t::f64_k)
405
+ else if (scalar_kind == scalar_kind_t::f64_k)
564
406
  return get_typed_member<f64_t>(index, label);
565
- else if (scalar_kind == common_scalar_kind_t::f16_k)
566
- return get_typed_member<f16_bits_t>(index, label);
567
- else if (scalar_kind == common_scalar_kind_t::f8_k)
568
- return get_typed_member<f8_bits_t>(index, label);
407
+ else if (scalar_kind == scalar_kind_t::f16_k)
408
+ return get_typed_member<f16_t>(index, label);
409
+ else if (scalar_kind == scalar_kind_t::f8_k)
410
+ return get_typed_member<f8_bits_t, std::int8_t>(index, label);
411
+ else if (scalar_kind == scalar_kind_t::b1x8_k)
412
+ return get_typed_member<b1x8_t, std::uint8_t>(index, label);
569
413
  else
570
414
  throw std::invalid_argument("Incompatible scalars in the query matrix!");
571
415
  }
@@ -603,10 +447,11 @@ void validate_set(py::array_t<set_member_t> const& set) {
603
447
  if (has_duplicates(begin, end))
604
448
  throw std::invalid_argument("Set must be deduplicated!");
605
449
  }
450
+ #if 0
606
451
 
607
- inline bits_native_word_t hash_ror64(bits_native_word_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
452
+ inline std::size_t hash_ror64(std::size_t v, int r) noexcept { return (v >> r) | (v << (64 - r)); }
608
453
 
609
- inline bits_native_word_t hash(bits_native_word_t v) noexcept {
454
+ inline std::size_t hash(std::size_t v) noexcept {
610
455
  v ^= hash_ror64(v, 25) ^ hash_ror64(v, 50);
611
456
  v *= 0xA24BAED4963EE407UL;
612
457
  v ^= hash_ror64(v, 24) ^ hash_ror64(v, 49);
@@ -617,18 +462,17 @@ inline bits_native_word_t hash(bits_native_word_t v) noexcept {
617
462
  template <typename scalar_at>
618
463
  void hash_typed_row( //
619
464
  byte_t const* scalars, std::size_t scalar_stride, std::size_t scalars_count, //
620
- bits_numpy_word_t* hashes, std::size_t bits) noexcept {
465
+ b1x8_t* hashes, std::size_t bits) noexcept {
621
466
 
622
467
  std::size_t words = divide_round_up<8>(bits);
623
468
  for (std::size_t i = 0; i != scalars_count; ++i) {
624
469
  scalar_at scalar;
625
470
  std::memcpy(&scalar, scalars + i * scalar_stride, sizeof(scalar_at));
626
- bits_native_word_t scalar_hash = hash(scalar);
471
+ std::size_t scalar_hash = hash(scalar);
627
472
  hashes[scalar_hash & (words - 1)] |= std::uint8_t(1) << (scalar_hash % 8);
628
473
  }
629
474
  }
630
475
 
631
- #if 0
632
476
  template <typename scalar_at>
633
477
  py::array_t<bits_numpy_word_t> hash_typed_buffer(py::buffer_info const& info, std::size_t bits) {
634
478
  byte_t const* data = reinterpret_cast<byte_t const*>(info.ptr);
@@ -700,32 +544,32 @@ PYBIND11_MODULE(compiled, m) {
700
544
  m.attr("USES_SIMSIMD") = py::int_(0);
701
545
  #endif
702
546
 
703
- py::enum_<common_metric_kind_t>(m, "MetricKind")
704
- .value("Unknown", common_metric_kind_t::unknown_k)
705
- .value("IP", common_metric_kind_t::ip_k)
706
- .value("Cos", common_metric_kind_t::cos_k)
707
- .value("L2sq", common_metric_kind_t::l2sq_k)
708
- .value("Haversine", common_metric_kind_t::haversine_k)
709
- .value("Pearson", common_metric_kind_t::pearson_k)
710
- .value("Jaccard", common_metric_kind_t::jaccard_k)
711
- .value("BitwiseHamming", common_metric_kind_t::bitwise_hamming_k)
712
- .value("BitwiseTanimoto", common_metric_kind_t::bitwise_tanimoto_k)
713
- .value("BitwiseSorensen", common_metric_kind_t::bitwise_sorensen_k);
714
-
715
- py::enum_<common_scalar_kind_t>(m, "ScalarKind")
716
- .value("F64", common_scalar_kind_t::f64_k)
717
- .value("F32", common_scalar_kind_t::f32_k)
718
- .value("F16", common_scalar_kind_t::f16_k)
719
- .value("F8", common_scalar_kind_t::f8_k)
720
- .value("B1", common_scalar_kind_t::b1_k);
547
+ py::enum_<metric_kind_t>(m, "MetricKind")
548
+ .value("Unknown", metric_kind_t::unknown_k)
549
+ .value("IP", metric_kind_t::ip_k)
550
+ .value("Cos", metric_kind_t::cos_k)
551
+ .value("L2sq", metric_kind_t::l2sq_k)
552
+ .value("Haversine", metric_kind_t::haversine_k)
553
+ .value("Pearson", metric_kind_t::pearson_k)
554
+ .value("Jaccard", metric_kind_t::jaccard_k)
555
+ .value("Hamming", metric_kind_t::hamming_k)
556
+ .value("Tanimoto", metric_kind_t::tanimoto_k)
557
+ .value("Sorensen", metric_kind_t::sorensen_k);
558
+
559
+ py::enum_<scalar_kind_t>(m, "ScalarKind")
560
+ .value("F64", scalar_kind_t::f64_k)
561
+ .value("F32", scalar_kind_t::f32_k)
562
+ .value("F16", scalar_kind_t::f16_k)
563
+ .value("F8", scalar_kind_t::f8_k)
564
+ .value("B1", scalar_kind_t::b1x8_k);
721
565
 
722
566
  auto i = py::class_<punned_index_py_t>(m, "Index");
723
567
 
724
568
  i.def(py::init(&make_index), //
725
569
  py::kw_only(), //
726
570
  py::arg("ndim") = 0, //
727
- py::arg("dtype") = std::string("f32"), //
728
- py::arg("metric") = common_metric_kind_t::ip_k, //
571
+ py::arg("dtype") = scalar_kind_t::f32_k, //
572
+ py::arg("metric") = metric_kind_t::ip_k, //
729
573
  py::arg("connectivity") = default_connectivity(), //
730
574
  py::arg("expansion_add") = default_expansion_add(), //
731
575
  py::arg("expansion_search") = default_expansion_search(), //
@@ -765,69 +609,28 @@ PYBIND11_MODULE(compiled, m) {
765
609
  i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
766
610
  i.def_property_readonly("capacity", &punned_index_py_t::capacity);
767
611
  i.def_property_readonly( //
768
- "dtype", [](punned_index_py_t const& index) -> std::string { return accuracy_name(index.accuracy()); });
612
+ "dtype", [](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.scalar_kind()); });
769
613
  i.def_property_readonly( //
770
614
  "memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
771
615
 
772
- i.def_property("expansion_add", &get_expansion_add<punned_index_py_t>, &punned_index_py_t::change_expansion_add);
773
- i.def_property("expansion_search", &get_expansion_search<punned_index_py_t>,
616
+ i.def_property("expansion_add", &punned_index_py_t::expansion_add, &punned_index_py_t::change_expansion_add);
617
+ i.def_property("expansion_search", &punned_index_py_t::expansion_search,
774
618
  &punned_index_py_t::change_expansion_search);
775
619
 
776
620
  i.def_property_readonly("labels", &get_labels<punned_index_py_t>);
777
621
  i.def("__contains__", &punned_index_py_t::contains);
778
622
  i.def( //
779
- "__getitem__", &get_member<punned_index_py_t>, py::arg("label"),
780
- py::arg("dtype") = common_scalar_kind_t::f32_k);
623
+ "__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
781
624
 
782
625
  i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
783
626
  i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
784
627
  i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
785
- i.def("clear", &clear_index<punned_index_py_t>);
786
-
787
- auto bi = py::class_<bits_index_py_t>(m, "BitsIndex");
788
-
789
- bi.def( //
790
- py::init(&make_hash_index), //
791
- py::kw_only(), //
792
- py::arg("bits"), //
793
- py::arg("metric") = common_metric_kind_t::bitwise_hamming_k, //
794
- py::arg("connectivity") = default_connectivity(), //
795
- py::arg("expansion_add") = default_expansion_add(), //
796
- py::arg("expansion_search") = default_expansion_search() //
797
- );
798
-
799
- bi.def( //
800
- "add",
801
- &add_many_to_bits_index, //
802
- py::arg("label"), //
803
- py::arg("array"), //
804
- py::kw_only(), //
805
- py::arg("copy") = true, //
806
- py::arg("threads") = 0 //
807
- );
808
-
809
- bi.def( //
810
- "search", &search_many_in_bits_index, //
811
- py::arg("query"), //
812
- py::arg("count") = 10, //
813
- py::arg("exact") = false, //
814
- py::arg("threads") = 0 //
815
- );
816
-
817
- bi.def("__len__", &bits_index_py_t::size);
818
- bi.def_property_readonly("size", &bits_index_py_t::size);
819
- bi.def_property_readonly("connectivity", &bits_index_py_t::connectivity);
820
- bi.def_property_readonly("capacity", &bits_index_py_t::capacity);
821
-
822
- bi.def("save", &save_index<bits_index_py_t>, py::arg("path"));
823
- bi.def("load", &load_index<bits_index_py_t>, py::arg("path"));
824
- bi.def("view", &view_index<bits_index_py_t>, py::arg("path"));
825
- bi.def("clear", &clear_index<bits_index_py_t>);
628
+ i.def("clear", &punned_index_py_t::clear);
826
629
 
827
- auto si = py::class_<sets_index_py_t>(m, "SetsIndex");
630
+ auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
828
631
 
829
632
  si.def( //
830
- py::init(&make_sets_index), //
633
+ py::init(&make_sparse_index), //
831
634
  py::kw_only(), //
832
635
  py::arg("connectivity") = default_connectivity(), //
833
636
  py::arg("expansion_add") = default_expansion_add(), //
@@ -836,7 +639,7 @@ PYBIND11_MODULE(compiled, m) {
836
639
 
837
640
  si.def( //
838
641
  "add",
839
- [](sets_index_py_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
642
+ [](sparse_index_py_t& index, label_t label, py::array_t<set_member_t> set, bool copy) {
840
643
  validate_set(set);
841
644
  if (index.size() + 1 >= index.capacity())
842
645
  index.reserve(ceil2(index.size() + 1));
@@ -855,7 +658,7 @@ PYBIND11_MODULE(compiled, m) {
855
658
 
856
659
  si.def( //
857
660
  "search",
858
- [](sets_index_py_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
661
+ [](sparse_index_py_t& index, py::array_t<set_member_t> set, std::size_t count) -> py::array_t<label_t> {
859
662
  validate_set(set);
860
663
  auto proxy = set.unchecked<1>();
861
664
  auto view = set_view_t{&proxy(0), static_cast<std::size_t>(proxy.shape(0))};
@@ -871,13 +674,13 @@ PYBIND11_MODULE(compiled, m) {
871
674
  py::arg("count") = 10 //
872
675
  );
873
676
 
874
- si.def("__len__", &sets_index_py_t::size);
875
- si.def_property_readonly("size", &sets_index_py_t::size);
876
- si.def_property_readonly("connectivity", &sets_index_py_t::connectivity);
877
- si.def_property_readonly("capacity", &sets_index_py_t::capacity);
677
+ si.def("__len__", &sparse_index_py_t::size);
678
+ si.def_property_readonly("size", &sparse_index_py_t::size);
679
+ si.def_property_readonly("connectivity", &sparse_index_py_t::connectivity);
680
+ si.def_property_readonly("capacity", &sparse_index_py_t::capacity);
878
681
 
879
- si.def("save", &save_index<sets_index_py_t>, py::arg("path"));
880
- si.def("load", &load_index<sets_index_py_t>, py::arg("path"));
881
- si.def("view", &view_index<sets_index_py_t>, py::arg("path"));
882
- si.def("clear", &clear_index<sets_index_py_t>);
682
+ si.def("save", &sparse_index_py_t::save, py::arg("path"));
683
+ si.def("load", &sparse_index_py_t::load, py::arg("path"));
684
+ si.def("view", &sparse_index_py_t::view, py::arg("path"));
685
+ si.def("clear", &sparse_index_py_t::clear);
883
686
  }