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.
- package/README.md +43 -47
- package/c/README.md +1 -1
- package/c/lib.cpp +28 -13
- package/c/test.c +41 -0
- package/cpp/test.cpp +99 -0
- package/include/usearch/index.hpp +338 -57
- package/include/usearch/index_dense.hpp +155 -50
- package/include/usearch/index_plugins.hpp +94 -35
- package/javascript/README.md +1 -1
- package/javascript/lib.cpp +4 -2
- package/javascript/usearch.test.js +117 -94
- package/numkong/CITATION.cff +1 -1
- package/numkong/Cargo.toml +1 -1
- package/numkong/bench/bench_cross_amx.cpp +12 -0
- package/numkong/bench/bench_mesh.cpp +12 -0
- package/numkong/bench/bench_spatial.cpp +0 -3
- package/numkong/binding.gyp +18 -0
- package/numkong/build.rs +1 -0
- package/numkong/c/dispatch_e5m2.c +23 -3
- package/numkong/include/numkong/capabilities.h +1 -1
- package/numkong/include/numkong/cast/README.md +3 -0
- package/numkong/include/numkong/cast/haswell.h +28 -64
- package/numkong/include/numkong/cast/serial.h +17 -0
- package/numkong/include/numkong/cast/skylake.h +67 -52
- package/numkong/include/numkong/cast.h +1 -0
- package/numkong/include/numkong/dot/README.md +1 -0
- package/numkong/include/numkong/dot/haswell.h +92 -13
- package/numkong/include/numkong/dot/serial.h +15 -0
- package/numkong/include/numkong/dot/skylake.h +61 -14
- package/numkong/include/numkong/dots/README.md +2 -0
- package/numkong/include/numkong/dots/graniteamx.h +434 -0
- package/numkong/include/numkong/dots/haswell.h +28 -28
- package/numkong/include/numkong/dots/sapphireamx.h +1 -1
- package/numkong/include/numkong/dots/serial.h +23 -8
- package/numkong/include/numkong/dots/skylake.h +28 -23
- package/numkong/include/numkong/dots.h +12 -0
- package/numkong/include/numkong/each/serial.h +18 -1
- package/numkong/include/numkong/geospatial/serial.h +14 -3
- package/numkong/include/numkong/maxsim/serial.h +15 -0
- package/numkong/include/numkong/mesh/README.md +50 -44
- package/numkong/include/numkong/mesh/genoa.h +462 -0
- package/numkong/include/numkong/mesh/haswell.h +806 -933
- package/numkong/include/numkong/mesh/neon.h +871 -943
- package/numkong/include/numkong/mesh/neonbfdot.h +382 -522
- package/numkong/include/numkong/mesh/neonfhm.h +676 -0
- package/numkong/include/numkong/mesh/rvv.h +404 -319
- package/numkong/include/numkong/mesh/serial.h +204 -162
- package/numkong/include/numkong/mesh/skylake.h +1029 -1585
- package/numkong/include/numkong/mesh/v128relaxed.h +403 -377
- package/numkong/include/numkong/mesh.h +38 -0
- package/numkong/include/numkong/reduce/serial.h +15 -1
- package/numkong/include/numkong/sparse/serial.h +17 -2
- package/numkong/include/numkong/spatial/genoa.h +0 -68
- package/numkong/include/numkong/spatial/haswell.h +98 -56
- package/numkong/include/numkong/spatial/serial.h +15 -0
- package/numkong/include/numkong/spatial/skylake.h +114 -54
- package/numkong/include/numkong/spatial.h +0 -12
- package/numkong/include/numkong/spatials/graniteamx.h +128 -0
- package/numkong/include/numkong/spatials/serial.h +18 -1
- package/numkong/include/numkong/spatials/skylake.h +2 -2
- package/numkong/include/numkong/spatials.h +17 -0
- package/numkong/include/numkong/tensor.hpp +107 -23
- package/numkong/javascript/@numkong-darwin-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-darwin-x64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-linux-x64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-arm64/package.json +1 -1
- package/numkong/javascript/@numkong-win32-x64/package.json +1 -1
- package/numkong/javascript/numkong.c +3 -2
- package/numkong/package.json +7 -7
- package/numkong/test/test.cu +841 -0
- package/numkong/test/test_cross_amx.cpp +13 -0
- package/numkong/test/test_dlpack.py +492 -0
- package/numkong/test/test_mesh.cpp +12 -0
- package/numkong/test/test_spatial.cpp +0 -3
- package/numkong/test/test_tensor.cpp +87 -27
- package/package.json +1 -1
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
|
@@ -276,15 +276,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
|
|
|
276
276
|
|
|
277
277
|
std::uint32_t dimensions_u32[2]{0};
|
|
278
278
|
std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
|
|
279
|
-
|
|
279
|
+
checked_size_result_t offset_if_u32 =
|
|
280
|
+
checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
|
|
280
281
|
|
|
281
282
|
std::uint64_t dimensions_u64[2]{0};
|
|
282
283
|
std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
|
|
283
|
-
|
|
284
|
+
checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
|
|
285
|
+
checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
|
|
286
|
+
checked_size_result_t offset_if_u64 =
|
|
287
|
+
rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
|
|
288
|
+
: checked_size_overflow();
|
|
284
289
|
|
|
285
290
|
// Check if it starts with 32-bit
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
checked_size_result_t head_offset_if_u32 =
|
|
292
|
+
offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
|
|
293
|
+
if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
|
|
294
|
+
if (std::fseek(file.get(), static_cast<long>(offset_if_u32.value), SEEK_SET) != 0)
|
|
288
295
|
return result.failed(std::strerror(errno));
|
|
289
296
|
read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
|
|
290
297
|
if (!read)
|
|
@@ -299,8 +306,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
|
|
|
299
306
|
}
|
|
300
307
|
|
|
301
308
|
// Check if it starts with 64-bit
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
checked_size_result_t head_offset_if_u64 =
|
|
310
|
+
offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
|
|
311
|
+
if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
|
|
312
|
+
if (std::fseek(file.get(), static_cast<long>(offset_if_u64.value), SEEK_SET) != 0)
|
|
304
313
|
return result.failed(std::strerror(errno));
|
|
305
314
|
read = std::fread(result.head_buffer, sizeof(index_dense_head_buffer_t), 1, file.get());
|
|
306
315
|
if (!read)
|
|
@@ -341,15 +350,22 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
|
|
|
341
350
|
// Check if it starts with 32-bit
|
|
342
351
|
std::uint32_t dimensions_u32[2]{0};
|
|
343
352
|
std::memcpy(dimensions_u32, result.head_buffer, sizeof(dimensions_u32));
|
|
344
|
-
|
|
353
|
+
checked_size_result_t offset_if_u32 =
|
|
354
|
+
checked_mul_add(std::size_t(dimensions_u32[0]), std::size_t(dimensions_u32[1]), sizeof(dimensions_u32));
|
|
345
355
|
|
|
346
356
|
std::uint64_t dimensions_u64[2]{0};
|
|
347
357
|
std::memcpy(dimensions_u64, result.head_buffer, sizeof(dimensions_u64));
|
|
348
|
-
|
|
358
|
+
checked_size_result_t rows_if_u64 = checked_size_from_u64(dimensions_u64[0]);
|
|
359
|
+
checked_size_result_t columns_if_u64 = checked_size_from_u64(dimensions_u64[1]);
|
|
360
|
+
checked_size_result_t offset_if_u64 =
|
|
361
|
+
rows_if_u64 && columns_if_u64 ? checked_mul_add(rows_if_u64.value, columns_if_u64.value, sizeof(dimensions_u64))
|
|
362
|
+
: checked_size_overflow();
|
|
349
363
|
|
|
350
364
|
// Check if it starts with 32-bit
|
|
351
|
-
|
|
352
|
-
|
|
365
|
+
checked_size_result_t head_offset_if_u32 =
|
|
366
|
+
offset_if_u32 ? checked_add(offset_if_u32.value, sizeof(index_dense_head_buffer_t)) : offset_if_u32;
|
|
367
|
+
if (head_offset_if_u32 && head_offset_if_u32.value < file_size) {
|
|
368
|
+
std::memcpy(&result.head_buffer, file_data + offset_if_u32.value, sizeof(index_dense_head_buffer_t));
|
|
353
369
|
result.config.exclude_vectors = false;
|
|
354
370
|
result.config.use_64_bit_dimensions = false;
|
|
355
371
|
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
|
|
@@ -357,8 +373,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_buffer(memory_map
|
|
|
357
373
|
}
|
|
358
374
|
|
|
359
375
|
// Check if it starts with 64-bit
|
|
360
|
-
|
|
361
|
-
|
|
376
|
+
checked_size_result_t head_offset_if_u64 =
|
|
377
|
+
offset_if_u64 ? checked_add(offset_if_u64.value, sizeof(index_dense_head_buffer_t)) : offset_if_u64;
|
|
378
|
+
if (head_offset_if_u64 && head_offset_if_u64.value < file_size) {
|
|
379
|
+
std::memcpy(&result.head_buffer, file_data + offset_if_u64.value, sizeof(index_dense_head_buffer_t));
|
|
362
380
|
result.config.exclude_vectors = false;
|
|
363
381
|
result.config.use_64_bit_dimensions = true;
|
|
364
382
|
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
|
|
@@ -532,6 +550,9 @@ class index_dense_gt {
|
|
|
532
550
|
: parent(other.parent), thread_id(other.thread_id), engaged(other.engaged) {
|
|
533
551
|
other.engaged = false;
|
|
534
552
|
}
|
|
553
|
+
explicit operator bool() const noexcept {
|
|
554
|
+
return parent.typed_ && thread_id != any_thread() && thread_id < parent.typed_->limits().threads();
|
|
555
|
+
}
|
|
535
556
|
};
|
|
536
557
|
|
|
537
558
|
public:
|
|
@@ -635,16 +656,14 @@ class index_dense_gt {
|
|
|
635
656
|
* @param[in] metric One of the provided or an @b ad-hoc metric, type-punned.
|
|
636
657
|
* @param[in] config The index configuration (optional).
|
|
637
658
|
* @param[in] free_key The key used for freed vectors (optional).
|
|
659
|
+
* @param[in] limits Initial reservation. Default sizes the thread pool to
|
|
660
|
+
* @c hardware_concurrency(); pass @c {unreserved} to skip.
|
|
638
661
|
* @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
|
|
639
|
-
*
|
|
640
|
-
* ! If the `metric` isn't provided in this method, it has to be set with
|
|
641
|
-
* ! the `change_metric` method before the index can be used. Alternatively,
|
|
642
|
-
* ! if you are loading an existing index, the metric will be set automatically.
|
|
643
662
|
*/
|
|
644
663
|
static state_result_t make( //
|
|
645
664
|
metric_t metric = {}, //
|
|
646
665
|
index_dense_config_t config = {}, //
|
|
647
|
-
vector_key_t free_key = default_free_value<vector_key_t>()) {
|
|
666
|
+
vector_key_t free_key = default_free_value<vector_key_t>(), index_limits_t limits = {}) {
|
|
648
667
|
|
|
649
668
|
if (metric.missing())
|
|
650
669
|
return state_result_t{}.failed("Metric won't be initialized!");
|
|
@@ -659,16 +678,15 @@ class index_dense_gt {
|
|
|
659
678
|
index_dense_gt& index = result.index;
|
|
660
679
|
index.config_ = config;
|
|
661
680
|
index.free_key_ = free_key;
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
if (metric) {
|
|
665
|
-
scalar_kind_t scalar_kind = metric.scalar_kind();
|
|
666
|
-
index.casts_ = casts_punned_t::make(scalar_kind);
|
|
667
|
-
index.metric_ = metric;
|
|
668
|
-
}
|
|
681
|
+
index.casts_ = casts_punned_t::make(metric.scalar_kind());
|
|
682
|
+
index.metric_ = metric;
|
|
669
683
|
|
|
670
684
|
new (raw) index_t(config);
|
|
671
685
|
index.typed_ = raw;
|
|
686
|
+
|
|
687
|
+
if (!index.try_reserve(limits))
|
|
688
|
+
return state_result_t{}.failed("Failed to reserve memory for the index!");
|
|
689
|
+
|
|
672
690
|
return result;
|
|
673
691
|
}
|
|
674
692
|
|
|
@@ -704,7 +722,34 @@ class index_dense_gt {
|
|
|
704
722
|
|
|
705
723
|
// The metric and its properties
|
|
706
724
|
metric_t const& metric() const { return metric_; }
|
|
707
|
-
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* @brief Replaces the active distance metric, resizing the per-thread cast
|
|
728
|
+
* buffer and rebuilding the cast dispatch table if the new metric
|
|
729
|
+
* changes @c bytes_per_vector() or @c scalar_kind().
|
|
730
|
+
* @return @c false if the cast buffer can't be re-allocated; the metric is
|
|
731
|
+
* left untouched in that case so the index stays consistent.
|
|
732
|
+
*/
|
|
733
|
+
bool try_change_metric(metric_t metric) noexcept {
|
|
734
|
+
checked_size_result_t needed_bytes = checked_mul(limits().threads(), metric.bytes_per_vector());
|
|
735
|
+
if (!needed_bytes)
|
|
736
|
+
return false;
|
|
737
|
+
if (needed_bytes.value > cast_buffer_.size()) {
|
|
738
|
+
cast_buffer_t new_buffer(needed_bytes.value);
|
|
739
|
+
if (!new_buffer)
|
|
740
|
+
return false;
|
|
741
|
+
cast_buffer_ = std::move(new_buffer);
|
|
742
|
+
}
|
|
743
|
+
casts_ = casts_punned_t::make(metric.scalar_kind());
|
|
744
|
+
metric_ = std::move(metric);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/// @brief Throwing counterpart of @ref try_change_metric.
|
|
749
|
+
void change_metric(metric_t metric) {
|
|
750
|
+
if (!try_change_metric(std::move(metric)))
|
|
751
|
+
usearch_raise_runtime_error("failed to grow cast buffer for the new metric");
|
|
752
|
+
}
|
|
708
753
|
|
|
709
754
|
scalar_kind_t scalar_kind() const { return metric_.scalar_kind(); }
|
|
710
755
|
metric_kind_t metric_kind() const { return metric_.metric_kind(); }
|
|
@@ -729,6 +774,45 @@ class index_dense_gt {
|
|
|
729
774
|
return typed_->stats(stats_per_level, max_level);
|
|
730
775
|
}
|
|
731
776
|
|
|
777
|
+
using neighbors_view_t = typename index_t::neighbors_view_t;
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* @brief Returns a read-only range over the neighbors of @p key at the given
|
|
781
|
+
* graph @p level.
|
|
782
|
+
*
|
|
783
|
+
* For multi-key indexes, returns the neighbors of the first matching
|
|
784
|
+
* slot; iterate through `cbegin()`/`cend()` and call the underlying
|
|
785
|
+
* `index_gt::neighbors(member_citerator_t, std::size_t)` overload to
|
|
786
|
+
* inspect every copy of a duplicated key.
|
|
787
|
+
*
|
|
788
|
+
* @return An empty view if the key is not present, or if @p level exceeds
|
|
789
|
+
* the node's level.
|
|
790
|
+
* @warning The view aliases the node's adjacency tape. Hold no concurrent
|
|
791
|
+
* `add`/`update`/`remove` for the lifetime of the view.
|
|
792
|
+
*/
|
|
793
|
+
neighbors_view_t neighbors(vector_key_t key, std::size_t level) const {
|
|
794
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
795
|
+
shared_lock_t lookup_lock(slot_lookup_mutex_);
|
|
796
|
+
auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
|
|
797
|
+
if (matching_slots.first == matching_slots.second)
|
|
798
|
+
return {};
|
|
799
|
+
compressed_slot_t slot = (*matching_slots.first).slot;
|
|
800
|
+
return typed_->neighbors(slot, level);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @brief Returns the top graph level at which @p key is present, or zero
|
|
805
|
+
* if the key is not in the index.
|
|
806
|
+
*/
|
|
807
|
+
std::size_t level_of(vector_key_t key) const {
|
|
808
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
809
|
+
shared_lock_t lookup_lock(slot_lookup_mutex_);
|
|
810
|
+
auto matching_slots = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
|
|
811
|
+
if (matching_slots.first == matching_slots.second)
|
|
812
|
+
return 0;
|
|
813
|
+
return typed_->level_of((*matching_slots.first).slot);
|
|
814
|
+
}
|
|
815
|
+
|
|
732
816
|
dynamic_allocator_t const& allocator() const { return typed_->dynamic_allocator(); }
|
|
733
817
|
vector_key_t const& free_key() const { return free_key_; }
|
|
734
818
|
|
|
@@ -935,6 +1019,8 @@ class index_dense_gt {
|
|
|
935
1019
|
|
|
936
1020
|
index_cluster_config_t cluster_config;
|
|
937
1021
|
thread_lock_t lock = thread_lock_(thread);
|
|
1022
|
+
if (!lock)
|
|
1023
|
+
return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
|
|
938
1024
|
cluster_config.thread = lock.thread_id;
|
|
939
1025
|
cluster_config.expansion = config_.expansion_search;
|
|
940
1026
|
metric_proxy_t metric{*this};
|
|
@@ -994,7 +1080,10 @@ class index_dense_gt {
|
|
|
994
1080
|
available_threads_.push(i);
|
|
995
1081
|
|
|
996
1082
|
// Allocate a buffer for the casted vectors.
|
|
997
|
-
|
|
1083
|
+
checked_size_result_t cast_buffer_bytes = checked_mul(limits.threads(), metric_.bytes_per_vector());
|
|
1084
|
+
if (!cast_buffer_bytes)
|
|
1085
|
+
return false;
|
|
1086
|
+
cast_buffer_t cast_buffer(cast_buffer_bytes.value);
|
|
998
1087
|
if (!cast_buffer)
|
|
999
1088
|
return false;
|
|
1000
1089
|
cast_buffer_ = std::move(cast_buffer);
|
|
@@ -1019,7 +1108,8 @@ class index_dense_gt {
|
|
|
1019
1108
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
1020
1109
|
typed_->clear();
|
|
1021
1110
|
slot_lookup_.clear();
|
|
1022
|
-
|
|
1111
|
+
// Tape pointers are about to be invalidated by the reset below.
|
|
1112
|
+
std::fill(vectors_lookup_.begin(), vectors_lookup_.end(), nullptr);
|
|
1023
1113
|
free_keys_.clear();
|
|
1024
1114
|
vectors_tape_allocator_.reset();
|
|
1025
1115
|
}
|
|
@@ -1144,8 +1234,9 @@ class index_dense_gt {
|
|
|
1144
1234
|
serialization_config_t config = {}, //
|
|
1145
1235
|
progress_at&& progress = {}) {
|
|
1146
1236
|
|
|
1147
|
-
//
|
|
1148
|
-
|
|
1237
|
+
// Preserve any explicit thread counts from the prior index state; fall
|
|
1238
|
+
// back to library defaults when they were never set (e.g. `unreserved`).
|
|
1239
|
+
index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
|
|
1149
1240
|
reset();
|
|
1150
1241
|
|
|
1151
1242
|
// Infer the new index size
|
|
@@ -1206,9 +1297,10 @@ class index_dense_gt {
|
|
|
1206
1297
|
|
|
1207
1298
|
config_.multi = head.multi;
|
|
1208
1299
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1300
|
+
checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
|
|
1301
|
+
if (!cast_buffer_bytes)
|
|
1302
|
+
return result.failed("Failed to allocate memory for the casts");
|
|
1303
|
+
cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
|
|
1212
1304
|
if (!cast_buffer_)
|
|
1213
1305
|
return result.failed("Failed to allocate memory for the casts");
|
|
1214
1306
|
casts_ = casts_punned_t::make(head.kind_scalar);
|
|
@@ -1227,14 +1319,13 @@ class index_dense_gt {
|
|
|
1227
1319
|
return result;
|
|
1228
1320
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
1229
1321
|
return result.failed("Index size and the number of vectors doesn't match");
|
|
1230
|
-
|
|
1231
|
-
if (!typed_->try_reserve(
|
|
1322
|
+
new_limits.members = static_cast<std::size_t>(matrix_rows);
|
|
1323
|
+
if (!typed_->try_reserve(new_limits))
|
|
1232
1324
|
return result.failed("Failed to reserve memory for the index");
|
|
1233
1325
|
|
|
1234
|
-
// After the index is loaded,
|
|
1235
|
-
// match the limits of the underlying engine.
|
|
1326
|
+
// After the index is loaded, resize `available_threads_` to match the new limits.
|
|
1236
1327
|
available_threads_t available_threads;
|
|
1237
|
-
std::size_t max_threads =
|
|
1328
|
+
std::size_t max_threads = new_limits.threads();
|
|
1238
1329
|
if (!available_threads.reserve(max_threads))
|
|
1239
1330
|
return result.failed("Failed to allocate memory for the available threads!");
|
|
1240
1331
|
for (std::size_t i = 0; i < max_threads; i++)
|
|
@@ -1258,8 +1349,9 @@ class index_dense_gt {
|
|
|
1258
1349
|
std::size_t offset = 0, serialization_config_t config = {}, //
|
|
1259
1350
|
progress_at&& progress = {}) {
|
|
1260
1351
|
|
|
1261
|
-
//
|
|
1262
|
-
|
|
1352
|
+
// Preserve any explicit thread counts from the prior index state; fall
|
|
1353
|
+
// back to library defaults when they were never set (e.g. `unreserved`).
|
|
1354
|
+
index_limits_t new_limits = typed_ ? typed_->limits().with_thread_defaults() : index_limits_t{};
|
|
1263
1355
|
reset();
|
|
1264
1356
|
|
|
1265
1357
|
serialization_result_t result = file.open_if_not();
|
|
@@ -1323,8 +1415,10 @@ class index_dense_gt {
|
|
|
1323
1415
|
config_.multi = head.multi;
|
|
1324
1416
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
1325
1417
|
// available_threads_.size() will be updated to old_limits.threads() later in this
|
|
1326
|
-
|
|
1327
|
-
|
|
1418
|
+
checked_size_result_t cast_buffer_bytes = checked_mul(new_limits.threads(), metric_.bytes_per_vector());
|
|
1419
|
+
if (!cast_buffer_bytes)
|
|
1420
|
+
return result.failed("Failed to allocate memory for the casts");
|
|
1421
|
+
cast_buffer_ = cast_buffer_t(cast_buffer_bytes.value);
|
|
1328
1422
|
if (!cast_buffer_)
|
|
1329
1423
|
return result.failed("Failed to allocate memory for the casts");
|
|
1330
1424
|
casts_ = casts_punned_t::make(head.kind_scalar);
|
|
@@ -1344,8 +1438,8 @@ class index_dense_gt {
|
|
|
1344
1438
|
return result;
|
|
1345
1439
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
1346
1440
|
return result.failed("Index size and the number of vectors doesn't match");
|
|
1347
|
-
|
|
1348
|
-
if (!typed_->try_reserve(
|
|
1441
|
+
new_limits.members = static_cast<std::size_t>(matrix_rows);
|
|
1442
|
+
if (!typed_->try_reserve(new_limits))
|
|
1349
1443
|
return result.failed("Failed to reserve memory for the index");
|
|
1350
1444
|
|
|
1351
1445
|
// Address the vectors
|
|
@@ -1356,10 +1450,9 @@ class index_dense_gt {
|
|
|
1356
1450
|
for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
|
|
1357
1451
|
vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
|
|
1358
1452
|
|
|
1359
|
-
// After the index is
|
|
1360
|
-
// match the limits of the underlying engine.
|
|
1453
|
+
// After the index is viewed, resize `available_threads_` to match the new limits.
|
|
1361
1454
|
available_threads_t available_threads;
|
|
1362
|
-
std::size_t max_threads =
|
|
1455
|
+
std::size_t max_threads = new_limits.threads();
|
|
1363
1456
|
if (!available_threads.reserve(max_threads))
|
|
1364
1457
|
return result.failed("Failed to allocate memory for the available threads!");
|
|
1365
1458
|
for (std::size_t i = 0; i < max_threads; i++)
|
|
@@ -1624,6 +1717,11 @@ class index_dense_gt {
|
|
|
1624
1717
|
*/
|
|
1625
1718
|
labeling_result_t rename(vector_key_t from, vector_key_t to) {
|
|
1626
1719
|
labeling_result_t result;
|
|
1720
|
+
if (from == to) {
|
|
1721
|
+
result.completed = count(from);
|
|
1722
|
+
return result;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1627
1725
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
1628
1726
|
|
|
1629
1727
|
if (!multi() && slot_lookup_.contains(key_and_slot_t::any_slot(to)))
|
|
@@ -2043,10 +2141,9 @@ class index_dense_gt {
|
|
|
2043
2141
|
if (thread_id != any_thread())
|
|
2044
2142
|
return {*this, thread_id, false};
|
|
2045
2143
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
available_threads_mutex_.unlock();
|
|
2144
|
+
std::unique_lock<std::mutex> lock(available_threads_mutex_);
|
|
2145
|
+
if (!available_threads_.try_pop(thread_id))
|
|
2146
|
+
return {*this, any_thread(), false};
|
|
2050
2147
|
return {*this, thread_id, true};
|
|
2051
2148
|
}
|
|
2052
2149
|
|
|
@@ -2067,6 +2164,8 @@ class index_dense_gt {
|
|
|
2067
2164
|
|
|
2068
2165
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
2069
2166
|
thread_lock_t lock = thread_lock_(thread);
|
|
2167
|
+
if (!lock)
|
|
2168
|
+
return add_result_t{}.failed("Reserve capacity ahead of insertions!");
|
|
2070
2169
|
byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
|
|
2071
2170
|
{
|
|
2072
2171
|
byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
|
|
@@ -2120,6 +2219,8 @@ class index_dense_gt {
|
|
|
2120
2219
|
|
|
2121
2220
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
2122
2221
|
thread_lock_t lock = thread_lock_(thread);
|
|
2222
|
+
if (!lock)
|
|
2223
|
+
return search_result_t{*this}.failed("Reserve capacity ahead of searches!");
|
|
2123
2224
|
byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
|
|
2124
2225
|
{
|
|
2125
2226
|
byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
|
|
@@ -2156,6 +2257,8 @@ class index_dense_gt {
|
|
|
2156
2257
|
|
|
2157
2258
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
2158
2259
|
thread_lock_t lock = thread_lock_(thread);
|
|
2260
|
+
if (!lock)
|
|
2261
|
+
return cluster_result_t{}.failed("Reserve capacity ahead of searches!");
|
|
2159
2262
|
byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
|
|
2160
2263
|
{
|
|
2161
2264
|
byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
|
|
@@ -2180,6 +2283,8 @@ class index_dense_gt {
|
|
|
2180
2283
|
|
|
2181
2284
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
2182
2285
|
thread_lock_t lock = thread_lock_(thread);
|
|
2286
|
+
if (!lock)
|
|
2287
|
+
return {};
|
|
2183
2288
|
byte_t const* vector_data = reinterpret_cast<byte_t const*>(vector);
|
|
2184
2289
|
{
|
|
2185
2290
|
byte_t* casted_data = cast_buffer_.data() + metric_.bytes_per_vector() * lock.thread_id;
|
|
@@ -1572,21 +1572,24 @@ class aligned_allocator_gt {
|
|
|
1572
1572
|
constexpr std::size_t alignment() const { return alignment_ak; }
|
|
1573
1573
|
|
|
1574
1574
|
pointer allocate(size_type length) const {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
if (length > length_bytes)
|
|
1575
|
+
checked_size_result_t bytes = checked_mul(length, sizeof(value_type));
|
|
1576
|
+
if (!bytes)
|
|
1578
1577
|
return nullptr;
|
|
1578
|
+
checked_size_result_t length_bytes = checked_round_up(bytes.value, alignment_ak);
|
|
1579
|
+
if (!length_bytes)
|
|
1580
|
+
return nullptr;
|
|
1581
|
+
|
|
1579
1582
|
std::size_t alignment = alignment_ak;
|
|
1580
1583
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
1581
|
-
return (pointer)_aligned_malloc(length_bytes, alignment);
|
|
1584
|
+
return (pointer)_aligned_malloc(length_bytes.value, alignment);
|
|
1582
1585
|
#elif defined(USEARCH_DEFINED_APPLE) || defined(USEARCH_DEFINED_ANDROID)
|
|
1583
1586
|
// Apple Clang keeps complaining that `aligned_alloc` is only available
|
|
1584
1587
|
// with macOS 10.15 and newer or Android API >= 28, so let's use `posix_memalign` there.
|
|
1585
1588
|
void* result = nullptr;
|
|
1586
|
-
int status = posix_memalign(&result, alignment, length_bytes);
|
|
1589
|
+
int status = posix_memalign(&result, alignment, length_bytes.value);
|
|
1587
1590
|
return status == 0 ? (pointer)result : nullptr;
|
|
1588
1591
|
#else
|
|
1589
|
-
return (pointer)aligned_alloc(alignment, length_bytes);
|
|
1592
|
+
return (pointer)aligned_alloc(alignment, length_bytes.value);
|
|
1590
1593
|
#endif
|
|
1591
1594
|
}
|
|
1592
1595
|
|
|
@@ -1615,7 +1618,10 @@ class page_allocator_t {
|
|
|
1615
1618
|
* @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
|
|
1616
1619
|
*/
|
|
1617
1620
|
byte_t* allocate(std::size_t count_bytes) const noexcept {
|
|
1618
|
-
|
|
1621
|
+
checked_size_result_t rounded_bytes = checked_round_up(count_bytes, page_size());
|
|
1622
|
+
if (!rounded_bytes)
|
|
1623
|
+
return nullptr;
|
|
1624
|
+
count_bytes = rounded_bytes.value;
|
|
1619
1625
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
1620
1626
|
return (byte_t*)(::VirtualAlloc(NULL, count_bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE));
|
|
1621
1627
|
#else
|
|
@@ -1628,7 +1634,10 @@ class page_allocator_t {
|
|
|
1628
1634
|
#if defined(USEARCH_DEFINED_WINDOWS)
|
|
1629
1635
|
::VirtualFree(page_pointer, 0, MEM_RELEASE);
|
|
1630
1636
|
#else
|
|
1631
|
-
|
|
1637
|
+
checked_size_result_t rounded_bytes = checked_round_up(count_bytes, page_size());
|
|
1638
|
+
if (!rounded_bytes)
|
|
1639
|
+
return;
|
|
1640
|
+
count_bytes = rounded_bytes.value;
|
|
1632
1641
|
munmap(page_pointer, count_bytes);
|
|
1633
1642
|
#endif
|
|
1634
1643
|
}
|
|
@@ -1724,25 +1733,39 @@ template <std::size_t alignment_ak = 1> class memory_mapping_allocator_gt {
|
|
|
1724
1733
|
* @return A pointer to the allocated memory block, or `nullptr` if allocation fails.
|
|
1725
1734
|
*/
|
|
1726
1735
|
inline byte_t* allocate(std::size_t count_bytes) noexcept {
|
|
1727
|
-
|
|
1736
|
+
checked_size_result_t extended_bytes = checked_round_up(count_bytes, alignment_ak);
|
|
1737
|
+
if (!extended_bytes)
|
|
1738
|
+
return nullptr;
|
|
1728
1739
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1740
|
+
checked_size_result_t next_usage = checked_add(last_usage_, extended_bytes.value);
|
|
1741
|
+
if (!next_usage)
|
|
1742
|
+
return nullptr;
|
|
1743
|
+
if (!last_arena_ || (next_usage.value >= last_capacity_)) {
|
|
1744
|
+
checked_size_result_t rounded_bytes = checked_ceil2(extended_bytes.value);
|
|
1745
|
+
if (!rounded_bytes)
|
|
1746
|
+
return nullptr;
|
|
1747
|
+
checked_size_result_t new_cap =
|
|
1748
|
+
checked_mul((std::max)(last_capacity_, rounded_bytes.value), capacity_multiplier());
|
|
1749
|
+
if (!new_cap)
|
|
1750
|
+
return nullptr;
|
|
1751
|
+
checked_size_result_t new_total_allocated = checked_add(total_allocated_, new_cap.value);
|
|
1752
|
+
if (!new_total_allocated)
|
|
1753
|
+
return nullptr;
|
|
1754
|
+
byte_t* new_arena = page_allocator_t{}.allocate(new_cap.value);
|
|
1732
1755
|
if (!new_arena)
|
|
1733
1756
|
return nullptr;
|
|
1734
1757
|
std::memcpy(new_arena, &last_arena_, sizeof(byte_t*));
|
|
1735
|
-
std::memcpy(new_arena + sizeof(byte_t*), &new_cap, sizeof(std::size_t));
|
|
1758
|
+
std::memcpy(new_arena + sizeof(byte_t*), &new_cap.value, sizeof(std::size_t));
|
|
1736
1759
|
|
|
1737
1760
|
wasted_space_ += total_reserved();
|
|
1738
1761
|
last_arena_ = new_arena;
|
|
1739
|
-
last_capacity_ = new_cap;
|
|
1762
|
+
last_capacity_ = new_cap.value;
|
|
1740
1763
|
last_usage_ = head_size();
|
|
1741
|
-
total_allocated_
|
|
1764
|
+
total_allocated_ = new_total_allocated.value;
|
|
1742
1765
|
}
|
|
1743
1766
|
|
|
1744
|
-
wasted_space_ += extended_bytes - count_bytes;
|
|
1745
|
-
return last_arena_ + exchange(last_usage_, last_usage_ + extended_bytes);
|
|
1767
|
+
wasted_space_ += extended_bytes.value - count_bytes;
|
|
1768
|
+
return last_arena_ + exchange(last_usage_, last_usage_ + extended_bytes.value);
|
|
1746
1769
|
}
|
|
1747
1770
|
|
|
1748
1771
|
/**
|
|
@@ -1948,6 +1971,11 @@ template <typename from_scalar_at> struct cast_to_i8_gt {
|
|
|
1948
1971
|
for (std::size_t i = 0; i != dim; ++i)
|
|
1949
1972
|
magnitude += (double)typed_input[i] * (double)typed_input[i];
|
|
1950
1973
|
magnitude = std::sqrt(magnitude);
|
|
1974
|
+
// `!(x > 0)` also catches NaN; cast-to-int of NaN is UB.
|
|
1975
|
+
if (!(magnitude > 0.0)) {
|
|
1976
|
+
std::fill_n(typed_output, dim, std::int8_t{0});
|
|
1977
|
+
return true;
|
|
1978
|
+
}
|
|
1951
1979
|
for (std::size_t i = 0; i != dim; ++i)
|
|
1952
1980
|
typed_output[i] =
|
|
1953
1981
|
static_cast<std::int8_t>(usearch::clamp<double>(typed_input[i] * 127.0 / magnitude, -127.0, 127.0));
|
|
@@ -1973,6 +2001,11 @@ template <typename from_scalar_at> struct cast_to_u8_gt {
|
|
|
1973
2001
|
for (std::size_t i = 0; i != dim; ++i)
|
|
1974
2002
|
magnitude += (double)typed_input[i] * (double)typed_input[i];
|
|
1975
2003
|
magnitude = std::sqrt(magnitude);
|
|
2004
|
+
// `!(x > 0)` also catches NaN; cast-to-int of NaN is UB.
|
|
2005
|
+
if (!(magnitude > 0.0)) {
|
|
2006
|
+
std::fill_n(typed_output, dim, std::uint8_t{0});
|
|
2007
|
+
return true;
|
|
2008
|
+
}
|
|
1976
2009
|
for (std::size_t i = 0; i != dim; ++i)
|
|
1977
2010
|
typed_output[i] =
|
|
1978
2011
|
static_cast<std::uint8_t>(usearch::clamp<double>(typed_input[i] * 255.0 / magnitude, 0.0, 255.0));
|
|
@@ -3750,7 +3783,8 @@ class flat_hash_multi_set_gt {
|
|
|
3750
3783
|
|
|
3751
3784
|
public:
|
|
3752
3785
|
std::size_t size() const noexcept { return populated_slots_; }
|
|
3753
|
-
std::size_t capacity() const noexcept { return capacity_slots_; }
|
|
3786
|
+
std::size_t capacity() const noexcept { return capacity_slots_ * 2u / 3u; }
|
|
3787
|
+
std::size_t capacity_slots() const noexcept { return capacity_slots_; }
|
|
3754
3788
|
|
|
3755
3789
|
flat_hash_multi_set_gt() noexcept {}
|
|
3756
3790
|
~flat_hash_multi_set_gt() noexcept { reset(); }
|
|
@@ -3764,7 +3798,10 @@ class flat_hash_multi_set_gt {
|
|
|
3764
3798
|
}
|
|
3765
3799
|
|
|
3766
3800
|
// Allocate new memory
|
|
3767
|
-
|
|
3801
|
+
checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
|
|
3802
|
+
if (!bytes)
|
|
3803
|
+
usearch_raise_runtime_error("failed memory allocation");
|
|
3804
|
+
data_ = (char*)allocator_t{}.allocate(bytes.value);
|
|
3768
3805
|
if (!data_)
|
|
3769
3806
|
usearch_raise_runtime_error("failed memory allocation");
|
|
3770
3807
|
|
|
@@ -3804,7 +3841,10 @@ class flat_hash_multi_set_gt {
|
|
|
3804
3841
|
allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
|
|
3805
3842
|
|
|
3806
3843
|
// Allocate new memory
|
|
3807
|
-
|
|
3844
|
+
checked_size_result_t bytes = checked_mul(other.buckets_, bytes_per_bucket());
|
|
3845
|
+
if (!bytes)
|
|
3846
|
+
usearch_raise_runtime_error("failed memory allocation");
|
|
3847
|
+
data_ = (char*)allocator_t{}.allocate(bytes.value);
|
|
3808
3848
|
if (!data_)
|
|
3809
3849
|
usearch_raise_runtime_error("failed memory allocation");
|
|
3810
3850
|
|
|
@@ -3853,22 +3893,37 @@ class flat_hash_multi_set_gt {
|
|
|
3853
3893
|
}
|
|
3854
3894
|
|
|
3855
3895
|
bool try_reserve(std::size_t capacity) noexcept {
|
|
3856
|
-
if (capacity
|
|
3896
|
+
if (capacity <= this->capacity())
|
|
3857
3897
|
return true;
|
|
3858
3898
|
|
|
3859
3899
|
// Calculate new sizes
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3900
|
+
checked_size_result_t scaled_capacity = checked_mul(capacity, std::size_t{3});
|
|
3901
|
+
if (!scaled_capacity)
|
|
3902
|
+
return false;
|
|
3903
|
+
checked_size_result_t slots_needed = checked_divide_round_up(scaled_capacity.value, std::size_t{2});
|
|
3904
|
+
if (!slots_needed)
|
|
3905
|
+
return false;
|
|
3906
|
+
checked_size_result_t new_slots_checked = checked_ceil2(slots_needed.value);
|
|
3907
|
+
if (!new_slots_checked)
|
|
3908
|
+
return false;
|
|
3909
|
+
checked_size_result_t new_buckets_checked =
|
|
3910
|
+
checked_divide_round_up(new_slots_checked.value, slots_per_bucket());
|
|
3911
|
+
if (!new_buckets_checked)
|
|
3912
|
+
return false;
|
|
3913
|
+
checked_size_result_t new_slots = checked_mul(new_buckets_checked.value, slots_per_bucket());
|
|
3914
|
+
if (!new_slots)
|
|
3915
|
+
return false;
|
|
3916
|
+
checked_size_result_t new_bytes = checked_mul(new_buckets_checked.value, bytes_per_bucket());
|
|
3917
|
+
if (!new_bytes)
|
|
3918
|
+
return false;
|
|
3864
3919
|
|
|
3865
3920
|
// Allocate new memory
|
|
3866
|
-
char* new_data = (char*)allocator_t{}.allocate(new_bytes);
|
|
3921
|
+
char* new_data = (char*)allocator_t{}.allocate(new_bytes.value);
|
|
3867
3922
|
if (!new_data)
|
|
3868
3923
|
return false;
|
|
3869
3924
|
|
|
3870
3925
|
// Initialize new buckets to empty
|
|
3871
|
-
std::memset(new_data, 0, new_bytes);
|
|
3926
|
+
std::memset(new_data, 0, new_bytes.value);
|
|
3872
3927
|
|
|
3873
3928
|
// Rehash and copy existing elements to new_data
|
|
3874
3929
|
hash_t hasher;
|
|
@@ -3879,7 +3934,7 @@ class flat_hash_multi_set_gt {
|
|
|
3879
3934
|
|
|
3880
3935
|
// Rehash
|
|
3881
3936
|
std::size_t hash_value = hasher(old_slot.element);
|
|
3882
|
-
std::size_t new_slot_index = hash_value & (new_slots - 1);
|
|
3937
|
+
std::size_t new_slot_index = hash_value & (new_slots.value - 1);
|
|
3883
3938
|
|
|
3884
3939
|
// Linear probing to find an empty slot in new_data
|
|
3885
3940
|
while (true) {
|
|
@@ -3889,7 +3944,7 @@ class flat_hash_multi_set_gt {
|
|
|
3889
3944
|
new_slot.header.populated |= new_slot.mask;
|
|
3890
3945
|
break;
|
|
3891
3946
|
}
|
|
3892
|
-
new_slot_index = (new_slot_index + 1) & (new_slots - 1);
|
|
3947
|
+
new_slot_index = (new_slot_index + 1) & (new_slots.value - 1);
|
|
3893
3948
|
}
|
|
3894
3949
|
}
|
|
3895
3950
|
|
|
@@ -3897,8 +3952,8 @@ class flat_hash_multi_set_gt {
|
|
|
3897
3952
|
if (data_)
|
|
3898
3953
|
allocator_t{}.deallocate(data_, buckets_ * bytes_per_bucket());
|
|
3899
3954
|
data_ = new_data;
|
|
3900
|
-
buckets_ =
|
|
3901
|
-
capacity_slots_ = new_slots;
|
|
3955
|
+
buckets_ = new_buckets_checked.value;
|
|
3956
|
+
capacity_slots_ = new_slots.value;
|
|
3902
3957
|
|
|
3903
3958
|
return true;
|
|
3904
3959
|
}
|
|
@@ -3916,16 +3971,20 @@ class flat_hash_multi_set_gt {
|
|
|
3916
3971
|
: index_(index), parent_(parent), query_(query), equals_(equals) {}
|
|
3917
3972
|
|
|
3918
3973
|
// Pre-increment: advance past tombstones and non-matching entries,
|
|
3919
|
-
// stopping at the next matching live entry or an empty slot.
|
|
3974
|
+
// stopping at the next matching live entry or an empty slot. When every
|
|
3975
|
+
// slot is either live or tombstoned (no empty slot exists), the probe
|
|
3976
|
+
// saturates after `capacity_slots_` steps and the iterator becomes
|
|
3977
|
+
// `end()` - otherwise the loop would spin forever.
|
|
3920
3978
|
equal_iterator_gt& operator++() {
|
|
3921
|
-
|
|
3979
|
+
for (std::size_t remaining = parent_->capacity_slots_; remaining; --remaining) {
|
|
3922
3980
|
index_ = (index_ + 1) & (parent_->capacity_slots_ - 1);
|
|
3923
3981
|
auto slot = parent_->slot_ref(index_);
|
|
3924
3982
|
bool is_empty = ~slot.header.populated & slot.mask;
|
|
3925
3983
|
bool is_match = !(slot.header.deleted & slot.mask) && equals_(slot.element, query_);
|
|
3926
3984
|
if (is_empty || is_match)
|
|
3927
|
-
|
|
3928
|
-
}
|
|
3985
|
+
return *this;
|
|
3986
|
+
}
|
|
3987
|
+
index_ = parent_->capacity_slots_; // saturated probe -> end()
|
|
3929
3988
|
return *this;
|
|
3930
3989
|
}
|
|
3931
3990
|
|
package/javascript/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Customize your index with additional configuration options:
|
|
|
48
48
|
const index = new usearch.Index({
|
|
49
49
|
dimensions: 128,
|
|
50
50
|
metric: 'ip',
|
|
51
|
-
quantization: 'f32',
|
|
51
|
+
quantization: 'f32', // or 'bf16', 'f16', 'e5m2', 'e4m3', 'e3m2', 'e2m3', 'u8', 'i8', 'b1'
|
|
52
52
|
connectivity: 10,
|
|
53
53
|
expansion_add: 5,
|
|
54
54
|
expansion_search: 3,
|