usearch 2.11.0 → 2.13.0
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 +126 -37
- package/binding.gyp +11 -12
- package/c/README.md +9 -3
- package/c/lib.cpp +50 -13
- package/c/test.c +60 -55
- package/c/usearch.h +16 -0
- package/cpp/CMakeLists.txt +6 -0
- package/cpp/README.md +46 -14
- package/cpp/bench.cpp +99 -47
- package/cpp/test.cpp +948 -132
- package/include/usearch/index.hpp +894 -272
- package/include/usearch/index_dense.hpp +419 -152
- package/include/usearch/index_plugins.hpp +99 -49
- package/javascript/dist/cjs/usearch.d.ts +39 -1
- package/javascript/dist/cjs/usearch.js +11 -1
- package/javascript/dist/esm/usearch.d.ts +39 -1
- package/javascript/dist/esm/usearch.js +11 -1
- package/javascript/tsconfig-esm.json +3 -2
- package/javascript/usearch.ts +18 -7
- package/package.json +2 -2
- 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
- package/simsimd/.cmake-format.py +19 -0
- package/simsimd/CONTRIBUTING.md +49 -8
- package/simsimd/Cargo.lock +1 -1
- package/simsimd/Cargo.toml +10 -5
- package/simsimd/Package.swift +16 -0
- package/simsimd/README.md +69 -15
- package/simsimd/binding.gyp +14 -19
- package/simsimd/build.rs +2 -0
- package/simsimd/c/lib.c +18 -0
- package/simsimd/include/module.modulemap +4 -0
- package/simsimd/include/simsimd/binary.h +2 -0
- package/simsimd/include/simsimd/dot.h +466 -32
- package/simsimd/include/simsimd/probability.h +20 -10
- package/simsimd/include/simsimd/simsimd.h +444 -98
- package/simsimd/include/simsimd/spatial.h +480 -44
- package/simsimd/include/simsimd/types.h +161 -39
- package/simsimd/javascript/simsimd.ts +26 -2
- package/simsimd/package.json +1 -1
- package/simsimd/rust/benches/cosine.rs +8 -2
- package/simsimd/rust/benches/native.rs +240 -2
- package/simsimd/rust/benches/sqeuclidean.rs +8 -2
- package/simsimd/rust/lib.rs +50 -13
- package/simsimd/swift/source/SimSIMD.swift +123 -0
- package/simsimd/swift/tests/SimSIMDTests.swift +92 -0
- package/prebuilds/win32-ia32/usearch.node +0 -0
- package/prebuilds/win32-x64/usearch.node +0 -0
- package/simsimd/.releaserc +0 -87
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file index_dense.hpp
|
|
3
|
+
* @author Ash Vardanian
|
|
4
|
+
* @brief Single-header Vector Search engine for equi-dimensional dense vectors.
|
|
5
|
+
* @date July 26, 2023
|
|
6
|
+
*/
|
|
1
7
|
#pragma once
|
|
8
|
+
#include "index.hpp"
|
|
2
9
|
#include <stdlib.h> // `aligned_alloc`
|
|
3
10
|
|
|
4
|
-
#include <functional> // `std::function`
|
|
5
|
-
#include <numeric> // `std::iota`
|
|
6
|
-
#include <thread> // `std::thread`
|
|
7
|
-
#include <vector> // `std::vector`
|
|
8
|
-
|
|
9
11
|
#include <usearch/index.hpp>
|
|
10
12
|
#include <usearch/index_plugins.hpp>
|
|
11
13
|
|
|
@@ -89,10 +91,32 @@ struct index_dense_head_result_t {
|
|
|
89
91
|
}
|
|
90
92
|
};
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @brief Configuration settings for the construction of dense
|
|
96
|
+
* equidimensional vector indexes.
|
|
97
|
+
*
|
|
98
|
+
* Unlike the underlying `index_gt` class, incroporates the
|
|
99
|
+
* `::expansion_add` and `::expansion_search` parameters passed
|
|
100
|
+
* separately for the lower-level engine.
|
|
101
|
+
*/
|
|
92
102
|
struct index_dense_config_t : public index_config_t {
|
|
93
103
|
std::size_t expansion_add = default_expansion_add();
|
|
94
104
|
std::size_t expansion_search = default_expansion_search();
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @brief Excludes vectors from the serialized file.
|
|
108
|
+
* This is handy when you want to store the vectors in a separate file.
|
|
109
|
+
*
|
|
110
|
+
* ! For advanced users only.
|
|
111
|
+
*/
|
|
95
112
|
bool exclude_vectors = false;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @brief Allows you to store multiple vectors per key.
|
|
116
|
+
* This is handy when a large document is chunked into many parts.
|
|
117
|
+
*
|
|
118
|
+
* ! May degrade the performance of iterators.
|
|
119
|
+
*/
|
|
96
120
|
bool multi = false;
|
|
97
121
|
|
|
98
122
|
/**
|
|
@@ -101,17 +125,37 @@ struct index_dense_config_t : public index_config_t {
|
|
|
101
125
|
* the vectors-to-keys mappings.
|
|
102
126
|
*
|
|
103
127
|
* ! This configuration parameter doesn't affect the serialized file,
|
|
104
|
-
* ! and is not preserved between runs. Makes sense for
|
|
105
|
-
* !
|
|
128
|
+
* ! and is not preserved between runs. Makes sense for smaller vectors
|
|
129
|
+
* ! that fit in a couple of cache lines.
|
|
130
|
+
*
|
|
131
|
+
* The trade-off is that some methods won't be available, like `get`, `rename`,
|
|
132
|
+
* and `remove`. The basic functionality, like `add` and `search` will work as
|
|
133
|
+
* expected even with `enable_key_lookups = false`.
|
|
134
|
+
*
|
|
135
|
+
* If both `!multi && !enable_key_lookups`, the "duplicate entry" checks won't
|
|
136
|
+
* be performed and no errors will be raised.
|
|
106
137
|
*/
|
|
107
138
|
bool enable_key_lookups = true;
|
|
108
139
|
|
|
109
|
-
index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
|
|
140
|
+
inline index_dense_config_t(index_config_t base) noexcept : index_config_t(base) {}
|
|
141
|
+
|
|
142
|
+
inline index_dense_config_t(std::size_t c = 0, std::size_t ea = 0, std::size_t es = 0) noexcept
|
|
143
|
+
: index_config_t(c), expansion_add(ea), expansion_search(es) {}
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
/**
|
|
146
|
+
* @brief Validates the configuration settings, updating them in-place.
|
|
147
|
+
* @return Error message, if any.
|
|
148
|
+
*/
|
|
149
|
+
inline error_t validate() noexcept {
|
|
150
|
+
error_t error = index_config_t::validate();
|
|
151
|
+
if (error)
|
|
152
|
+
return error;
|
|
153
|
+
if (expansion_add == 0)
|
|
154
|
+
expansion_add = default_expansion_add();
|
|
155
|
+
if (expansion_search == 0)
|
|
156
|
+
expansion_search = default_expansion_search();
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
115
159
|
};
|
|
116
160
|
|
|
117
161
|
struct index_dense_clustering_config_t {
|
|
@@ -163,6 +207,40 @@ struct index_dense_metadata_result_t {
|
|
|
163
207
|
}
|
|
164
208
|
};
|
|
165
209
|
|
|
210
|
+
inline scalar_kind_t convert_pre_2_10_scalar_kind(scalar_kind_t scalar_kind) noexcept {
|
|
211
|
+
// The old enum `scalar_kind_t` is defined without explicit constants from 0
|
|
212
|
+
switch (static_cast<std::underlying_type<scalar_kind_t>::type>(scalar_kind)) {
|
|
213
|
+
case 0: return scalar_kind_t::unknown_k;
|
|
214
|
+
case 1: return scalar_kind_t::b1x8_k;
|
|
215
|
+
case 2: return scalar_kind_t::u40_k;
|
|
216
|
+
case 3: return scalar_kind_t::uuid_k;
|
|
217
|
+
case 4: return scalar_kind_t::f64_k;
|
|
218
|
+
case 5: return scalar_kind_t::f32_k;
|
|
219
|
+
case 6: return scalar_kind_t::f16_k;
|
|
220
|
+
case 7: return scalar_kind_t::f8_k;
|
|
221
|
+
case 8: return scalar_kind_t::u64_k;
|
|
222
|
+
case 9: return scalar_kind_t::u32_k;
|
|
223
|
+
case 10: return scalar_kind_t::u8_k;
|
|
224
|
+
case 11: return scalar_kind_t::i64_k;
|
|
225
|
+
case 12: return scalar_kind_t::i32_k;
|
|
226
|
+
case 13: return scalar_kind_t::i16_k;
|
|
227
|
+
case 14: return scalar_kind_t::i8_k;
|
|
228
|
+
default: return scalar_kind;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
inline void fix_pre_2_10_metadata(index_dense_head_t& head) {
|
|
233
|
+
// deal with incompatibility with pre-2.10 versions: https://github.com/unum-cloud/usearch/issues/423
|
|
234
|
+
if (head.version_major == 2 && head.version_minor < 10) {
|
|
235
|
+
head.kind_scalar = convert_pre_2_10_scalar_kind(head.kind_scalar);
|
|
236
|
+
head.kind_key = convert_pre_2_10_scalar_kind(head.kind_key);
|
|
237
|
+
head.kind_compressed_slot = convert_pre_2_10_scalar_kind(head.kind_compressed_slot);
|
|
238
|
+
// update minor versions so that no repeated conversion occurs
|
|
239
|
+
head.version_minor = 10;
|
|
240
|
+
head.version_patch = 0;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
166
244
|
/**
|
|
167
245
|
* @brief Extracts metadata from a pre-constructed index on disk,
|
|
168
246
|
* without loading it or mapping the whole binary file.
|
|
@@ -180,8 +258,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
|
|
|
180
258
|
|
|
181
259
|
// Check if the file immediately starts with the index, instead of vectors
|
|
182
260
|
result.config.exclude_vectors = true;
|
|
183
|
-
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
|
|
261
|
+
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
|
|
262
|
+
fix_pre_2_10_metadata(result.head);
|
|
184
263
|
return result;
|
|
264
|
+
}
|
|
185
265
|
|
|
186
266
|
if (std::fseek(file.get(), 0L, SEEK_END) != 0)
|
|
187
267
|
return result.failed("Can't infer file size");
|
|
@@ -207,8 +287,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
|
|
|
207
287
|
|
|
208
288
|
result.config.exclude_vectors = false;
|
|
209
289
|
result.config.use_64_bit_dimensions = false;
|
|
210
|
-
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
|
|
290
|
+
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
|
|
291
|
+
fix_pre_2_10_metadata(result.head);
|
|
211
292
|
return result;
|
|
293
|
+
}
|
|
212
294
|
}
|
|
213
295
|
|
|
214
296
|
// Check if it starts with 64-bit
|
|
@@ -222,8 +304,10 @@ inline index_dense_metadata_result_t index_dense_metadata_from_path(char const*
|
|
|
222
304
|
// Check if it starts with 64-bit
|
|
223
305
|
result.config.exclude_vectors = false;
|
|
224
306
|
result.config.use_64_bit_dimensions = true;
|
|
225
|
-
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0)
|
|
307
|
+
if (std::memcmp(result.head_buffer, default_magic(), std::strlen(default_magic())) == 0) {
|
|
308
|
+
fix_pre_2_10_metadata(result.head);
|
|
226
309
|
return result;
|
|
310
|
+
}
|
|
227
311
|
}
|
|
228
312
|
|
|
229
313
|
return result.failed("Not a dense USearch index!");
|
|
@@ -318,7 +402,7 @@ class index_dense_gt {
|
|
|
318
402
|
|
|
319
403
|
private:
|
|
320
404
|
/// @brief Schema: input buffer, bytes in input buffer, output buffer.
|
|
321
|
-
using cast_t =
|
|
405
|
+
using cast_t = bool (*)(byte_t const*, std::size_t, byte_t*);
|
|
322
406
|
/// @brief Punned index.
|
|
323
407
|
using index_t = index_gt< //
|
|
324
408
|
distance_t, vector_key_t, compressed_slot_t, //
|
|
@@ -353,7 +437,10 @@ class index_dense_gt {
|
|
|
353
437
|
index_dense_config_t config_;
|
|
354
438
|
index_t* typed_ = nullptr;
|
|
355
439
|
|
|
356
|
-
|
|
440
|
+
using cast_buffer_t = buffer_gt<byte_t, dynamic_allocator_t>;
|
|
441
|
+
|
|
442
|
+
/// @brief Temporary memory for every thread to store a casted vector.
|
|
443
|
+
mutable cast_buffer_t cast_buffer_;
|
|
357
444
|
struct casts_t {
|
|
358
445
|
cast_t from_b1x8;
|
|
359
446
|
cast_t from_i8;
|
|
@@ -375,11 +462,17 @@ class index_dense_gt {
|
|
|
375
462
|
/// @brief Allocator for the copied vectors, aligned to widest double-precision scalars.
|
|
376
463
|
vectors_tape_allocator_t vectors_tape_allocator_;
|
|
377
464
|
|
|
465
|
+
using vectors_lookup_allocator_t = aligned_allocator_gt<byte_t*, 64>;
|
|
466
|
+
using vectors_lookup_t = buffer_gt<byte_t*, vectors_lookup_allocator_t>;
|
|
467
|
+
|
|
378
468
|
/// @brief For every managed `compressed_slot_t` stores a pointer to the allocated vector copy.
|
|
379
|
-
mutable
|
|
469
|
+
mutable vectors_lookup_t vectors_lookup_;
|
|
380
470
|
|
|
381
|
-
|
|
382
|
-
|
|
471
|
+
using available_threads_allocator_t = aligned_allocator_gt<std::size_t, 64>;
|
|
472
|
+
using available_threads_t = ring_gt<std::size_t, available_threads_allocator_t>;
|
|
473
|
+
|
|
474
|
+
/// @brief Originally forms and array of integers [0, threads], marking all as available.
|
|
475
|
+
mutable available_threads_t available_threads_;
|
|
383
476
|
|
|
384
477
|
/// @brief Mutex, controlling concurrent access to `available_threads_`.
|
|
385
478
|
mutable std::mutex available_threads_mutex_;
|
|
@@ -402,8 +495,8 @@ class index_dense_gt {
|
|
|
402
495
|
|
|
403
496
|
struct lookup_key_hash_t {
|
|
404
497
|
using is_transparent = void;
|
|
405
|
-
std::size_t operator()(key_and_slot_t const& k) const noexcept { return
|
|
406
|
-
std::size_t operator()(vector_key_t const& k) const noexcept { return
|
|
498
|
+
std::size_t operator()(key_and_slot_t const& k) const noexcept { return hash_gt<vector_key_t>{}(k.key); }
|
|
499
|
+
std::size_t operator()(vector_key_t const& k) const noexcept { return hash_gt<vector_key_t>{}(k); }
|
|
407
500
|
};
|
|
408
501
|
|
|
409
502
|
struct lookup_key_same_t {
|
|
@@ -428,13 +521,57 @@ class index_dense_gt {
|
|
|
428
521
|
/// @brief A constant for the reserved key value, used to mark deleted entries.
|
|
429
522
|
vector_key_t free_key_ = default_free_value<vector_key_t>();
|
|
430
523
|
|
|
524
|
+
/// @brief Locks the thread for the duration of the operation.
|
|
525
|
+
struct thread_lock_t {
|
|
526
|
+
index_dense_gt const& parent;
|
|
527
|
+
std::size_t thread_id = 0;
|
|
528
|
+
bool engaged = false;
|
|
529
|
+
|
|
530
|
+
~thread_lock_t() usearch_noexcept_m {
|
|
531
|
+
if (engaged)
|
|
532
|
+
parent.thread_unlock_(thread_id);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
thread_lock_t(thread_lock_t const&) = delete;
|
|
536
|
+
thread_lock_t& operator=(thread_lock_t const&) = delete;
|
|
537
|
+
|
|
538
|
+
thread_lock_t(index_dense_gt const& parent, std::size_t thread_id, bool engaged = true) noexcept
|
|
539
|
+
: parent(parent), thread_id(thread_id), engaged(engaged) {}
|
|
540
|
+
thread_lock_t(thread_lock_t&& other) noexcept
|
|
541
|
+
: parent(other.parent), thread_id(other.thread_id), engaged(other.engaged) {
|
|
542
|
+
other.engaged = false;
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
431
546
|
public:
|
|
432
|
-
using search_result_t = typename index_t::search_result_t;
|
|
433
547
|
using cluster_result_t = typename index_t::cluster_result_t;
|
|
434
548
|
using add_result_t = typename index_t::add_result_t;
|
|
435
549
|
using stats_t = typename index_t::stats_t;
|
|
436
550
|
using match_t = typename index_t::match_t;
|
|
437
551
|
|
|
552
|
+
/**
|
|
553
|
+
* @brief A search result, containing the found keys and distances.
|
|
554
|
+
*
|
|
555
|
+
* As the `index_dense_gt` manages the thread-pool on its own, the search result
|
|
556
|
+
* preserves the thread-lock to avoid undefined behaviors, when other threads
|
|
557
|
+
* start overwriting the results.
|
|
558
|
+
*/
|
|
559
|
+
struct search_result_t : public index_t::search_result_t {
|
|
560
|
+
inline search_result_t(index_dense_gt const& parent) noexcept
|
|
561
|
+
: index_t::search_result_t(), lock_(parent, 0, false) {}
|
|
562
|
+
search_result_t failed(error_t message) noexcept {
|
|
563
|
+
this->error = std::move(message);
|
|
564
|
+
return std::move(*this);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private:
|
|
568
|
+
friend class index_dense_gt;
|
|
569
|
+
thread_lock_t lock_;
|
|
570
|
+
|
|
571
|
+
inline search_result_t(typename index_t::search_result_t result, thread_lock_t lock) noexcept
|
|
572
|
+
: index_t::search_result_t(std::move(result)), lock_(std::move(lock)) {}
|
|
573
|
+
};
|
|
574
|
+
|
|
438
575
|
index_dense_gt() = default;
|
|
439
576
|
index_dense_gt(index_dense_gt&& other)
|
|
440
577
|
: config_(std::move(other.config_)),
|
|
@@ -485,51 +622,76 @@ class index_dense_gt {
|
|
|
485
622
|
typed_ = nullptr;
|
|
486
623
|
}
|
|
487
624
|
|
|
625
|
+
struct state_result_t {
|
|
626
|
+
index_dense_gt index;
|
|
627
|
+
error_t error;
|
|
628
|
+
|
|
629
|
+
explicit operator bool() const noexcept { return !error; }
|
|
630
|
+
state_result_t failed(error_t message) noexcept {
|
|
631
|
+
error = std::move(message);
|
|
632
|
+
return std::move(*this);
|
|
633
|
+
}
|
|
634
|
+
operator index_dense_gt&&() && {
|
|
635
|
+
if (error)
|
|
636
|
+
__usearch_raise_runtime_error(error.what());
|
|
637
|
+
return std::move(index);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
using copy_result_t = state_result_t;
|
|
641
|
+
|
|
488
642
|
/**
|
|
489
643
|
* @brief Constructs an instance of ::index_dense_gt.
|
|
490
644
|
* @param[in] metric One of the provided or an @b ad-hoc metric, type-punned.
|
|
491
645
|
* @param[in] config The index configuration (optional).
|
|
492
646
|
* @param[in] free_key The key used for freed vectors (optional).
|
|
493
|
-
* @return An instance of ::index_dense_gt
|
|
647
|
+
* @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
|
|
648
|
+
*
|
|
649
|
+
* ! If the `metric` isn't provided in this method, it has to be set with
|
|
650
|
+
* ! the `change_metric` method before the index can be used. Alternatively,
|
|
651
|
+
* ! if you are loading an existing index, the metric will be set automatically.
|
|
494
652
|
*/
|
|
495
|
-
static
|
|
496
|
-
metric_t metric,
|
|
653
|
+
static state_result_t make( //
|
|
654
|
+
metric_t metric = {}, //
|
|
497
655
|
index_dense_config_t config = {}, //
|
|
498
656
|
vector_key_t free_key = default_free_value<vector_key_t>()) {
|
|
499
657
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
result.cast_buffer_.resize(hardware_threads * metric.bytes_per_vector());
|
|
506
|
-
result.casts_ = make_casts_(scalar_kind);
|
|
507
|
-
result.metric_ = metric;
|
|
508
|
-
result.free_key_ = free_key;
|
|
509
|
-
|
|
510
|
-
// Fill the thread IDs.
|
|
511
|
-
result.available_threads_.resize(hardware_threads);
|
|
512
|
-
std::iota(result.available_threads_.begin(), result.available_threads_.end(), 0ul);
|
|
513
|
-
|
|
514
|
-
// Available since C11, but only C++17, so we use the C version.
|
|
658
|
+
if (metric.missing())
|
|
659
|
+
return state_result_t{}.failed("Metric won't be initialized!");
|
|
660
|
+
error_t error = config.validate();
|
|
661
|
+
if (error)
|
|
662
|
+
return state_result_t{}.failed(std::move(error));
|
|
515
663
|
index_t* raw = index_allocator_t{}.allocate(1);
|
|
664
|
+
if (!raw)
|
|
665
|
+
return state_result_t{}.failed("Failed to allocate memory for the index!");
|
|
666
|
+
|
|
667
|
+
state_result_t result;
|
|
668
|
+
index_dense_gt& index = result.index;
|
|
669
|
+
index.config_ = config;
|
|
670
|
+
index.free_key_ = free_key;
|
|
671
|
+
|
|
672
|
+
// In some cases the metric is not provided, and will be set later.
|
|
673
|
+
if (metric) {
|
|
674
|
+
scalar_kind_t scalar_kind = metric.scalar_kind();
|
|
675
|
+
index.casts_ = make_casts_(scalar_kind);
|
|
676
|
+
index.metric_ = metric;
|
|
677
|
+
}
|
|
678
|
+
|
|
516
679
|
new (raw) index_t(config);
|
|
517
|
-
|
|
680
|
+
index.typed_ = raw;
|
|
518
681
|
return result;
|
|
519
682
|
}
|
|
520
683
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
result.load(path);
|
|
684
|
+
/**
|
|
685
|
+
* @brief Constructs an instance of ::index_dense_gt from a serialized binary file.
|
|
686
|
+
* @param[in] path The path to the binary file.
|
|
687
|
+
* @param[in] view Whether to map the file into memory or load it.
|
|
688
|
+
* @return An instance of ::index_dense_gt or error, wrapped in a `state_result_t`.
|
|
689
|
+
*/
|
|
690
|
+
static state_result_t make(char const* path, bool view = false) {
|
|
691
|
+
state_result_t result;
|
|
692
|
+
serialization_result_t serialization_result = view ? result.index.view(path) : result.index.load(path);
|
|
693
|
+
if (!serialization_result)
|
|
694
|
+
return result.failed(std::move(serialization_result.error));
|
|
533
695
|
return result;
|
|
534
696
|
}
|
|
535
697
|
|
|
@@ -537,19 +699,23 @@ class index_dense_gt {
|
|
|
537
699
|
std::size_t connectivity() const { return typed_->connectivity(); }
|
|
538
700
|
std::size_t size() const { return typed_->size() - free_keys_.size(); }
|
|
539
701
|
std::size_t capacity() const { return typed_->capacity(); }
|
|
540
|
-
std::size_t max_level() const
|
|
702
|
+
std::size_t max_level() const { return typed_->max_level(); }
|
|
541
703
|
index_dense_config_t const& config() const { return config_; }
|
|
542
704
|
index_limits_t const& limits() const { return typed_->limits(); }
|
|
543
705
|
bool multi() const { return config_.multi; }
|
|
706
|
+
std::size_t currently_available_threads() const {
|
|
707
|
+
std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
|
|
708
|
+
return available_threads_.size();
|
|
709
|
+
}
|
|
544
710
|
|
|
545
711
|
// The metric and its properties
|
|
546
712
|
metric_t const& metric() const { return metric_; }
|
|
547
713
|
void change_metric(metric_t metric) { metric_ = std::move(metric); }
|
|
548
714
|
|
|
549
|
-
scalar_kind_t scalar_kind() const
|
|
550
|
-
std::size_t bytes_per_vector() const
|
|
551
|
-
std::size_t scalar_words() const
|
|
552
|
-
std::size_t dimensions() const
|
|
715
|
+
scalar_kind_t scalar_kind() const { return metric_.scalar_kind(); }
|
|
716
|
+
std::size_t bytes_per_vector() const { return metric_.bytes_per_vector(); }
|
|
717
|
+
std::size_t scalar_words() const { return metric_.scalar_words(); }
|
|
718
|
+
std::size_t dimensions() const { return metric_.dimensions(); }
|
|
553
719
|
|
|
554
720
|
// Fetching and changing search criteria
|
|
555
721
|
std::size_t expansion_add() const { return config_.expansion_add; }
|
|
@@ -559,8 +725,6 @@ class index_dense_gt {
|
|
|
559
725
|
|
|
560
726
|
member_citerator_t cbegin() const { return typed_->cbegin(); }
|
|
561
727
|
member_citerator_t cend() const { return typed_->cend(); }
|
|
562
|
-
member_citerator_t begin() const { return typed_->begin(); }
|
|
563
|
-
member_citerator_t end() const { return typed_->end(); }
|
|
564
728
|
member_iterator_t begin() { return typed_->begin(); }
|
|
565
729
|
member_iterator_t end() { return typed_->end(); }
|
|
566
730
|
|
|
@@ -641,6 +805,7 @@ class index_dense_gt {
|
|
|
641
805
|
* exporting the mean, maximum, and minimum values.
|
|
642
806
|
*/
|
|
643
807
|
aggregated_distances_t distance_between(vector_key_t a, vector_key_t b, std::size_t = any_thread()) const {
|
|
808
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled!");
|
|
644
809
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
645
810
|
aggregated_distances_t result;
|
|
646
811
|
if (!multi()) {
|
|
@@ -714,9 +879,8 @@ class index_dense_gt {
|
|
|
714
879
|
cluster_config.thread = lock.thread_id;
|
|
715
880
|
cluster_config.expansion = config_.expansion_search;
|
|
716
881
|
metric_proxy_t metric{*this};
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
};
|
|
882
|
+
vector_key_t free_key_copy = free_key_;
|
|
883
|
+
auto allow = [free_key_copy](member_cref_t const& member) noexcept { return member.key != free_key_copy; };
|
|
720
884
|
|
|
721
885
|
// Find the closest cluster for any vector under that key.
|
|
722
886
|
while (key_range.first != key_range.second) {
|
|
@@ -736,16 +900,54 @@ class index_dense_gt {
|
|
|
736
900
|
/**
|
|
737
901
|
* @brief Reserves memory for the index and the keyed lookup.
|
|
738
902
|
* @return `true` if the memory reservation was successful, `false` otherwise.
|
|
903
|
+
*
|
|
904
|
+
* ! No update or search operations should be running during this operation.
|
|
739
905
|
*/
|
|
740
|
-
bool
|
|
741
|
-
|
|
906
|
+
bool try_reserve(index_limits_t limits) {
|
|
907
|
+
|
|
908
|
+
// The slot lookup system will generally prefer power-of-two sizes.
|
|
909
|
+
if (config_.enable_key_lookups) {
|
|
742
910
|
unique_lock_t lock(slot_lookup_mutex_);
|
|
743
|
-
slot_lookup_.
|
|
744
|
-
|
|
911
|
+
if (!slot_lookup_.try_reserve(limits.members))
|
|
912
|
+
return false;
|
|
913
|
+
limits.members = slot_lookup_.capacity();
|
|
745
914
|
}
|
|
915
|
+
|
|
916
|
+
// Once the `slot_lookup_` grows, let's use its capacity as the new
|
|
917
|
+
// target for the `vectors_lookup_` to synchronize allocations and
|
|
918
|
+
// expensive index re-organizations.
|
|
919
|
+
if (limits.members != vectors_lookup_.size()) {
|
|
920
|
+
vectors_lookup_t new_vectors_lookup(limits.members);
|
|
921
|
+
if (!new_vectors_lookup)
|
|
922
|
+
return false;
|
|
923
|
+
if (vectors_lookup_.size() > 0)
|
|
924
|
+
std::memcpy(new_vectors_lookup.data(), vectors_lookup_.data(),
|
|
925
|
+
vectors_lookup_.size() * sizeof(byte_t*));
|
|
926
|
+
vectors_lookup_ = std::move(new_vectors_lookup);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// During reserve, no insertions may be happening, so we can safely overwrite the whole collection.
|
|
930
|
+
std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
|
|
931
|
+
available_threads_.clear();
|
|
932
|
+
if (!available_threads_.reserve(limits.threads()))
|
|
933
|
+
return false;
|
|
934
|
+
for (std::size_t i = 0; i < limits.threads(); i++)
|
|
935
|
+
available_threads_.push(i);
|
|
936
|
+
|
|
937
|
+
// Allocate a buffer for the casted vectors.
|
|
938
|
+
cast_buffer_t cast_buffer(limits.threads() * metric_.bytes_per_vector());
|
|
939
|
+
if (!cast_buffer)
|
|
940
|
+
return false;
|
|
941
|
+
cast_buffer_ = std::move(cast_buffer);
|
|
942
|
+
|
|
746
943
|
return typed_->reserve(limits);
|
|
747
944
|
}
|
|
748
945
|
|
|
946
|
+
void reserve(index_limits_t limits) {
|
|
947
|
+
if (!try_reserve(limits))
|
|
948
|
+
throw std::bad_alloc();
|
|
949
|
+
}
|
|
950
|
+
|
|
749
951
|
/**
|
|
750
952
|
* @brief Erases all the vectors from the index.
|
|
751
953
|
*
|
|
@@ -758,7 +960,7 @@ class index_dense_gt {
|
|
|
758
960
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
759
961
|
typed_->clear();
|
|
760
962
|
slot_lookup_.clear();
|
|
761
|
-
vectors_lookup_.
|
|
963
|
+
vectors_lookup_.reset();
|
|
762
964
|
free_keys_.clear();
|
|
763
965
|
vectors_tape_allocator_.reset();
|
|
764
966
|
}
|
|
@@ -771,19 +973,18 @@ class index_dense_gt {
|
|
|
771
973
|
* If the index is memory-mapped - releases the mapping and the descriptor.
|
|
772
974
|
*/
|
|
773
975
|
void reset() {
|
|
774
|
-
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
775
976
|
|
|
977
|
+
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
776
978
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
777
979
|
std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
|
|
778
|
-
|
|
980
|
+
|
|
981
|
+
if (typed_)
|
|
982
|
+
typed_->reset();
|
|
779
983
|
slot_lookup_.clear();
|
|
780
|
-
vectors_lookup_.
|
|
984
|
+
vectors_lookup_.reset();
|
|
781
985
|
free_keys_.clear();
|
|
782
986
|
vectors_tape_allocator_.reset();
|
|
783
|
-
|
|
784
|
-
// Reset the thread IDs.
|
|
785
|
-
available_threads_.resize(std::thread::hardware_concurrency());
|
|
786
|
-
std::iota(available_threads_.begin(), available_threads_.end(), 0ul);
|
|
987
|
+
available_threads_.reset();
|
|
787
988
|
}
|
|
788
989
|
|
|
789
990
|
/**
|
|
@@ -862,7 +1063,7 @@ class index_dense_gt {
|
|
|
862
1063
|
/**
|
|
863
1064
|
* @brief Estimate the binary length (in bytes) of the serialized index.
|
|
864
1065
|
*/
|
|
865
|
-
std::size_t serialized_length(serialization_config_t config = {}) const
|
|
1066
|
+
std::size_t serialized_length(serialization_config_t config = {}) const {
|
|
866
1067
|
std::size_t dimensions_length = 0;
|
|
867
1068
|
std::size_t matrix_length = 0;
|
|
868
1069
|
if (!config.exclude_vectors) {
|
|
@@ -884,6 +1085,7 @@ class index_dense_gt {
|
|
|
884
1085
|
progress_at&& progress = {}) {
|
|
885
1086
|
|
|
886
1087
|
// Discard all previous memory allocations of `vectors_tape_allocator_`
|
|
1088
|
+
index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
|
|
887
1089
|
reset();
|
|
888
1090
|
|
|
889
1091
|
// Infer the new index size
|
|
@@ -908,7 +1110,9 @@ class index_dense_gt {
|
|
|
908
1110
|
matrix_cols = dimensions[1];
|
|
909
1111
|
}
|
|
910
1112
|
// Load the vectors one after another
|
|
911
|
-
vectors_lookup_
|
|
1113
|
+
vectors_lookup_ = vectors_lookup_t(matrix_rows);
|
|
1114
|
+
if (!vectors_lookup_)
|
|
1115
|
+
return result.failed("Failed to allocate memory to address vectors");
|
|
912
1116
|
for (std::uint64_t slot = 0; slot != matrix_rows; ++slot) {
|
|
913
1117
|
byte_t* vector = vectors_tape_allocator_.allocate(matrix_cols);
|
|
914
1118
|
if (!input(vector, matrix_cols))
|
|
@@ -927,6 +1131,9 @@ class index_dense_gt {
|
|
|
927
1131
|
if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
|
|
928
1132
|
return result.failed("Magic header mismatch - the file isn't an index");
|
|
929
1133
|
|
|
1134
|
+
// fix pre-2.10 headers
|
|
1135
|
+
fix_pre_2_10_metadata(head);
|
|
1136
|
+
|
|
930
1137
|
// Validate the software version
|
|
931
1138
|
if (head.version_major != USEARCH_VERSION_MAJOR)
|
|
932
1139
|
return result.failed("File format may be different, please rebuild");
|
|
@@ -939,16 +1146,38 @@ class index_dense_gt {
|
|
|
939
1146
|
|
|
940
1147
|
config_.multi = head.multi;
|
|
941
1148
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
942
|
-
cast_buffer_
|
|
1149
|
+
cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
|
|
1150
|
+
if (!cast_buffer_)
|
|
1151
|
+
return result.failed("Failed to allocate memory for the casts");
|
|
943
1152
|
casts_ = make_casts_(head.kind_scalar);
|
|
944
1153
|
}
|
|
945
1154
|
|
|
946
1155
|
// Pull the actual proximity graph
|
|
1156
|
+
if (!typed_) {
|
|
1157
|
+
index_t* raw = index_allocator_t{}.allocate(1);
|
|
1158
|
+
if (!raw)
|
|
1159
|
+
return result.failed("Failed to allocate memory for the index");
|
|
1160
|
+
new (raw) index_t(config_);
|
|
1161
|
+
typed_ = raw;
|
|
1162
|
+
}
|
|
947
1163
|
result = typed_->load_from_stream(std::forward<input_callback_at>(input), std::forward<progress_at>(progress));
|
|
948
1164
|
if (!result)
|
|
949
1165
|
return result;
|
|
950
1166
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
951
1167
|
return result.failed("Index size and the number of vectors doesn't match");
|
|
1168
|
+
old_limits.members = static_cast<std::size_t>(matrix_rows);
|
|
1169
|
+
if (!typed_->try_reserve(old_limits))
|
|
1170
|
+
return result.failed("Failed to reserve memory for the index");
|
|
1171
|
+
|
|
1172
|
+
// After the index is loaded, we may have to resize the `available_threads_` to
|
|
1173
|
+
// match the limits of the underlying engine.
|
|
1174
|
+
available_threads_t available_threads;
|
|
1175
|
+
std::size_t max_threads = old_limits.threads();
|
|
1176
|
+
if (!available_threads.reserve(max_threads))
|
|
1177
|
+
return result.failed("Failed to allocate memory for the available threads!");
|
|
1178
|
+
for (std::size_t i = 0; i < max_threads; i++)
|
|
1179
|
+
available_threads.push(i);
|
|
1180
|
+
available_threads_ = std::move(available_threads);
|
|
952
1181
|
|
|
953
1182
|
reindex_keys_();
|
|
954
1183
|
return result;
|
|
@@ -966,6 +1195,7 @@ class index_dense_gt {
|
|
|
966
1195
|
progress_at&& progress = {}) {
|
|
967
1196
|
|
|
968
1197
|
// Discard all previous memory allocations of `vectors_tape_allocator_`
|
|
1198
|
+
index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
|
|
969
1199
|
reset();
|
|
970
1200
|
|
|
971
1201
|
serialization_result_t result = file.open_if_not();
|
|
@@ -1013,6 +1243,9 @@ class index_dense_gt {
|
|
|
1013
1243
|
if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
|
|
1014
1244
|
return result.failed("Magic header mismatch - the file isn't an index");
|
|
1015
1245
|
|
|
1246
|
+
// fix pre-2.10 headers
|
|
1247
|
+
fix_pre_2_10_metadata(head);
|
|
1248
|
+
|
|
1016
1249
|
// Validate the software version
|
|
1017
1250
|
if (head.version_major != USEARCH_VERSION_MAJOR)
|
|
1018
1251
|
return result.failed("File format may be different, please rebuild");
|
|
@@ -1025,24 +1258,48 @@ class index_dense_gt {
|
|
|
1025
1258
|
|
|
1026
1259
|
config_.multi = head.multi;
|
|
1027
1260
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
1028
|
-
cast_buffer_
|
|
1261
|
+
cast_buffer_ = cast_buffer_t(available_threads_.size() * metric_.bytes_per_vector());
|
|
1262
|
+
if (!cast_buffer_)
|
|
1263
|
+
return result.failed("Failed to allocate memory for the casts");
|
|
1029
1264
|
casts_ = make_casts_(head.kind_scalar);
|
|
1030
1265
|
offset += sizeof(buffer);
|
|
1031
1266
|
}
|
|
1032
1267
|
|
|
1033
1268
|
// Pull the actual proximity graph
|
|
1269
|
+
if (!typed_) {
|
|
1270
|
+
index_t* raw = index_allocator_t{}.allocate(1);
|
|
1271
|
+
if (!raw)
|
|
1272
|
+
return result.failed("Failed to allocate memory for the index");
|
|
1273
|
+
new (raw) index_t(config_);
|
|
1274
|
+
typed_ = raw;
|
|
1275
|
+
}
|
|
1034
1276
|
result = typed_->view(std::move(file), offset, std::forward<progress_at>(progress));
|
|
1035
1277
|
if (!result)
|
|
1036
1278
|
return result;
|
|
1037
1279
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
1038
1280
|
return result.failed("Index size and the number of vectors doesn't match");
|
|
1281
|
+
old_limits.members = static_cast<std::size_t>(matrix_rows);
|
|
1282
|
+
if (!typed_->try_reserve(old_limits))
|
|
1283
|
+
return result.failed("Failed to reserve memory for the index");
|
|
1039
1284
|
|
|
1040
1285
|
// Address the vectors
|
|
1041
|
-
vectors_lookup_
|
|
1286
|
+
vectors_lookup_ = vectors_lookup_t(matrix_rows);
|
|
1287
|
+
if (!vectors_lookup_)
|
|
1288
|
+
return result.failed("Failed to allocate memory to address vectors");
|
|
1042
1289
|
if (!config.exclude_vectors)
|
|
1043
1290
|
for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
|
|
1044
1291
|
vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
|
|
1045
1292
|
|
|
1293
|
+
// After the index is loaded, we may have to resize the `available_threads_` to
|
|
1294
|
+
// match the limits of the underlying engine.
|
|
1295
|
+
available_threads_t available_threads;
|
|
1296
|
+
std::size_t max_threads = old_limits.threads();
|
|
1297
|
+
if (!available_threads.reserve(max_threads))
|
|
1298
|
+
return result.failed("Failed to allocate memory for the available threads!");
|
|
1299
|
+
for (std::size_t i = 0; i < max_threads; i++)
|
|
1300
|
+
available_threads.push(i);
|
|
1301
|
+
available_threads_ = std::move(available_threads);
|
|
1302
|
+
|
|
1046
1303
|
reindex_keys_();
|
|
1047
1304
|
return result;
|
|
1048
1305
|
}
|
|
@@ -1175,6 +1432,7 @@ class index_dense_gt {
|
|
|
1175
1432
|
* @return `true` if the key is present in the index, `false` otherwise.
|
|
1176
1433
|
*/
|
|
1177
1434
|
bool contains(vector_key_t key) const {
|
|
1435
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1178
1436
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1179
1437
|
return slot_lookup_.contains(key_and_slot_t::any_slot(key));
|
|
1180
1438
|
}
|
|
@@ -1184,6 +1442,7 @@ class index_dense_gt {
|
|
|
1184
1442
|
* @return Zero if nothing is found, a positive integer otherwise.
|
|
1185
1443
|
*/
|
|
1186
1444
|
std::size_t count(vector_key_t key) const {
|
|
1445
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1187
1446
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1188
1447
|
return slot_lookup_.count(key_and_slot_t::any_slot(key));
|
|
1189
1448
|
}
|
|
@@ -1208,6 +1467,7 @@ class index_dense_gt {
|
|
|
1208
1467
|
* If an error occurred during the removal operation, `result.error` will contain an error message.
|
|
1209
1468
|
*/
|
|
1210
1469
|
labeling_result_t remove(vector_key_t key) {
|
|
1470
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1211
1471
|
labeling_result_t result;
|
|
1212
1472
|
|
|
1213
1473
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
@@ -1218,7 +1478,8 @@ class index_dense_gt {
|
|
|
1218
1478
|
// Grow the removed entries ring, if needed
|
|
1219
1479
|
std::size_t matching_count = std::distance(matching_slots.first, matching_slots.second);
|
|
1220
1480
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
1221
|
-
|
|
1481
|
+
std::size_t free_count_old = free_keys_.size();
|
|
1482
|
+
if (!free_keys_.reserve(free_count_old + matching_count))
|
|
1222
1483
|
return result.failed("Can't allocate memory for a free-list");
|
|
1223
1484
|
|
|
1224
1485
|
// A removed entry would be:
|
|
@@ -1232,6 +1493,7 @@ class index_dense_gt {
|
|
|
1232
1493
|
}
|
|
1233
1494
|
slot_lookup_.erase(key);
|
|
1234
1495
|
result.completed = matching_count;
|
|
1496
|
+
usearch_assert_m(free_keys_.size() == free_count_old + matching_count, "Free keys count mismatch");
|
|
1235
1497
|
|
|
1236
1498
|
return result;
|
|
1237
1499
|
}
|
|
@@ -1246,6 +1508,7 @@ class index_dense_gt {
|
|
|
1246
1508
|
*/
|
|
1247
1509
|
template <typename keys_iterator_at>
|
|
1248
1510
|
labeling_result_t remove(keys_iterator_at keys_begin, keys_iterator_at keys_end) {
|
|
1511
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1249
1512
|
|
|
1250
1513
|
labeling_result_t result;
|
|
1251
1514
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
@@ -1332,17 +1595,6 @@ class index_dense_gt {
|
|
|
1332
1595
|
});
|
|
1333
1596
|
}
|
|
1334
1597
|
|
|
1335
|
-
struct copy_result_t {
|
|
1336
|
-
index_dense_gt index;
|
|
1337
|
-
error_t error;
|
|
1338
|
-
|
|
1339
|
-
explicit operator bool() const noexcept { return !error; }
|
|
1340
|
-
copy_result_t failed(error_t message) noexcept {
|
|
1341
|
-
error = std::move(message);
|
|
1342
|
-
return std::move(*this);
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
|
|
1346
1598
|
/**
|
|
1347
1599
|
* @brief Copies the ::index_dense_gt @b with all the data in it.
|
|
1348
1600
|
* @param config The copy configuration (optional).
|
|
@@ -1365,19 +1617,22 @@ class index_dense_gt {
|
|
|
1365
1617
|
copy.free_keys_.push(free_keys_[i]);
|
|
1366
1618
|
|
|
1367
1619
|
// Allocate buffers and move the vectors themselves
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1620
|
+
copy.vectors_lookup_ = vectors_lookup_t(vectors_lookup_.size());
|
|
1621
|
+
if (!copy.vectors_lookup_)
|
|
1622
|
+
return result.failed("Out of memory!");
|
|
1623
|
+
if (!config.force_vector_copy && copy.config_.exclude_vectors) {
|
|
1624
|
+
std::memcpy(copy.vectors_lookup_.data(), vectors_lookup_.data(), vectors_lookup_.size() * sizeof(byte_t*));
|
|
1625
|
+
} else {
|
|
1626
|
+
std::size_t slots_count = typed_result.index.size();
|
|
1627
|
+
for (std::size_t slot = 0; slot != slots_count; ++slot)
|
|
1373
1628
|
copy.vectors_lookup_[slot] = copy.vectors_tape_allocator_.allocate(copy.metric_.bytes_per_vector());
|
|
1374
|
-
if (std::count(copy.vectors_lookup_.begin(), copy.vectors_lookup_.
|
|
1629
|
+
if (std::count(copy.vectors_lookup_.begin(), copy.vectors_lookup_.begin() + slots_count, nullptr))
|
|
1375
1630
|
return result.failed("Out of memory!");
|
|
1376
|
-
for (std::size_t slot = 0; slot !=
|
|
1631
|
+
for (std::size_t slot = 0; slot != slots_count; ++slot)
|
|
1377
1632
|
std::memcpy(copy.vectors_lookup_[slot], vectors_lookup_[slot], metric_.bytes_per_vector());
|
|
1378
1633
|
}
|
|
1379
1634
|
|
|
1380
|
-
copy.slot_lookup_ = slot_lookup_;
|
|
1635
|
+
copy.slot_lookup_ = slot_lookup_; // TODO: Handle out of memory
|
|
1381
1636
|
*copy.typed_ = std::move(typed_result.index);
|
|
1382
1637
|
return result;
|
|
1383
1638
|
}
|
|
@@ -1387,22 +1642,34 @@ class index_dense_gt {
|
|
|
1387
1642
|
* @return A similarly configured ::index_dense_gt instance.
|
|
1388
1643
|
*/
|
|
1389
1644
|
copy_result_t fork() const {
|
|
1645
|
+
|
|
1646
|
+
cast_buffer_t cast_buffer(cast_buffer_.size());
|
|
1647
|
+
if (!cast_buffer)
|
|
1648
|
+
return state_result_t{}.failed("Failed to allocate memory for the casts!");
|
|
1649
|
+
available_threads_t available_threads;
|
|
1650
|
+
std::size_t max_threads = limits().threads();
|
|
1651
|
+
if (!available_threads.reserve(max_threads))
|
|
1652
|
+
return state_result_t{}.failed("Failed to allocate memory for the available threads!");
|
|
1653
|
+
for (std::size_t i = 0; i < max_threads; i++)
|
|
1654
|
+
available_threads.push(i);
|
|
1655
|
+
index_t* raw = index_allocator_t{}.allocate(1);
|
|
1656
|
+
if (!raw)
|
|
1657
|
+
return state_result_t{}.failed("Failed to allocate memory for the index!");
|
|
1658
|
+
|
|
1390
1659
|
copy_result_t result;
|
|
1391
1660
|
index_dense_gt& other = result.index;
|
|
1392
|
-
|
|
1661
|
+
index_limits_t other_limits = limits();
|
|
1662
|
+
other_limits.members = 0;
|
|
1393
1663
|
other.config_ = config_;
|
|
1394
|
-
other.cast_buffer_ =
|
|
1664
|
+
other.cast_buffer_ = std::move(cast_buffer);
|
|
1395
1665
|
other.casts_ = casts_;
|
|
1396
1666
|
|
|
1397
1667
|
other.metric_ = metric_;
|
|
1398
|
-
other.available_threads_ =
|
|
1668
|
+
other.available_threads_ = std::move(available_threads);
|
|
1399
1669
|
other.free_key_ = free_key_;
|
|
1400
1670
|
|
|
1401
|
-
index_t* raw = index_allocator_t{}.allocate(1);
|
|
1402
|
-
if (!raw)
|
|
1403
|
-
return result.failed("Can't allocate the index");
|
|
1404
|
-
|
|
1405
1671
|
new (raw) index_t(config());
|
|
1672
|
+
raw->try_reserve(other_limits);
|
|
1406
1673
|
other.typed_ = raw;
|
|
1407
1674
|
return result;
|
|
1408
1675
|
}
|
|
@@ -1461,7 +1728,10 @@ class index_dense_gt {
|
|
|
1461
1728
|
compaction_result_t compact(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
|
|
1462
1729
|
compaction_result_t result;
|
|
1463
1730
|
|
|
1464
|
-
|
|
1731
|
+
vectors_lookup_t new_vectors_lookup(vectors_lookup_.size());
|
|
1732
|
+
if (!new_vectors_lookup)
|
|
1733
|
+
return result.failed("Out of memory!");
|
|
1734
|
+
|
|
1465
1735
|
vectors_tape_allocator_t new_vectors_allocator;
|
|
1466
1736
|
|
|
1467
1737
|
auto track_slot_change = [&](vector_key_t, compressed_slot_t old_slot, compressed_slot_t new_slot) {
|
|
@@ -1698,31 +1968,21 @@ class index_dense_gt {
|
|
|
1698
1968
|
}
|
|
1699
1969
|
|
|
1700
1970
|
private:
|
|
1701
|
-
|
|
1702
|
-
index_dense_gt const& parent;
|
|
1703
|
-
std::size_t thread_id;
|
|
1704
|
-
bool engaged;
|
|
1705
|
-
|
|
1706
|
-
~thread_lock_t() {
|
|
1707
|
-
if (engaged)
|
|
1708
|
-
parent.thread_unlock_(thread_id);
|
|
1709
|
-
}
|
|
1710
|
-
};
|
|
1711
|
-
|
|
1712
|
-
thread_lock_t thread_lock_(std::size_t thread_id) const {
|
|
1971
|
+
thread_lock_t thread_lock_(std::size_t thread_id) const usearch_noexcept_m {
|
|
1713
1972
|
if (thread_id != any_thread())
|
|
1714
1973
|
return {*this, thread_id, false};
|
|
1715
1974
|
|
|
1716
1975
|
available_threads_mutex_.lock();
|
|
1717
|
-
|
|
1718
|
-
available_threads_.
|
|
1976
|
+
usearch_assert_m(available_threads_.size(), "No available threads to lock");
|
|
1977
|
+
available_threads_.try_pop(thread_id);
|
|
1719
1978
|
available_threads_mutex_.unlock();
|
|
1720
1979
|
return {*this, thread_id, true};
|
|
1721
1980
|
}
|
|
1722
1981
|
|
|
1723
|
-
void thread_unlock_(std::size_t thread_id) const {
|
|
1982
|
+
void thread_unlock_(std::size_t thread_id) const usearch_noexcept_m {
|
|
1724
1983
|
available_threads_mutex_.lock();
|
|
1725
|
-
available_threads_.
|
|
1984
|
+
usearch_assert_m(available_threads_.size() < available_threads_.capacity(), "Too many threads unlocked");
|
|
1985
|
+
available_threads_.push(thread_id);
|
|
1726
1986
|
available_threads_mutex_.unlock();
|
|
1727
1987
|
}
|
|
1728
1988
|
|
|
@@ -1731,7 +1991,7 @@ class index_dense_gt {
|
|
|
1731
1991
|
vector_key_t key, scalar_at const* vector, //
|
|
1732
1992
|
std::size_t thread, bool force_vector_copy, cast_t const& cast) {
|
|
1733
1993
|
|
|
1734
|
-
if (!multi() && contains(key))
|
|
1994
|
+
if (!multi() && config().enable_key_lookups && contains(key))
|
|
1735
1995
|
return add_result_t{}.failed("Duplicate keys not allowed in high-level wrappers");
|
|
1736
1996
|
|
|
1737
1997
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
@@ -1755,8 +2015,10 @@ class index_dense_gt {
|
|
|
1755
2015
|
// Perform the insertion or the update
|
|
1756
2016
|
bool reuse_node = free_slot != default_free_value<compressed_slot_t>();
|
|
1757
2017
|
auto on_success = [&](member_ref_t member) {
|
|
1758
|
-
|
|
1759
|
-
|
|
2018
|
+
if (config_.enable_key_lookups) {
|
|
2019
|
+
unique_lock_t slot_lock(slot_lookup_mutex_);
|
|
2020
|
+
slot_lookup_.try_emplace(key_and_slot_t{key, static_cast<compressed_slot_t>(member.slot)});
|
|
2021
|
+
}
|
|
1760
2022
|
if (copy_vector) {
|
|
1761
2023
|
if (!reuse_node)
|
|
1762
2024
|
vectors_lookup_[member.slot] = vectors_tape_allocator_.allocate(metric_.bytes_per_vector());
|
|
@@ -1794,16 +2056,19 @@ class index_dense_gt {
|
|
|
1794
2056
|
search_config.expansion = config_.expansion_search;
|
|
1795
2057
|
search_config.exact = exact;
|
|
1796
2058
|
|
|
2059
|
+
vector_key_t free_key_copy = free_key_;
|
|
1797
2060
|
if (std::is_same<typename std::decay<predicate_at>::type, dummy_predicate_t>::value) {
|
|
1798
|
-
auto allow = [
|
|
1799
|
-
return member.key !=
|
|
2061
|
+
auto allow = [free_key_copy](member_cref_t const& member) noexcept {
|
|
2062
|
+
return (vector_key_t)member.key != free_key_copy;
|
|
1800
2063
|
};
|
|
1801
|
-
|
|
2064
|
+
auto typed_result = typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
|
|
2065
|
+
return search_result_t{std::move(typed_result), std::move(lock)};
|
|
1802
2066
|
} else {
|
|
1803
|
-
auto allow = [
|
|
1804
|
-
return member.key !=
|
|
2067
|
+
auto allow = [free_key_copy, &predicate](member_cref_t const& member) noexcept {
|
|
2068
|
+
return (vector_key_t)member.key != free_key_copy && predicate(member.key);
|
|
1805
2069
|
};
|
|
1806
|
-
|
|
2070
|
+
auto typed_result = typed_->search(vector_data, wanted, metric_proxy_t{*this}, search_config, allow);
|
|
2071
|
+
return search_result_t{std::move(typed_result), std::move(lock)};
|
|
1807
2072
|
}
|
|
1808
2073
|
}
|
|
1809
2074
|
|
|
@@ -1826,9 +2091,8 @@ class index_dense_gt {
|
|
|
1826
2091
|
cluster_config.thread = lock.thread_id;
|
|
1827
2092
|
cluster_config.expansion = config_.expansion_search;
|
|
1828
2093
|
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
};
|
|
2094
|
+
vector_key_t free_key_copy = free_key_;
|
|
2095
|
+
auto allow = [free_key_copy](member_cref_t const& member) noexcept { return member.key != free_key_copy; };
|
|
1832
2096
|
return typed_->cluster(vector_data, level, metric_proxy_t{*this}, cluster_config, allow);
|
|
1833
2097
|
}
|
|
1834
2098
|
|
|
@@ -1848,6 +2112,7 @@ class index_dense_gt {
|
|
|
1848
2112
|
}
|
|
1849
2113
|
|
|
1850
2114
|
// Check if such `key` is even present.
|
|
2115
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled!");
|
|
1851
2116
|
shared_lock_t slots_lock(slot_lookup_mutex_);
|
|
1852
2117
|
auto key_range = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
|
|
1853
2118
|
aggregated_distances_t result;
|
|
@@ -1884,7 +2149,8 @@ class index_dense_gt {
|
|
|
1884
2149
|
std::size_t count_total = typed_->size();
|
|
1885
2150
|
std::size_t count_removed = 0;
|
|
1886
2151
|
for (std::size_t i = 0; i != count_total; ++i) {
|
|
1887
|
-
|
|
2152
|
+
auto member_slot = static_cast<compressed_slot_t>(i);
|
|
2153
|
+
member_cref_t member = typed_->at(member_slot);
|
|
1888
2154
|
count_removed += member.key == free_key_;
|
|
1889
2155
|
}
|
|
1890
2156
|
|
|
@@ -1900,11 +2166,12 @@ class index_dense_gt {
|
|
|
1900
2166
|
free_keys_.clear();
|
|
1901
2167
|
free_keys_.reserve(count_removed);
|
|
1902
2168
|
for (std::size_t i = 0; i != typed_->size(); ++i) {
|
|
1903
|
-
|
|
2169
|
+
auto member_slot = static_cast<compressed_slot_t>(i);
|
|
2170
|
+
member_cref_t member = typed_->at(member_slot);
|
|
1904
2171
|
if (member.key == free_key_)
|
|
1905
|
-
free_keys_.push(
|
|
2172
|
+
free_keys_.push(member_slot);
|
|
1906
2173
|
else if (config_.enable_key_lookups)
|
|
1907
|
-
slot_lookup_.try_emplace(key_and_slot_t{vector_key_t(member.key),
|
|
2174
|
+
slot_lookup_.try_emplace(key_and_slot_t{vector_key_t(member.key), member_slot});
|
|
1908
2175
|
}
|
|
1909
2176
|
}
|
|
1910
2177
|
|
|
@@ -1948,17 +2215,17 @@ class index_dense_gt {
|
|
|
1948
2215
|
template <typename to_scalar_at> static casts_t make_casts_() {
|
|
1949
2216
|
casts_t result;
|
|
1950
2217
|
|
|
1951
|
-
result.from_b1x8 = cast_gt<b1x8_t, to_scalar_at
|
|
1952
|
-
result.from_i8 = cast_gt<i8_t, to_scalar_at
|
|
1953
|
-
result.from_f16 = cast_gt<f16_t, to_scalar_at
|
|
1954
|
-
result.from_f32 = cast_gt<f32_t, to_scalar_at
|
|
1955
|
-
result.from_f64 = cast_gt<f64_t, to_scalar_at
|
|
2218
|
+
result.from_b1x8 = &cast_gt<b1x8_t, to_scalar_at>::try_;
|
|
2219
|
+
result.from_i8 = &cast_gt<i8_t, to_scalar_at>::try_;
|
|
2220
|
+
result.from_f16 = &cast_gt<f16_t, to_scalar_at>::try_;
|
|
2221
|
+
result.from_f32 = &cast_gt<f32_t, to_scalar_at>::try_;
|
|
2222
|
+
result.from_f64 = &cast_gt<f64_t, to_scalar_at>::try_;
|
|
1956
2223
|
|
|
1957
|
-
result.to_b1x8 = cast_gt<to_scalar_at, b1x8_t
|
|
1958
|
-
result.to_i8 = cast_gt<to_scalar_at, i8_t
|
|
1959
|
-
result.to_f16 = cast_gt<to_scalar_at, f16_t
|
|
1960
|
-
result.to_f32 = cast_gt<to_scalar_at, f32_t
|
|
1961
|
-
result.to_f64 = cast_gt<to_scalar_at, f64_t
|
|
2224
|
+
result.to_b1x8 = &cast_gt<to_scalar_at, b1x8_t>::try_;
|
|
2225
|
+
result.to_i8 = &cast_gt<to_scalar_at, i8_t>::try_;
|
|
2226
|
+
result.to_f16 = &cast_gt<to_scalar_at, f16_t>::try_;
|
|
2227
|
+
result.to_f32 = &cast_gt<to_scalar_at, f32_t>::try_;
|
|
2228
|
+
result.to_f64 = &cast_gt<to_scalar_at, f64_t>::try_;
|
|
1962
2229
|
|
|
1963
2230
|
return result;
|
|
1964
2231
|
}
|
|
@@ -2008,7 +2275,7 @@ static join_result_t join( //
|
|
|
2008
2275
|
man_to_woman_at&& man_to_woman = man_to_woman_at{}, //
|
|
2009
2276
|
woman_to_man_at&& woman_to_man = woman_to_man_at{}, //
|
|
2010
2277
|
executor_at&& executor = executor_at{}, //
|
|
2011
|
-
progress_at&& progress = progress_at{})
|
|
2278
|
+
progress_at&& progress = progress_at{}) {
|
|
2012
2279
|
|
|
2013
2280
|
return men.join( //
|
|
2014
2281
|
women, config, //
|