usearch 2.26.3 → 2.26.5

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
@@ -525,20 +525,39 @@ index = Index(ndim=ndim, metric=CompiledMetric(
525
525
 
526
526
  ## Integrations & Users
527
527
 
528
+ ### Databases & Query Engines
529
+
528
530
  - [x] ClickHouse: [C++](https://github.com/ClickHouse/ClickHouse/pull/53447), [docs](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/annindexes#usearch).
529
531
  - [x] DuckDB: [post](https://duckdb.org/2024/05/03/vector-similarity-search-vss.html).
530
532
  - [x] ScyllaDB: [Rust](https://github.com/scylladb/vector-store), [presentation](https://www.slideshare.net/slideshow/vector-search-with-scylladb-by-szymon-wasik/276571548).
531
533
  - [x] TiDB & TiFlash: [C++](https://github.com/pingcap/tiflash), [announcement](https://www.pingcap.com/article/introduce-vector-search-indexes-in-tidb/).
532
- - [x] YugaByte: [C++](https://github.com/yugabyte/yugabyte-db/blob/366b9f5e3c4df3a1a17d553db41d6dc50146f488/src/yb/vector_index/usearch_wrapper.cc).
534
+ - [x] YugaByte: [C++](https://github.com/yugabyte/yugabyte-db/blob/366b9f5e3c4df3a1a17d553db41d6dc50146f488/src/yb/vector_index/usearch_wrapper.cc), [architecture](https://www.yugabyte.com/blog/yugabytedb-vector-indexing-architecture/), [1B-vector benchmark](https://www.yugabyte.com/blog/benchmarking-1-billion-vectors-in-yugabytedb/).
533
535
  - [x] MemGraph: [C++](https://github.com/memgraph/memgraph/blob/784dd8520f65050d033aea8b29446e84e487d091/src/storage/v2/indices/vector_index.cpp), [announcement](https://memgraph.com/blog/simplify-data-retrieval-memgraph-vector-search).
534
- - [x] Google: [UniSim](https://github.com/google/unisim), [RetSim](https://arxiv.org/abs/2311.17264) paper.
535
- - [x] LangChain: [Python](https://github.com/langchain-ai/langchain/releases/tag/v0.0.257) and [JavaScript](https://github.com/hwchase17/langchainjs/releases/tag/0.0.125).
536
- - [x] Microsoft Semantic Kernel: [Python](https://github.com/microsoft/semantic-kernel/releases/tag/python-0.3.9.dev) and C#.
536
+ - [x] MatrixOne: [GoLang](https://github.com/matrixorigin/matrixone).
537
+ - [x] GreptimeDB: [Rust](https://github.com/GreptimeTeam/greptimedb/blob/main/src/index/src/vector.rs).
538
+ - [x] KurrentDB: [C#](https://github.com/kurrent-io/KurrentDB/blob/master/src/KurrentDB.Kontext/Indexing/USearch/USearchVectorStore.cs), [credits](https://github.com/kurrent-io/KurrentDB/blob/master/NOTICE.md).
539
+ - [x] Chroma: [Rust](https://github.com/chroma-core/chroma/blob/main/rust/index/src/usearch.rs), behind their RaBitQ-quantized SPANN index.
540
+
541
+ ### AI Frameworks & Memory
542
+
543
+ - [x] Google: [Perch](https://github.com/google-research/perch-hoplite) bioacoustics DB, [UniSim](https://github.com/google/unisim), [RetSim](https://arxiv.org/abs/2311.17264) paper.
544
+ - [x] Microsoft: [Semantic Kernel](https://github.com/microsoft/semantic-kernel/releases/tag/python-0.3.9.dev) and [vcpkg](https://github.com/microsoft/vcpkg/tree/master/ports/usearch).
545
+ - [x] LangChain: [Python](https://github.com/langchain-ai/langchain/releases/tag/v0.0.257) and [JavaScript](https://github.com/langchain-ai/langchainjs/releases/tag/0.0.125).
546
+ - [x] Hugging Face: Sentence-Transformers [docs](https://www.sbert.net/docs/package_reference/quantization.html#sentence_transformers.quantization.semantic_search_usearch), [quantization benchmarks](https://huggingface.co/blog/embedding-quantization).
537
547
  - [x] GPTCache: [Python](https://github.com/zilliztech/GPTCache/releases/tag/0.1.29).
538
- - [x] Sentence-Transformers: Python [docs](https://www.sbert.net/docs/package_reference/quantization.html#sentence_transformers.quantization.semantic_search_usearch).
548
+ - [x] MemMachine: [Python](https://github.com/MemMachine/MemMachine/blob/main/packages/server/src/memmachine_server/common/vector_store/vector_search_engine/usearch_engine.py).
539
549
  - [x] Pathway: [Rust](https://github.com/pathwaycom/pathway).
540
550
  - [x] Vald: [GoLang](https://github.com/vdaas/vald).
541
- - [x] MatrixOne: [GoLang](https://github.com/matrixorigin/matrixone).
551
+
552
+ ### Applications & Products
553
+
554
+ - [x] Arm MCP: [Python](https://github.com/arm/mcp/blob/main/arm_kb_search/loaders.py).
555
+ - [x] GPT4All: [C++](https://github.com/nomic-ai/gpt4all/blob/main/gpt4all-chat/src/database.cpp).
556
+ - [x] Ente: [Rust](https://github.com/ente-io/ente/blob/main/rust/crates/photos/src/ml/vector_db.rs), [post](https://ente.com/blog/vector-db/).
557
+ - [x] Tracy profiler: [C++](https://github.com/wolfpld/tracy/blob/master/profiler/src/profiler/TracyLlmEmbeddings.cpp), behind the embeddings index of its LLM assistant.
558
+ - [x] Copilot for Xcode by Intitni: [Swift](https://github.com/intitni/CopilotForXcode/blob/main/Tool/Sources/USearchIndex/UsearchIndex.swift).
559
+ - [x] LLM for Unity: [C#](https://github.com/undreamai/LLMUnity/tree/main/Runtime/RAG/usearch), [docs](https://undream.ai/LLMUnity/).
560
+ - [x] Music Assistant: [Python](https://github.com/music-assistant/server/blob/dev/music_assistant/providers/sonic_similarity/vectors.py), [docs](https://www.music-assistant.io/plugins/sonic-similarity/).
542
561
 
543
562
 
544
563
  ## Citations
@@ -549,7 +568,7 @@ doi = {10.5281/zenodo.7949415},
549
568
  author = {Vardanian, Ash},
550
569
  title = {{USearch by Unum Cloud}},
551
570
  url = {https://github.com/unum-cloud/USearch},
552
- version = {2.26.3},
571
+ version = {2.26.5},
553
572
  year = {2026},
554
573
  }
555
574
  ```
package/c/CMakeLists.txt CHANGED
@@ -30,4 +30,13 @@ if (USEARCH_BUILD_LIB_C)
30
30
  setup_target(usearch_c)
31
31
  setup_target(usearch_static_c)
32
32
 
33
+ if (USEARCH_INSTALL)
34
+ install(TARGETS usearch_c usearch_static_c
35
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
36
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
37
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})
38
+ install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/usearch.h
39
+ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
40
+ endif ()
41
+
33
42
  endif ()
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, // or f32_k, bf16_k, e5m2_k, e4m3_k, e3m2_k, e2m3_k, i8_k, u8_k
22
+ .quantization = usearch_scalar_f16_k, // or f32_k, bf16_k, e5m2_k, e4m3_k, e3m2_k, e2m3_k, i8_k, u8_k, b1_k
23
23
  .dimensions = dimensions,
24
24
  .expansion_add = 0, // for defaults
25
25
  .expansion_search = 0 // for defaults
@@ -1,4 +1,7 @@
1
- Name: ${PROJECT_NAME}
1
+ prefix=@CMAKE_INSTALL_PREFIX@
2
+ includedir=@CMAKE_INSTALL_FULL_INCLUDEDIR@
3
+
4
+ Name: @PROJECT_NAME@
2
5
  Description: Smaller & Faster Single-File Vector Search Engine from Unum
3
- Version: ${PROJECT_VERSION}
4
- Cflags: -I${CMAKE_INSTALL_FULL_INCLUDEDIR}
6
+ Version: @PROJECT_VERSION@
7
+ Cflags: -I${includedir}
package/cpp/test.cpp CHANGED
@@ -1319,6 +1319,51 @@ void test_isolate() {
1319
1319
  }
1320
1320
  }
1321
1321
 
1322
+ /**
1323
+ * @brief Regression test: `compact()` permutes the slots of all members, so the
1324
+ * key-to-slot lookup and the free-slot list must follow the permutation.
1325
+ * Otherwise `get` returns other members' vectors and later `add` calls
1326
+ * overwrite live members.
1327
+ */
1328
+ void test_compact() {
1329
+ constexpr std::size_t dataset_count = 256;
1330
+ constexpr std::size_t added_count = 64;
1331
+ constexpr std::size_t dimensions = 16;
1332
+ metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind_t::f32_k);
1333
+
1334
+ std::mt19937 generator(42);
1335
+ std::uniform_real_distribution<float> distribution(0.0, 1.0);
1336
+ std::vector<std::vector<float>> vectors(dataset_count + added_count, std::vector<float>(dimensions));
1337
+ for (auto& vector : vectors)
1338
+ std::generate(vector.begin(), vector.end(), [&] { return distribution(generator); });
1339
+
1340
+ index_dense_t index = index_dense_t::make(metric);
1341
+ index.reserve(dataset_count + added_count);
1342
+ for (std::size_t idx = 0; idx < dataset_count; ++idx)
1343
+ expect(index.add(idx, vectors[idx].data()));
1344
+ for (std::size_t idx = 0; idx < dataset_count; idx += 3)
1345
+ expect(index.remove(idx));
1346
+ std::size_t const removed_count = (dataset_count + 2) / 3;
1347
+
1348
+ expect(index.compact());
1349
+ expect_eq(index.size(), dataset_count - removed_count);
1350
+
1351
+ // Freed slots must be reused without clobbering live members
1352
+ for (std::size_t idx = dataset_count; idx < dataset_count + added_count; ++idx)
1353
+ expect(index.add(idx, vectors[idx].data()));
1354
+ expect_eq(index.size(), dataset_count - removed_count + added_count);
1355
+
1356
+ std::vector<float> reconstructed(dimensions);
1357
+ for (std::size_t idx = 0; idx < dataset_count + added_count; ++idx) {
1358
+ bool const removed = idx < dataset_count && idx % 3 == 0;
1359
+ expect_eq(index.contains(idx), !removed);
1360
+ if (removed)
1361
+ continue;
1362
+ expect_eq(index.get(idx, reconstructed.data()), std::size_t(1));
1363
+ expect(std::equal(reconstructed.begin(), reconstructed.end(), vectors[idx].begin()));
1364
+ }
1365
+ }
1366
+
1322
1367
  static void usearch_write_backtrace(int signal_number) {
1323
1368
  std::fprintf(stderr, "\n[usearch] Fatal signal %d. Back-trace:\n", signal_number);
1324
1369
  #if USEARCH_HAS_STD_STACKTRACE
@@ -1489,6 +1534,36 @@ void test_view_of_truncated_file() {
1489
1534
  std::remove(path);
1490
1535
  }
1491
1536
 
1537
+ /**
1538
+ * @brief Regression test: every thread context must draw node levels of its own. Default-seeded, all
1539
+ * contexts drew the same levels and started over after every `reserve`, so every per-level node
1540
+ * count was a multiple of `contexts * steps`, the upper levels too full and the top ones missing.
1541
+ */
1542
+ void test_level_generators() {
1543
+ constexpr std::size_t contexts = 16, steps = 16, per_context_step = 64, dimensions = 2;
1544
+ metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind_t::f32_k);
1545
+ index_dense_t index = index_dense_t::make(metric);
1546
+
1547
+ std::mt19937 generator(42);
1548
+ std::uniform_real_distribution<float> distribution(0.0, 1.0);
1549
+
1550
+ std::size_t key = 0;
1551
+ for (std::size_t step = 0; step != steps; ++step) {
1552
+ expect(index.try_reserve(index_limits_t((step + 1) * contexts * per_context_step, contexts)));
1553
+ for (std::size_t idx = 0; idx != contexts * per_context_step; ++idx, ++key) {
1554
+ float vector[dimensions] = {distribution(generator), distribution(generator)};
1555
+ expect(index.add(key, vector, idx % contexts));
1556
+ }
1557
+ }
1558
+ expect_eq(index.size(), contexts * steps * per_context_step);
1559
+
1560
+ // Independent draws put about 1024 nodes on level 1 and 64 on level 2: almost never multiples of 256.
1561
+ bool all_multiples = true;
1562
+ for (std::size_t level = 1; level <= index.max_level(); ++level)
1563
+ all_multiples = all_multiples && index.stats(level).nodes % (contexts * steps) == 0;
1564
+ expect(!all_multiples);
1565
+ }
1566
+
1492
1567
  int main(int, char**) {
1493
1568
  install_crash_handlers();
1494
1569
 
@@ -1584,7 +1659,9 @@ int main(int, char**) {
1584
1659
 
1585
1660
  test_filtered_search();
1586
1661
  test_isolate();
1662
+ test_compact();
1587
1663
  test_load_after_metric_make();
1588
1664
  test_view_of_truncated_file();
1665
+ test_level_generators();
1589
1666
  return 0;
1590
1667
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
11
  #define USEARCH_VERSION_MINOR 26
12
- #define USEARCH_VERSION_PATCH 3
12
+ #define USEARCH_VERSION_PATCH 5
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -96,7 +96,7 @@
96
96
  #include <cstring> // `std::memset`
97
97
  #include <iterator> // `std::reverse_iterator`
98
98
  #include <mutex> // `std::unique_lock` - replacement candidate
99
- #include <random> // `std::default_random_engine` - replacement candidate
99
+ #include <random> // `std::uniform_real_distribution`
100
100
  #include <stdexcept> // `std::runtime_exception`
101
101
  #include <thread> // `std::thread`
102
102
  #include <utility> // `std::pair`
@@ -1277,6 +1277,29 @@ template <typename element_at> struct hash_gt {
1277
1277
  std::size_t operator()(element_at const& element) const noexcept { return std::hash<element_at>{}(element); }
1278
1278
  };
1279
1279
 
1280
+ /**
1281
+ * @brief Eight-byte, non-cryptographic uniform random bit generator for STL distributions.
1282
+ * @see https://prng.di.unimi.it/splitmix64.c (public-domain reference implementation).
1283
+ * @details Every seed is valid, including zero. Generation and skipping use unsigned wraparound.
1284
+ */
1285
+ class splitmix64_t {
1286
+ std::uint64_t state_;
1287
+
1288
+ public:
1289
+ using result_type = std::uint64_t;
1290
+ static result_type mix_uint64(result_type x) noexcept {
1291
+ x = (x ^ (x >> 30u)) * 0xBF58476D1CE4E5B9ULL;
1292
+ x = (x ^ (x >> 27u)) * 0x94D049BB133111EBULL;
1293
+ return x ^ (x >> 31u);
1294
+ }
1295
+ constexpr explicit splitmix64_t(result_type value = 0) noexcept : state_(value) {}
1296
+ static constexpr result_type(min)() noexcept { return 0; }
1297
+ static constexpr result_type(max)() noexcept { return (std::numeric_limits<result_type>::max)(); }
1298
+ void seed(result_type value = 0) noexcept { state_ = value; }
1299
+ result_type operator()() noexcept { return mix_uint64(state_ += 0x9E3779B97F4A7C15ULL); }
1300
+ void discard(unsigned long long count) noexcept { state_ += result_type(count) * 0x9E3779B97F4A7C15ULL; }
1301
+ };
1302
+
1280
1303
  /**
1281
1304
  * @brief SplitMix64 finalizer, used to scatter integer keys before masking.
1282
1305
  *
@@ -1289,10 +1312,7 @@ template <typename element_at> struct hash_gt {
1289
1312
  */
1290
1313
  template <> struct hash_gt<std::uint64_t> {
1291
1314
  std::size_t operator()(std::uint64_t const& element) const noexcept {
1292
- std::uint64_t x = element;
1293
- x = (x ^ (x >> 30u)) * 0xBF58476D1CE4E5B9ULL;
1294
- x = (x ^ (x >> 27u)) * 0x94D049BB133111EBULL;
1295
- return static_cast<std::size_t>(x ^ (x >> 31u));
1315
+ return static_cast<std::size_t>(splitmix64_t::mix_uint64(element));
1296
1316
  }
1297
1317
  };
1298
1318
 
@@ -2489,7 +2509,7 @@ class index_gt {
2489
2509
  top_candidates_t top_for_refine{};
2490
2510
  next_candidates_t next_candidates{};
2491
2511
  visits_hash_set_t visits{};
2492
- std::default_random_engine level_generator{};
2512
+ splitmix64_t level_generator{};
2493
2513
  std::size_t iteration_cycles{};
2494
2514
  std::size_t computed_distances{};
2495
2515
  std::size_t computed_distances_in_refines{};
@@ -2687,6 +2707,10 @@ class index_gt {
2687
2707
  if (!other.reserve(limits_))
2688
2708
  return result.failed("Failed to reserve the contexts");
2689
2709
 
2710
+ // A copy must continue the same node-level streams as the original.
2711
+ for (std::size_t i = 0; i != contexts_.size(); ++i)
2712
+ other.contexts_[i].level_generator = contexts_[i].level_generator;
2713
+
2690
2714
  // Now all is left - is to allocate new `node_t` instances and populate
2691
2715
  // the `other.nodes_` array into it.
2692
2716
  for (std::size_t i = 0; i != nodes_count_; ++i)
@@ -2892,10 +2916,15 @@ class index_gt {
2892
2916
 
2893
2917
  /**
2894
2918
  * @brief Increases the `capacity()` of the index to allow adding more vectors.
2919
+ * @note Thread reservations never shrink; `reset()` releases all contexts.
2895
2920
  * @return `true` on success, `false` on memory allocation errors.
2896
2921
  */
2897
2922
  bool try_reserve(index_limits_t limits) usearch_noexcept_m {
2898
2923
 
2924
+ // Keep previously allocated contexts, including streams for temporarily unused threads.
2925
+ limits.threads_add = (std::max)(limits.threads_add, limits_.threads_add);
2926
+ limits.threads_search = (std::max)(limits.threads_search, limits_.threads_search);
2927
+
2899
2928
  if (limits.threads_add <= limits_.threads_add //
2900
2929
  && limits.threads_search <= limits_.threads_search //
2901
2930
  && limits.members <= limits_.members)
@@ -2918,10 +2947,19 @@ class index_gt {
2918
2947
  // Move the nodes info, and deallocate previous buffers.
2919
2948
  if (nodes_)
2920
2949
  std::memcpy(new_nodes.data(), nodes_.data(), sizeof(node_t) * size());
2921
- for (std::size_t i = 0; i != new_contexts.size(); ++i)
2950
+ for (std::size_t i = 0; i != new_contexts.size(); ++i) {
2922
2951
  if (!new_contexts[i].top_for_refine.reserve(connectivity_max + 1))
2923
2952
  return false;
2924
2953
 
2954
+ // Copy rather than advance the old stream, so a failed reserve leaves it unchanged.
2955
+ if (i < contexts_.size())
2956
+ new_contexts[i].level_generator = contexts_[i].level_generator;
2957
+ else {
2958
+ // Mix logical thread slots instead of using adjacent seeds (or an OS thread ID).
2959
+ new_contexts[i].level_generator.seed(splitmix64_t::mix_uint64(static_cast<std::uint64_t>(i) + 1));
2960
+ }
2961
+ }
2962
+
2925
2963
  limits_ = limits;
2926
2964
  nodes_capacity_ = limits.members;
2927
2965
  nodes_ = std::move(new_nodes);
@@ -3211,11 +3249,13 @@ class index_gt {
3211
3249
  if (!next.reserve(config.expansion))
3212
3250
  return result.failed("Out of memory!");
3213
3251
 
3252
+ // Each caller exclusively owns its context; sampling needs no shared graph lock.
3253
+ level_t new_target_level = choose_random_level_(context.level_generator);
3254
+
3214
3255
  // Determining how much memory to allocate for the node depends on the target level
3215
3256
  std::unique_lock<std::mutex> new_level_lock(global_mutex_);
3216
3257
  level_t max_level_copy = max_level_; // Copy under lock
3217
3258
  compressed_slot_t entry_slot_copy = static_cast<compressed_slot_t>(entry_slot_); // Copy under lock
3218
- level_t new_target_level = choose_random_level_(context.level_generator);
3219
3259
 
3220
3260
  // Make sure we are not overflowing
3221
3261
  std::size_t capacity = nodes_capacity_.load();
@@ -4366,10 +4406,11 @@ class index_gt {
4366
4406
  }
4367
4407
  }
4368
4408
 
4369
- level_t choose_random_level_(std::default_random_engine& level_generator) const noexcept {
4409
+ level_t choose_random_level_(splitmix64_t& level_generator) const noexcept {
4370
4410
  std::uniform_real_distribution<double> distribution(0.0, 1.0);
4371
- double r = -std::log(distribution(level_generator)) * pre_.inverse_log_connectivity;
4372
- return (level_t)r;
4411
+ // Reflect into (0, 1] and guard against a rounded upper endpoint from the STL.
4412
+ double uniform = (std::max)(1.0 - distribution(level_generator), (std::numeric_limits<double>::min)());
4413
+ return static_cast<level_t>(-std::log(uniform) * pre_.inverse_log_connectivity);
4373
4414
  }
4374
4415
 
4375
4416
  struct candidates_range_t;
@@ -1053,6 +1053,9 @@ class index_dense_gt {
1053
1053
  // Reserving is monotonic: reducing thread limits must not shrink member-addressed storage.
1054
1054
  limits.members = (std::max)(limits.members, typed_->capacity());
1055
1055
  limits.members = (std::max)(limits.members, vectors_lookup_.size());
1056
+ // Match the core's retained contexts so cast buffers and available thread IDs stay consistent.
1057
+ limits.threads_add = (std::max)(limits.threads_add, typed_->limits().threads_add);
1058
+ limits.threads_search = (std::max)(limits.threads_search, typed_->limits().threads_search);
1056
1059
 
1057
1060
  // The slot lookup system will generally prefer power-of-two sizes.
1058
1061
  if (config_.enable_key_lookups) {
@@ -1923,6 +1926,9 @@ class index_dense_gt {
1923
1926
  std::forward<executor_at>(executor), std::forward<progress_at>(progress));
1924
1927
  vectors_lookup_ = std::move(new_vectors_lookup);
1925
1928
  vectors_tape_allocator_ = std::move(new_vectors_allocator);
1929
+
1930
+ // Slots were permuted, so the key-to-slot lookup and the free-slot list are stale
1931
+ reindex_keys_();
1926
1932
  return result;
1927
1933
  }
1928
1934
 
@@ -3,10 +3,11 @@
3
3
  #include <float.h> // `_Float16`
4
4
  #include <stdlib.h> // `aligned_alloc`
5
5
 
6
- #include <atomic> // `std::atomic`
7
- #include <chrono> // `std::chrono`
8
- #include <cstring> // `std::strncmp`
9
- #include <thread> // `std::thread`
6
+ #include <atomic> // `std::atomic`
7
+ #include <chrono> // `std::chrono`
8
+ #include <cstring> // `std::strncmp`
9
+ #include <thread> // `std::thread`
10
+ #include <type_traits> // `std::is_floating_point`
10
11
 
11
12
  #include <usearch/index.hpp> // `expected_gt` and macros
12
13
 
@@ -374,10 +375,10 @@ inline expected_gt<scalar_kind_t> scalar_kind_from_name(char const* name, std::s
374
375
  parsed.result = scalar_kind_t::i8_k;
375
376
  else if (str_equals(name, len, "u8"))
376
377
  parsed.result = scalar_kind_t::u8_k;
377
- else if (str_equals(name, len, "b1"))
378
+ else if (str_equals(name, len, "b1") || str_equals(name, len, "b1x8"))
378
379
  parsed.result = scalar_kind_t::b1x8_k;
379
380
  else
380
- parsed.failed("Unknown type, choose: f64, f32, bf16, f16, e5m2, e4m3, e3m2, e2m3, i8, u8, b1");
381
+ parsed.failed("Unknown type, choose: f64, f32, bf16, f16, e5m2, e4m3, e3m2, e2m3, i8, u8, b1, b1x8");
381
382
  return parsed;
382
383
  }
383
384
 
@@ -525,8 +526,10 @@ inline std::uint16_t f32_to_f16(float f32) noexcept {
525
526
  */
526
527
  inline float bf16_to_f32(std::uint16_t u16) noexcept {
527
528
  #if USEARCH_USE_NUMKONG
529
+ nk_bf16_t bits;
530
+ std::memcpy(&bits, &u16, sizeof(bits));
528
531
  nk_f32_t result;
529
- nk_bf16_to_f32_serial((nk_bf16_t const*)&u16, &result);
532
+ nk_bf16_to_f32_serial(&bits, &result);
530
533
  return result;
531
534
  #else
532
535
  union float_or_unsigned_int_t {
@@ -2280,8 +2283,23 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct met
2280
2283
 
2281
2284
  inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim) const noexcept {
2282
2285
  result_t ab{}, a2{}, b2{};
2286
+ // OpenMP only accepts `x += expr` as a reduction update, not a comma-expression.
2287
+ // With a function-call cast, GCC leaves `a2` and `b2` at zero and every distance becomes 0.
2288
+ add_products_(a, b, dim, ab, a2, b2, std::is_floating_point<scalar_t>());
2289
+
2290
+ result_t result_if_zero[2][2];
2291
+ result_if_zero[0][0] = 1 - ab / (std::sqrt(a2) * std::sqrt(b2));
2292
+ result_if_zero[0][1] = result_if_zero[1][0] = 1;
2293
+ result_if_zero[1][1] = 0;
2294
+ return result_if_zero[a2 == 0][b2 == 0];
2295
+ }
2296
+
2297
+ private:
2298
+ inline void add_products_(scalar_t const* a, scalar_t const* b, std::size_t dim, result_t& ab, result_t& a2,
2299
+ result_t& b2, std::true_type) const noexcept {
2300
+ result_t product{}, first_squared{}, second_squared{};
2283
2301
  #if USEARCH_USE_OPENMP
2284
- #pragma omp simd reduction(+ : ab, a2, b2)
2302
+ #pragma omp simd reduction(+ : product, first_squared, second_squared)
2285
2303
  #elif defined(USEARCH_DEFINED_CLANG)
2286
2304
  #pragma clang loop vectorize(enable)
2287
2305
  #elif defined(USEARCH_DEFINED_GCC)
@@ -2290,14 +2308,24 @@ template <typename scalar_at = float, typename result_at = scalar_at> struct met
2290
2308
  for (std::size_t i = 0; i != dim; ++i) {
2291
2309
  result_t ai = static_cast<result_t>(a[i]);
2292
2310
  result_t bi = static_cast<result_t>(b[i]);
2293
- ab += ai * bi, a2 += square(ai), b2 += square(bi);
2311
+ product += ai * bi;
2312
+ first_squared += square(ai);
2313
+ second_squared += square(bi);
2294
2314
  }
2315
+ ab = product;
2316
+ a2 = first_squared;
2317
+ b2 = second_squared;
2318
+ }
2295
2319
 
2296
- result_t result_if_zero[2][2];
2297
- result_if_zero[0][0] = 1 - ab / (std::sqrt(a2) * std::sqrt(b2));
2298
- result_if_zero[0][1] = result_if_zero[1][0] = 1;
2299
- result_if_zero[1][1] = 0;
2300
- return result_if_zero[a2 == 0][b2 == 0];
2320
+ inline void add_products_(scalar_t const* a, scalar_t const* b, std::size_t dim, result_t& ab, result_t& a2,
2321
+ result_t& b2, std::false_type) const noexcept {
2322
+ for (std::size_t i = 0; i != dim; ++i) {
2323
+ result_t ai = static_cast<result_t>(a[i]);
2324
+ result_t bi = static_cast<result_t>(b[i]);
2325
+ ab += ai * bi;
2326
+ a2 += square(ai);
2327
+ b2 += square(bi);
2328
+ }
2301
2329
  }
2302
2330
  };
2303
2331
 
@@ -2544,9 +2572,14 @@ struct metric_cos_i8_t {
2544
2572
  a2 += square(ai);
2545
2573
  b2 += square(bi);
2546
2574
  }
2547
- result_t a2f = std::sqrt(static_cast<result_t>(a2));
2548
- result_t b2f = std::sqrt(static_cast<result_t>(b2));
2549
- return (ab != 0) ? (1.f - ab / (a2f * b2f)) : 0;
2575
+ // Same tail as `nk_angular_i8_serial`: a zero dot product is orthogonal, not identical.
2576
+ if (a2 == 0 && b2 == 0)
2577
+ return 0;
2578
+ if (ab == 0)
2579
+ return 1;
2580
+ result_t norms = std::sqrt(static_cast<result_t>(a2)) * std::sqrt(static_cast<result_t>(b2));
2581
+ result_t unclipped = 1 - static_cast<result_t>(ab) / norms;
2582
+ return unclipped < 0 ? 0 : unclipped;
2550
2583
  }
2551
2584
  };
2552
2585
 
@@ -2596,9 +2629,14 @@ struct metric_cos_u8_t {
2596
2629
  a2 += square(ai);
2597
2630
  b2 += square(bi);
2598
2631
  }
2599
- result_t a2f = std::sqrt(static_cast<result_t>(a2));
2600
- result_t b2f = std::sqrt(static_cast<result_t>(b2));
2601
- return (ab != 0) ? (1.f - ab / (a2f * b2f)) : 0;
2632
+ // Same tail as `nk_angular_u8_serial`: a zero dot product is orthogonal, not identical.
2633
+ if (a2 == 0 && b2 == 0)
2634
+ return 0;
2635
+ if (ab == 0)
2636
+ return 1;
2637
+ result_t norms = std::sqrt(static_cast<result_t>(a2)) * std::sqrt(static_cast<result_t>(b2));
2638
+ result_t unclipped = 1 - static_cast<result_t>(ab) / norms;
2639
+ return unclipped < 0 ? 0 : unclipped;
2602
2640
  }
2603
2641
  };
2604
2642
 
@@ -1,6 +1,7 @@
1
1
  # USearch for JavaScript
2
2
 
3
- USearch is a high-performance library for building and querying vector search indexes, optimized for Node.js and WASM environments.
3
+ USearch is a high-performance library for building and querying vector search indexes.
4
+ This page is the Node.js addon. The browser build is the C library compiled with Emscripten, documented in `wasm/README.md`.
4
5
 
5
6
  ## Installation
6
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "2.26.3",
3
+ "version": "2.26.5",
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
Binary file