usearch 0.19.0 → 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.
@@ -206,6 +206,10 @@ scalar_kind_t to_native_scalar(USearchScalar m) {
206
206
  _native->clear();
207
207
  }
208
208
 
209
+ - (void)reserve:(UInt32)count {
210
+ _native->reserve(static_cast<std::size_t>(count));
211
+ }
212
+
209
213
  - (void)save:(NSString *)path {
210
214
  char const *path_c = [path UTF8String];
211
215
 
@@ -51,6 +51,11 @@ API_AVAILABLE(ios(13.0), macos(10.15), tvos(13.0), watchos(6.0))
51
51
  */
52
52
  + (instancetype)make:(USearchMetric)metric dimensions:(UInt32)dimensions connectivity:(UInt32)connectivity quantization:(USearchScalar)quantization NS_SWIFT_NAME(make(metric:dimensions:connectivity:quantization:));
53
53
 
54
+ /**
55
+ * @brief Pre-allocates space in the index for the given number of vectors.
56
+ */
57
+ - (void)reserve:(UInt32)count NS_SWIFT_NAME(reserve(_:));
58
+
54
59
  /**
55
60
  * @brief Adds a labeled vector to the index.
56
61
  * @param vector Single-precision vector.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",
@@ -24,6 +24,6 @@
24
24
  "@semantic-release/exec": "^6.0.3",
25
25
  "@semantic-release/git": "^10.0.1",
26
26
  "conventional-changelog-eslint": "^3.0.9",
27
- "semantic-release": "^21.0.5"
27
+ "semantic-release": "^21.0.6"
28
28
  }
29
29
  }
package/python/lib.cpp CHANGED
@@ -76,10 +76,12 @@ struct sparse_index_py_t : public sparse_index_t {
76
76
  };
77
77
 
78
78
  template <typename scalar_at>
79
- metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr) {
79
+ metric_t typed_udf( //
80
+ metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
80
81
  //
81
82
  metric_t result;
82
83
  result.kind_ = kind;
84
+ result.scalar_kind_ = accuracy;
83
85
  switch (signature) {
84
86
  case metric_signature_t::array_array_k:
85
87
  result.func_ = [metric_uintptr](punned_vector_view_t a, punned_vector_view_t b) -> distance_t {
@@ -110,10 +112,11 @@ metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t me
110
112
 
111
113
  metric_t udf(metric_kind_t kind, metric_signature_t signature, std::uintptr_t metric_uintptr, scalar_kind_t accuracy) {
112
114
  switch (accuracy) {
113
- case scalar_kind_t::f8_k: return udf<f8_bits_t>(kind, signature, metric_uintptr);
114
- case scalar_kind_t::f16_k: return udf<f16_t>(kind, signature, metric_uintptr);
115
- case scalar_kind_t::f32_k: return udf<f32_t>(kind, signature, metric_uintptr);
116
- case scalar_kind_t::f64_k: return udf<f64_t>(kind, signature, 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);
117
120
  default: return {};
118
121
  }
119
122
  }
@@ -154,6 +157,22 @@ static std::unique_ptr<sparse_index_py_t> make_sparse_index( //
154
157
  return std::unique_ptr<sparse_index_py_t>(new sparse_index_py_t(sparse_index_t(config)));
155
158
  }
156
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
+
157
176
  static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer vector, bool copy, std::size_t) {
158
177
 
159
178
  py::buffer_info vector_info = vector.request();
@@ -171,19 +190,37 @@ static void add_one_to_index(punned_index_py_t& index, label_t label, py::buffer
171
190
  add_config_t config;
172
191
  config.store_vector = copy;
173
192
 
174
- // https://docs.python.org/3/library/struct.html#format-characters
175
- if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1")
176
- index.add(label, reinterpret_cast<b1x8_t const*>(vector_data), config).error.raise();
177
- else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1")
178
- index.add(label, reinterpret_cast<f8_bits_t const*>(vector_data), config).error.raise();
179
- else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2")
180
- index.add(label, reinterpret_cast<f16_t const*>(vector_data), config).error.raise();
181
- else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4")
182
- index.add(label, reinterpret_cast<float const*>(vector_data), config).error.raise();
183
- else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8")
184
- index.add(label, reinterpret_cast<double const*>(vector_data), config).error.raise();
185
- 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:
186
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
+ });
187
224
  }
188
225
 
189
226
  static void add_many_to_index( //
@@ -217,58 +254,15 @@ static void add_many_to_index( //
217
254
  if (index.size() + vectors_count >= index.capacity())
218
255
  index.reserve(ceil2(index.size() + vectors_count));
219
256
 
220
- char const* vectors_data = reinterpret_cast<char const*>(vectors_info.ptr);
221
- char const* labels_data = reinterpret_cast<char const*>(labels_info.ptr);
222
-
223
- // https://docs.python.org/3/library/struct.html#format-characters
224
- if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
225
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
226
- add_config_t config;
227
- config.store_vector = copy;
228
- config.thread = thread_idx;
229
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
230
- b1x8_t const* vector = reinterpret_cast<b1x8_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
231
- index.add(label, vector, config).error.raise();
232
- });
233
- else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
234
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
235
- add_config_t config;
236
- config.store_vector = copy;
237
- config.thread = thread_idx;
238
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
239
- f8_bits_t const* vector =
240
- reinterpret_cast<f8_bits_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
241
- index.add(label, vector, config).error.raise();
242
- });
243
- else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
244
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
245
- add_config_t config;
246
- config.store_vector = copy;
247
- config.thread = thread_idx;
248
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
249
- f16_t const* vector = reinterpret_cast<f16_t const*>(vectors_data + task_idx * vectors_info.strides[0]);
250
- index.add(label, vector, config).error.raise();
251
- });
252
- else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
253
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
254
- add_config_t config;
255
- config.store_vector = copy;
256
- config.thread = thread_idx;
257
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
258
- float const* vector = reinterpret_cast<float const*>(vectors_data + task_idx * vectors_info.strides[0]);
259
- index.add(label, vector, config).error.raise();
260
- });
261
- else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
262
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
263
- add_config_t config;
264
- config.store_vector = copy;
265
- config.thread = thread_idx;
266
- label_t label = *reinterpret_cast<label_t const*>(labels_data + task_idx * labels_info.strides[0]);
267
- double const* vector = reinterpret_cast<double const*>(vectors_data + task_idx * vectors_info.strides[0]);
268
- index.add(label, vector, config).error.raise();
269
- });
270
- 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:
271
264
  throw std::invalid_argument("Incompatible scalars in the vectors matrix: " + vectors_info.format);
265
+ }
272
266
  }
273
267
 
274
268
  static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector, std::size_t wanted, bool exact) {
@@ -288,29 +282,20 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
288
282
  search_config_t config;
289
283
  config.exact = exact;
290
284
 
291
- // https://docs.python.org/3/library/struct.html#format-characters
292
- if (vector_info.format == "B" || vector_info.format == "u1" || vector_info.format == "|u1") {
293
- punned_search_result_t result = index.search(reinterpret_cast<b1x8_t const*>(vector_data), wanted, config);
294
- result.error.raise();
295
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
296
- } else if (vector_info.format == "b" || vector_info.format == "i1" || vector_info.format == "|i1") {
297
- punned_search_result_t result = index.search(reinterpret_cast<f8_bits_t const*>(vector_data), wanted, config);
285
+ auto raise_and_dump = [&](punned_search_result_t result) {
298
286
  result.error.raise();
299
287
  count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
300
- } else if (vector_info.format == "e" || vector_info.format == "f2" || vector_info.format == "<f2") {
301
- punned_search_result_t result = index.search(reinterpret_cast<f16_t const*>(vector_data), wanted, config);
302
- result.error.raise();
303
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
304
- } else if (vector_info.format == "f" || vector_info.format == "f4" || vector_info.format == "<f4") {
305
- punned_search_result_t result = index.search(reinterpret_cast<float const*>(vector_data), wanted, config);
306
- result.error.raise();
307
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
308
- } else if (vector_info.format == "d" || vector_info.format == "f8" || vector_info.format == "<f8") {
309
- punned_search_result_t result = index.search(reinterpret_cast<double const*>(vector_data), wanted, config);
310
- result.error.raise();
311
- count = result.dump_to(&labels_py1d(0), &distances_py1d(0));
312
- } 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:
313
297
  throw std::invalid_argument("Incompatible scalars in the query vector: " + vector_info.format);
298
+ }
314
299
 
315
300
  labels_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
316
301
  distances_py.resize(py_shape_t{static_cast<Py_ssize_t>(count)});
@@ -322,6 +307,33 @@ static py::tuple search_one_in_index(punned_index_py_t& index, py::buffer vector
322
307
  return results;
323
308
  }
324
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
+
325
337
  /**
326
338
  * @param vectors Matrix of vectors to search for.
327
339
  * @param wanted Number of matches per request.
@@ -337,7 +349,7 @@ static py::tuple search_many_in_index( //
337
349
  if (wanted == 0)
338
350
  return py::tuple(3);
339
351
 
340
- if (index.limits().threads_add < threads)
352
+ if (index.limits().threads_search < threads)
341
353
  throw std::invalid_argument("Can't use that many threads!");
342
354
 
343
355
  py::buffer_info vectors_info = vectors.request();
@@ -352,76 +364,24 @@ static py::tuple search_many_in_index( //
352
364
  if (vectors_dimensions != static_cast<Py_ssize_t>(index.scalar_words()))
353
365
  throw std::invalid_argument("The number of vector dimensions doesn't match!");
354
366
 
355
- py::array_t<label_t> labels_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
356
- py::array_t<distance_t> distances_py({vectors_count, static_cast<Py_ssize_t>(wanted)});
357
- py::array_t<Py_ssize_t> counts_py(vectors_count);
358
- auto labels_py2d = labels_py.template mutable_unchecked<2>();
359
- auto distances_py2d = distances_py.template mutable_unchecked<2>();
360
- auto counts_py1d = counts_py.template mutable_unchecked<1>();
361
-
362
- // https://docs.python.org/3/library/struct.html#format-characters
363
- if (vectors_info.format == "B" || vectors_info.format == "u1" || vectors_info.format == "|u1")
364
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
365
- search_config_t config;
366
- config.thread = thread_idx;
367
- config.exact = exact;
368
- b1x8_t const* vector = (b1x8_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
369
- punned_search_result_t result = index.search(vector, wanted, config);
370
- result.error.raise();
371
- counts_py1d(task_idx) =
372
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
373
- });
374
- else if (vectors_info.format == "b" || vectors_info.format == "i1" || vectors_info.format == "|i1")
375
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
376
- search_config_t config;
377
- config.thread = thread_idx;
378
- config.exact = exact;
379
- f8_bits_t const* vector = (f8_bits_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
380
- punned_search_result_t result = index.search(vector, wanted, config);
381
- result.error.raise();
382
- counts_py1d(task_idx) =
383
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
384
- });
385
- else if (vectors_info.format == "e" || vectors_info.format == "f2" || vectors_info.format == "<f2")
386
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
387
- search_config_t config;
388
- config.thread = thread_idx;
389
- config.exact = exact;
390
- f16_t const* vector = (f16_t const*)(vectors_data + task_idx * vectors_info.strides[0]);
391
- punned_search_result_t result = index.search(vector, wanted, config);
392
- result.error.raise();
393
- counts_py1d(task_idx) =
394
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
395
- });
396
- else if (vectors_info.format == "f" || vectors_info.format == "f4" || vectors_info.format == "<f4")
397
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
398
- search_config_t config;
399
- config.thread = thread_idx;
400
- config.exact = exact;
401
- float const* vector = (float const*)(vectors_data + task_idx * vectors_info.strides[0]);
402
- punned_search_result_t result = index.search(vector, wanted, config);
403
- result.error.raise();
404
- counts_py1d(task_idx) =
405
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
406
- });
407
- else if (vectors_info.format == "d" || vectors_info.format == "f8" || vectors_info.format == "<f8")
408
- executor_default_t{threads}.execute_bulk(vectors_count, [&](std::size_t thread_idx, std::size_t task_idx) {
409
- search_config_t config;
410
- config.thread = thread_idx;
411
- config.exact = exact;
412
- double const* vector = (double const*)(vectors_data + task_idx * vectors_info.strides[0]);
413
- punned_search_result_t result = index.search(vector, wanted, config);
414
- result.error.raise();
415
- counts_py1d(task_idx) =
416
- static_cast<Py_ssize_t>(result.dump_to(&labels_py2d(task_idx, 0), &distances_py2d(task_idx, 0)));
417
- });
418
- 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:
419
378
  throw std::invalid_argument("Incompatible scalars in the query matrix: " + vectors_info.format);
379
+ }
420
380
 
421
381
  py::tuple results(3);
422
- results[0] = labels_py;
423
- results[1] = distances_py;
424
- results[2] = counts_py;
382
+ results[0] = ls;
383
+ results[1] = ds;
384
+ results[2] = cs;
425
385
  return results;
426
386
  }
427
387
 
@@ -447,7 +407,7 @@ template <typename index_at> py::object get_member(index_at const& index, label_
447
407
  else if (scalar_kind == scalar_kind_t::f64_k)
448
408
  return get_typed_member<f64_t>(index, label);
449
409
  else if (scalar_kind == scalar_kind_t::f16_k)
450
- return get_typed_member<f16_t>(index, label);
410
+ return get_typed_member<f16_t, std::uint16_t>(index, label);
451
411
  else if (scalar_kind == scalar_kind_t::f8_k)
452
412
  return get_typed_member<f8_bits_t, std::int8_t>(index, label);
453
413
  else if (scalar_kind == scalar_kind_t::b1x8_k)
@@ -678,10 +638,10 @@ PYBIND11_MODULE(compiled, m) {
678
638
  i.def_property_readonly("connectivity", &punned_index_py_t::connectivity);
679
639
  i.def_property_readonly("capacity", &punned_index_py_t::capacity);
680
640
  i.def_property_readonly( //
681
- "dtype",
682
- [](punned_index_py_t const& index) -> std::string { return scalar_kind_name(index.metric().scalar_kind_); });
641
+ "dtype", [](punned_index_py_t const& index) -> scalar_kind_t { return index.metric().scalar_kind_; });
683
642
  i.def_property_readonly( //
684
- "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>());
685
645
 
686
646
  i.def_property("expansion_add", &punned_index_py_t::expansion_add, &punned_index_py_t::change_expansion_add);
687
647
  i.def_property("expansion_search", &punned_index_py_t::expansion_search,
@@ -690,13 +650,12 @@ PYBIND11_MODULE(compiled, m) {
690
650
  i.def_property_readonly("labels", &get_all_labels<punned_index_py_t>);
691
651
  i.def("get_labels", &get_labels<punned_index_py_t>, py::arg("limit") = std::numeric_limits<std::size_t>::max());
692
652
  i.def("__contains__", &punned_index_py_t::contains);
693
- i.def( //
694
- "__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);
695
654
 
696
- i.def("save", &save_index<punned_index_py_t>, py::arg("path"));
697
- i.def("load", &load_index<punned_index_py_t>, py::arg("path"));
698
- i.def("view", &view_index<punned_index_py_t>, py::arg("path"));
699
- 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>());
700
659
 
701
660
  auto si = py::class_<sparse_index_py_t>(m, "SparseIndex");
702
661
 
@@ -24,7 +24,7 @@ from usearch.index import (
24
24
 
25
25
 
26
26
  dimensions = [3, 97, 256]
27
- batch_sizes = [1, 33]
27
+ batch_sizes = [1, 77]
28
28
  index_types = [
29
29
  ScalarKind.F32,
30
30
  ScalarKind.F64,
@@ -75,7 +75,7 @@ def test_serializing_ibin_matrix(rows: int, cols: int):
75
75
  def test_index(
76
76
  ndim: int,
77
77
  metric: MetricKind,
78
- index_type: str,
78
+ index_type: ScalarKind,
79
79
  numpy_type: str,
80
80
  connectivity: int,
81
81
  jit: bool,
@@ -135,7 +135,11 @@ def test_index(
135
135
  @pytest.mark.parametrize("index_type", index_types)
136
136
  @pytest.mark.parametrize("numpy_type", numpy_types)
137
137
  def test_index_batch(
138
- ndim: int, metric: MetricKind, batch_size: int, index_type: str, numpy_type: str
138
+ ndim: int,
139
+ metric: MetricKind,
140
+ batch_size: int,
141
+ index_type: ScalarKind,
142
+ numpy_type: str,
139
143
  ):
140
144
  index = Index(ndim=ndim, metric=metric, dtype=index_type)
141
145
 
@@ -151,8 +155,50 @@ def test_index_batch(
151
155
  assert matches.counts.shape[0] == batch_size
152
156
  assert np.all(np.sort(index.labels) == np.sort(labels))
153
157
 
154
- if _normalize_dtype(numpy_type) == _normalize_dtype(index_type):
155
- assert recall_members(index, exact=True) == 1
158
+ index.save("tmp.usearch")
159
+ index.clear()
160
+ assert len(index) == 0
161
+
162
+ index.load("tmp.usearch")
163
+ assert len(index) == batch_size
164
+ assert len(index[0]) == ndim
165
+
166
+ index = Index.restore("tmp.usearch")
167
+ assert len(index) == batch_size
168
+ assert len(index[0]) == ndim
169
+
170
+ # Cleanup
171
+ os.remove("tmp.usearch")
172
+
173
+
174
+ @pytest.mark.parametrize("metric", [MetricKind.L2sq])
175
+ @pytest.mark.parametrize("batch_size", batch_sizes)
176
+ @pytest.mark.parametrize("index_type", index_types)
177
+ @pytest.mark.parametrize("numpy_type", numpy_types)
178
+ def test_exact_recall(
179
+ metric: MetricKind, batch_size: int, index_type: ScalarKind, numpy_type: str
180
+ ):
181
+ ndim: int = batch_size
182
+ index = Index(ndim=ndim, metric=metric, dtype=index_type)
183
+
184
+ vectors = np.zeros((batch_size, ndim), dtype=numpy_type)
185
+ for i in range(batch_size):
186
+ vectors[i, i] = 1
187
+ labels = np.arange(batch_size)
188
+ index.add(labels, vectors)
189
+ assert len(index) == batch_size
190
+
191
+ # Search one at a time
192
+ for i in range(batch_size):
193
+ matches: Matches = index.search(vectors[i], 10, exact=True)
194
+ found_labels = matches.labels
195
+ assert found_labels[0] == i
196
+
197
+ # Search the whole batch
198
+ matches: Matches = index.search(vectors, 10, exact=True)
199
+ found_labels = matches.labels
200
+ for i in range(batch_size):
201
+ assert found_labels[i, 0] == i
156
202
 
157
203
 
158
204
  @pytest.mark.parametrize("ndim", dimensions)
@@ -234,8 +280,6 @@ def test_index_numba(ndim: int, batch_size: int):
234
280
  assert matches.shape[0] == distances.shape[0]
235
281
  assert count.shape[0] == batch_size
236
282
 
237
- assert recall_members(index, exact=True) == 1
238
-
239
283
 
240
284
  @pytest.mark.parametrize("ndim", dimensions[-1:])
241
285
  @pytest.mark.parametrize("batch_size", batch_sizes[-1:])
@@ -300,8 +344,6 @@ def test_index_cppyy(ndim: int, batch_size: int):
300
344
  assert matches.shape[0] == distances.shape[0]
301
345
  assert count.shape[0] == batch_size
302
346
 
303
- assert recall_members(index, exact=True) == 1
304
-
305
347
 
306
348
  @pytest.mark.parametrize("ndim", [8])
307
349
  @pytest.mark.parametrize("batch_size", batch_sizes)
@@ -388,8 +430,6 @@ def test_index_peachpy(ndim: int, batch_size: int):
388
430
  assert matches.shape[0] == distances.shape[0]
389
431
  assert count.shape[0] == batch_size
390
432
 
391
- assert recall_members(index, exact=True) == 1
392
-
393
433
 
394
434
  @pytest.mark.parametrize("bits", dimensions)
395
435
  @pytest.mark.parametrize("metric", hash_metrics)