usearch 0.18.8 → 0.19.1

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/python/lib.cpp CHANGED
@@ -13,8 +13,9 @@
13
13
  * @copyright Copyright (c) 2023
14
14
  */
15
15
  #define PY_SSIZE_T_CLEAN
16
- #define NOMINMAX // Some of our dependencies call `std::max(x, y)`, which crashes Windows builds
17
- #include <thread>
16
+ #define NOMINMAX // Some of our dependencies call `std::max(x, y)`, which crashes Windows builds
17
+ #include <limits> // `std::numeric_limits`
18
+ #include <thread> // `std::thread`
18
19
 
19
20
  #include <pybind11/numpy.h>
20
21
  #include <pybind11/pybind11.h>
@@ -24,12 +25,23 @@
24
25
  using namespace unum::usearch;
25
26
  using namespace unum;
26
27
 
28
+ /**
29
+ * @brief The signature of the user-defined function.
30
+ * Can be just two array pointers, precompiled for a specific array length,
31
+ * or include one or two array sizes as 64-bit unsigned integers.
32
+ */
33
+ enum class metric_signature_t {
34
+ array_array_k = 0,
35
+ array_array_size_k,
36
+ array_size_array_size_k,
37
+ };
38
+
27
39
  namespace py = pybind11;
28
40
  using py_shape_t = py::array::ShapeContainer;
29
41
 
30
42
  using label_t = std::uint32_t;
31
43
  using distance_t = punned_distance_t;
32
- using metric_t = punned_metric_t;
44
+ using metric_t = index_punned_dense_metric_t;
33
45
  using id_t = std::uint32_t;
34
46
  using big_id_t = std::uint64_t;
35
47
 
@@ -50,7 +62,7 @@ struct punned_index_py_t : public punned_index_t {
50
62
 
51
63
  using set_member_t = std::uint32_t;
52
64
  using set_view_t = span_gt<set_member_t const>;
53
- using sparse_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t, set_member_t>;
65
+ using sparse_index_t = index_gt<jaccard_gt<set_member_t>, label_t, id_t>;
54
66
 
55
67
  struct sparse_index_py_t : public sparse_index_t {
56
68
  using native_t = sparse_index_t;
@@ -63,32 +75,61 @@ struct sparse_index_py_t : public sparse_index_t {
63
75
  sparse_index_py_t(native_t&& base) : native_t(std::move(base)) {}
64
76
  };
65
77
 
66
- template <typename scalar_at> metric_t udf(std::size_t metric_uintptr) {
67
- return [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
68
- using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*);
69
- metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
70
- return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data());
71
- };
78
+ template <typename scalar_at>
79
+ metric_t typed_udf( //
80
+ metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
81
+ //
82
+ metric_t result;
83
+ result.kind_ = kind;
84
+ result.scalar_kind_ = accuracy;
85
+ switch (signature) {
86
+ case metric_signature_t::array_array_k:
87
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
88
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*);
89
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
90
+ return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data());
91
+ };
92
+ break;
93
+ case metric_signature_t::array_array_size_k:
94
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
95
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, scalar_at const*, size_t);
96
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
97
+ return metric_ptr((scalar_at const*)a.data(), (scalar_at const*)b.data(), a.size() / sizeof(scalar_at));
98
+ };
99
+ break;
100
+ case metric_signature_t::array_size_array_size_k:
101
+ result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
102
+ using metric_raw_t = punned_distance_t (*)(scalar_at const*, size_t, scalar_at const*, size_t);
103
+ metric_raw_t metric_ptr = reinterpret_cast<metric_raw_t>(metric_uintptr);
104
+ return metric_ptr( //
105
+ (scalar_at const*)a.data(), a.size() / sizeof(scalar_at), //
106
+ (scalar_at const*)b.data(), b.size() / sizeof(scalar_at));
107
+ };
108
+ break;
109
+ }
110
+ return result;
72
111
  }
73
112
 
74
- metric_t udf(std::size_t metric_uintptr, scalar_kind_t accuracy) {
113
+ metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
75
114
  switch (accuracy) {
76
- case scalar_kind_t::f8_k: return udf<f8_bits_t>(metric_uintptr);
77
- case scalar_kind_t::f16_k: return udf<f16_t>(metric_uintptr);
78
- case scalar_kind_t::f32_k: return udf<f32_t>(metric_uintptr);
79
- case scalar_kind_t::f64_k: return udf<f64_t>(metric_uintptr);
115
+ case scalar_kind_t::b1x8_k: return typed_udf<b1x8_t>(kind, signature, metric_uintptr, accuracy);
116
+ case scalar_kind_t::f8_k: return typed_udf<f8_bits_t>(kind, signature, metric_uintptr, accuracy);
117
+ case scalar_kind_t::f16_k: return typed_udf<f16_t>(kind, signature, metric_uintptr, accuracy);
118
+ case scalar_kind_t::f32_k: return typed_udf<f32_t>(kind, signature, metric_uintptr, accuracy);
119
+ case scalar_kind_t::f64_k: return typed_udf<f64_t>(kind, signature, metric_uintptr, accuracy);
80
120
  default: return {};
81
121
  }
82
122
  }
83
123
 
84
- static punned_index_py_t make_index( //
85
- std::size_t dimensions, //
86
- scalar_kind_t scalar_kind, //
87
- metric_kind_t metric_kind, //
88
- std::size_t connectivity, //
89
- std::size_t expansion_add, //
90
- std::size_t expansion_search, //
91
- std::size_t metric_uintptr, //
124
+ static punned_index_py_t make_index( //
125
+ std::size_t dimensions, //
126
+ scalar_kind_t scalar_kind, //
127
+ metric_kind_t metric_kind, //
128
+ std::size_t connectivity, //
129
+ std::size_t expansion_add, //
130
+ std::size_t expansion_search, //
131
+ metric_signature_t metric_signature, //
132
+ std::uintptr_t metric_uintptr, //
92
133
  bool tune) {
93
134
 
94
135
  index_config_t config;
@@ -99,7 +140,8 @@ static punned_index_py_t make_index( //
99
140
 
100
141
  if (metric_uintptr)
101
142
  return punned_index_t::make( //
102
- dimensions, udf(metric_uintptr, scalar_kind), config, scalar_kind, expansion_add, expansion_search);
143
+ dimensions, udf(metric_kind, metric_signature, metric_uintptr, scalar_kind), config, scalar_kind,
144
+ expansion_add, expansion_search);
103
145
  else
104
146
  return punned_index_t::make(dimensions, metric_kind, config, scalar_kind, expansion_add, expansion_search);
105
147
  }
@@ -115,6 +157,22 @@ static std::unique_ptr<sparse_index_py_t> make_sparse_index( //
115
157
  return std::unique_ptr<sparse_index_py_t>(new sparse_index_py_t(sparse_index_t(config)));
116
158
  }
117
159
 
160
+ scalar_kind_t numpy_string_to_kind(std::string const& name) {
161
+ // https://docs.python.org/3/library/struct.html#format-characters
162
+ if (name == "B" || name == "<B" || name == "u1" || name == "|u1")
163
+ return scalar_kind_t::b1x8_k;
164
+ else if (name == "b" || name == "<b" || name == "i1" || name == "|i1")
165
+ return scalar_kind_t::f8_k;
166
+ else if (name == "e" || name == "<e" || name == "f2" || name == "<f2")
167
+ return scalar_kind_t::f16_k;
168
+ else if (name == "f" || name == "<f" || name == "f4" || name == "<f4")
169
+ return scalar_kind_t::f32_k;
170
+ else if (name == "d" || name == "<d" || name == "f8" || name == "<f8")
171
+ return scalar_kind_t::f64_k;
172
+ else
173
+ return scalar_kind_t::unknown_k;
174
+ }
175
+
118
176
  static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
119
177
 
120
178
  py::buffer_info vector_info = vector.request();
@@ -132,19 +190,37 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
132
190
  add_config_t config;
133
191
  config.store_vector = copy;
134
192
 
135
- // https://docs.python.org/3/library/struct.html#format-characters
136
- if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1")
137
- index.add(label, reinterpret_cast<b1x8_t const*>(vector_data), config).error.raise();
138
- else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1")
139
- index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
140
- else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2")
141
- index.add(label, reinterpret_cast<f16_t const*>(vector_data), config).error.raise();
142
- else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
143
- index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
144
- else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
145
- index.add(label, reinterpret_cast<double const*>(vector_data), config).error.raise();
146
- else
193
+ switch (numpy_string_to_kind(vector_info.format)) {
194
+ case scalar_kind_t::b1x8_k: index.add(label, (b1x8_t const*)(vector_data), config).error.raise(); break;
195
+ case scalar_kind_t::f8_k: index.add(label, (f8_bits_t const*)(vector_data), config).error.raise(); break;
196
+ case scalar_kind_t::f16_k: index.add(label, (f16_t const*)(vector_data), config).error.raise(); break;
197
+ case scalar_kind_t::f32_k: index.add(label, (f32_t const*)(vector_data), config).error.raise(); break;
198
+ case scalar_kind_t::f64_k: index.add(label, (f64_t const*)(vector_data), config).error.raise(); break;
199
+ case scalar_kind_t::unknown_k:
147
200
  throw std::invalid_argument("Incompatible scalars in the vector: " + vector_info.format);
201
+ }
202
+ }
203
+
204
+ template <typename scalar_at>
205
+ static void add_typed_to_index( //
206
+ punned_index_py_t& index, //
207
+ py::buffer_info const& labels_info, py::buffer_info const& vectors_info, //
208
+ bool copy, std::size_t threads) {
209
+
210
+ Py_ssize_t vectors_count = vectors_info.shape[0];
211
+ char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
212
+ char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
213
+
214
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
215
+ add_config_t config;
216
+ config.store_vector = copy;
217
+ config.thread = thread_idx;
218
+ label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
219
+ scalar_at const* vector = reinterpret_cast<scalar_at const*>(vectors_data + task_idx * vectors_info.strides[0]);
220
+ index.add(label, vector, config).error.raise();
221
+ if (PyErr_CheckSignals() != 0)
222
+ throw py::error_already_set();
223
+ });
148
224
  }
149
225
 
150
226
  static void add_many_to_index( //
@@ -178,58 +254,15 @@ static void add_many_to_index( //
178
254
  if (index.size() + vectors_count >= index.capacity())
179
255
  index.reserve(ceil2(index.size() + vectors_count));
180
256
 
181
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
182
- char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
183
-
184
- // https://docs.python.org/3/library/struct.html#format-characters
185
- if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
186
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
187
- add_config_t config;
188
- config.store_vector = copy;
189
- config.thread = thread_idx;
190
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
191
- b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
192
- index.add(label, vector, config).error.raise();
193
- });
194
- else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
195
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
196
- add_config_t config;
197
- config.store_vector = copy;
198
- config.thread = thread_idx;
199
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
200
- f8_bits_t const* vector =
201
- reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
202
- index.add(label, vector, config).error.raise();
203
- });
204
- else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
205
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
206
- add_config_t config;
207
- config.store_vector = copy;
208
- config.thread = thread_idx;
209
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
210
- f16_t const* vector = reinterpret_cast<f16_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
211
- index.add(label, vector, config).error.raise();
212
- });
213
- else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
214
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
215
- add_config_t config;
216
- config.store_vector = copy;
217
- config.thread = thread_idx;
218
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
219
- float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
220
- index.add(label, vector, config).error.raise();
221
- });
222
- else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
223
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
224
- add_config_t config;
225
- config.store_vector = copy;
226
- config.thread = thread_idx;
227
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
228
- double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
229
- index.add(label, vector, config).error.raise();
230
- });
231
- else
257
+ switch (numpy_string_to_kind(vectors_info.format)) {
258
+ case scalar_kind_t::b1x8_k: add_typed_to_index<b1x8_t>(index, labels_info, vectors_info, copy, threads); break;
259
+ case scalar_kind_t::f8_k: add_typed_to_index<f8_bits_t>(index, labels_info, vectors_info, copy, threads); break;
260
+ case scalar_kind_t::f16_k: add_typed_to_index<f16_t>(index, labels_info, vectors_info, copy, threads); break;
261
+ case scalar_kind_t::f32_k: add_typed_to_index<f32_t>(index, labels_info, vectors_info, copy, threads); break;
262
+ case scalar_kind_t::f64_k: add_typed_to_index<f64_t>(index, labels_info, vectors_info, copy, threads); break;
263
+ case scalar_kind_t::unknown_k:
232
264
  throw std::invalid_argument("Incompatible scalars in the vectors matrix: " + vectors_info.format);
265
+ }
233
266
  }
234
267
 
235
268
  static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
@@ -249,29 +282,20 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
249
282
  search_config_t config;
250
283
  config.exact = exact;
251
284
 
252
- // https://docs.python.org/3/library/struct.html#format-characters
253
- if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1") {
254
- punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
255
- result.error.raise();
256
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
257
- } else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1") {
258
- punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
259
- result.error.raise();
260
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
261
- } else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2") {
262
- punned_search_result_t result = index.search(reinterpret_cast<f16_t const*>(vector_data), wanted, config);
285
+ auto raise_and_dump = [&](punned_search_result_t result) {
263
286
  result.error.raise();
264
287
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
265
- } else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4") {
266
- punned_search_result_t result = index.search(reinterpret_cast<float const*>(vector_data), wanted, config);
267
- result.error.raise();
268
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
269
- } else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8") {
270
- punned_search_result_t result = index.search(reinterpret_cast<double const*>(vector_data), wanted, config);
271
- result.error.raise();
272
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
273
- } else
288
+ };
289
+
290
+ switch (numpy_string_to_kind(vector_info.format)) {
291
+ case scalar_kind_t::b1x8_k: raise_and_dump(index.search((b1x8_t const*)(vector_data), wanted, config)); break;
292
+ case scalar_kind_t::f8_k: raise_and_dump(index.search((f8_bits_t const*)(vector_data), wanted, config)); break;
293
+ case scalar_kind_t::f16_k: raise_and_dump(index.search((f16_t const*)(vector_data), wanted, config)); break;
294
+ case scalar_kind_t::f32_k: raise_and_dump(index.search((f32_t const*)(vector_data), wanted, config)); break;
295
+ case scalar_kind_t::f64_k: raise_and_dump(index.search((f64_t const*)(vector_data), wanted, config)); break;
296
+ case scalar_kind_t::unknown_k:
274
297
  throw std::invalid_argument("Incompatible scalars in the query vector: " + vector_info.format);
298
+ }
275
299
 
276
300
  labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
277
301
  distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
@@ -283,6 +307,33 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
283
307
  return results;
284
308
  }
285
309
 
310
+ template <typename scalar_at>
311
+ static void search_typed( //
312
+ punned_index_py_t& index, py::buffer_info& vectors_info, //
313
+ std::size_t wanted, bool exact, std::size_t threads, //
314
+ py::array_t<label_t>& labels_py, py::array_t<distance_t>& distances_py, py::array_t<Py_ssize_t>& counts_py) {
315
+
316
+ auto labels_py2d = labels_py.template mutable_unchecked<2>();
317
+ auto distances_py2d = distances_py.template mutable_unchecked<2>();
318
+ auto counts_py1d = counts_py.template mutable_unchecked<1>();
319
+
320
+ Py_ssize_t vectors_count = vectors_info.shape[0];
321
+ char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
322
+
323
+ executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
324
+ search_config_t config;
325
+ config.thread = thread_idx;
326
+ config.exact = exact;
327
+ scalar_at const* vector = (scalar_at const*)(vectors_data + task_idx * vectors_info.strides[0]);
328
+ punned_search_result_t result = index.search(vector, wanted, config);
329
+ result.error.raise();
330
+ counts_py1d(task_idx) =
331
+ static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
332
+ if (PyErr_CheckSignals() != 0)
333
+ throw py::error_already_set();
334
+ });
335
+ }
336
+
286
337
  /**
287
338
  * @param vectors Matrix of vectors to search for.
288
339
  * @param wanted Number of matches per request.
@@ -298,7 +349,7 @@ static py::tuple search_many_in_index( //
298
349
  if (wanted == 0)
299
350
  return py::tuple(3);
300
351
 
301
- if (index.limits().threads_add < threads)
352
+ if (index.limits().threads_search < threads)
302
353
  throw std::invalid_argument("Can't use that many threads!");
303
354
 
304
355
  py::buffer_info vectors_info = vectors.request();
@@ -313,76 +364,24 @@ static py::tuple search_many_in_index( //
313
364
  if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
314
365
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
315
366
 
316
- py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
317
- py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
318
- py::array_t<Py_ssize_t> counts_py(vectors_count);
319
- auto labels_py2d = labels_py.template mutable_unchecked<2>();
320
- auto distances_py2d = distances_py.template mutable_unchecked<2>();
321
- auto counts_py1d = counts_py.template mutable_unchecked<1>();
322
-
323
- // https://docs.python.org/3/library/struct.html#format-characters
324
- if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
325
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
326
- search_config_t config;
327
- config.thread = thread_idx;
328
- config.exact = exact;
329
- b1x8_t const* vector = (b1x8_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
330
- punned_search_result_t result = index.search(vector, wanted, config);
331
- result.error.raise();
332
- counts_py1d(task_idx) =
333
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
334
- });
335
- else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
336
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
337
- search_config_t config;
338
- config.thread = thread_idx;
339
- config.exact = exact;
340
- f8_bits_t const* vector = (f8_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
341
- punned_search_result_t result = index.search(vector, wanted, config);
342
- result.error.raise();
343
- counts_py1d(task_idx) =
344
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
345
- });
346
- else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
347
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
348
- search_config_t config;
349
- config.thread = thread_idx;
350
- config.exact = exact;
351
- f16_t const* vector = (f16_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
352
- punned_search_result_t result = index.search(vector, wanted, config);
353
- result.error.raise();
354
- counts_py1d(task_idx) =
355
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
356
- });
357
- else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
358
- executor_default_t{threads}.execute_bulk(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
- float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
363
- punned_search_result_t result = index.search(vector, wanted, config);
364
- result.error.raise();
365
- counts_py1d(task_idx) =
366
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
367
- });
368
- else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
369
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
370
- search_config_t config;
371
- config.thread = thread_idx;
372
- config.exact = exact;
373
- double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
374
- punned_search_result_t result = index.search(vector, wanted, config);
375
- result.error.raise();
376
- counts_py1d(task_idx) =
377
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
378
- });
379
- else
367
+ py::array_t<label_t> ls({vectors_count, static_cast<Py_ssize_t>(wanted)});
368
+ py::array_t<distance_t> ds({vectors_count, static_cast<Py_ssize_t>(wanted)});
369
+ py::array_t<Py_ssize_t> cs(vectors_count);
370
+
371
+ switch (numpy_string_to_kind(vectors_info.format)) {
372
+ case scalar_kind_t::b1x8_k: search_typed<b1x8_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
373
+ case scalar_kind_t::f8_k: search_typed<f8_bits_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
374
+ case scalar_kind_t::f16_k: search_typed<f16_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
375
+ case scalar_kind_t::f32_k: search_typed<f32_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
376
+ case scalar_kind_t::f64_k: search_typed<f64_t>(index, vectors_info, wanted, exact, threads, ls, ds, cs); break;
377
+ case scalar_kind_t::unknown_k:
380
378
  throw std::invalid_argument("Incompatible scalars in the query matrix: " + vectors_info.format);
379
+ }
381
380
 
382
381
  py::tuple results(3);
383
- results[0] = labels_py;
384
- results[1] = distances_py;
385
- results[2] = counts_py;
382
+ results[0] = ls;
383
+ results[1] = ds;
384
+ results[2] = cs;
386
385
  return results;
387
386
  }
388
387
 
@@ -408,7 +407,7 @@ template <typename index_at> py::object get_member(index_at const& index, label_
408
407
  else if (scalar_kind == scalar_kind_t::f64_k)
409
408
  return get_typed_member<f64_t>(index, label);
410
409
  else if (scalar_kind == scalar_kind_t::f16_k)
411
- return get_typed_member<f16_t>(index, label);
410
+ return get_typed_member<f16_t, std::uint16_t>(index, label);
412
411
  else if (scalar_kind == scalar_kind_t::f8_k)
413
412
  return get_typed_member<f8_bits_t, std::int8_t>(index, label);
414
413
  else if (scalar_kind == scalar_kind_t::b1x8_k)
@@ -417,14 +416,18 @@ template <typename index_at> py::object get_member(index_at const& index, label_
417
416
  throw std::invalid_argument("Incompatible scalars in the query matrix!");
418
417
  }
419
418
 
420
- template <typename index_at> py::array_t<label_t> get_labels(index_at const& index) {
421
- std::size_t result_length = index.size();
422
- py::array_t<label_t> result_py(static_cast<Py_ssize_t>(result_length));
419
+ template <typename index_at> py::array_t<label_t> get_labels(index_at const& index, std::size_t limit) {
420
+ limit = std::min(index.size(), limit);
421
+ py::array_t<label_t> result_py(static_cast<Py_ssize_t>(limit));
423
422
  auto result_py1d = result_py.template mutable_unchecked<1>();
424
- index.export_labels(&result_py1d(0), result_length);
423
+ index.export_labels(&result_py1d(0), limit);
425
424
  return result_py;
426
425
  }
427
426
 
427
+ template <typename index_at> py::array_t<label_t> get_all_labels(index_at const& index) {
428
+ return get_labels(index, index.size());
429
+ }
430
+
428
431
  template <typename element_at> bool has_duplicates(element_at const* begin, element_at const* end) {
429
432
  if (begin == end)
430
433
  return false;
@@ -539,6 +542,11 @@ PYBIND11_MODULE(compiled, m) {
539
542
  m.attr("USES_SIMSIMD") = py::int_(USEARCH_USE_SIMSIMD);
540
543
  m.attr("USES_NATIVE_F16") = py::int_(USEARCH_USE_NATIVE_F16);
541
544
 
545
+ py::enum_<metric_signature_t>(m, "MetricSignature")
546
+ .value("ArrayArray", metric_signature_t::array_array_k)
547
+ .value("ArrayArraySize", metric_signature_t::array_array_size_k)
548
+ .value("ArraySizeArraySize", metric_signature_t::array_size_array_size_k);
549
+
542
550
  py::enum_<metric_kind_t>(m, "MetricKind")
543
551
  .value("Unknown", metric_kind_t::unknown_k)
544
552
  .value("IP", metric_kind_t::ip_k)
@@ -558,18 +566,44 @@ PYBIND11_MODULE(compiled, m) {
558
566
  .value("F8", scalar_kind_t::f8_k)
559
567
  .value("B1", scalar_kind_t::b1x8_k);
560
568
 
569
+ auto h = py::class_<file_head_result_t>(m, "IndexMetadata");
570
+ h.def(py::init([](std::string const& path) {
571
+ file_head_result_t h = index_metadata(path.c_str());
572
+ h.error.raise();
573
+ return h;
574
+ }));
575
+ h.def_property_readonly("version", [](file_head_result_t const& h) {
576
+ return //
577
+ std::to_string(h.version_major) + "." + //
578
+ std::to_string(h.version_minor) + "." + //
579
+ std::to_string(h.version_patch);
580
+ });
581
+ h.def_readonly("metric", &file_head_result_t::metric);
582
+ h.def_readonly("connectivity", &file_head_result_t::connectivity);
583
+ h.def_readonly("max_level", &file_head_result_t::max_level);
584
+ h.def_readonly("vector_alignment", &file_head_result_t::vector_alignment);
585
+ h.def_readonly("bytes_per_label", &file_head_result_t::bytes_per_label);
586
+ h.def_readonly("bytes_per_id", &file_head_result_t::bytes_per_id);
587
+ h.def_readonly("scalar_kind", &file_head_result_t::scalar_kind);
588
+ h.def_readonly("size", &file_head_result_t::size);
589
+ h.def_readonly("entry_idx", &file_head_result_t::entry_idx);
590
+ h.def_readonly("bytes_for_graphs", &file_head_result_t::bytes_for_graphs);
591
+ h.def_readonly("bytes_for_vectors", &file_head_result_t::bytes_for_vectors);
592
+ h.def_readonly("bytes_checksum", &file_head_result_t::bytes_checksum);
593
+
561
594
  auto i = py::class_<punned_index_py_t>(m, "Index");
562
595
 
563
- i.def(py::init(&make_index), //
564
- py::kw_only(), //
565
- py::arg("ndim") = 0, //
566
- py::arg("dtype") = scalar_kind_t::f32_k, //
567
- py::arg("metric") = metric_kind_t::ip_k, //
568
- py::arg("connectivity") = default_connectivity(), //
569
- py::arg("expansion_add") = default_expansion_add(), //
570
- py::arg("expansion_search") = default_expansion_search(), //
571
- py::arg("metric_pointer") = 0, //
572
- py::arg("tune") = false //
596
+ i.def(py::init(&make_index), //
597
+ py::kw_only(), //
598
+ py::arg("ndim") = 0, //
599
+ py::arg("dtype") = scalar_kind_t::f32_k, //
600
+ py::arg("metric") = metric_kind_t::ip_k, //
601
+ py::arg("connectivity") = default_connectivity(), //
602
+ py::arg("expansion_add") = default_expansion_add(), //
603
+ py::arg("expansion_search") = default_expansion_search(), //
604
+ py::arg("metric_signature") = metric_signature_t::array_array_k, //
605
+ py::arg("metric_pointer") = 0, //
606
+ py::arg("tune") = false //
573
607
  );
574
608
 
575
609
  i.def( //
@@ -604,23 +638,24 @@ PYBIND11_MODULE(compiled, m) {
604
638
  i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
605
639
  i.def_property_readonly("capacity", &punned_index_py_t::capacity);
606
640
  i.def_property_readonly( //
607
- "dtype", [](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.scalar_kind()); });
641
+ "dtype", [](punned_index_py_t const& index) -> scalar_kind_t { return index.metric().scalar_kind_; });
608
642
  i.def_property_readonly( //
609
- "memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); });
643
+ "memory_usage", [](punned_index_py_t const& index) -> std::size_t { return index.memory_usage(); },
644
+ py::call_guard<py::gil_scoped_release>());
610
645
 
611
646
  i.def_property("expansion_add", &punned_index_py_t::expansion_add, &punned_index_py_t::change_expansion_add);
612
647
  i.def_property("expansion_search", &punned_index_py_t::expansion_search,
613
648
  &punned_index_py_t::change_expansion_search);
614
649
 
615
- i.def_property_readonly("labels", &get_labels<punned_index_py_t>);
650
+ i.def_property_readonly("labels", &get_all_labels<punned_index_py_t>);
651
+ i.def("get_labels", &get_labels<punned_index_py_t>, py::arg("limit") = std::numeric_limits<std::size_t>::max());
616
652
  i.def("__contains__", &punned_index_py_t::contains);
617
- i.def( //
618
- "__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
653
+ i.def("__getitem__", &get_member<punned_index_py_t>, py::arg("label"), py::arg("dtype") = scalar_kind_t::f32_k);
619
654
 
620
- i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
621
- i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
622
- i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
623
- i.def("clear", &punned_index_py_t::clear);
655
+ i.def("save", &save_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
656
+ i.def("load", &load_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
657
+ i.def("view", &view_index<punned_index_py_t>, py::arg("path"), py::call_guard<py::gil_scoped_release>());
658
+ i.def("clear", &punned_index_py_t::clear, py::call_guard<py::gil_scoped_release>());
624
659
 
625
660
  auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
626
661