usearch 2.15.3 → 2.16.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.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/cpp/test.cpp +16 -0
  3. package/include/usearch/index.hpp +21 -5
  4. package/include/usearch/index_dense.hpp +6 -1
  5. package/include/usearch/index_plugins.hpp +345 -8
  6. package/javascript/lib.cpp +17 -5
  7. package/javascript/usearch.test.js +190 -21
  8. package/package.json +1 -1
  9. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  10. package/prebuilds/linux-arm64/usearch.node +0 -0
  11. package/prebuilds/linux-x64/usearch.node +0 -0
  12. package/simsimd/.git-blame-ignore-revs +2 -0
  13. package/simsimd/CONTRIBUTING.md +71 -23
  14. package/simsimd/Cargo.lock +1 -1
  15. package/simsimd/Cargo.toml +4 -4
  16. package/simsimd/MANIFEST.in +2 -1
  17. package/simsimd/Package.swift +15 -3
  18. package/simsimd/README.md +104 -35
  19. package/simsimd/binding.gyp +6 -2
  20. package/simsimd/build.rs +2 -0
  21. package/simsimd/c/lib.c +180 -99
  22. package/simsimd/include/simsimd/binary.h +311 -102
  23. package/simsimd/include/simsimd/curved.h +134 -135
  24. package/simsimd/include/simsimd/dot.h +448 -274
  25. package/simsimd/include/simsimd/elementwise.h +1437 -0
  26. package/simsimd/include/simsimd/geospatial.h +2 -2
  27. package/simsimd/include/simsimd/mesh.h +2 -2
  28. package/simsimd/include/simsimd/probability.h +88 -88
  29. package/simsimd/include/simsimd/simsimd.h +1157 -772
  30. package/simsimd/include/simsimd/sparse.h +768 -147
  31. package/simsimd/include/simsimd/spatial.h +955 -376
  32. package/simsimd/include/simsimd/types.h +145 -71
  33. package/simsimd/javascript/lib.c +1 -2
  34. package/simsimd/package.json +4 -4
  35. package/simsimd/rust/lib.rs +14 -1
  36. package/simsimd/scripts/bench.cxx +1070 -0
  37. package/simsimd/scripts/bench.ipynb +187 -0
  38. package/simsimd/{javascript → scripts}/bench.js +1 -1
  39. package/simsimd/{rust/benches/sqeuclidean.rs → scripts/bench_sqeuclidean.rs} +6 -6
  40. package/simsimd/scripts/bench_vectors.py +700 -0
  41. package/simsimd/scripts/bench_vectors_live.py +213 -0
  42. package/simsimd/scripts/test.c +155 -0
  43. package/simsimd/{javascript → scripts}/test.mjs +2 -2
  44. package/simsimd/scripts/test.py +1410 -0
  45. package/simsimd/swift/{source/SimSIMD.swift → SimSIMD.swift} +6 -2
  46. package/simsimd/swift/{tests/SimSIMDTests.swift → Test.swift} +1 -1
  47. /package/simsimd/{rust/benches/cosine.rs → scripts/bench_cosine.rs} +0 -0
  48. /package/simsimd/{rust/benches/native.rs → scripts/bench_native.rs} +0 -0
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.15.3},
547
+ version = {2.16.1},
548
548
  year = {2023},
549
549
  month = oct,
550
550
  }
package/cpp/test.cpp CHANGED
@@ -1100,6 +1100,22 @@ int main(int, char**) {
1100
1100
  test_uint40();
1101
1101
  test_cosine<float, std::int64_t, uint40_t>(10, 10);
1102
1102
 
1103
+ // Test plugins, like K-Means clustering.
1104
+ {
1105
+ std::size_t vectors_count = 1000, centroids_count = 10, dimensions = 256;
1106
+ kmeans_clustering_t clustering;
1107
+ clustering.max_iterations = 2;
1108
+ std::vector<float> vectors(vectors_count * dimensions), centroids(centroids_count * dimensions);
1109
+ matrix_slice_gt<float const> vectors_slice(vectors.data(), dimensions, vectors_count);
1110
+ matrix_slice_gt<float> centroids_slice(centroids.data(), dimensions, centroids_count);
1111
+ std::generate(vectors.begin(), vectors.end(), [] { return float(std::rand()) / float(INT_MAX); });
1112
+ std::vector<std::size_t> assignments(vectors_count);
1113
+ std::vector<distance_punned_t> distances(vectors_count);
1114
+ auto clustering_result = clustering(vectors_slice, centroids_slice, {assignments.data(), assignments.size()},
1115
+ {distances.data(), distances.size()});
1116
+ expect(clustering_result);
1117
+ }
1118
+
1103
1119
  // Exact search without constructing indexes.
1104
1120
  // Great for validating the distance functions.
1105
1121
  std::printf("Testing exact search\n");
@@ -8,8 +8,8 @@
8
8
  #define UNUM_USEARCH_HPP
9
9
 
10
10
  #define USEARCH_VERSION_MAJOR 2
11
- #define USEARCH_VERSION_MINOR 15
12
- #define USEARCH_VERSION_PATCH 3
11
+ #define USEARCH_VERSION_MINOR 16
12
+ #define USEARCH_VERSION_PATCH 1
13
13
 
14
14
  // Inferring C++ version
15
15
  // https://stackoverflow.com/a/61552074
@@ -370,7 +370,7 @@ template <typename scalar_at, typename allocator_at = std::allocator<scalar_at>>
370
370
  void reset() noexcept {
371
371
  if (!std::is_trivially_destructible<scalar_at>::value)
372
372
  for (std::size_t i = 0; i != size_; ++i)
373
- destroy_at(data_ + i);
373
+ unum::usearch::destroy_at(data_ + i); //< Facing some symbol visibility/ambiguity issues
374
374
  allocator_at{}.deallocate(data_, size_);
375
375
  data_ = nullptr;
376
376
  size_ = 0;
@@ -960,6 +960,10 @@ class sorted_buffer_gt {
960
960
 
961
961
  /**
962
962
  * @brief Five-byte integer type to address node clouds with over 4B entries.
963
+ *
964
+ * 40 bits is enough to address a @b Trillion entries potentially colocated on 1 machine.
965
+ * At roughly 5 bytes * 20 neighbors + 100 bytes per entry, this translates to 200 TB of data,
966
+ * which is similar to a single-server capacity of modern NVME arrays.
963
967
  */
964
968
  class usearch_pack_m uint40_t {
965
969
  unsigned char octets[5];
@@ -1214,8 +1218,8 @@ class ring_gt {
1214
1218
  using element_t = element_at;
1215
1219
  using allocator_t = allocator_at;
1216
1220
 
1217
- static_assert(std::is_trivially_destructible<element_t>(), "This heap is designed for trivial structs");
1218
- static_assert(std::is_trivially_copy_constructible<element_t>(), "This heap is designed for trivial structs");
1221
+ static_assert(std::is_trivially_destructible<element_t>(), "This ring is designed for trivial structs");
1222
+ static_assert(std::is_trivially_copy_constructible<element_t>(), "This ring is designed for trivial structs");
1219
1223
 
1220
1224
  using value_type = element_t;
1221
1225
 
@@ -3175,6 +3179,18 @@ class index_gt {
3175
3179
 
3176
3180
  std::size_t memory_usage_per_node(level_t level) const noexcept { return node_bytes_(level); }
3177
3181
 
3182
+ double inverse_log_connectivity() const {
3183
+ return pre_.inverse_log_connectivity;
3184
+ }
3185
+
3186
+ std::size_t neighbors_base_bytes() const {
3187
+ return pre_.neighbors_base_bytes;
3188
+ }
3189
+
3190
+ std::size_t neighbors_bytes() const {
3191
+ return pre_.neighbors_bytes;
3192
+ }
3193
+
3178
3194
  #if defined(USEARCH_USE_PRAGMA_REGION)
3179
3195
  #pragma endregion
3180
3196
 
@@ -693,6 +693,9 @@ class index_dense_gt {
693
693
  std::size_t max_level() const { return typed_->max_level(); }
694
694
  index_dense_config_t const& config() const { return config_; }
695
695
  index_limits_t const& limits() const { return typed_->limits(); }
696
+ double inverse_log_connectivity() const { return typed_->inverse_log_connectivity(); }
697
+ std::size_t neighbors_base_bytes() const { return typed_->neighbors_base_bytes(); }
698
+ std::size_t neighbors_bytes() const { return typed_->neighbors_bytes(); }
696
699
  bool multi() const { return config_.multi; }
697
700
  std::size_t currently_available_threads() const {
698
701
  std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
@@ -1460,6 +1463,8 @@ class index_dense_gt {
1460
1463
  labeling_result_t remove(vector_key_t key) {
1461
1464
  usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
1462
1465
  labeling_result_t result;
1466
+ if (typed_->is_immutable())
1467
+ return result.failed("Can't remove from an immutable index");
1463
1468
 
1464
1469
  unique_lock_t lookup_lock(slot_lookup_mutex_);
1465
1470
  auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
@@ -2249,4 +2254,4 @@ static join_result_t join( //
2249
2254
  }
2250
2255
 
2251
2256
  } // namespace usearch
2252
- } // namespace unum
2257
+ } // namespace unum
@@ -4,6 +4,7 @@
4
4
  #include <stdlib.h> // `aligned_alloc`
5
5
 
6
6
  #include <atomic> // `std::atomic`
7
+ #include <chrono> // `std::chrono`
7
8
  #include <cstring> // `std::strncmp`
8
9
  #include <thread> // `std::thread`
9
10
 
@@ -65,11 +66,11 @@
65
66
  #pragma GCC diagnostic ignored "-Wunused-parameter"
66
67
  #pragma GCC diagnostic ignored "-Wunused-variable"
67
68
  #pragma GCC diagnostic ignored "-Wunused-but-set-variable"
68
- #ifdef _MSC_VER
69
+ #if defined(_MSC_VER)
69
70
  #pragma warning(push)
70
71
  #pragma warning(disable : 4101) // "Unused variables"
71
72
  #pragma warning(disable : 4068) // "Unknown pragmas", when MSVC tries to read GCC pragmas
72
- #endif // _MSC_VER
73
+ #endif // _MSC_VER
73
74
  #include <simsimd/simsimd.h>
74
75
  #ifdef _MSC_VER
75
76
  #pragma warning(pop)
@@ -2020,7 +2021,7 @@ class matrix_slice_gt {
2020
2021
  explicit operator bool() const noexcept { return begin_; }
2021
2022
  std::size_t size() const noexcept { return count_; }
2022
2023
  std::size_t dimensions() const noexcept { return dimensions_; }
2023
- std::size_t stride() const noexcept { return stride_bytes_; }
2024
+ std::size_t stride_bytes() const noexcept { return stride_bytes_; }
2024
2025
  scalar_t* data() const noexcept { return begin_; }
2025
2026
  scalar_t* at(std::size_t i) const noexcept {
2026
2027
  return reinterpret_cast<scalar_t*>(reinterpret_cast<byte_addressable_t*>(begin_) + i * stride_bytes_);
@@ -2032,7 +2033,7 @@ struct exact_offset_and_distance_t {
2032
2033
  f32_t distance;
2033
2034
  };
2034
2035
 
2035
- using exact_search_results_t = matrix_slice_gt<exact_offset_and_distance_t>;
2036
+ using exact_search_results_t = matrix_slice_gt<exact_offset_and_distance_t const>;
2036
2037
 
2037
2038
  /**
2038
2039
  * @brief Helper-structure for exact search operations.
@@ -2057,10 +2058,10 @@ class exact_search_t {
2057
2058
  matrix_slice_gt<scalar_at const> dataset, matrix_slice_gt<scalar_at const> queries, //
2058
2059
  std::size_t wanted, metric_punned_t const& metric, //
2059
2060
  executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
2060
- return operator()( //
2061
- metric, //
2062
- reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.stride(), //
2063
- reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.stride(), //
2061
+ return operator()( //
2062
+ metric, //
2063
+ reinterpret_cast<byte_t const*>(dataset.data()), dataset.size(), dataset.stride_bytes(), //
2064
+ reinterpret_cast<byte_t const*>(queries.data()), queries.size(), queries.stride_bytes(), //
2064
2065
  wanted, executor, progress);
2065
2066
  }
2066
2067
 
@@ -2137,6 +2138,342 @@ class exact_search_t {
2137
2138
  }
2138
2139
  };
2139
2140
 
2141
+ struct kmeans_clustering_result_t {
2142
+ error_t error{};
2143
+ std::size_t computed_distances{};
2144
+ /// @brief The number of iterations the algorithm took to converge.
2145
+ std::size_t iterations{};
2146
+ /// @brief The number of points that changed clusters in the last iteration.
2147
+ std::size_t last_iteration_points_shifted{};
2148
+ /// @brief The inertia of the last iteration (sum of squared distances to centroids).
2149
+ f64_t last_iteration_inertia{};
2150
+ /// @brief The total elapsed runtime of the algorithm in seconds.
2151
+ f64_t runtime_seconds{};
2152
+ /// @brief The total distance between the points and their assigned centroids.
2153
+ f64_t aggregate_distance{};
2154
+
2155
+ explicit operator bool() const noexcept { return !error; }
2156
+ kmeans_clustering_result_t failed(error_t message) noexcept {
2157
+ error = std::move(message);
2158
+ return std::move(*this);
2159
+ }
2160
+ };
2161
+
2162
+ /**
2163
+ * @brief Helper-class for K-Means clustering of dense vectors.
2164
+ * Doesn't require constructing the index, but benefits from mixed-precision logic.
2165
+ * ! Doesn't guarantee that the clusters are balanced in size.
2166
+ *
2167
+ * The algorithm is as follows:
2168
+ * - Initialization: Select K initial centroids (randomly or with a heuristic).
2169
+ * - Assignment: Assign each data point to the nearest centroid based on the Euclidean distance.
2170
+ * - Update: Recalculate the centroids as the mean of all points assigned to each centroid.
2171
+ * - Repeat: Repeat the assignment and update steps until the centroids no longer change significantly
2172
+ * or an early-exit condition is met.
2173
+ */
2174
+ template <typename allocator_at = std::allocator<char>> class kmeans_clustering_gt {
2175
+ public:
2176
+ using distance_t = distance_punned_t;
2177
+
2178
+ metric_kind_t metric_kind{metric_kind_t::l2sq_k};
2179
+ scalar_kind_t quantization_kind{scalar_kind_t::bf16_k};
2180
+
2181
+ static constexpr std::size_t max_iterations_default_k = 300;
2182
+ static constexpr f64_t inertia_threshold_default_k = 1e-4;
2183
+ static constexpr f64_t max_seconds_default_k = 60.0;
2184
+ static constexpr f64_t min_shifts_default_k = 0.01;
2185
+
2186
+ /// @brief Early-exit parameter - the maximum number of iterations to perform.
2187
+ std::size_t max_iterations{max_iterations_default_k};
2188
+ /// @brief Early-exit parameter - the threshold for the final inertia to terminate early.
2189
+ f64_t inertia_threshold{inertia_threshold_default_k};
2190
+ /// @brief Early-exit parameter - the maximum runtime allowed in seconds.
2191
+ f64_t max_seconds{max_seconds_default_k};
2192
+ /// @brief Early-exit parameter - the minimum share of points that must change clusters per iteration.
2193
+ f64_t min_shifts{min_shifts_default_k};
2194
+ /// @brief The random seed to use for centroid initialization.
2195
+ std::uint64_t seed{0};
2196
+
2197
+ kmeans_clustering_gt(std::uint64_t seed) noexcept : seed(seed) {}
2198
+ kmeans_clustering_gt() noexcept(false) {
2199
+ std::random_device random_device;
2200
+ seed = random_device();
2201
+ }
2202
+
2203
+ kmeans_clustering_gt(kmeans_clustering_gt const&) = default;
2204
+ kmeans_clustering_gt& operator=(kmeans_clustering_gt const&) = default;
2205
+
2206
+ template <typename scalar_at, typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
2207
+ kmeans_clustering_result_t operator()( //
2208
+ matrix_slice_gt<scalar_at const> points, matrix_slice_gt<scalar_at> centroids,
2209
+ span_gt<std::size_t> point_to_centroid_index, span_gt<distance_t> point_to_centroid_distance, //
2210
+ executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
2211
+ return operator()( //
2212
+ reinterpret_cast<byte_t const*>(points.data()), points.size(), points.stride_bytes(), //
2213
+ reinterpret_cast<byte_t*>(centroids.data()), centroids.size(), centroids.stride_bytes(),
2214
+ point_to_centroid_index.data(), point_to_centroid_distance.data(), //
2215
+ scalar_kind<scalar_at>(), points.dimensions(), executor, progress);
2216
+ }
2217
+
2218
+ template <typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
2219
+ kmeans_clustering_result_t operator()( //
2220
+ byte_t const* points_data, std::size_t points_count, std::size_t points_stride_bytes, //
2221
+ byte_t* centroids_data, std::size_t wanted_clusters, std::size_t centroids_stride_bytes, //
2222
+ std::size_t* point_to_centroid_index, distance_t* point_to_centroid_distance, //
2223
+ scalar_kind_t original_scalar_kind, std::size_t dimensions, executor_at&& executor = executor_at{},
2224
+ progress_at&& progress = progress_at{}) {
2225
+
2226
+ // Perform sanity checks for algorithm settings.
2227
+ kmeans_clustering_result_t result;
2228
+ if (max_iterations < 1)
2229
+ return result.failed("The number of iterations must be at least 1");
2230
+
2231
+ // Perform sanity checks for input arguments.
2232
+ if (wanted_clusters < 2)
2233
+ return result.failed("The number of clusters must be at least 2");
2234
+ if (wanted_clusters >= points_count)
2235
+ return result.failed("The number of clusters must be less than the number of vectors");
2236
+
2237
+ metric_punned_t metric = metric_punned_t::builtin(dimensions, metric_kind, quantization_kind);
2238
+ if (!metric)
2239
+ return result.failed("Unsupported metric or scalar kind");
2240
+
2241
+ // Let's allocate memory for the centroids coordinates and make sure it's
2242
+ // rows are aligned to cache lines to avoid false sharing.
2243
+ buffer_gt<distance_t, aligned_allocator_gt<distance_t, 64>> point_to_centroid_distance_buffer(points_count);
2244
+ buffer_gt<std::size_t, aligned_allocator_gt<std::size_t, 64>> point_to_centroid_index_buffer(points_count);
2245
+ buffer_gt<std::atomic<std::size_t>, aligned_allocator_gt<std::atomic<std::size_t>, 64>> cluster_sizes_buffer(
2246
+ wanted_clusters);
2247
+
2248
+ // For a mixed precision computation, we keep the centroids represented in two forms -
2249
+ // double precision and quantized the same way as in the index, to avoid paying conversion penalties.
2250
+ // Double precision is needed to avoid accumulating errors when aggregating too many entries.
2251
+ std::size_t const bytes_per_vector_original =
2252
+ divide_round_up<CHAR_BIT>(dimensions * bits_per_scalar(original_scalar_kind));
2253
+ std::size_t const bytes_per_vector_quantized = metric.bytes_per_vector();
2254
+ std::size_t const stride_per_vector_quantized = divide_round_up<64>(bytes_per_vector_quantized) * 64;
2255
+ buffer_gt<byte_t, aligned_allocator_gt<byte_t, 64>> points_quantized_buffer( //
2256
+ points_count * stride_per_vector_quantized);
2257
+ buffer_gt<byte_t, aligned_allocator_gt<byte_t, 64>> centroids_quantized_buffer( //
2258
+ wanted_clusters * stride_per_vector_quantized);
2259
+
2260
+ // When aggregating centroids, we want to parallelize the operation and need more memory.
2261
+ // For every thread we keep two double-precision vectors. One is the up-casting output buffer for quantized
2262
+ // coordinates, and the other is the temporary buffer for the partial sums of the double-precision coordinates.
2263
+ // The ordering:
2264
+ //
2265
+ // - thread 0: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
2266
+ // - thread 1: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
2267
+ // - thread 2: [centroid 0, centroid 1, centroid 2, centroid 3, ...]
2268
+ //
2269
+ std::size_t const thread_count = executor.size();
2270
+ buffer_gt<f64_t, aligned_allocator_gt<f64_t, 64>> centroids_precise_buffer( //
2271
+ wanted_clusters * dimensions * thread_count);
2272
+ buffer_gt<f64_t, aligned_allocator_gt<f64_t, 64>> points_precise_buffer( //
2273
+ wanted_clusters * dimensions * thread_count);
2274
+
2275
+ // Check if all memory allocations were successful.
2276
+ if (!centroids_precise_buffer || !points_precise_buffer || !centroids_quantized_buffer ||
2277
+ !point_to_centroid_index_buffer || !cluster_sizes_buffer || !point_to_centroid_distance_buffer ||
2278
+ !points_quantized_buffer)
2279
+ return result.failed("No memory for result outputs!");
2280
+
2281
+ std::fill_n(point_to_centroid_index_buffer.data(), points_count, wanted_clusters);
2282
+ std::fill_n(point_to_centroid_distance_buffer.data(), points_count, std::numeric_limits<distance_t>::max());
2283
+
2284
+ // Initialize the casting kernel for quantization and export.
2285
+ casts_punned_t casts = casts_punned_t::make(quantization_kind);
2286
+ cast_punned_t const& compress_points = casts.from[original_scalar_kind];
2287
+ cast_punned_t const& decompress_points = casts.to[original_scalar_kind];
2288
+ cast_punned_t const& compress_precise = casts.from.f64;
2289
+ cast_punned_t const& decompress_precise = casts.to.f64;
2290
+ for (std::size_t i = 0; i < points_count; i++) {
2291
+ byte_t const* vector = points_data + i * points_stride_bytes;
2292
+ byte_t* quantized = points_quantized_buffer.data() + i * stride_per_vector_quantized;
2293
+ if (!compress_points(vector, dimensions, quantized))
2294
+ std::memcpy(quantized, vector, bytes_per_vector_original);
2295
+ }
2296
+
2297
+ // Initialize centroids with random points vectors.
2298
+ std::mt19937_64 random_engine;
2299
+ random_engine.seed(seed);
2300
+ for (std::size_t i = 0; i < wanted_clusters; i++) {
2301
+ // Generate the random index of the points vector,
2302
+ // that is unique and not already used as a centroid.
2303
+ std::size_t random_index;
2304
+ do {
2305
+ random_index = random_engine() % points_count;
2306
+ bool is_unique = true;
2307
+ for (std::size_t j = 0; j < i; j++) {
2308
+ if (point_to_centroid_index_buffer[j] == random_index) {
2309
+ is_unique = false;
2310
+ break;
2311
+ }
2312
+ }
2313
+ if (is_unique)
2314
+ break;
2315
+ } while (true);
2316
+
2317
+ // Copy the vector to the centroid and quantize it.
2318
+ byte_t const* quantized_point = points_quantized_buffer.data() + random_index * stride_per_vector_quantized;
2319
+ byte_t* quantized_centroid = centroids_quantized_buffer.data() + i * stride_per_vector_quantized;
2320
+ std::memcpy(quantized_centroid, quantized_point, bytes_per_vector_quantized);
2321
+ point_to_centroid_index_buffer[random_index] = i;
2322
+ point_to_centroid_distance_buffer[random_index] = 0;
2323
+ }
2324
+
2325
+ auto start_time = std::chrono::high_resolution_clock::now();
2326
+ std::size_t iterations = 0;
2327
+ std::size_t const min_points_shifted_per_iteration = static_cast<std::size_t>(min_shifts * points_count);
2328
+ f64_t last_aggregate_distance = std::numeric_limits<f64_t>::max();
2329
+
2330
+ while (iterations < max_iterations) {
2331
+ iterations++;
2332
+
2333
+ // For every point, find the closest centroid.
2334
+ std::atomic<std::size_t> points_shifted{0};
2335
+ executor.dynamic(points_count, [&](std::size_t thread_idx, std::size_t points_idx) {
2336
+ byte_t const* quantized_point =
2337
+ points_quantized_buffer.data() + points_idx * stride_per_vector_quantized;
2338
+ byte_t const* quantized_centroids = centroids_quantized_buffer.data();
2339
+ distance_t closest_distance_local = std::numeric_limits<distance_t>::max();
2340
+ std::size_t closest_idx_local = 0;
2341
+ for (std::size_t centroid_idx = 0; centroid_idx < wanted_clusters; centroid_idx++) {
2342
+ byte_t const* quantized_centroid = quantized_centroids + centroid_idx * stride_per_vector_quantized;
2343
+ distance_t distance = metric(quantized_point, quantized_centroid);
2344
+ if (distance < closest_distance_local) {
2345
+ closest_distance_local = distance;
2346
+ closest_idx_local = centroid_idx;
2347
+ }
2348
+ }
2349
+
2350
+ distance_t& closest_distance_ref = point_to_centroid_distance_buffer[points_idx];
2351
+ std::size_t& closest_idx_ref = point_to_centroid_index_buffer[points_idx];
2352
+ if (closest_idx_local != closest_idx_ref) {
2353
+ closest_idx_ref = closest_idx_local;
2354
+ points_shifted.fetch_add(1, std::memory_order_relaxed);
2355
+ }
2356
+
2357
+ closest_distance_ref = closest_distance_local;
2358
+ return true;
2359
+ });
2360
+
2361
+ f64_t aggregate_distance = 0.0;
2362
+ for (std::size_t i = 0; i < points_count; i++)
2363
+ aggregate_distance += point_to_centroid_distance_buffer[i];
2364
+ f64_t aggregate_distance_change =
2365
+ std::abs(aggregate_distance - last_aggregate_distance) / last_aggregate_distance;
2366
+
2367
+ auto current_time = std::chrono::high_resolution_clock::now();
2368
+ std::chrono::duration<f64_t> elapsed_time = current_time - start_time;
2369
+ result.runtime_seconds = elapsed_time.count();
2370
+ result.last_iteration_inertia = aggregate_distance_change;
2371
+ result.last_iteration_points_shifted = points_shifted.load(std::memory_order_relaxed);
2372
+
2373
+ // Check for early-exit conditions
2374
+ if (last_aggregate_distance != 0.0 && inertia_threshold != 0.0)
2375
+ if (aggregate_distance_change <= inertia_threshold)
2376
+ break;
2377
+ if (min_points_shifted_per_iteration != 0 || result.last_iteration_points_shifted == 0)
2378
+ if (result.last_iteration_points_shifted <= min_points_shifted_per_iteration)
2379
+ break;
2380
+ if (max_seconds != 0)
2381
+ if (result.runtime_seconds >= max_seconds)
2382
+ break;
2383
+
2384
+ // For every centroid, recalculate the mean of all points assigned to it.
2385
+ // That part is problematic to parallelize on many-core-systems, because of the contention.
2386
+ // Alternatively, a tree-like approach can be used, where every core accumulates it's own partial sums.
2387
+ // And those are later aggregated by a single thread.
2388
+ std::memset(centroids_precise_buffer.data(), 0,
2389
+ wanted_clusters * dimensions * thread_count * sizeof(f64_t));
2390
+ std::memset(reinterpret_cast<byte_t*>(cluster_sizes_buffer.data()), 0,
2391
+ wanted_clusters * sizeof(std::atomic<std::size_t>));
2392
+ executor.dynamic(points_count, [&](std::size_t thread_idx, std::size_t points_idx) {
2393
+ std::size_t centroid_idx = point_to_centroid_index_buffer[points_idx];
2394
+ byte_t const* quantized_point =
2395
+ points_quantized_buffer.data() + points_idx * stride_per_vector_quantized;
2396
+ f64_t* centroid_precise = centroids_precise_buffer.data() + wanted_clusters * dimensions * thread_idx +
2397
+ centroid_idx * dimensions;
2398
+
2399
+ // Upcast the points point into a buffer of double-precision floats.
2400
+ f64_t* point_precise = points_precise_buffer.data() + wanted_clusters * dimensions * thread_idx +
2401
+ centroid_idx * dimensions;
2402
+ if (!decompress_precise(quantized_point, dimensions, reinterpret_cast<byte_t*>(point_precise)))
2403
+ std::memcpy(reinterpret_cast<byte_t*>(point_precise), quantized_point, bytes_per_vector_quantized);
2404
+
2405
+ // Now add the vector from the points into the centroid partial sum.
2406
+ for (std::size_t i = 0; i < dimensions; i++)
2407
+ centroid_precise[i] += point_precise[i];
2408
+
2409
+ cluster_sizes_buffer[centroid_idx].fetch_add(1, std::memory_order_relaxed);
2410
+ return true;
2411
+ });
2412
+
2413
+ // Aggregate the partial sums into the final centroids - storing them in the high-precision
2414
+ // buffer of the first thread. Normalization procedure is different for different metrics.
2415
+ for (std::size_t centroid_idx = 0; centroid_idx < wanted_clusters; centroid_idx++) {
2416
+ f64_t* centroid_precise_aggregated = centroids_precise_buffer.data() + centroid_idx * dimensions;
2417
+ for (std::size_t thread_idx = 1; thread_idx < thread_count; thread_idx++) {
2418
+ f64_t* centroid_precise = centroids_precise_buffer.data() +
2419
+ wanted_clusters * dimensions * thread_idx + centroid_idx * dimensions;
2420
+ for (std::size_t i = 0; i < dimensions; i++)
2421
+ centroid_precise_aggregated[i] += centroid_precise[i];
2422
+ }
2423
+
2424
+ // Normalize based on the metric kind
2425
+ if (metric_kind == metric_kind_t::l2sq_k) {
2426
+ // Normalize for Euclidean distance (L2)
2427
+ std::size_t cluster_size = cluster_sizes_buffer[centroid_idx].load(std::memory_order_relaxed);
2428
+ if (cluster_size > 0)
2429
+ for (std::size_t i = 0; i < dimensions; i++)
2430
+ centroid_precise_aggregated[i] /= static_cast<f64_t>(cluster_size);
2431
+
2432
+ } else if (metric_kind == metric_kind_t::cos_k) {
2433
+ // Normalize for Cosine distance
2434
+ f64_t norm = 0.0;
2435
+ for (std::size_t i = 0; i < dimensions; i++)
2436
+ norm += centroid_precise_aggregated[i] * centroid_precise_aggregated[i];
2437
+ norm = std::sqrt(norm);
2438
+ if (norm > 0.0)
2439
+ for (std::size_t i = 0; i < dimensions; i++)
2440
+ centroid_precise_aggregated[i] /= norm;
2441
+ }
2442
+
2443
+ // Quantize the centroid after normalization for further iterations
2444
+ byte_t* centroid_quantized =
2445
+ centroids_quantized_buffer.data() + centroid_idx * stride_per_vector_quantized;
2446
+ if (!compress_precise(reinterpret_cast<byte_t*>(centroid_precise_aggregated), dimensions,
2447
+ centroid_quantized))
2448
+ std::memcpy(centroid_quantized, reinterpret_cast<byte_t*>(centroid_precise_aggregated),
2449
+ bytes_per_vector_quantized);
2450
+ }
2451
+ }
2452
+
2453
+ // Export stats.
2454
+ result.iterations = iterations;
2455
+ result.computed_distances = points_count * wanted_clusters * iterations;
2456
+ result.aggregate_distance = 0;
2457
+ for (distance_t distance : point_to_centroid_distance_buffer)
2458
+ result.aggregate_distance += distance;
2459
+
2460
+ // We've finished all the iterations, now we can export the centroids back to the original precision.
2461
+ std::memcpy(point_to_centroid_index, point_to_centroid_index_buffer.data(), points_count * sizeof(std::size_t));
2462
+ std::memcpy(point_to_centroid_distance, point_to_centroid_distance_buffer.data(),
2463
+ points_count * sizeof(distance_t));
2464
+ for (std::size_t i = 0; i < wanted_clusters; i++) {
2465
+ byte_t const* quantized_centroid = centroids_quantized_buffer.data() + i * stride_per_vector_quantized;
2466
+ byte_t* centroid = centroids_data + i * centroids_stride_bytes;
2467
+ if (!decompress_points(quantized_centroid, dimensions, centroid))
2468
+ std::memcpy(centroid, quantized_centroid, bytes_per_vector_quantized);
2469
+ }
2470
+
2471
+ return result;
2472
+ }
2473
+ };
2474
+
2475
+ using kmeans_clustering_t = kmeans_clustering_gt<>;
2476
+
2140
2477
  /**
2141
2478
  * @brief C++11 Multi-Hash-Set with Linear Probing.
2142
2479
  *
@@ -20,6 +20,8 @@
20
20
  using namespace unum::usearch;
21
21
  using namespace unum;
22
22
 
23
+ using add_result_t = typename index_dense_t::add_result_t;
24
+
23
25
  class CompiledIndex : public Napi::ObjectWrap<CompiledIndex> {
24
26
  public:
25
27
  static Napi::Object Init(Napi::Env env, Napi::Object exports);
@@ -129,6 +131,8 @@ void CompiledIndex::Load(Napi::CallbackInfo const& ctx) {
129
131
  auto result = native_->load(path.c_str());
130
132
  if (!result)
131
133
  Napi::TypeError::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
134
+ if (!native_->try_reserve(ceil2(native_->size())))
135
+ Napi::Error::New(ctx.Env(), "Failed to reserve memory").ThrowAsJavaScriptException();
132
136
 
133
137
  } catch (...) {
134
138
  Napi::TypeError::New(ctx.Env(), "Loading failed").ThrowAsJavaScriptException();
@@ -142,6 +146,8 @@ void CompiledIndex::View(Napi::CallbackInfo const& ctx) {
142
146
  auto result = native_->view(path.c_str());
143
147
  if (!result)
144
148
  Napi::TypeError::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
149
+ if (!native_->try_reserve(ceil2(native_->size())))
150
+ Napi::Error::New(ctx.Env(), "Failed to reserve memory").ThrowAsJavaScriptException();
145
151
 
146
152
  } catch (...) {
147
153
  Napi::TypeError::New(ctx.Env(), "Memory-mapping failed").ThrowAsJavaScriptException();
@@ -161,7 +167,10 @@ void CompiledIndex::Add(Napi::CallbackInfo const& ctx) {
161
167
  auto run_parallel = [&](auto vectors) {
162
168
  executor_stl_t executor;
163
169
  executor.fixed(tasks, [&](std::size_t /*thread_idx*/, std::size_t task_idx) {
164
- native_->add(static_cast<default_key_t>(keys[task_idx]), vectors + task_idx * native_->dimensions());
170
+ add_result_t result = native_->add(static_cast<default_key_t>(keys[task_idx]),
171
+ vectors + task_idx * native_->dimensions());
172
+ if (!result)
173
+ Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
165
174
  });
166
175
  };
167
176
 
@@ -245,11 +254,14 @@ Napi::Value CompiledIndex::Remove(Napi::CallbackInfo const& ctx) {
245
254
  Napi::Env env = ctx.Env();
246
255
  Napi::BigUint64Array keys = ctx[0].As<Napi::BigUint64Array>();
247
256
  std::size_t length = keys.ElementLength();
248
- Napi::Array result = Napi::Array::New(env, length);
257
+ Napi::Array results = Napi::Array::New(env, length);
249
258
  for (std::size_t i = 0; i < length; ++i) {
250
- result[i] = Napi::Number::New(env, native_->remove(static_cast<default_key_t>(keys[i])).completed);
259
+ auto result = native_->remove(static_cast<default_key_t>(keys[i]));
260
+ if (!result)
261
+ Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
262
+ results[i] = Napi::Number::New(env, result.completed);
251
263
  }
252
- return result;
264
+ return results;
253
265
  }
254
266
 
255
267
  Napi::Value CompiledIndex::Contains(Napi::CallbackInfo const& ctx) {
@@ -268,7 +280,7 @@ Napi::Value CompiledIndex::Count(Napi::CallbackInfo const& ctx) {
268
280
  std::size_t length = keys.ElementLength();
269
281
  Napi::Array result = Napi::Array::New(env, length);
270
282
  for (std::size_t i = 0; i < length; ++i)
271
- result[i] = Napi::Boolean::New(env, native_->count(static_cast<default_key_t>(keys[i])));
283
+ result[i] = Napi::Number::New(env, native_->count(static_cast<default_key_t>(keys[i])));
272
284
  return result;
273
285
  }
274
286