usearch 2.25.0 → 2.25.3

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 (80) hide show
  1. package/README.md +43 -47
  2. package/c/README.md +1 -1
  3. package/c/lib.cpp +28 -13
  4. package/c/test.c +41 -0
  5. package/cpp/test.cpp +99 -0
  6. package/include/usearch/index.hpp +338 -57
  7. package/include/usearch/index_dense.hpp +155 -50
  8. package/include/usearch/index_plugins.hpp +94 -35
  9. package/javascript/README.md +1 -1
  10. package/javascript/lib.cpp +4 -2
  11. package/javascript/usearch.test.js +117 -94
  12. package/numkong/CITATION.cff +1 -1
  13. package/numkong/Cargo.toml +1 -1
  14. package/numkong/bench/bench_cross_amx.cpp +12 -0
  15. package/numkong/bench/bench_mesh.cpp +12 -0
  16. package/numkong/bench/bench_spatial.cpp +0 -3
  17. package/numkong/binding.gyp +18 -0
  18. package/numkong/build.rs +1 -0
  19. package/numkong/c/dispatch_e5m2.c +23 -3
  20. package/numkong/include/numkong/capabilities.h +1 -1
  21. package/numkong/include/numkong/cast/README.md +3 -0
  22. package/numkong/include/numkong/cast/haswell.h +28 -64
  23. package/numkong/include/numkong/cast/serial.h +17 -0
  24. package/numkong/include/numkong/cast/skylake.h +67 -52
  25. package/numkong/include/numkong/cast.h +1 -0
  26. package/numkong/include/numkong/dot/README.md +1 -0
  27. package/numkong/include/numkong/dot/haswell.h +92 -13
  28. package/numkong/include/numkong/dot/serial.h +15 -0
  29. package/numkong/include/numkong/dot/skylake.h +61 -14
  30. package/numkong/include/numkong/dots/README.md +2 -0
  31. package/numkong/include/numkong/dots/graniteamx.h +434 -0
  32. package/numkong/include/numkong/dots/haswell.h +28 -28
  33. package/numkong/include/numkong/dots/sapphireamx.h +1 -1
  34. package/numkong/include/numkong/dots/serial.h +23 -8
  35. package/numkong/include/numkong/dots/skylake.h +28 -23
  36. package/numkong/include/numkong/dots.h +12 -0
  37. package/numkong/include/numkong/each/serial.h +18 -1
  38. package/numkong/include/numkong/geospatial/serial.h +14 -3
  39. package/numkong/include/numkong/maxsim/serial.h +15 -0
  40. package/numkong/include/numkong/mesh/README.md +50 -44
  41. package/numkong/include/numkong/mesh/genoa.h +462 -0
  42. package/numkong/include/numkong/mesh/haswell.h +806 -933
  43. package/numkong/include/numkong/mesh/neon.h +871 -943
  44. package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
  45. package/numkong/include/numkong/mesh/neonfhm.h +676 -0
  46. package/numkong/include/numkong/mesh/rvv.h +404 -319
  47. package/numkong/include/numkong/mesh/serial.h +204 -162
  48. package/numkong/include/numkong/mesh/skylake.h +1029 -1585
  49. package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
  50. package/numkong/include/numkong/mesh.h +38 -0
  51. package/numkong/include/numkong/reduce/serial.h +15 -1
  52. package/numkong/include/numkong/sparse/serial.h +17 -2
  53. package/numkong/include/numkong/spatial/genoa.h +0 -68
  54. package/numkong/include/numkong/spatial/haswell.h +98 -56
  55. package/numkong/include/numkong/spatial/serial.h +15 -0
  56. package/numkong/include/numkong/spatial/skylake.h +114 -54
  57. package/numkong/include/numkong/spatial.h +0 -12
  58. package/numkong/include/numkong/spatials/graniteamx.h +128 -0
  59. package/numkong/include/numkong/spatials/serial.h +18 -1
  60. package/numkong/include/numkong/spatials/skylake.h +2 -2
  61. package/numkong/include/numkong/spatials.h +17 -0
  62. package/numkong/include/numkong/tensor.hpp +107 -23
  63. package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
  64. package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
  65. package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
  66. package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
  67. package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
  68. package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
  69. package/numkong/javascript/numkong.c +3 -2
  70. package/numkong/package.json +7 -7
  71. package/numkong/test/test.cu +841 -0
  72. package/numkong/test/test_cross_amx.cpp +13 -0
  73. package/numkong/test/test_dlpack.py +492 -0
  74. package/numkong/test/test_mesh.cpp +12 -0
  75. package/numkong/test/test_spatial.cpp +0 -3
  76. package/numkong/test/test_tensor.cpp +87 -27
  77. package/package.json +1 -1
  78. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  79. package/prebuilds/linux-arm64/usearch.node +0 -0
  80. package/prebuilds/linux-x64/usearch.node +0 -0
package/README.md CHANGED
@@ -42,7 +42,7 @@ Linux • macOS • Windows • iOS • Android • WebAssembly •
42
42
  <a href="https://crates.io/crates/usearch"> <img alt="Crate" src="https://img.shields.io/crates/d/usearch?label=Rust%20Crate%20installs"> </a>
43
43
  <a href="https://www.nuget.org/packages/Cloud.Unum.USearch"> <img alt="NuGet" src="https://img.shields.io/nuget/dt/Cloud.Unum.USearch?label=CSharp%20NuGet%20installs"> </a>
44
44
  <!-- Maven Central publishing is deprecated for now; fat-JAR download is the supported path. -->
45
- <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/unum-cloud/usearch?label=Repo%20size">
45
+ <img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/unum-cloud/USearch?label=Repo%20size">
46
46
  </div>
47
47
 
48
48
  ---
@@ -51,7 +51,7 @@ Linux • macOS • Windows • iOS • Android • WebAssembly •
51
51
  - ✅ Simple and extensible [single C++11 header][usearch-header] __library__.
52
52
  - ✅ [Trusted](#integrations) by giants like Google and DBs like [ClickHouse][clickhouse-docs] & [DuckDB][duckdb-docs].
53
53
  - ✅ [SIMD][simd]-optimized and [user-defined metrics](#user-defined-functions) with JIT compilation.
54
- - ✅ Hardware-agnostic `f16` & `i8` - [half-precision & quarter-precision support](#memory-efficiency-downcasting-and-quantization).
54
+ - ✅ Hardware-agnostic `bf16`, `e5m2`, & `i8` - [half-precision & quarter-precision support](#memory-efficiency-downcasting-and-quantization).
55
55
  - ✅ [View large indexes from disk](#serialization--serving-index-from-disk) without loading into RAM.
56
56
  - ✅ Heterogeneous lookups, renaming/relabeling, and on-the-fly deletions.
57
57
  - ✅ Binary Tanimoto and Sorensen coefficients for [Genomics and Chemistry applications](#usearch--rdkit--molecular-search).
@@ -138,7 +138,7 @@ The default storage/quantization level is hardware-dependant for efficiency, but
138
138
  index = Index(
139
139
  ndim=3, # Define the number of dimensions in input vectors
140
140
  metric='cos', # Choose 'l2sq', 'ip', 'haversine' or other metric, default = 'cos'
141
- dtype='bf16', # Store as 'f64', 'f32', 'f16', 'i8', 'b1'..., default = None
141
+ dtype='bf16', # Store as 'f64', 'f32', 'bf16', 'f16', 'e5m2', 'e4m3', 'e3m2', 'e2m3', 'u8', 'i8', 'b1'..., default = None
142
142
  connectivity=16, # Optional: Limit number of neighbors per graph node
143
143
  expansion_add=128, # Optional: Control the recall of indexing
144
144
  expansion_search=64, # Optional: Control the quality of the search
@@ -251,7 +251,7 @@ assert!(
251
251
  Training a quantization model and dimension-reduction is a common approach to accelerate vector search.
252
252
  Those, however, are only sometimes reliable, can significantly affect the statistical properties of your data, and require regular adjustments if your distribution shifts.
253
253
  Instead, we have focused on high-precision arithmetic over low-precision downcasted vectors.
254
- The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f64_t`, `f32_t`, `f16_t`, `i8_t`, and single-bit `b1x8_t` representations.
254
+ The same index, and `add` and `search` operations will automatically down-cast or up-cast between `f64_t`, `f32_t`, `bf16_t`, `f16_t`, `e5m2_t`, `e4m3_t`, `e3m2_t`, `e2m3_t`, `u8_t`, `i8_t`, and single-bit `b1x8_t` representations.
255
255
  You can use the following command to check, if hardware acceleration is enabled:
256
256
 
257
257
  ```sh
@@ -261,7 +261,9 @@ $ python -c 'from usearch.index import Index; print(Index(ndim=166, metric="tani
261
261
  > ice
262
262
  ```
263
263
 
264
- In most cases, it's recommended to use half-precision floating-point numbers on modern hardware.
264
+ In most cases, `bf16` is recommended for modern CPUs.
265
+ For even smaller footprints, USearch supports IEEE & MX-compatible Float8 (`e5m2` and `e4m3`) and Float6 (`e3m2` and `e2m3`) formats.
266
+ You can pass pre-quantized buffers from [NumKong](https://github.com/ashvardanian/numkong) with the explicit `dtype=` parameter on `add` and `search`, or let USearch handle the quantization internally from higher-precision inputs.
265
267
  When quantization is enabled, the "get"-like functions won't be able to recover the original data, so you may want to replicate the original vectors elsewhere.
266
268
  When quantizing to `i8_t` integers, note that it's only valid for cosine-like metrics.
267
269
  As part of the quantization process, the vectors are normalized to unit length and later scaled to [-127, 127] range to occupy the full 8-bit range.
@@ -479,46 +481,41 @@ The Haversine distance is available out of the box, but you can also define more
479
481
  from numba import cfunc, types, carray
480
482
  import math
481
483
 
482
- # Define the dimension as 2 for latitude and longitude
483
484
  ndim = 2
485
+ semi_major, flattening = 6378137.0, 1 / 298.257223563
486
+ semi_minor = (1 - flattening) * semi_major
487
+
488
+ def vincenty_distance(first_ptr, second_ptr):
489
+ first, second = carray(first_ptr, ndim), carray(second_ptr, ndim)
490
+ lat1, lon1, lat2, lon2 = first[0], first[1], second[0], second[1]
491
+ diff_lon = lon2 - lon1
492
+ rlat1, rlat2 = math.atan((1 - flattening) * math.tan(lat1)), math.atan((1 - flattening) * math.tan(lat2))
493
+ sin_rlat1, cos_rlat1 = math.sin(rlat1), math.cos(rlat1)
494
+ sin_rlat2, cos_rlat2 = math.sin(rlat2), math.cos(rlat2)
495
+ lon_on_sphere = diff_lon
496
+ for _ in range(100):
497
+ sin_lon, cos_lon = math.sin(lon_on_sphere), math.cos(lon_on_sphere)
498
+ sin_ang = math.sqrt((cos_rlat2 * sin_lon) ** 2 + (cos_rlat1 * sin_rlat2 - sin_rlat1 * cos_rlat2 * cos_lon) ** 2)
499
+ if sin_ang == 0: return 0.0
500
+ cos_ang = sin_rlat1 * sin_rlat2 + cos_rlat1 * cos_rlat2 * cos_lon
501
+ ang = math.atan2(sin_ang, cos_ang)
502
+ sin_az = cos_rlat1 * cos_rlat2 * sin_lon / sin_ang
503
+ cos2_az = 1 - sin_az ** 2
504
+ cos2_mid = cos_ang - 2 * sin_rlat1 * sin_rlat2 / cos2_az if cos2_az != 0 else 0.0
505
+ corr = flattening / 16 * cos2_az * (4 + flattening * (4 - 3 * cos2_az))
506
+ prev = lon_on_sphere
507
+ lon_on_sphere = diff_lon + (1 - corr) * flattening * (
508
+ sin_az * (ang + corr * sin_ang * (cos2_mid + corr * cos_ang * (-1 + 2 * cos2_mid ** 2))))
509
+ if abs(lon_on_sphere - prev) <= 1e-12: break
510
+ else:
511
+ return float('nan')
512
+ u_sq = cos2_az * (semi_major ** 2 - semi_minor ** 2) / (semi_minor ** 2)
513
+ ca = 1 + u_sq / 16384 * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq)))
514
+ cb = u_sq / 1024 * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq)))
515
+ delta = cb * sin_ang * (cos2_mid + cb / 4 * (cos_ang * (-1 + 2 * cos2_mid ** 2)
516
+ - cb / 6 * cos2_mid * (-3 + 4 * sin_ang ** 2) * (-3 + 4 * cos2_mid ** 2)))
517
+ return semi_minor * ca * (ang - delta) / 1000.0
484
518
 
485
- # Signature for the custom metric
486
- signature = types.float32(
487
- types.CPointer(types.float32),
488
- types.CPointer(types.float32))
489
-
490
- # WGS-84 ellipsoid parameters
491
- a = 6378137.0 # major axis in meters
492
- f = 1 / 298.257223563 # flattening
493
- b = (1 - f) * a # minor axis
494
-
495
- def vincenty_distance(a_ptr, b_ptr):
496
- a_array = carray(a_ptr, ndim)
497
- b_array = carray(b_ptr, ndim)
498
- lat1, lon1, lat2, lon2 = a_array[0], a_array[1], b_array[0], b_array[1]
499
- L, U1, U2 = lon2 - lon1, math.atan((1 - f) * math.tan(lat1)), math.atan((1 - f) * math.tan(lat2))
500
- sinU1, cosU1, sinU2, cosU2 = math.sin(U1), math.cos(U1), math.sin(U2), math.cos(U2)
501
- lambda_, iterLimit = L, 100
502
- while iterLimit > 0:
503
- iterLimit -= 1
504
- sinLambda, cosLambda = math.sin(lambda_), math.cos(lambda_)
505
- sinSigma = math.sqrt((cosU2 * sinLambda) ** 2 + (cosU1 * sinU2 - sinU1 * cosU2 * cosLambda) ** 2)
506
- if sinSigma == 0: return 0.0 # Co-incident points
507
- cosSigma, sigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda, math.atan2(sinSigma, cosSigma)
508
- sinAlpha, cos2Alpha = cosU1 * cosU2 * sinLambda / sinSigma, 1 - (cosU1 * cosU2 * sinLambda / sinSigma) ** 2
509
- cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cos2Alpha if not math.isnan(cosSigma - 2 * sinU1 * sinU2 / cos2Alpha) else 0 # Equatorial line
510
- C = f / 16 * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha))
511
- lambda_, lambdaP = L + (1 - C) * f * (sinAlpha * (sigma + C * sinSigma * (cos2SigmaM + C * cosSigma * (-1 + 2 * cos2SigmaM ** 2)))), lambda_
512
- if abs(lambda_ - lambdaP) <= 1e-12: break
513
- if iterLimit == 0: return float('nan') # formula failed to converge
514
- u2 = cos2Alpha * (a ** 2 - b ** 2) / (b ** 2)
515
- A = 1 + u2 / 16384 * (4096 + u2 * (-768 + u2 * (320 - 175 * u2)))
516
- B = u2 / 1024 * (256 + u2 * (-128 + u2 * (74 - 47 * u2)))
517
- deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * (cosSigma * (-1 + 2 * cos2SigmaM ** 2) - B / 6 * cos2SigmaM * (-3 + 4 * sinSigma ** 2) * (-3 + 4 * cos2SigmaM ** 2)))
518
- s = b * A * (sigma - deltaSigma)
519
- return s / 1000.0 # Distance in kilometers
520
-
521
- # Example usage:
522
519
  index = Index(ndim=ndim, metric=CompiledMetric(
523
520
  pointer=vincenty_distance.address,
524
521
  kind=MetricKind.Haversine,
@@ -547,13 +544,12 @@ index = Index(ndim=ndim, metric=CompiledMetric(
547
544
  ## Citations
548
545
 
549
546
  ```bibtex
550
- @software{Vardanian_USearch_2023,
547
+ @software{Vardanian_USearch,
551
548
  doi = {10.5281/zenodo.7949416},
552
549
  author = {Vardanian, Ash},
553
550
  title = {{USearch by Unum Cloud}},
554
551
  url = {https://github.com/unum-cloud/USearch},
555
- version = {2.25.0},
556
- year = {2023},
557
- month = oct,
552
+ version = {2.25.3},
553
+ year = {2026},
558
554
  }
559
555
  ```
package/c/README.md CHANGED
@@ -19,7 +19,7 @@ int main() {
19
19
  usearch_error_t error = NULL;
20
20
  usearch_init_options_t opts = {
21
21
  .metric_kind = usearch_metric_cos_k,
22
- .scalar_kind = usearch_scalar_f16_k,
22
+ .scalar_kind = usearch_scalar_f16_k, // or f32_k, bf16_k, e5m2_k, e4m3_k, e3m2_k, e2m3_k, i8_k, u8_k
23
23
  .dimensions = dimensions,
24
24
  .expansion_add = 0, // for defaults
25
25
  .expansion_search = 0 // for defaults
package/c/lib.cpp CHANGED
@@ -186,16 +186,14 @@ USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, use
186
186
 
187
187
  using state_result_t = typename index_dense_t::state_result_t;
188
188
  state_result_t state = index_dense_t::make(metric, config);
189
- if (!state)
189
+ if (!state) {
190
190
  *error = state.error.release();
191
- index_dense_t* result_ptr = new index_dense_t(std::move(state.index));
191
+ return NULL;
192
+ }
193
+ index_dense_t* result_ptr = new (std::nothrow) index_dense_t(std::move(state.index));
192
194
  if (!result_ptr)
193
195
  *error = "Out of memory!";
194
196
 
195
- // Let's immediately make it usable by reserving enough threads for this machine:
196
- if (!result_ptr->try_reserve(index_limits_t()))
197
- *error = "Out of memory when preparing contexts!";
198
-
199
197
  return result_ptr;
200
198
  }
201
199
 
@@ -236,8 +234,10 @@ USEARCH_EXPORT void usearch_metadata(char const* path, usearch_init_options_t* o
236
234
 
237
235
  USEARCH_ASSERT(path && options && error && "Missing arguments");
238
236
  index_dense_metadata_result_t result = index_dense_metadata_from_path(path);
239
- if (!result)
237
+ if (!result) {
240
238
  *error = result.error.release();
239
+ return;
240
+ }
241
241
 
242
242
  options->metric_kind = metric_kind_to_c(result.head.kind_metric);
243
243
  options->quantization = scalar_kind_to_c(result.head.kind_scalar);
@@ -285,8 +285,10 @@ USEARCH_EXPORT void usearch_metadata_buffer(void const* buffer, size_t length, u
285
285
  USEARCH_ASSERT(buffer && length && options && error && "Missing arguments");
286
286
  index_dense_metadata_result_t result =
287
287
  index_dense_metadata_from_buffer(memory_mapped_file_t((byte_t*)(buffer), length));
288
- if (!result)
288
+ if (!result) {
289
289
  *error = result.error.release();
290
+ return;
291
+ }
290
292
 
291
293
  options->metric_kind = metric_kind_to_c(result.head.kind_metric);
292
294
  options->quantization = scalar_kind_to_c(result.head.kind_scalar);
@@ -354,7 +356,8 @@ USEARCH_EXPORT void usearch_change_threads_add(usearch_index_t index, size_t thr
354
356
  auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
355
357
  index_limits_t limits = index_dense.limits();
356
358
  limits.threads_add = threads;
357
- index_dense.try_reserve(limits);
359
+ if (!index_dense.try_reserve(limits))
360
+ *error = "Out of memory!";
358
361
  }
359
362
 
360
363
  USEARCH_EXPORT void usearch_change_threads_search(usearch_index_t index, size_t threads, usearch_error_t* error) {
@@ -362,15 +365,22 @@ USEARCH_EXPORT void usearch_change_threads_search(usearch_index_t index, size_t
362
365
  auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
363
366
  index_limits_t limits = index_dense.limits();
364
367
  limits.threads_search = threads;
365
- index_dense.try_reserve(limits);
368
+ if (!index_dense.try_reserve(limits))
369
+ *error = "Out of memory!";
366
370
  }
367
371
 
368
372
  USEARCH_EXPORT void usearch_change_metric_kind(usearch_index_t index, usearch_metric_kind_t kind,
369
373
  usearch_error_t* error) {
370
374
  USEARCH_ASSERT(index && error && "Missing arguments");
371
375
  auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
372
- index_dense.change_metric(
373
- metric_punned_t::builtin(index_dense.dimensions(), metric_kind_to_cpp(kind), index_dense.scalar_kind()));
376
+ auto metric_punned =
377
+ metric_punned_t::builtin(index_dense.dimensions(), metric_kind_to_cpp(kind), index_dense.scalar_kind());
378
+ if (metric_punned.missing()) {
379
+ *error = "Unsupported metric for this index's dimensions and scalar kind!";
380
+ return;
381
+ }
382
+ if (!index_dense.try_change_metric(std::move(metric_punned)))
383
+ *error = "Failed to grow cast buffer for the new metric!";
374
384
  }
375
385
 
376
386
  USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_t metric, void* state,
@@ -384,7 +394,12 @@ USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_
384
394
  : metric_punned_t::stateless(index_dense.dimensions(), reinterpret_cast<std::uintptr_t>(metric),
385
395
  metric_punned_signature_t::array_array_k, metric_kind_to_cpp(kind),
386
396
  index_dense.scalar_kind());
387
- index_dense.change_metric(std::move(metric_punned));
397
+ if (metric_punned.missing()) {
398
+ *error = "Unsupported metric for this index's dimensions and scalar kind!";
399
+ return;
400
+ }
401
+ if (!index_dense.try_change_metric(std::move(metric_punned)))
402
+ *error = "Failed to grow cast buffer for the new metric!";
388
403
  }
389
404
 
390
405
  USEARCH_EXPORT void usearch_reserve(usearch_index_t index, size_t capacity, usearch_error_t* error) {
package/c/test.c CHANGED
@@ -448,6 +448,46 @@ void test_view(size_t const collection_size, size_t const dimensions) {
448
448
  printf("Test: View - PASSED\n");
449
449
  }
450
450
 
451
+ void test_mini_float_quantizations(size_t const collection_size, size_t const dimensions) {
452
+ printf("Test: Mini-float quantizations... %zu vectors, %zu dimensions\n", collection_size, dimensions);
453
+ usearch_scalar_kind_t kinds[] = {
454
+ usearch_scalar_e5m2_k,
455
+ usearch_scalar_e4m3_k,
456
+ usearch_scalar_e3m2_k,
457
+ usearch_scalar_e2m3_k,
458
+ };
459
+ float* data = create_vectors(collection_size, dimensions);
460
+ usearch_key_t* keys = (usearch_key_t*)malloc(collection_size * sizeof(usearch_key_t));
461
+ float* distances = (float*)malloc(collection_size * sizeof(float));
462
+ expect(keys && distances, "Failed to allocate memory");
463
+
464
+ for (size_t k = 0; k < sizeof(kinds) / sizeof(kinds[0]); ++k) {
465
+ usearch_error_t error = NULL;
466
+ usearch_init_options_t opts = create_options(dimensions);
467
+ opts.quantization = kinds[k];
468
+ usearch_index_t index = usearch_init(&opts, &error);
469
+ expect(!error, error);
470
+ usearch_reserve(index, collection_size, &error);
471
+ expect(!error, error);
472
+ for (size_t i = 0; i < collection_size; ++i) {
473
+ usearch_add(index, (usearch_key_t)i, data + i * dimensions, usearch_scalar_f32_k, &error);
474
+ expect(!error, error);
475
+ }
476
+ expect_eq(usearch_size(index, &error), collection_size, error);
477
+ for (size_t i = 0; i < collection_size; ++i) {
478
+ size_t found =
479
+ usearch_search(index, data + i * dimensions, usearch_scalar_f32_k, 1, keys, distances, &error);
480
+ expect(!error, error);
481
+ expect(found >= 1, "Vector not found");
482
+ }
483
+ usearch_free(index, &error);
484
+ }
485
+ free(data);
486
+ free(keys);
487
+ free(distances);
488
+ printf("Test: Mini-float quantizations - PASSED\n");
489
+ }
490
+
451
491
  int main(int argc, char const* argv[]) {
452
492
  install_crash_handlers();
453
493
  printf("Running tests...\n");
@@ -464,6 +504,7 @@ int main(int argc, char const* argv[]) {
464
504
  test_remove_vector(collection_sizes[index], dimensions[jdx]);
465
505
  test_save_load(collection_sizes[index], dimensions[jdx]);
466
506
  test_view(collection_sizes[index], dimensions[jdx]);
507
+ test_mini_float_quantizations(collection_sizes[index], dimensions[jdx]);
467
508
  }
468
509
  }
469
510
 
package/cpp/test.cpp CHANGED
@@ -17,6 +17,7 @@
17
17
  #include <csignal> // `std::signal`, `SIGSEGV`, ...
18
18
  #include <cstdio> // `std::fprintf`
19
19
  #include <cstdlib> // `std::_Exit`
20
+ #include <limits> // `std::numeric_limits`
20
21
 
21
22
  #include <algorithm> // `std::shuffle`
22
23
  #include <random> // `std::default_random_engine`
@@ -216,6 +217,47 @@ void test_uint40() {
216
217
  expect_eq(u40_default, uint40_t(0u));
217
218
  }
218
219
 
220
+ void test_checked_size_arithmetic() {
221
+
222
+ std::printf("Testing checked size arithmetic\n");
223
+ std::size_t max = (std::numeric_limits<std::size_t>::max)();
224
+
225
+ checked_size_result_t cast = checked_size_from_u64(42);
226
+ expect(cast);
227
+ expect_eq(cast.value, static_cast<std::size_t>(42));
228
+ if (sizeof(std::size_t) < sizeof(std::uint64_t))
229
+ expect(!checked_size_from_u64((std::numeric_limits<std::uint64_t>::max)()));
230
+
231
+ checked_size_result_t sum = checked_add(max - 1, 1);
232
+ expect(sum);
233
+ expect_eq(sum.value, max);
234
+ expect(!checked_add(max, 1));
235
+
236
+ checked_size_result_t product = checked_mul(max / 2, 2);
237
+ expect(product);
238
+ expect_eq(product.value, max - 1);
239
+ expect(!checked_mul(max / 2 + 1, 2));
240
+
241
+ checked_size_result_t fused = checked_mul_add(10, 20, 30);
242
+ expect(fused);
243
+ expect_eq(fused.value, static_cast<std::size_t>(230));
244
+ expect(!checked_mul_add(max / 2 + 1, 2, 0));
245
+ expect(!checked_mul_add(max / 2, 2, 2));
246
+
247
+ checked_size_result_t rounded = checked_round_up(17, 8);
248
+ expect(rounded);
249
+ expect_eq(rounded.value, static_cast<std::size_t>(24));
250
+ expect(!checked_round_up(max, 8));
251
+
252
+ checked_size_result_t power = checked_ceil2(17);
253
+ expect(power);
254
+ expect_eq(power.value, static_cast<std::size_t>(32));
255
+ checked_size_result_t zero_power = checked_ceil2(0);
256
+ expect(zero_power);
257
+ expect_eq(zero_power.value, static_cast<std::size_t>(0));
258
+ expect(!checked_ceil2(max));
259
+ }
260
+
219
261
  /**
220
262
  * @brief Tests the functionality of the custom float16_t type ensuring consistent.
221
263
  */
@@ -1264,6 +1306,61 @@ static void install_crash_handlers() {
1264
1306
  std::signal(signal_number, &usearch_crash_handler);
1265
1307
  }
1266
1308
 
1309
+ /**
1310
+ * @brief Regression test: `make(metric, config)` must return an index that is
1311
+ * immediately usable - no explicit `reserve` required before `load` /
1312
+ * `view` / `search`. Previously the typed graph's `{0, 0}` thread
1313
+ * limits leaked into `load_from_stream`, leaving `available_threads_`
1314
+ * empty and making the first `search` throw "No available threads to
1315
+ * lock".
1316
+ */
1317
+ void test_load_after_metric_make() {
1318
+ std::printf("Testing load and view into a metric-made index\n");
1319
+
1320
+ using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
1321
+ std::size_t const dimensions = 32;
1322
+ std::size_t const collection = 64;
1323
+
1324
+ std::default_random_engine rng(7);
1325
+ std::uniform_real_distribution<float> distribution(-1.f, 1.f);
1326
+ std::vector<std::vector<float>> data(collection);
1327
+ for (auto& vector : data) {
1328
+ vector.resize(dimensions);
1329
+ for (auto& value : vector)
1330
+ value = distribution(rng);
1331
+ }
1332
+
1333
+ metric_punned_t metric(dimensions, metric_kind_t::cos_k, scalar_kind<f32_t>());
1334
+ index_dense_config_t config(16);
1335
+
1336
+ // Build an index and persist it to disk.
1337
+ index_t::state_result_t built = index_t::make(metric, config);
1338
+ expect(built);
1339
+ expect(built.index.try_reserve(collection));
1340
+ for (std::size_t i = 0; i != collection; ++i)
1341
+ expect(built.index.add(static_cast<std::int64_t>(i), data[i].data()));
1342
+ char const* path = "tmp_metric_make.usearch";
1343
+ expect(built.index.save(path));
1344
+
1345
+ // Load into a fresh, metric-made index that was never explicitly reserved.
1346
+ // The first `search` must not throw "No available threads to lock".
1347
+ index_t::state_result_t loaded = index_t::make(metric, config);
1348
+ expect(loaded);
1349
+ expect(loaded.index.load(path));
1350
+ expect_eq(loaded.index.size(), collection);
1351
+ std::int64_t found[8];
1352
+ expect(loaded.index.search(data[0].data(), 5).dump_to(found) != 0);
1353
+
1354
+ // Same check for the memory-mapped `view` path.
1355
+ index_t::state_result_t viewed = index_t::make(metric, config);
1356
+ expect(viewed);
1357
+ expect(viewed.index.view(path));
1358
+ expect_eq(viewed.index.size(), collection);
1359
+ expect(viewed.index.search(data[0].data(), 5).dump_to(found) != 0);
1360
+
1361
+ std::remove(path);
1362
+ }
1363
+
1267
1364
  int main(int, char**) {
1268
1365
  install_crash_handlers();
1269
1366
 
@@ -1272,6 +1369,7 @@ int main(int, char**) {
1272
1369
 
1273
1370
  // Non-default floating-point types may result in many compilation & rounding issues.
1274
1371
  test_uint40();
1372
+ test_checked_size_arithmetic();
1275
1373
  test_cosine<f32_t, std::int64_t, uint40_t>(10, 10);
1276
1374
  test_cosine<bf16_t, std::int64_t, uint40_t>(10, 10);
1277
1375
  test_cosine<f16_t, std::int64_t, uint40_t>(10, 10);
@@ -1354,5 +1452,6 @@ int main(int, char**) {
1354
1452
 
1355
1453
  test_filtered_search();
1356
1454
  test_isolate();
1455
+ test_load_after_metric_make();
1357
1456
  return 0;
1358
1457
  }