usearch 2.14.0 → 2.15.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/README.md CHANGED
@@ -544,7 +544,7 @@ doi = {10.5281/zenodo.7949416},
544
544
  author = {Vardanian, Ash},
545
545
  title = {{USearch by Unum Cloud}},
546
546
  url = {https://github.com/unum-cloud/usearch},
547
- version = {2.14.0},
547
+ version = {2.15.1},
548
548
  year = {2023},
549
549
  month = oct,
550
550
  }
package/cpp/bench.cpp CHANGED
@@ -468,6 +468,7 @@ struct args_t {
468
468
 
469
469
  bool big = false;
470
470
 
471
+ bool quantize_bf16 = false;
471
472
  bool quantize_f16 = false;
472
473
  bool quantize_i8 = false;
473
474
  bool quantize_b1 = false;
@@ -500,6 +501,8 @@ struct args_t {
500
501
  }
501
502
 
502
503
  scalar_kind_t quantization() const noexcept {
504
+ if (quantize_bf16)
505
+ return scalar_kind_t::bf16_k;
503
506
  if (quantize_f16)
504
507
  return scalar_kind_t::f16_k;
505
508
  if (quantize_i8)
@@ -611,6 +614,7 @@ int main(int argc, char** argv) {
611
614
  (option("--rows-skip") & value("integer", args.vectors_to_skip)).doc("Number of vectors to skip"),
612
615
  (option("--rows-take") & value("integer", args.vectors_to_take)).doc("Number of vectors to take"),
613
616
  ( //
617
+ option("-bf16", "--bf16quant").set(args.quantize_bf16).doc("Enable `bf16_t` quantization") |
614
618
  option("-f16", "--f16quant").set(args.quantize_f16).doc("Enable `f16_t` quantization") |
615
619
  option("-i8", "--i8quant").set(args.quantize_i8).doc("Enable `i8_t` quantization") |
616
620
  option("-b1", "--b1quant").set(args.quantize_b1).doc("Enable `b1x8_t` quantization")),
@@ -8,8 +8,8 @@
8
8
  #define UNUM_USEARCH_HPP
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
- #define USEARCH_VERSION_MINOR 14
12
- #define USEARCH_VERSION_PATCH 0
11
+ #define USEARCH_VERSION_MINOR 15
12
+ #define USEARCH_VERSION_PATCH 1
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -950,7 +950,7 @@ class index_dense_gt {
950
950
 
951
951
  void reserve(index_limits_t limits) {
952
952
  if (!try_reserve(limits))
953
- throw std::bad_alloc();
953
+ __usearch_raise_runtime_error("failed to reserve memory");
954
954
  }
955
955
 
956
956
  /**
@@ -72,25 +72,8 @@ struct uuid_t {
72
72
  class f16_bits_t;
73
73
  class bf16_bits_t;
74
74
 
75
- #if !USEARCH_USE_FP16LIB
76
- #if USEARCH_USE_SIMSIMD
77
- using f16_native_t = simsimd_f16_t;
78
- using bf16_native_t = simsimd_bf16_t;
79
- #elif defined(USEARCH_DEFINED_ARM)
80
- using f16_native_t = __fp16;
81
- using bf16_native_t = __fp16; // No better choice on most compilers!
82
- #else
83
- using f16_native_t = _Float16;
84
- using bf16_native_t = _Float16; // No better choice on most compilers!
85
- #endif
86
- using f16_t = f16_native_t;
87
- using bf16_t = bf16_native_t;
88
- #else
89
- using f16_native_t = void;
90
- using bf16_native_t = void;
91
75
  using f16_t = f16_bits_t;
92
76
  using bf16_t = bf16_bits_t;
93
- #endif
94
77
 
95
78
  using f64_t = double;
96
79
  using f32_t = float;
@@ -156,12 +139,9 @@ enum class scalar_kind_t : std::uint8_t {
156
139
  i8_k = 23,
157
140
  };
158
141
 
159
- enum class prefetching_kind_t {
160
- none_k,
161
- cpu_k,
162
- io_uring_k,
163
- };
164
-
142
+ /**
143
+ * @brief Maps a scalar type to its corresponding scalar_kind_t enumeration value.
144
+ */
165
145
  template <typename scalar_at> scalar_kind_t scalar_kind() noexcept {
166
146
  if (std::is_same<scalar_at, b1x8_t>())
167
147
  return scalar_kind_t::b1x8_k;
@@ -198,22 +178,43 @@ template <typename scalar_at> scalar_kind_t scalar_kind() noexcept {
198
178
  return scalar_kind_t::unknown_k;
199
179
  }
200
180
 
181
+ /**
182
+ * @brief Converts an angle from degrees to radians.
183
+ */
201
184
  template <typename at> at angle_to_radians(at angle) noexcept { return angle * at(3.14159265358979323846) / at(180); }
202
185
 
186
+ /**
187
+ * @brief Readability helper to compute the square of a given value.
188
+ */
203
189
  template <typename at> at square(at value) noexcept { return value * value; }
204
190
 
191
+ /**
192
+ * @brief Clamps a value between a lower and upper bound using a custom comparator. Similar to `std::clamp`.
193
+ * https://en.cppreference.com/w/cpp/algorithm/clamp
194
+ */
205
195
  template <typename at, typename compare_at> inline at clamp(at v, at lo, at hi, compare_at comp) noexcept {
206
196
  return comp(v, lo) ? lo : comp(hi, v) ? hi : v;
207
197
  }
198
+
199
+ /**
200
+ * @brief Clamps a value between a lower and upper bound. Similar to `std::clamp`.
201
+ * https://en.cppreference.com/w/cpp/algorithm/clamp
202
+ */
208
203
  template <typename at> inline at clamp(at v, at lo, at hi) noexcept {
209
204
  return usearch::clamp(v, lo, hi, std::less<at>{});
210
205
  }
211
206
 
212
- inline bool str_equals(char const* begin, std::size_t len, char const* other_begin) noexcept {
213
- std::size_t other_len = std::strlen(other_begin);
214
- return len == other_len && std::strncmp(begin, other_begin, len) == 0;
207
+ /**
208
+ * @brief Compares two strings for equality, given a length for the first string.
209
+ */
210
+ inline bool str_equals(char const* first_begin, std::size_t first_len, char const* second_begin) noexcept {
211
+ std::size_t second_len = std::strlen(second_begin);
212
+ return first_len == second_len && std::strncmp(first_begin, second_begin, first_len) == 0;
215
213
  }
216
214
 
215
+ /**
216
+ * @brief Returns the number of bits required to represent a scalar type.
217
+ */
217
218
  inline std::size_t bits_per_scalar(scalar_kind_t scalar_kind) noexcept {
218
219
  switch (scalar_kind) {
219
220
  case scalar_kind_t::uuid_k: return 128;
@@ -236,6 +237,10 @@ inline std::size_t bits_per_scalar(scalar_kind_t scalar_kind) noexcept {
236
237
  }
237
238
  }
238
239
 
240
+ /**
241
+ * @brief Returns the number of bits in a scalar word for a given scalar type.
242
+ * Equivalent to `bits_per_scalar` for types that are not bit-packed.
243
+ */
239
244
  inline std::size_t bits_per_scalar_word(scalar_kind_t scalar_kind) noexcept {
240
245
  switch (scalar_kind) {
241
246
  case scalar_kind_t::uuid_k: return 128;
@@ -258,6 +263,9 @@ inline std::size_t bits_per_scalar_word(scalar_kind_t scalar_kind) noexcept {
258
263
  }
259
264
  }
260
265
 
266
+ /**
267
+ * @brief Returns the string name of a given scalar type.
268
+ */
261
269
  inline char const* scalar_kind_name(scalar_kind_t scalar_kind) noexcept {
262
270
  switch (scalar_kind) {
263
271
  case scalar_kind_t::uuid_k: return "uuid";
@@ -280,6 +288,9 @@ inline char const* scalar_kind_name(scalar_kind_t scalar_kind) noexcept {
280
288
  }
281
289
  }
282
290
 
291
+ /**
292
+ * @brief Returns the string name of a given distance metric.
293
+ */
283
294
  inline char const* metric_kind_name(metric_kind_t metric) noexcept {
284
295
  switch (metric) {
285
296
  case metric_kind_t::unknown_k: return "unknown";
@@ -296,6 +307,10 @@ inline char const* metric_kind_name(metric_kind_t metric) noexcept {
296
307
  default: return "";
297
308
  }
298
309
  }
310
+
311
+ /**
312
+ * @brief Parses a string to identify the corresponding `scalar_kind_t` enumeration value.
313
+ */
299
314
  inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::size_t len) {
300
315
  expected_gt<scalar_kind_t> parsed;
301
316
  if (str_equals(name, len, "f32"))
@@ -315,10 +330,16 @@ inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::s
315
330
  return parsed;
316
331
  }
317
332
 
333
+ /**
334
+ * @brief Parses a string to identify the corresponding `scalar_kind_t` enumeration value.
335
+ */
318
336
  inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name) {
319
337
  return scalar_kind_from_name(name, std::strlen(name));
320
338
  }
321
339
 
340
+ /**
341
+ * @brief Parses a string to identify the corresponding `metric_kind_t` enumeration value.
342
+ */
322
343
  inline expected_gt<metric_kind_t> metric_from_name(char const* name, std::size_t len) {
323
344
  expected_gt<metric_kind_t> parsed;
324
345
  if (str_equals(name, len, "l2sq") || str_equals(name, len, "euclidean_sq")) {
@@ -344,6 +365,10 @@ inline expected_gt<metric_kind_t> metric_from_name(char const* name, std::size_t
344
365
  "tanimoto, sorensen");
345
366
  return parsed;
346
367
  }
368
+
369
+ /**
370
+ * @brief Parses a string to identify the corresponding `metric_kind_t` enumeration value.
371
+ */
347
372
  inline expected_gt<metric_kind_t> metric_from_name(char const* name) {
348
373
  return metric_from_name(name, std::strlen(name));
349
374
  }
@@ -352,12 +377,14 @@ inline expected_gt<metric_kind_t> metric_from_name(char const* name) {
352
377
  * @brief Convenience function to upcast a half-precision floating point number to a single-precision one.
353
378
  */
354
379
  inline float f16_to_f32(std::uint16_t u16) noexcept {
355
- #if !USEARCH_USE_FP16LIB
380
+ #if USEARCH_USE_FP16LIB
381
+ return fp16_ieee_to_fp32_value(u16);
382
+ #elif USEARCH_USE_SIMSIMD
383
+ return simsimd_uncompress_f16(u16);
384
+ #else
356
385
  f16_native_t f16;
357
386
  std::memcpy(&f16, &u16, sizeof(std::uint16_t));
358
387
  return float(f16);
359
- #else
360
- return fp16_ieee_to_fp32_value(u16);
361
388
  #endif
362
389
  }
363
390
 
@@ -365,13 +392,15 @@ inline float f16_to_f32(std::uint16_t u16) noexcept {
365
392
  * @brief Convenience function to downcast a single-precision floating point number to a half-precision one.
366
393
  */
367
394
  inline std::uint16_t f32_to_f16(float f32) noexcept {
368
- #if !USEARCH_USE_FP16LIB
395
+ #if USEARCH_USE_FP16LIB
396
+ return fp16_ieee_from_fp32_value(f32);
397
+ #elif USEARCH_USE_SIMSIMD
398
+ return simsimd_compress_f16(f32);
399
+ #else
369
400
  f16_native_t f16 = f16_native_t(f32);
370
401
  std::uint16_t u16;
371
402
  std::memcpy(&u16, &f16, sizeof(std::uint16_t));
372
403
  return u16;
373
- #else
374
- return fp16_ieee_from_fp32_value(f32);
375
404
  #endif
376
405
  }
377
406
 
@@ -380,6 +409,9 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
380
409
  * https://github.com/ashvardanian/SimSIMD/blob/ff51434d90c66f916e94ff05b24530b127aa4cff/include/simsimd/types.h#L395-L410
381
410
  */
382
411
  inline float bf16_to_f32(std::uint16_t u16) noexcept {
412
+ #if USEARCH_USE_SIMSIMD
413
+ return simsimd_uncompress_bf16(u16);
414
+ #else
383
415
  union float_or_unsigned_int_t {
384
416
  float f;
385
417
  unsigned int i;
@@ -387,6 +419,7 @@ inline float bf16_to_f32(std::uint16_t u16) noexcept {
387
419
  union float_or_unsigned_int_t result_union;
388
420
  result_union.i = u16 << 16; // Zero extends the mantissa
389
421
  return result_union.f;
422
+ #endif
390
423
  }
391
424
 
392
425
  /**
@@ -394,6 +427,9 @@ inline float bf16_to_f32(std::uint16_t u16) noexcept {
394
427
  * https://github.com/ashvardanian/SimSIMD/blob/ff51434d90c66f916e94ff05b24530b127aa4cff/include/simsimd/types.h#L412-L425
395
428
  */
396
429
  inline std::uint16_t f32_to_bf16(float f32) noexcept {
430
+ #if USEARCH_USE_SIMSIMD
431
+ return simsimd_compress_bf16(f32);
432
+ #else
397
433
  union float_or_unsigned_int_t {
398
434
  float f;
399
435
  unsigned int i;
@@ -403,6 +439,7 @@ inline std::uint16_t f32_to_bf16(float f32) noexcept {
403
439
  value.i >>= 16;
404
440
  value.i &= 0xFFFF;
405
441
  return (unsigned short)value.i;
442
+ #endif
406
443
  }
407
444
 
408
445
  /**
@@ -428,7 +465,7 @@ class f16_bits_t {
428
465
  inline f16_bits_t(float v) noexcept : uint16_(f32_to_f16(v)) {}
429
466
  inline f16_bits_t(double v) noexcept : uint16_(f32_to_f16(static_cast<float>(v))) {}
430
467
 
431
- inline bool operator<(const f16_bits_t& other) const noexcept { return float(*this) < float(other); }
468
+ inline bool operator<(f16_bits_t const& other) const noexcept { return float(*this) < float(other); }
432
469
 
433
470
  inline f16_bits_t operator+(f16_bits_t other) const noexcept { return {float(*this) + float(other)}; }
434
471
  inline f16_bits_t operator-(f16_bits_t other) const noexcept { return {float(*this) - float(other)}; }
@@ -1047,6 +1084,10 @@ template <> struct cast_gt<f16_bits_t, f16_bits_t> {
1047
1084
  static bool try_(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
1048
1085
  };
1049
1086
 
1087
+ template <> struct cast_gt<bf16_bits_t, bf16_bits_t> {
1088
+ static bool try_(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
1089
+ };
1090
+
1050
1091
  template <> struct cast_gt<i8_t, i8_t> {
1051
1092
  static bool try_(byte_t const*, std::size_t, byte_t*) noexcept { return false; }
1052
1093
  };
@@ -1119,23 +1160,23 @@ template <typename to_scalar_at> struct cast_from_i8_gt {
1119
1160
  }
1120
1161
  };
1121
1162
 
1122
- template <> struct cast_gt<i8_t, f16_t> : public cast_from_i8_gt<f16_t> {};
1123
- template <> struct cast_gt<i8_t, bf16_t> : public cast_from_i8_gt<bf16_t> {};
1163
+ template <> struct cast_gt<i8_t, f16_bits_t> : public cast_from_i8_gt<f16_t> {};
1164
+ template <> struct cast_gt<i8_t, bf16_bits_t> : public cast_from_i8_gt<bf16_t> {};
1124
1165
  template <> struct cast_gt<i8_t, f32_t> : public cast_from_i8_gt<f32_t> {};
1125
1166
  template <> struct cast_gt<i8_t, f64_t> : public cast_from_i8_gt<f64_t> {};
1126
1167
 
1127
- template <> struct cast_gt<f16_t, i8_t> : public cast_to_i8_gt<f16_t> {};
1128
- template <> struct cast_gt<bf16_t, i8_t> : public cast_to_i8_gt<bf16_t> {};
1168
+ template <> struct cast_gt<f16_bits_t, i8_t> : public cast_to_i8_gt<f16_t> {};
1169
+ template <> struct cast_gt<bf16_bits_t, i8_t> : public cast_to_i8_gt<bf16_t> {};
1129
1170
  template <> struct cast_gt<f32_t, i8_t> : public cast_to_i8_gt<f32_t> {};
1130
1171
  template <> struct cast_gt<f64_t, i8_t> : public cast_to_i8_gt<f64_t> {};
1131
1172
 
1132
- template <> struct cast_gt<b1x8_t, f16_t> : public cast_from_b1x8_gt<f16_t> {};
1133
- template <> struct cast_gt<b1x8_t, bf16_t> : public cast_from_b1x8_gt<bf16_t> {};
1173
+ template <> struct cast_gt<b1x8_t, f16_bits_t> : public cast_from_b1x8_gt<f16_t> {};
1174
+ template <> struct cast_gt<b1x8_t, bf16_bits_t> : public cast_from_b1x8_gt<bf16_t> {};
1134
1175
  template <> struct cast_gt<b1x8_t, f32_t> : public cast_from_b1x8_gt<f32_t> {};
1135
1176
  template <> struct cast_gt<b1x8_t, f64_t> : public cast_from_b1x8_gt<f64_t> {};
1136
1177
 
1137
- template <> struct cast_gt<f16_t, b1x8_t> : public cast_to_b1x8_gt<f16_t> {};
1138
- template <> struct cast_gt<bf16_t, b1x8_t> : public cast_to_b1x8_gt<bf16_t> {};
1178
+ template <> struct cast_gt<f16_bits_t, b1x8_t> : public cast_to_b1x8_gt<f16_t> {};
1179
+ template <> struct cast_gt<bf16_bits_t, b1x8_t> : public cast_to_b1x8_gt<bf16_t> {};
1139
1180
  template <> struct cast_gt<f32_t, b1x8_t> : public cast_to_b1x8_gt<f32_t> {};
1140
1181
  template <> struct cast_gt<f64_t, b1x8_t> : public cast_to_b1x8_gt<f64_t> {};
1141
1182
 
@@ -1426,7 +1467,10 @@ template <typename scalar_at = float, typename result_at = float> struct metric_
1426
1467
  }
1427
1468
  };
1428
1469
 
1429
- struct cos_i8_t {
1470
+ /**
1471
+ * @brief Cosine (Angular) distance for signed 8-bit integers using 16-bit intermediates.
1472
+ */
1473
+ struct metric_cos_i8_t {
1430
1474
  using scalar_t = i8_t;
1431
1475
  using result_t = f32_t;
1432
1476
 
@@ -1452,7 +1496,11 @@ struct cos_i8_t {
1452
1496
  }
1453
1497
  };
1454
1498
 
1455
- struct l2sq_i8_t {
1499
+ /**
1500
+ * @brief Squared Euclidean (L2) distance for signed 8-bit integers using 16-bit intermediates.
1501
+ * Square root is avoided at the end, as it won't affect the ordering.
1502
+ */
1503
+ struct metric_l2sq_i8_t {
1456
1504
  using scalar_t = i8_t;
1457
1505
  using result_t = f32_t;
1458
1506
 
@@ -1782,7 +1830,7 @@ class metric_punned_t {
1782
1830
  case metric_kind_t::cos_k: {
1783
1831
  switch (scalar_kind_) {
1784
1832
  case scalar_kind_t::bf16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<bf16_t, f32_t>>; break;
1785
- case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<i8_t, f32_t>>; break;
1833
+ case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_i8_t>; break;
1786
1834
  case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f16_t, f32_t>>; break;
1787
1835
  case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f32_t>>; break;
1788
1836
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_cos_gt<f64_t>>; break;
@@ -1793,7 +1841,7 @@ class metric_punned_t {
1793
1841
  case metric_kind_t::l2sq_k: {
1794
1842
  switch (scalar_kind_) {
1795
1843
  case scalar_kind_t::bf16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<bf16_t, f32_t>>; break;
1796
- case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<i8_t, f32_t>>; break;
1844
+ case scalar_kind_t::i8_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_i8_t>; break;
1797
1845
  case scalar_kind_t::f16_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f16_t, f32_t>>; break;
1798
1846
  case scalar_kind_t::f32_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f32_t>>; break;
1799
1847
  case scalar_kind_t::f64_k: metric_ptr_ = (uptr_t)&equidimensional_<metric_l2sq_gt<f64_t>>; break;
@@ -2088,7 +2136,7 @@ class flat_hash_multi_set_gt {
2088
2136
  // Allocate new memory
2089
2137
  data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
2090
2138
  if (!data_)
2091
- throw std::bad_alloc();
2139
+ __usearch_raise_runtime_error("failed memory allocation");
2092
2140
 
2093
2141
  // Copy metadata
2094
2142
  buckets_ = other.buckets_;
@@ -2128,7 +2176,7 @@ class flat_hash_multi_set_gt {
2128
2176
  // Allocate new memory
2129
2177
  data_ = (char*)allocator_t{}.allocate(other.buckets_ * bytes_per_bucket());
2130
2178
  if (!data_)
2131
- throw std::bad_alloc();
2179
+ __usearch_raise_runtime_error("failed memory allocation");
2132
2180
 
2133
2181
  // Copy metadata
2134
2182
  buckets_ = other.buckets_;
@@ -2501,7 +2549,7 @@ class flat_hash_multi_set_gt {
2501
2549
 
2502
2550
  void reserve(std::size_t capacity) {
2503
2551
  if (!try_reserve(capacity))
2504
- throw std::bad_alloc();
2552
+ __usearch_raise_runtime_error("failed to reserve memory");
2505
2553
  }
2506
2554
 
2507
2555
  bool try_emplace(element_t const& element) noexcept {
@@ -21,8 +21,9 @@ wasmer install unum/usearch
21
21
  Create an index, add vectors, and perform searches with ease:
22
22
 
23
23
  ```js
24
+ const assert = require('node:assert');
24
25
  const usearch = require('usearch');
25
- const index = new usearch.Index({ metric: 'cos', connectivity: 16, dimensions: 3 });
26
+ const index = new usearch.Index({ metric: 'l2sq', connectivity: 16, dimensions: 3 });
26
27
  index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
27
28
  const results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);
28
29
 
@@ -47,13 +47,13 @@ test('Batch operations', () => {
47
47
  });
48
48
 
49
49
  test("Expected results", () => {
50
- var index = new usearch.Index({
50
+ const index = new usearch.Index({
51
51
  metric: "l2sq",
52
52
  connectivity: 16,
53
53
  dimensions: 3,
54
54
  });
55
55
  index.add(42n, new Float32Array([0.2, 0.6, 0.4]));
56
- var results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);
56
+ const results = index.search(new Float32Array([0.2, 0.6, 0.4]), 10);
57
57
 
58
58
  assert.equal(index.size(), 1);
59
59
  assert.deepEqual(results.keys, new BigUint64Array([42n]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.14.0",
3
+ "version": "2.15.1",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ash Vardanian (https://ashvardanian.com/)",
6
6
  "license": "Apache 2.0",
Binary file
Binary file