usearch 2.11.1 → 2.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -30
- package/binding.gyp +11 -12
- package/c/README.md +3 -0
- 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 +34 -13
- package/cpp/bench.cpp +99 -47
- package/cpp/test.cpp +947 -132
- package/include/usearch/index.hpp +894 -272
- package/include/usearch/index_dense.hpp +418 -156
- 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_;
|
|
470
|
+
|
|
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>;
|
|
380
473
|
|
|
381
|
-
/// @brief Originally forms and array of integers [0, threads], marking all
|
|
382
|
-
mutable
|
|
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,21 +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();
|
|
914
|
+
}
|
|
745
915
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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);
|
|
750
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
|
+
|
|
751
943
|
return typed_->reserve(limits);
|
|
752
944
|
}
|
|
753
945
|
|
|
946
|
+
void reserve(index_limits_t limits) {
|
|
947
|
+
if (!try_reserve(limits))
|
|
948
|
+
throw std::bad_alloc();
|
|
949
|
+
}
|
|
950
|
+
|
|
754
951
|
/**
|
|
755
952
|
* @brief Erases all the vectors from the index.
|
|
756
953
|
*
|
|
@@ -763,7 +960,7 @@ class index_dense_gt {
|
|
|
763
960
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
764
961
|
typed_->clear();
|
|
765
962
|
slot_lookup_.clear();
|
|
766
|
-
vectors_lookup_.
|
|
963
|
+
vectors_lookup_.reset();
|
|
767
964
|
free_keys_.clear();
|
|
768
965
|
vectors_tape_allocator_.reset();
|
|
769
966
|
}
|
|
@@ -776,19 +973,18 @@ class index_dense_gt {
|
|
|
776
973
|
* If the index is memory-mapped - releases the mapping and the descriptor.
|
|
777
974
|
*/
|
|
778
975
|
void reset() {
|
|
779
|
-
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
780
976
|
|
|
977
|
+
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
781
978
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
782
979
|
std::unique_lock<std::mutex> available_threads_lock(available_threads_mutex_);
|
|
783
|
-
|
|
980
|
+
|
|
981
|
+
if (typed_)
|
|
982
|
+
typed_->reset();
|
|
784
983
|
slot_lookup_.clear();
|
|
785
|
-
vectors_lookup_.
|
|
984
|
+
vectors_lookup_.reset();
|
|
786
985
|
free_keys_.clear();
|
|
787
986
|
vectors_tape_allocator_.reset();
|
|
788
|
-
|
|
789
|
-
// Reset the thread IDs.
|
|
790
|
-
available_threads_.resize(std::thread::hardware_concurrency());
|
|
791
|
-
std::iota(available_threads_.begin(), available_threads_.end(), 0ul);
|
|
987
|
+
available_threads_.reset();
|
|
792
988
|
}
|
|
793
989
|
|
|
794
990
|
/**
|
|
@@ -867,7 +1063,7 @@ class index_dense_gt {
|
|
|
867
1063
|
/**
|
|
868
1064
|
* @brief Estimate the binary length (in bytes) of the serialized index.
|
|
869
1065
|
*/
|
|
870
|
-
std::size_t serialized_length(serialization_config_t config = {}) const
|
|
1066
|
+
std::size_t serialized_length(serialization_config_t config = {}) const {
|
|
871
1067
|
std::size_t dimensions_length = 0;
|
|
872
1068
|
std::size_t matrix_length = 0;
|
|
873
1069
|
if (!config.exclude_vectors) {
|
|
@@ -889,6 +1085,7 @@ class index_dense_gt {
|
|
|
889
1085
|
progress_at&& progress = {}) {
|
|
890
1086
|
|
|
891
1087
|
// Discard all previous memory allocations of `vectors_tape_allocator_`
|
|
1088
|
+
index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
|
|
892
1089
|
reset();
|
|
893
1090
|
|
|
894
1091
|
// Infer the new index size
|
|
@@ -913,7 +1110,9 @@ class index_dense_gt {
|
|
|
913
1110
|
matrix_cols = dimensions[1];
|
|
914
1111
|
}
|
|
915
1112
|
// Load the vectors one after another
|
|
916
|
-
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");
|
|
917
1116
|
for (std::uint64_t slot = 0; slot != matrix_rows; ++slot) {
|
|
918
1117
|
byte_t* vector = vectors_tape_allocator_.allocate(matrix_cols);
|
|
919
1118
|
if (!input(vector, matrix_cols))
|
|
@@ -932,6 +1131,9 @@ class index_dense_gt {
|
|
|
932
1131
|
if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
|
|
933
1132
|
return result.failed("Magic header mismatch - the file isn't an index");
|
|
934
1133
|
|
|
1134
|
+
// fix pre-2.10 headers
|
|
1135
|
+
fix_pre_2_10_metadata(head);
|
|
1136
|
+
|
|
935
1137
|
// Validate the software version
|
|
936
1138
|
if (head.version_major != USEARCH_VERSION_MAJOR)
|
|
937
1139
|
return result.failed("File format may be different, please rebuild");
|
|
@@ -944,16 +1146,38 @@ class index_dense_gt {
|
|
|
944
1146
|
|
|
945
1147
|
config_.multi = head.multi;
|
|
946
1148
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
947
|
-
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");
|
|
948
1152
|
casts_ = make_casts_(head.kind_scalar);
|
|
949
1153
|
}
|
|
950
1154
|
|
|
951
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
|
+
}
|
|
952
1163
|
result = typed_->load_from_stream(std::forward<input_callback_at>(input), std::forward<progress_at>(progress));
|
|
953
1164
|
if (!result)
|
|
954
1165
|
return result;
|
|
955
1166
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
956
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);
|
|
957
1181
|
|
|
958
1182
|
reindex_keys_();
|
|
959
1183
|
return result;
|
|
@@ -971,6 +1195,7 @@ class index_dense_gt {
|
|
|
971
1195
|
progress_at&& progress = {}) {
|
|
972
1196
|
|
|
973
1197
|
// Discard all previous memory allocations of `vectors_tape_allocator_`
|
|
1198
|
+
index_limits_t old_limits = typed_ ? typed_->limits() : index_limits_t{};
|
|
974
1199
|
reset();
|
|
975
1200
|
|
|
976
1201
|
serialization_result_t result = file.open_if_not();
|
|
@@ -1018,6 +1243,9 @@ class index_dense_gt {
|
|
|
1018
1243
|
if (std::memcmp(buffer, default_magic(), std::strlen(default_magic())) != 0)
|
|
1019
1244
|
return result.failed("Magic header mismatch - the file isn't an index");
|
|
1020
1245
|
|
|
1246
|
+
// fix pre-2.10 headers
|
|
1247
|
+
fix_pre_2_10_metadata(head);
|
|
1248
|
+
|
|
1021
1249
|
// Validate the software version
|
|
1022
1250
|
if (head.version_major != USEARCH_VERSION_MAJOR)
|
|
1023
1251
|
return result.failed("File format may be different, please rebuild");
|
|
@@ -1030,24 +1258,48 @@ class index_dense_gt {
|
|
|
1030
1258
|
|
|
1031
1259
|
config_.multi = head.multi;
|
|
1032
1260
|
metric_ = metric_t::builtin(head.dimensions, head.kind_metric, head.kind_scalar);
|
|
1033
|
-
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");
|
|
1034
1264
|
casts_ = make_casts_(head.kind_scalar);
|
|
1035
1265
|
offset += sizeof(buffer);
|
|
1036
1266
|
}
|
|
1037
1267
|
|
|
1038
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
|
+
}
|
|
1039
1276
|
result = typed_->view(std::move(file), offset, std::forward<progress_at>(progress));
|
|
1040
1277
|
if (!result)
|
|
1041
1278
|
return result;
|
|
1042
1279
|
if (typed_->size() != static_cast<std::size_t>(matrix_rows))
|
|
1043
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");
|
|
1044
1284
|
|
|
1045
1285
|
// Address the vectors
|
|
1046
|
-
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");
|
|
1047
1289
|
if (!config.exclude_vectors)
|
|
1048
1290
|
for (std::uint64_t slot = 0; slot != matrix_rows; ++slot)
|
|
1049
1291
|
vectors_lookup_[slot] = (byte_t*)vectors_buffer.data() + matrix_cols * slot;
|
|
1050
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
|
+
|
|
1051
1303
|
reindex_keys_();
|
|
1052
1304
|
return result;
|
|
1053
1305
|
}
|
|
@@ -1180,6 +1432,7 @@ class index_dense_gt {
|
|
|
1180
1432
|
* @return `true` if the key is present in the index, `false` otherwise.
|
|
1181
1433
|
*/
|
|
1182
1434
|
bool contains(vector_key_t key) const {
|
|
1435
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1183
1436
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1184
1437
|
return slot_lookup_.contains(key_and_slot_t::any_slot(key));
|
|
1185
1438
|
}
|
|
@@ -1189,6 +1442,7 @@ class index_dense_gt {
|
|
|
1189
1442
|
* @return Zero if nothing is found, a positive integer otherwise.
|
|
1190
1443
|
*/
|
|
1191
1444
|
std::size_t count(vector_key_t key) const {
|
|
1445
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1192
1446
|
shared_lock_t lock(slot_lookup_mutex_);
|
|
1193
1447
|
return slot_lookup_.count(key_and_slot_t::any_slot(key));
|
|
1194
1448
|
}
|
|
@@ -1213,6 +1467,7 @@ class index_dense_gt {
|
|
|
1213
1467
|
* If an error occurred during the removal operation, `result.error` will contain an error message.
|
|
1214
1468
|
*/
|
|
1215
1469
|
labeling_result_t remove(vector_key_t key) {
|
|
1470
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled");
|
|
1216
1471
|
labeling_result_t result;
|
|
1217
1472
|
|
|
1218
1473
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
@@ -1223,7 +1478,8 @@ class index_dense_gt {
|
|
|
1223
1478
|
// Grow the removed entries ring, if needed
|
|
1224
1479
|
std::size_t matching_count = std::distance(matching_slots.first, matching_slots.second);
|
|
1225
1480
|
std::unique_lock<std::mutex> free_lock(free_keys_mutex_);
|
|
1226
|
-
|
|
1481
|
+
std::size_t free_count_old = free_keys_.size();
|
|
1482
|
+
if (!free_keys_.reserve(free_count_old + matching_count))
|
|
1227
1483
|
return result.failed("Can't allocate memory for a free-list");
|
|
1228
1484
|
|
|
1229
1485
|
// A removed entry would be:
|
|
@@ -1237,6 +1493,7 @@ class index_dense_gt {
|
|
|
1237
1493
|
}
|
|
1238
1494
|
slot_lookup_.erase(key);
|
|
1239
1495
|
result.completed = matching_count;
|
|
1496
|
+
usearch_assert_m(free_keys_.size() == free_count_old + matching_count, "Free keys count mismatch");
|
|
1240
1497
|
|
|
1241
1498
|
return result;
|
|
1242
1499
|
}
|
|
@@ -1251,6 +1508,7 @@ class index_dense_gt {
|
|
|
1251
1508
|
*/
|
|
1252
1509
|
template <typename keys_iterator_at>
|
|
1253
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");
|
|
1254
1512
|
|
|
1255
1513
|
labeling_result_t result;
|
|
1256
1514
|
unique_lock_t lookup_lock(slot_lookup_mutex_);
|
|
@@ -1337,17 +1595,6 @@ class index_dense_gt {
|
|
|
1337
1595
|
});
|
|
1338
1596
|
}
|
|
1339
1597
|
|
|
1340
|
-
struct copy_result_t {
|
|
1341
|
-
index_dense_gt index;
|
|
1342
|
-
error_t error;
|
|
1343
|
-
|
|
1344
|
-
explicit operator bool() const noexcept { return !error; }
|
|
1345
|
-
copy_result_t failed(error_t message) noexcept {
|
|
1346
|
-
error = std::move(message);
|
|
1347
|
-
return std::move(*this);
|
|
1348
|
-
}
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
1598
|
/**
|
|
1352
1599
|
* @brief Copies the ::index_dense_gt @b with all the data in it.
|
|
1353
1600
|
* @param config The copy configuration (optional).
|
|
@@ -1370,19 +1617,22 @@ class index_dense_gt {
|
|
|
1370
1617
|
copy.free_keys_.push(free_keys_[i]);
|
|
1371
1618
|
|
|
1372
1619
|
// Allocate buffers and move the vectors themselves
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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)
|
|
1378
1628
|
copy.vectors_lookup_[slot] = copy.vectors_tape_allocator_.allocate(copy.metric_.bytes_per_vector());
|
|
1379
|
-
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))
|
|
1380
1630
|
return result.failed("Out of memory!");
|
|
1381
|
-
for (std::size_t slot = 0; slot !=
|
|
1631
|
+
for (std::size_t slot = 0; slot != slots_count; ++slot)
|
|
1382
1632
|
std::memcpy(copy.vectors_lookup_[slot], vectors_lookup_[slot], metric_.bytes_per_vector());
|
|
1383
1633
|
}
|
|
1384
1634
|
|
|
1385
|
-
copy.slot_lookup_ = slot_lookup_;
|
|
1635
|
+
copy.slot_lookup_ = slot_lookup_; // TODO: Handle out of memory
|
|
1386
1636
|
*copy.typed_ = std::move(typed_result.index);
|
|
1387
1637
|
return result;
|
|
1388
1638
|
}
|
|
@@ -1392,22 +1642,34 @@ class index_dense_gt {
|
|
|
1392
1642
|
* @return A similarly configured ::index_dense_gt instance.
|
|
1393
1643
|
*/
|
|
1394
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
|
+
|
|
1395
1659
|
copy_result_t result;
|
|
1396
1660
|
index_dense_gt& other = result.index;
|
|
1397
|
-
|
|
1661
|
+
index_limits_t other_limits = limits();
|
|
1662
|
+
other_limits.members = 0;
|
|
1398
1663
|
other.config_ = config_;
|
|
1399
|
-
other.cast_buffer_ =
|
|
1664
|
+
other.cast_buffer_ = std::move(cast_buffer);
|
|
1400
1665
|
other.casts_ = casts_;
|
|
1401
1666
|
|
|
1402
1667
|
other.metric_ = metric_;
|
|
1403
|
-
other.available_threads_ =
|
|
1668
|
+
other.available_threads_ = std::move(available_threads);
|
|
1404
1669
|
other.free_key_ = free_key_;
|
|
1405
1670
|
|
|
1406
|
-
index_t* raw = index_allocator_t{}.allocate(1);
|
|
1407
|
-
if (!raw)
|
|
1408
|
-
return result.failed("Can't allocate the index");
|
|
1409
|
-
|
|
1410
1671
|
new (raw) index_t(config());
|
|
1672
|
+
raw->try_reserve(other_limits);
|
|
1411
1673
|
other.typed_ = raw;
|
|
1412
1674
|
return result;
|
|
1413
1675
|
}
|
|
@@ -1466,7 +1728,10 @@ class index_dense_gt {
|
|
|
1466
1728
|
compaction_result_t compact(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
|
|
1467
1729
|
compaction_result_t result;
|
|
1468
1730
|
|
|
1469
|
-
|
|
1731
|
+
vectors_lookup_t new_vectors_lookup(vectors_lookup_.size());
|
|
1732
|
+
if (!new_vectors_lookup)
|
|
1733
|
+
return result.failed("Out of memory!");
|
|
1734
|
+
|
|
1470
1735
|
vectors_tape_allocator_t new_vectors_allocator;
|
|
1471
1736
|
|
|
1472
1737
|
auto track_slot_change = [&](vector_key_t, compressed_slot_t old_slot, compressed_slot_t new_slot) {
|
|
@@ -1703,31 +1968,21 @@ class index_dense_gt {
|
|
|
1703
1968
|
}
|
|
1704
1969
|
|
|
1705
1970
|
private:
|
|
1706
|
-
|
|
1707
|
-
index_dense_gt const& parent;
|
|
1708
|
-
std::size_t thread_id;
|
|
1709
|
-
bool engaged;
|
|
1710
|
-
|
|
1711
|
-
~thread_lock_t() {
|
|
1712
|
-
if (engaged)
|
|
1713
|
-
parent.thread_unlock_(thread_id);
|
|
1714
|
-
}
|
|
1715
|
-
};
|
|
1716
|
-
|
|
1717
|
-
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 {
|
|
1718
1972
|
if (thread_id != any_thread())
|
|
1719
1973
|
return {*this, thread_id, false};
|
|
1720
1974
|
|
|
1721
1975
|
available_threads_mutex_.lock();
|
|
1722
|
-
|
|
1723
|
-
available_threads_.
|
|
1976
|
+
usearch_assert_m(available_threads_.size(), "No available threads to lock");
|
|
1977
|
+
available_threads_.try_pop(thread_id);
|
|
1724
1978
|
available_threads_mutex_.unlock();
|
|
1725
1979
|
return {*this, thread_id, true};
|
|
1726
1980
|
}
|
|
1727
1981
|
|
|
1728
|
-
void thread_unlock_(std::size_t thread_id) const {
|
|
1982
|
+
void thread_unlock_(std::size_t thread_id) const usearch_noexcept_m {
|
|
1729
1983
|
available_threads_mutex_.lock();
|
|
1730
|
-
available_threads_.
|
|
1984
|
+
usearch_assert_m(available_threads_.size() < available_threads_.capacity(), "Too many threads unlocked");
|
|
1985
|
+
available_threads_.push(thread_id);
|
|
1731
1986
|
available_threads_mutex_.unlock();
|
|
1732
1987
|
}
|
|
1733
1988
|
|
|
@@ -1736,7 +1991,7 @@ class index_dense_gt {
|
|
|
1736
1991
|
vector_key_t key, scalar_at const* vector, //
|
|
1737
1992
|
std::size_t thread, bool force_vector_copy, cast_t const& cast) {
|
|
1738
1993
|
|
|
1739
|
-
if (!multi() && contains(key))
|
|
1994
|
+
if (!multi() && config().enable_key_lookups && contains(key))
|
|
1740
1995
|
return add_result_t{}.failed("Duplicate keys not allowed in high-level wrappers");
|
|
1741
1996
|
|
|
1742
1997
|
// Cast the vector, if needed for compatibility with `metric_`
|
|
@@ -1760,8 +2015,10 @@ class index_dense_gt {
|
|
|
1760
2015
|
// Perform the insertion or the update
|
|
1761
2016
|
bool reuse_node = free_slot != default_free_value<compressed_slot_t>();
|
|
1762
2017
|
auto on_success = [&](member_ref_t member) {
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
+
}
|
|
1765
2022
|
if (copy_vector) {
|
|
1766
2023
|
if (!reuse_node)
|
|
1767
2024
|
vectors_lookup_[member.slot] = vectors_tape_allocator_.allocate(metric_.bytes_per_vector());
|
|
@@ -1799,16 +2056,19 @@ class index_dense_gt {
|
|
|
1799
2056
|
search_config.expansion = config_.expansion_search;
|
|
1800
2057
|
search_config.exact = exact;
|
|
1801
2058
|
|
|
2059
|
+
vector_key_t free_key_copy = free_key_;
|
|
1802
2060
|
if (std::is_same<typename std::decay<predicate_at>::type, dummy_predicate_t>::value) {
|
|
1803
|
-
auto allow = [
|
|
1804
|
-
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;
|
|
1805
2063
|
};
|
|
1806
|
-
|
|
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)};
|
|
1807
2066
|
} else {
|
|
1808
|
-
auto allow = [
|
|
1809
|
-
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);
|
|
1810
2069
|
};
|
|
1811
|
-
|
|
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)};
|
|
1812
2072
|
}
|
|
1813
2073
|
}
|
|
1814
2074
|
|
|
@@ -1831,9 +2091,8 @@ class index_dense_gt {
|
|
|
1831
2091
|
cluster_config.thread = lock.thread_id;
|
|
1832
2092
|
cluster_config.expansion = config_.expansion_search;
|
|
1833
2093
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
};
|
|
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; };
|
|
1837
2096
|
return typed_->cluster(vector_data, level, metric_proxy_t{*this}, cluster_config, allow);
|
|
1838
2097
|
}
|
|
1839
2098
|
|
|
@@ -1853,6 +2112,7 @@ class index_dense_gt {
|
|
|
1853
2112
|
}
|
|
1854
2113
|
|
|
1855
2114
|
// Check if such `key` is even present.
|
|
2115
|
+
usearch_assert_m(config().enable_key_lookups, "Key lookups are disabled!");
|
|
1856
2116
|
shared_lock_t slots_lock(slot_lookup_mutex_);
|
|
1857
2117
|
auto key_range = slot_lookup_.equal_range(key_and_slot_t::any_slot(key));
|
|
1858
2118
|
aggregated_distances_t result;
|
|
@@ -1889,7 +2149,8 @@ class index_dense_gt {
|
|
|
1889
2149
|
std::size_t count_total = typed_->size();
|
|
1890
2150
|
std::size_t count_removed = 0;
|
|
1891
2151
|
for (std::size_t i = 0; i != count_total; ++i) {
|
|
1892
|
-
|
|
2152
|
+
auto member_slot = static_cast<compressed_slot_t>(i);
|
|
2153
|
+
member_cref_t member = typed_->at(member_slot);
|
|
1893
2154
|
count_removed += member.key == free_key_;
|
|
1894
2155
|
}
|
|
1895
2156
|
|
|
@@ -1905,11 +2166,12 @@ class index_dense_gt {
|
|
|
1905
2166
|
free_keys_.clear();
|
|
1906
2167
|
free_keys_.reserve(count_removed);
|
|
1907
2168
|
for (std::size_t i = 0; i != typed_->size(); ++i) {
|
|
1908
|
-
|
|
2169
|
+
auto member_slot = static_cast<compressed_slot_t>(i);
|
|
2170
|
+
member_cref_t member = typed_->at(member_slot);
|
|
1909
2171
|
if (member.key == free_key_)
|
|
1910
|
-
free_keys_.push(
|
|
2172
|
+
free_keys_.push(member_slot);
|
|
1911
2173
|
else if (config_.enable_key_lookups)
|
|
1912
|
-
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});
|
|
1913
2175
|
}
|
|
1914
2176
|
}
|
|
1915
2177
|
|
|
@@ -1953,17 +2215,17 @@ class index_dense_gt {
|
|
|
1953
2215
|
template <typename to_scalar_at> static casts_t make_casts_() {
|
|
1954
2216
|
casts_t result;
|
|
1955
2217
|
|
|
1956
|
-
result.from_b1x8 = cast_gt<b1x8_t, to_scalar_at
|
|
1957
|
-
result.from_i8 = cast_gt<i8_t, to_scalar_at
|
|
1958
|
-
result.from_f16 = cast_gt<f16_t, to_scalar_at
|
|
1959
|
-
result.from_f32 = cast_gt<f32_t, to_scalar_at
|
|
1960
|
-
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_;
|
|
1961
2223
|
|
|
1962
|
-
result.to_b1x8 = cast_gt<to_scalar_at, b1x8_t
|
|
1963
|
-
result.to_i8 = cast_gt<to_scalar_at, i8_t
|
|
1964
|
-
result.to_f16 = cast_gt<to_scalar_at, f16_t
|
|
1965
|
-
result.to_f32 = cast_gt<to_scalar_at, f32_t
|
|
1966
|
-
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_;
|
|
1967
2229
|
|
|
1968
2230
|
return result;
|
|
1969
2231
|
}
|
|
@@ -2013,7 +2275,7 @@ static join_result_t join( //
|
|
|
2013
2275
|
man_to_woman_at&& man_to_woman = man_to_woman_at{}, //
|
|
2014
2276
|
woman_to_man_at&& woman_to_man = woman_to_man_at{}, //
|
|
2015
2277
|
executor_at&& executor = executor_at{}, //
|
|
2016
|
-
progress_at&& progress = progress_at{})
|
|
2278
|
+
progress_at&& progress = progress_at{}) {
|
|
2017
2279
|
|
|
2018
2280
|
return men.join( //
|
|
2019
2281
|
women, config, //
|