usearch 0.2.4 → 0.4.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/.github/ISSUE_TEMPLATE/bug_report.yml +90 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +64 -0
- package/.github/workflows/release.yml +5 -4
- package/.vscode/settings.json +4 -1
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +12 -0
- package/Cargo.toml +1 -1
- package/README.md +13 -5
- package/VERSION +1 -1
- package/binding.gyp +13 -9
- package/build.rs +1 -0
- package/conanfile.py +1 -1
- package/docs/benchmarks.rst +5 -0
- package/docs/compilation.rst +5 -0
- package/docs/conf.dox +2668 -0
- package/docs/index.rst +7 -0
- package/include/usearch/usearch.hpp +260 -140
- package/javascript/lib.cpp +82 -8
- package/objc/USearchObjective.mm +2 -2
- package/package.json +1 -1
- package/python/lib.cpp +47 -20
- package/rust/lib.cpp +6 -2
- package/src/advanced.hpp +23 -19
- package/src/bench.cpp +14 -9
- package/swift/Index+Sugar.swift +1 -0
- package/swift/Test.swift +1 -0
- /package/docs/{Benchmarking.md → benchmarks.md} +0 -0
|
@@ -85,6 +85,9 @@ inline std::size_t ceil2(std::size_t v) noexcept {
|
|
|
85
85
|
return v;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* @brief Inner (Dot) Product distance.
|
|
90
|
+
*/
|
|
88
91
|
template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
89
92
|
using scalar_t = scalar_at;
|
|
90
93
|
using result_t = result_at;
|
|
@@ -92,10 +95,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
|
92
95
|
|
|
93
96
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
94
97
|
result_type ab{};
|
|
95
|
-
#if defined(
|
|
96
|
-
#pragma GCC ivdep
|
|
97
|
-
#elif defined(__clang__)
|
|
98
|
+
#if defined(__clang__)
|
|
98
99
|
#pragma clang loop vectorize(enable)
|
|
100
|
+
#elif defined(__GNUC__)
|
|
101
|
+
#pragma GCC ivdep
|
|
99
102
|
#elif defined(_OPENMP)
|
|
100
103
|
#pragma omp simd reduction(+ : ab)
|
|
101
104
|
#endif
|
|
@@ -105,6 +108,12 @@ template <typename scalar_at, typename result_at = scalar_at> struct ip_gt {
|
|
|
105
108
|
}
|
|
106
109
|
};
|
|
107
110
|
|
|
111
|
+
/**
|
|
112
|
+
* @brief Cosine (Angular) distance.
|
|
113
|
+
* Identical to the Inner Product of normalized vectors.
|
|
114
|
+
* Unless you are running on an tiny embedded platform, this metric
|
|
115
|
+
* is recommended over `::ip_gt` for low-precision scalars.
|
|
116
|
+
*/
|
|
108
117
|
template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
109
118
|
using scalar_t = scalar_at;
|
|
110
119
|
using result_t = result_at;
|
|
@@ -112,10 +121,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
|
112
121
|
|
|
113
122
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
114
123
|
result_t ab{}, a2{}, b2{};
|
|
115
|
-
#if defined(
|
|
116
|
-
#pragma GCC ivdep
|
|
117
|
-
#elif defined(__clang__)
|
|
124
|
+
#if defined(__clang__)
|
|
118
125
|
#pragma clang loop vectorize(enable)
|
|
126
|
+
#elif defined(__GNUC__)
|
|
127
|
+
#pragma GCC ivdep
|
|
119
128
|
#elif defined(_OPENMP)
|
|
120
129
|
#pragma omp simd reduction(+ : ab, a2, b2)
|
|
121
130
|
#endif
|
|
@@ -127,6 +136,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct cos_gt {
|
|
|
127
136
|
}
|
|
128
137
|
};
|
|
129
138
|
|
|
139
|
+
/**
|
|
140
|
+
* @brief Squared Euclidean (L2) distance.
|
|
141
|
+
* Square root is avoided at the end, as it won't affect the ordering.
|
|
142
|
+
*/
|
|
130
143
|
template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
|
|
131
144
|
using scalar_t = scalar_at;
|
|
132
145
|
using result_t = result_at;
|
|
@@ -134,10 +147,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct l2sq_gt {
|
|
|
134
147
|
|
|
135
148
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
136
149
|
result_t ab_deltas_sq{};
|
|
137
|
-
#if defined(
|
|
138
|
-
#pragma GCC ivdep
|
|
139
|
-
#elif defined(__clang__)
|
|
150
|
+
#if defined(__clang__)
|
|
140
151
|
#pragma clang loop vectorize(enable)
|
|
152
|
+
#elif defined(__GNUC__)
|
|
153
|
+
#pragma GCC ivdep
|
|
141
154
|
#elif defined(_OPENMP)
|
|
142
155
|
#pragma omp simd reduction(+ : ab_deltas_sq)
|
|
143
156
|
#endif
|
|
@@ -159,10 +172,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct hamming_g
|
|
|
159
172
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t elements,
|
|
160
173
|
std::size_t = 0) const noexcept {
|
|
161
174
|
result_t matches{};
|
|
162
|
-
#if defined(
|
|
163
|
-
#pragma GCC ivdep
|
|
164
|
-
#elif defined(__clang__)
|
|
175
|
+
#if defined(__clang__)
|
|
165
176
|
#pragma clang loop vectorize(enable)
|
|
177
|
+
#elif defined(__GNUC__)
|
|
178
|
+
#pragma GCC ivdep
|
|
166
179
|
#elif defined(_OPENMP)
|
|
167
180
|
#pragma omp simd reduction(+ : matches)
|
|
168
181
|
#endif
|
|
@@ -187,10 +200,10 @@ template <typename scalar_at, typename result_at = std::size_t> struct bit_hammi
|
|
|
187
200
|
inline result_t operator()(scalar_t const* a, scalar_t const* b, std::size_t words,
|
|
188
201
|
std::size_t = 0) const noexcept {
|
|
189
202
|
result_t matches{};
|
|
190
|
-
#if defined(
|
|
191
|
-
#pragma GCC ivdep
|
|
192
|
-
#elif defined(__clang__)
|
|
203
|
+
#if defined(__clang__)
|
|
193
204
|
#pragma clang loop vectorize(enable)
|
|
205
|
+
#elif defined(__GNUC__)
|
|
206
|
+
#pragma GCC ivdep
|
|
194
207
|
#elif defined(_OPENMP)
|
|
195
208
|
#pragma omp simd reduction(+ : matches)
|
|
196
209
|
#endif
|
|
@@ -239,10 +252,10 @@ template <typename scalar_at, typename result_at = scalar_at> struct pearson_cor
|
|
|
239
252
|
scalar_t const* a, scalar_t const* b, std::size_t dim, std::size_t = 0) const noexcept {
|
|
240
253
|
result_t a_sum{}, b_sum{}, ab_sum{};
|
|
241
254
|
result_t a_sq_sum{}, b_sq_sum{};
|
|
242
|
-
#if defined(
|
|
243
|
-
#pragma GCC ivdep
|
|
244
|
-
#elif defined(__clang__)
|
|
255
|
+
#if defined(__clang__)
|
|
245
256
|
#pragma clang loop vectorize(enable)
|
|
257
|
+
#elif defined(__GNUC__)
|
|
258
|
+
#pragma GCC ivdep
|
|
246
259
|
#elif defined(_OPENMP)
|
|
247
260
|
#pragma omp simd reduction(+ : a_sum, b_sum, ab_sum, a_sq_sum, b_sq_sum)
|
|
248
261
|
#endif
|
|
@@ -291,6 +304,9 @@ template <std::size_t multiple_ak> inline std::size_t divide_round_up(std::size_
|
|
|
291
304
|
return (num + multiple_ak - 1) / multiple_ak;
|
|
292
305
|
}
|
|
293
306
|
|
|
307
|
+
/**
|
|
308
|
+
* @brief Light-weight bitset implementation to track visited nodes during graph traversal.
|
|
309
|
+
*/
|
|
294
310
|
template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
|
|
295
311
|
using allocator_t = allocator_at;
|
|
296
312
|
using byte_t = typename allocator_t::value_type;
|
|
@@ -565,7 +581,7 @@ class sorted_buffer_gt {
|
|
|
565
581
|
};
|
|
566
582
|
|
|
567
583
|
/**
|
|
568
|
-
*
|
|
584
|
+
* @brief Tiny userspace mutex, designed to fit in a single integer.
|
|
569
585
|
*/
|
|
570
586
|
class mutex_t {
|
|
571
587
|
#if defined(WINDOWS)
|
|
@@ -614,13 +630,13 @@ static_assert(sizeof(mutex_t) == sizeof(std::int32_t), "Mutex is larger than exp
|
|
|
614
630
|
|
|
615
631
|
using lock_t = std::unique_lock<mutex_t>;
|
|
616
632
|
|
|
617
|
-
/**
|
|
618
|
-
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
619
|
-
*/
|
|
620
633
|
#if defined(WINDOWS)
|
|
621
634
|
#pragma pack(push, 1) // Pack struct members on 1-byte alignment
|
|
622
635
|
#endif
|
|
623
636
|
|
|
637
|
+
/**
|
|
638
|
+
* @brief Five-byte integer type to address node clouds with over 4B entries.
|
|
639
|
+
*/
|
|
624
640
|
class usearch_pack_m uint40_t {
|
|
625
641
|
unsigned char octets[5];
|
|
626
642
|
|
|
@@ -658,6 +674,7 @@ class usearch_pack_m uint40_t {
|
|
|
658
674
|
return old;
|
|
659
675
|
}
|
|
660
676
|
};
|
|
677
|
+
|
|
661
678
|
#if defined(WINDOWS)
|
|
662
679
|
#pragma pack(pop) // Reset alignment to default
|
|
663
680
|
#endif
|
|
@@ -677,10 +694,9 @@ template <typename scalar_at> class span_gt {
|
|
|
677
694
|
};
|
|
678
695
|
|
|
679
696
|
/**
|
|
680
|
-
* @brief
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
* and two expansion factors - for construction and search.
|
|
697
|
+
* @brief Configuration settings for the index construction.
|
|
698
|
+
* Includes the main `::connectivity` parameter (`M` in the paper)
|
|
699
|
+
* and two expansion factors - for construction and search.
|
|
684
700
|
*/
|
|
685
701
|
struct config_t {
|
|
686
702
|
|
|
@@ -709,10 +725,35 @@ struct config_t {
|
|
|
709
725
|
std::size_t max_threads_search = 0;
|
|
710
726
|
};
|
|
711
727
|
|
|
728
|
+
struct add_config_t {
|
|
729
|
+
/// @brief Optional thread identifier for multi-threaded construction.
|
|
730
|
+
std::size_t thread{};
|
|
731
|
+
/// @brief Don't copy the ::vector, if it's persisted elsewhere.
|
|
732
|
+
bool store_vector{true};
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
struct add_result_t {
|
|
736
|
+
std::size_t new_size{};
|
|
737
|
+
std::size_t cycles{};
|
|
738
|
+
std::size_t measurements{};
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
struct search_config_t {
|
|
742
|
+
std::size_t thread{};
|
|
743
|
+
bool exact{};
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
struct search_result_t {
|
|
747
|
+
std::size_t count{};
|
|
748
|
+
std::size_t cycles{};
|
|
749
|
+
std::size_t measurements{};
|
|
750
|
+
|
|
751
|
+
inline operator std::size_t() const noexcept { return count; }
|
|
752
|
+
};
|
|
753
|
+
|
|
712
754
|
/**
|
|
713
|
-
* @brief
|
|
714
|
-
*
|
|
715
|
-
* Hierarchical Navigable Small World graph algorithm.
|
|
755
|
+
* @brief Approximate Nearest Neighbors Search index using the
|
|
756
|
+
* Hierarchical Navigable Small World graph algorithm.
|
|
716
757
|
*
|
|
717
758
|
* @section Features
|
|
718
759
|
* - Search for vectors of different dimensionality.
|
|
@@ -747,8 +788,15 @@ class index_gt {
|
|
|
747
788
|
using id_t = id_at;
|
|
748
789
|
using allocator_t = allocator_at;
|
|
749
790
|
|
|
791
|
+
using vector_view_t = span_gt<scalar_t const>;
|
|
792
|
+
|
|
793
|
+
#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || __cplusplus >= 201703L) // C++17 ann newer
|
|
794
|
+
using distance_t =
|
|
795
|
+
typename std::invoke_result<metric_t, scalar_t const*, scalar_t const*, std::size_t, std::size_t>::type;
|
|
796
|
+
#else
|
|
750
797
|
using distance_t =
|
|
751
798
|
typename std::result_of<metric_t(scalar_t const*, scalar_t const*, std::size_t, std::size_t)>::type;
|
|
799
|
+
#endif
|
|
752
800
|
|
|
753
801
|
private:
|
|
754
802
|
using neighbors_count_t = id_t;
|
|
@@ -835,16 +883,25 @@ class index_gt {
|
|
|
835
883
|
scalar_t* vector{};
|
|
836
884
|
|
|
837
885
|
inline node_ref_t(mutex_t& m, node_head_t& h, scalar_t* s) noexcept : mutex_(&m), head(h), vector(s) {}
|
|
886
|
+
/// @brief Locks the list of neighbors for concurrent updates.
|
|
838
887
|
inline lock_t lock() const noexcept { return mutex_ ? lock_t{*mutex_} : lock_t{}; }
|
|
839
888
|
inline operator node_t() const noexcept { return node_t{mutex_ ? (byte_t*)mutex_ : (byte_t*)&head, vector}; }
|
|
889
|
+
inline operator vector_view_t() const noexcept { return {vector, head.dim}; }
|
|
840
890
|
};
|
|
841
891
|
|
|
842
892
|
struct usearch_align_m thread_context_t {
|
|
843
|
-
top_candidates_t top_candidates;
|
|
844
|
-
next_candidates_t next_candidates;
|
|
845
|
-
visits_bitset_t visits;
|
|
846
|
-
std::default_random_engine level_generator;
|
|
847
|
-
metric_t metric;
|
|
893
|
+
top_candidates_t top_candidates{};
|
|
894
|
+
next_candidates_t next_candidates{};
|
|
895
|
+
visits_bitset_t visits{};
|
|
896
|
+
std::default_random_engine level_generator{};
|
|
897
|
+
metric_t metric{};
|
|
898
|
+
std::size_t iteration_cycles{};
|
|
899
|
+
std::size_t measurements_count{};
|
|
900
|
+
|
|
901
|
+
inline distance_t measure(vector_view_t a, vector_view_t b) noexcept {
|
|
902
|
+
measurements_count++;
|
|
903
|
+
return metric(a.data(), b.data(), a.size(), b.size());
|
|
904
|
+
}
|
|
848
905
|
};
|
|
849
906
|
|
|
850
907
|
config_t config_{};
|
|
@@ -894,6 +951,9 @@ class index_gt {
|
|
|
894
951
|
reserve(config.max_elements);
|
|
895
952
|
}
|
|
896
953
|
|
|
954
|
+
/**
|
|
955
|
+
* @brief Clones the structure with the same hyper-parameters, but without contents.
|
|
956
|
+
*/
|
|
897
957
|
index_gt fork() noexcept(false) { return {config_, metric_, allocator_}; }
|
|
898
958
|
|
|
899
959
|
~index_gt() noexcept { clear(); }
|
|
@@ -907,6 +967,10 @@ class index_gt {
|
|
|
907
967
|
|
|
908
968
|
#pragma region Adjusting Configuration
|
|
909
969
|
|
|
970
|
+
/**
|
|
971
|
+
* @brief Erases all the vectors from the index.
|
|
972
|
+
* Will change `size()` to zero, but will keep the same `capacity()`.
|
|
973
|
+
*/
|
|
910
974
|
void clear() noexcept {
|
|
911
975
|
std::size_t n = size_;
|
|
912
976
|
for (std::size_t i = 0; i != n; ++i)
|
|
@@ -916,6 +980,9 @@ class index_gt {
|
|
|
916
980
|
entry_id_ = 0u;
|
|
917
981
|
}
|
|
918
982
|
|
|
983
|
+
/**
|
|
984
|
+
* @brief Swaps the underlying memory buffers and thread contexts.
|
|
985
|
+
*/
|
|
919
986
|
void swap(index_gt& other) noexcept {
|
|
920
987
|
std::swap(config_, other.config_);
|
|
921
988
|
std::swap(metric_, other.metric_);
|
|
@@ -936,6 +1003,9 @@ class index_gt {
|
|
|
936
1003
|
other.size_ = size;
|
|
937
1004
|
}
|
|
938
1005
|
|
|
1006
|
+
/**
|
|
1007
|
+
* @brief Increases the `capacity()` of the index to allow adding more vectors.
|
|
1008
|
+
*/
|
|
939
1009
|
void reserve(std::size_t new_capacity) noexcept(false) {
|
|
940
1010
|
|
|
941
1011
|
assert_m(new_capacity >= size_, "Can't drop existing values");
|
|
@@ -946,6 +1016,10 @@ class index_gt {
|
|
|
946
1016
|
capacity_ = new_capacity;
|
|
947
1017
|
}
|
|
948
1018
|
|
|
1019
|
+
/**
|
|
1020
|
+
* @brief Optimizes configuration options to fit the maximum number
|
|
1021
|
+
* of neighbors in CPU-cache-aligned buffers.
|
|
1022
|
+
*/
|
|
949
1023
|
static config_t optimize(config_t const& config) noexcept {
|
|
950
1024
|
precomputed_constants_t pre = precompute(config);
|
|
951
1025
|
std::size_t bytes_per_node_base = head_bytes_k + pre.neighbors_base_bytes + pre.mutex_bytes;
|
|
@@ -960,99 +1034,140 @@ class index_gt {
|
|
|
960
1034
|
|
|
961
1035
|
#pragma region Construction and Search
|
|
962
1036
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1037
|
+
/**
|
|
1038
|
+
* @brief Inserts a new vector into the index. Thread-safe.
|
|
1039
|
+
*
|
|
1040
|
+
* @param[in] label External identifier/name/descriptor for the vector.
|
|
1041
|
+
* @param[in] vector Contiguous range of scalars forming a vector view.
|
|
1042
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1043
|
+
*/
|
|
1044
|
+
add_result_t add(label_t label, vector_view_t vector, add_config_t config = {}) noexcept(false) {
|
|
966
1045
|
|
|
967
1046
|
assert_m(!is_immutable(), "Can't add to an immutable index");
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1047
|
+
add_result_t result;
|
|
1048
|
+
result.new_size = size_.fetch_add(1);
|
|
1049
|
+
id_t id = static_cast<id_t>(result.new_size);
|
|
971
1050
|
|
|
972
|
-
// Determining how much memory to allocate depends on the target level
|
|
1051
|
+
// Determining how much memory to allocate depends on the target level
|
|
973
1052
|
lock_t new_level_lock(global_mutex_);
|
|
974
1053
|
level_t max_level = max_level_;
|
|
975
|
-
thread_context_t& context = thread_contexts_[
|
|
976
|
-
level_t
|
|
977
|
-
if (
|
|
1054
|
+
thread_context_t& context = thread_contexts_[config.thread];
|
|
1055
|
+
level_t target_level = choose_random_level(context.level_generator);
|
|
1056
|
+
if (target_level <= max_level)
|
|
978
1057
|
new_level_lock.unlock();
|
|
979
1058
|
|
|
980
1059
|
// Allocate the neighbors
|
|
981
|
-
node_ref_t new_node = node_malloc(
|
|
1060
|
+
node_ref_t new_node = node_malloc(label, vector, target_level, config.store_vector);
|
|
982
1061
|
lock_t new_lock = new_node.lock();
|
|
983
|
-
nodes_[
|
|
1062
|
+
nodes_[id] = new_node;
|
|
984
1063
|
|
|
985
1064
|
// Do nothing for the first element
|
|
986
|
-
if (!
|
|
987
|
-
max_level_ =
|
|
988
|
-
return
|
|
1065
|
+
if (!id) {
|
|
1066
|
+
max_level_ = target_level;
|
|
1067
|
+
return result;
|
|
989
1068
|
}
|
|
990
1069
|
|
|
1070
|
+
// Pull stats
|
|
1071
|
+
result.measurements = context.measurements_count;
|
|
1072
|
+
result.cycles = context.iteration_cycles;
|
|
1073
|
+
|
|
991
1074
|
// Go down the level, tracking only the closest match
|
|
992
|
-
id_t closest_id = search_for_one(entry_id_,
|
|
1075
|
+
id_t closest_id = search_for_one(entry_id_, vector, max_level, target_level, context);
|
|
993
1076
|
|
|
994
|
-
// From `
|
|
995
|
-
for (level_t level = (std::min)(
|
|
996
|
-
search_to_insert(closest_id,
|
|
997
|
-
closest_id = connect_new_element(
|
|
1077
|
+
// From `target_level` down perform proper extensive search
|
|
1078
|
+
for (level_t level = (std::min)(target_level, max_level); level >= 0; level--) {
|
|
1079
|
+
search_to_insert(closest_id, vector, level, context);
|
|
1080
|
+
closest_id = connect_new_element(id, level, context);
|
|
998
1081
|
}
|
|
999
1082
|
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1083
|
+
// Normalize stats
|
|
1084
|
+
result.measurements = context.measurements_count - result.measurements;
|
|
1085
|
+
result.cycles = context.iteration_cycles - result.cycles;
|
|
1086
|
+
|
|
1087
|
+
// Updating the entry point if needed
|
|
1088
|
+
if (target_level > max_level) {
|
|
1089
|
+
entry_id_ = id;
|
|
1090
|
+
max_level_ = target_level;
|
|
1004
1091
|
}
|
|
1005
|
-
return
|
|
1092
|
+
return result;
|
|
1006
1093
|
}
|
|
1007
1094
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1095
|
+
/**
|
|
1096
|
+
* @brief Searches for the closest members to the given ::query. Thread-safe.
|
|
1097
|
+
*
|
|
1098
|
+
* @param[in] query Contiguous range of scalars forming a vector view.
|
|
1099
|
+
* @param[in] wanted The upper bound for the number of results to return.
|
|
1100
|
+
* @param[in] callback Function receiving the labels and distances to matches in ascending order.
|
|
1101
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1102
|
+
*/
|
|
1103
|
+
template <typename callback_at>
|
|
1104
|
+
search_result_t search( //
|
|
1105
|
+
vector_view_t query, std::size_t wanted, //
|
|
1106
|
+
callback_at&& callback, search_config_t config = {}) const noexcept(false) {
|
|
1012
1107
|
|
|
1108
|
+
search_result_t result;
|
|
1013
1109
|
if (!size_)
|
|
1014
|
-
return;
|
|
1015
|
-
|
|
1016
|
-
scalar_t const* query_vec = query_span.data();
|
|
1017
|
-
std::size_t query_dim = query_span.size();
|
|
1110
|
+
return result;
|
|
1018
1111
|
|
|
1019
1112
|
// Go down the level, tracking only the closest match
|
|
1020
|
-
thread_context_t& context = thread_contexts_[
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1113
|
+
thread_context_t& context = thread_contexts_[config.thread];
|
|
1114
|
+
result.measurements = context.measurements_count;
|
|
1115
|
+
result.cycles = context.iteration_cycles;
|
|
1116
|
+
|
|
1117
|
+
if (config.exact) {
|
|
1118
|
+
search_exact(query, wanted, context);
|
|
1119
|
+
} else {
|
|
1120
|
+
id_t closest_id = search_for_one(entry_id_, query, max_level_, 0, context);
|
|
1121
|
+
std::size_t expansion = (std::max)(config_.expansion_search, wanted);
|
|
1122
|
+
// For bottom layer we need a more optimized procedure
|
|
1123
|
+
search_to_find_in_base(closest_id, query, expansion, context);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
top_candidates_t& top = context.top_candidates;
|
|
1127
|
+
top.sort_ascending();
|
|
1128
|
+
top.shrink(wanted);
|
|
1129
|
+
|
|
1130
|
+
candidate_t const* top_ordered = top.data();
|
|
1131
|
+
for (std::size_t i = 0; i < top.size(); ++i)
|
|
1132
|
+
callback(node(top_ordered[i].second).head.label, top_ordered[i].first);
|
|
1133
|
+
|
|
1134
|
+
// Normalize stats
|
|
1135
|
+
result.measurements = context.measurements_count - result.measurements;
|
|
1136
|
+
result.cycles = context.iteration_cycles - result.cycles;
|
|
1137
|
+
result.count = top.size();
|
|
1138
|
+
return result;
|
|
1032
1139
|
}
|
|
1033
1140
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1141
|
+
/**
|
|
1142
|
+
* @brief Searches for the closest members to the given ::query. Thread-safe.
|
|
1143
|
+
* @note Convenience method for callback-based `search()`.
|
|
1144
|
+
*
|
|
1145
|
+
* @param[in] query Contiguous range of scalars forming a vector view.
|
|
1146
|
+
* @param[in] wanted The upper bound for the number of results to return.
|
|
1147
|
+
* @param[out] matches Contiguous output buffer for the labels of closest members.
|
|
1148
|
+
* @param[out] distances Contiguous output buffer for the distances to the closest members.
|
|
1149
|
+
* @param[in] config Configuration options for this specific operation.
|
|
1150
|
+
* @return The number of found approximate matches. Always equal or less then ::wanted.
|
|
1151
|
+
*/
|
|
1152
|
+
search_result_t search( //
|
|
1153
|
+
vector_view_t query, std::size_t wanted, //
|
|
1154
|
+
label_t* matches, distance_t* distances, search_config_t config = {}) const noexcept(false) {
|
|
1038
1155
|
|
|
1039
|
-
std::size_t found = 0;
|
|
1040
1156
|
auto callback = [&](label_t label, distance_t distance) noexcept {
|
|
1041
1157
|
if (matches)
|
|
1042
|
-
matches
|
|
1158
|
+
*(matches++) = label;
|
|
1043
1159
|
if (distances)
|
|
1044
|
-
distances
|
|
1045
|
-
++found;
|
|
1160
|
+
*(distances++) = distance;
|
|
1046
1161
|
};
|
|
1047
|
-
search(
|
|
1048
|
-
return found;
|
|
1162
|
+
return search(query, wanted, callback, config);
|
|
1049
1163
|
}
|
|
1050
1164
|
|
|
1051
1165
|
#pragma endregion
|
|
1052
1166
|
|
|
1053
1167
|
#pragma region Serialization
|
|
1054
1168
|
|
|
1055
|
-
|
|
1169
|
+
private:
|
|
1170
|
+
struct serialized_state_t {
|
|
1056
1171
|
// Check compatibility
|
|
1057
1172
|
std::uint64_t bytes_per_label{};
|
|
1058
1173
|
std::uint64_t bytes_per_id{};
|
|
@@ -1064,12 +1179,15 @@ class index_gt {
|
|
|
1064
1179
|
std::uint64_t max_level{};
|
|
1065
1180
|
};
|
|
1066
1181
|
|
|
1182
|
+
public:
|
|
1067
1183
|
/**
|
|
1068
|
-
*
|
|
1184
|
+
* @brief Saves serialized binary index representation to disk,
|
|
1185
|
+
* co-locating vectors and neighbors lists.
|
|
1186
|
+
* Available on Linux, MacOS, Windows.
|
|
1069
1187
|
*/
|
|
1070
1188
|
void save(char const* file_path) const noexcept(false) {
|
|
1071
1189
|
|
|
1072
|
-
|
|
1190
|
+
serialized_state_t state;
|
|
1073
1191
|
// Check compatibility
|
|
1074
1192
|
state.bytes_per_label = sizeof(label_t);
|
|
1075
1193
|
state.bytes_per_id = sizeof(id_t);
|
|
@@ -1115,10 +1233,12 @@ class index_gt {
|
|
|
1115
1233
|
}
|
|
1116
1234
|
|
|
1117
1235
|
/**
|
|
1118
|
-
*
|
|
1236
|
+
* @brief Loads the serialized binary index representation from disk,
|
|
1237
|
+
* copying both vectors and neighbors lists into RAM.
|
|
1238
|
+
* Available on Linux, MacOS, Windows.
|
|
1119
1239
|
*/
|
|
1120
1240
|
void load(char const* file_path) noexcept(false) {
|
|
1121
|
-
|
|
1241
|
+
serialized_state_t state;
|
|
1122
1242
|
std::FILE* file = std::fopen(file_path, "r");
|
|
1123
1243
|
if (!file)
|
|
1124
1244
|
throw std::runtime_error(std::strerror(errno));
|
|
@@ -1158,7 +1278,7 @@ class index_gt {
|
|
|
1158
1278
|
}
|
|
1159
1279
|
|
|
1160
1280
|
std::size_t bytes_to_dump = node_dump_size(head.dim, head.level);
|
|
1161
|
-
node_ref_t node_ref = node_malloc(head.label, nullptr, head.dim, head.level, true);
|
|
1281
|
+
node_ref_t node_ref = node_malloc(head.label, {nullptr, head.dim}, head.level, true);
|
|
1162
1282
|
read = std::fread((byte_t*)&node_ref.head + head_bytes_k, bytes_to_dump - head_bytes_k, 1, file);
|
|
1163
1283
|
if (!read) {
|
|
1164
1284
|
std::fclose(file);
|
|
@@ -1173,13 +1293,15 @@ class index_gt {
|
|
|
1173
1293
|
}
|
|
1174
1294
|
|
|
1175
1295
|
/**
|
|
1176
|
-
*
|
|
1296
|
+
* @brief Memory-maps the serialized binary index representation from disk,
|
|
1297
|
+
* @b without copying the vectors and neighbors lists into RAM.
|
|
1298
|
+
* Available on Linux, MacOS, but @b not on Windows.
|
|
1177
1299
|
*/
|
|
1178
1300
|
void view(char const* file_path) noexcept(false) {
|
|
1179
1301
|
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__)
|
|
1180
1302
|
throw std::logic_error("Memory-mapping is not yet available for Windows");
|
|
1181
1303
|
#else
|
|
1182
|
-
|
|
1304
|
+
serialized_state_t state;
|
|
1183
1305
|
int open_flags = O_RDONLY;
|
|
1184
1306
|
#if __linux__
|
|
1185
1307
|
open_flags |= O_NOATIME;
|
|
@@ -1280,11 +1402,10 @@ class index_gt {
|
|
|
1280
1402
|
node = node_t{};
|
|
1281
1403
|
}
|
|
1282
1404
|
|
|
1283
|
-
node_ref_t node_malloc(
|
|
1284
|
-
label_t label, scalar_t const* vector, std::size_t dim, //
|
|
1285
|
-
level_t level, bool store_vector = true) noexcept(false) {
|
|
1405
|
+
node_ref_t node_malloc(label_t label, vector_view_t vector, level_t level, bool store_vector) noexcept(false) {
|
|
1286
1406
|
|
|
1287
1407
|
// This function is rarely called and can be as expensive as needed for higher space-efficiency.
|
|
1408
|
+
std::size_t dim = vector.size();
|
|
1288
1409
|
std::size_t levels_bytes = pre_.neighbors_base_bytes + pre_.neighbors_bytes * level;
|
|
1289
1410
|
std::size_t node_bytes = //
|
|
1290
1411
|
pre_.mutex_bytes + // Optional concurrency-control
|
|
@@ -1298,10 +1419,10 @@ class index_gt {
|
|
|
1298
1419
|
mutex_t* mutex = synchronize() ? (mutex_t*)data : nullptr;
|
|
1299
1420
|
scalar_t* scalars = store_vector //
|
|
1300
1421
|
? (scalar_t*)(data + pre_.mutex_bytes + head_bytes_k + levels_bytes)
|
|
1301
|
-
: (scalar_t*)(vector);
|
|
1422
|
+
: (scalar_t*)(vector.data());
|
|
1302
1423
|
|
|
1303
1424
|
std::memset(data, 0, node_bytes);
|
|
1304
|
-
std::memcpy(scalars, vector, sizeof(scalar_t) * dim * (store_vector && vector));
|
|
1425
|
+
std::memcpy(scalars, vector.data(), sizeof(scalar_t) * dim * (store_vector && vector.data()));
|
|
1305
1426
|
|
|
1306
1427
|
node_head_t& head = *(node_head_t*)(data + pre_.mutex_bytes);
|
|
1307
1428
|
head.label = label;
|
|
@@ -1371,17 +1492,9 @@ class index_gt {
|
|
|
1371
1492
|
// To fit a new connection we need to drop an existing one.
|
|
1372
1493
|
top.clear();
|
|
1373
1494
|
top.reserve(close_header.size() + 1);
|
|
1374
|
-
top.emplace_reserved(
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
new_id);
|
|
1378
|
-
for (id_t successor_id : close_header) {
|
|
1379
|
-
node_ref_t successor_node = node(successor_id);
|
|
1380
|
-
top.emplace_reserved( //
|
|
1381
|
-
context.metric( //
|
|
1382
|
-
successor_node.vector, close_node.vector, successor_node.head.dim, close_node.head.dim),
|
|
1383
|
-
successor_id);
|
|
1384
|
-
}
|
|
1495
|
+
top.emplace_reserved(context.measure(new_node, close_node), new_id);
|
|
1496
|
+
for (id_t successor_id : close_header)
|
|
1497
|
+
top.emplace_reserved(context.measure(node(successor_id), close_node), successor_id);
|
|
1385
1498
|
|
|
1386
1499
|
// Export the results:
|
|
1387
1500
|
close_header.clear();
|
|
@@ -1399,13 +1512,12 @@ class index_gt {
|
|
|
1399
1512
|
return (level_t)r;
|
|
1400
1513
|
}
|
|
1401
1514
|
|
|
1402
|
-
id_t search_for_one(
|
|
1403
|
-
id_t
|
|
1404
|
-
level_t begin_level, level_t end_level,
|
|
1515
|
+
id_t search_for_one( //
|
|
1516
|
+
id_t closest_id, vector_view_t query, //
|
|
1517
|
+
level_t begin_level, level_t end_level, //
|
|
1518
|
+
thread_context_t& context) const noexcept {
|
|
1405
1519
|
|
|
1406
|
-
|
|
1407
|
-
distance_t closest_dist =
|
|
1408
|
-
context.metric(query_vec, node(closest_id).vector, query_dim, node(closest_id).head.dim);
|
|
1520
|
+
distance_t closest_dist = context.measure(query, node(closest_id));
|
|
1409
1521
|
for (level_t level = begin_level; level > end_level; level--) {
|
|
1410
1522
|
bool changed;
|
|
1411
1523
|
do {
|
|
@@ -1414,23 +1526,22 @@ class index_gt {
|
|
|
1414
1526
|
lock_t closest_lock = closest_node.lock();
|
|
1415
1527
|
neighbors_ref_t closest_header = neighbors_non_base(closest_node, level);
|
|
1416
1528
|
for (id_t candidate_id : closest_header) {
|
|
1417
|
-
|
|
1418
|
-
distance_t candidate_dist =
|
|
1419
|
-
context.metric(query_vec, candidate_node.vector, query_dim, candidate_node.head.dim);
|
|
1529
|
+
distance_t candidate_dist = context.measure(query, node(candidate_id));
|
|
1420
1530
|
if (candidate_dist < closest_dist) {
|
|
1421
1531
|
closest_dist = candidate_dist;
|
|
1422
1532
|
closest_id = candidate_id;
|
|
1423
1533
|
changed = true;
|
|
1424
1534
|
}
|
|
1425
1535
|
}
|
|
1536
|
+
context.iteration_cycles++;
|
|
1426
1537
|
} while (changed);
|
|
1427
1538
|
}
|
|
1428
1539
|
return closest_id;
|
|
1429
1540
|
}
|
|
1430
1541
|
|
|
1431
|
-
void search_to_insert(
|
|
1432
|
-
id_t start_id,
|
|
1433
|
-
|
|
1542
|
+
void search_to_insert( //
|
|
1543
|
+
id_t start_id, vector_view_t query, level_t level, //
|
|
1544
|
+
thread_context_t& context) noexcept(false) {
|
|
1434
1545
|
|
|
1435
1546
|
visits_bitset_t& visits = context.visits;
|
|
1436
1547
|
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
@@ -1442,7 +1553,7 @@ class index_gt {
|
|
|
1442
1553
|
top.clear();
|
|
1443
1554
|
top.reserve(top_limit + 1);
|
|
1444
1555
|
|
|
1445
|
-
distance_t radius = context.
|
|
1556
|
+
distance_t radius = context.measure(query, node(start_id));
|
|
1446
1557
|
candidates.emplace(-radius, start_id);
|
|
1447
1558
|
top.emplace_reserved(radius, start_id);
|
|
1448
1559
|
visits.set(start_id);
|
|
@@ -1454,6 +1565,8 @@ class index_gt {
|
|
|
1454
1565
|
break;
|
|
1455
1566
|
|
|
1456
1567
|
candidates.pop();
|
|
1568
|
+
context.iteration_cycles++;
|
|
1569
|
+
|
|
1457
1570
|
id_t candidate_id = candidacy.second;
|
|
1458
1571
|
node_ref_t candidate_node = node(candidate_id);
|
|
1459
1572
|
lock_t candidate_lock = candidate_node.lock();
|
|
@@ -1465,9 +1578,8 @@ class index_gt {
|
|
|
1465
1578
|
continue;
|
|
1466
1579
|
|
|
1467
1580
|
visits.set(successor_id);
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
|
|
1581
|
+
distance_t successor_dist = context.measure(query, node(successor_id));
|
|
1582
|
+
|
|
1471
1583
|
if (top.size() < top_limit || successor_dist < radius) {
|
|
1472
1584
|
candidates.emplace(-successor_dist, successor_id);
|
|
1473
1585
|
top.emplace_reserved(successor_dist, successor_id);
|
|
@@ -1479,9 +1591,9 @@ class index_gt {
|
|
|
1479
1591
|
}
|
|
1480
1592
|
}
|
|
1481
1593
|
|
|
1482
|
-
void search_to_find_in_base(
|
|
1483
|
-
id_t start_id,
|
|
1484
|
-
|
|
1594
|
+
void search_to_find_in_base( //
|
|
1595
|
+
id_t start_id, vector_view_t query, std::size_t expansion, //
|
|
1596
|
+
thread_context_t& context) const noexcept(false) {
|
|
1485
1597
|
|
|
1486
1598
|
visits_bitset_t& visits = context.visits;
|
|
1487
1599
|
next_candidates_t& candidates = context.next_candidates; // pop min, push
|
|
@@ -1493,7 +1605,7 @@ class index_gt {
|
|
|
1493
1605
|
top.clear();
|
|
1494
1606
|
top.reserve(top_limit + 1);
|
|
1495
1607
|
|
|
1496
|
-
distance_t radius = context.
|
|
1608
|
+
distance_t radius = context.measure(query, node(start_id));
|
|
1497
1609
|
candidates.emplace(-radius, start_id);
|
|
1498
1610
|
top.emplace_reserved(radius, start_id);
|
|
1499
1611
|
visits.set(start_id);
|
|
@@ -1505,6 +1617,7 @@ class index_gt {
|
|
|
1505
1617
|
break;
|
|
1506
1618
|
|
|
1507
1619
|
candidates.pop();
|
|
1620
|
+
context.iteration_cycles++;
|
|
1508
1621
|
|
|
1509
1622
|
id_t candidate_id = candidate.second;
|
|
1510
1623
|
neighbors_ref_t candidate_header = neighbors_base(node(candidate_id));
|
|
@@ -1515,9 +1628,7 @@ class index_gt {
|
|
|
1515
1628
|
continue;
|
|
1516
1629
|
|
|
1517
1630
|
visits.set(successor_id);
|
|
1518
|
-
|
|
1519
|
-
distance_t successor_dist =
|
|
1520
|
-
context.metric(query_vec, successor_node.vector, query_dim, successor_node.head.dim);
|
|
1631
|
+
distance_t successor_dist = context.measure(query, node(successor_id));
|
|
1521
1632
|
|
|
1522
1633
|
if (top.size() < top_limit || successor_dist < radius) {
|
|
1523
1634
|
candidates.emplace(-successor_dist, successor_id);
|
|
@@ -1530,6 +1641,14 @@ class index_gt {
|
|
|
1530
1641
|
}
|
|
1531
1642
|
}
|
|
1532
1643
|
|
|
1644
|
+
void search_exact(vector_view_t query, std::size_t count, thread_context_t& context) const noexcept(false) {
|
|
1645
|
+
top_candidates_t& top = context.top_candidates;
|
|
1646
|
+
top.clear();
|
|
1647
|
+
top.reserve(count);
|
|
1648
|
+
for (std::size_t i = 0; i != nodes_.size(); ++i)
|
|
1649
|
+
top.emplace_reserved(context.measure(query, node(nodes_[i])), static_cast<id_t>(i));
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1533
1652
|
void prefetch_neighbors(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
|
|
1534
1653
|
|
|
1535
1654
|
/**
|
|
@@ -1537,7 +1656,10 @@ class index_gt {
|
|
|
1537
1656
|
* that massively reduces the number of connections a point has,
|
|
1538
1657
|
* to keep only the neighbors, that are from each other.
|
|
1539
1658
|
*/
|
|
1540
|
-
|
|
1659
|
+
|
|
1660
|
+
candidates_view_t refine( //
|
|
1661
|
+
top_candidates_t& top, std::size_t needed, //
|
|
1662
|
+
thread_context_t& context) const noexcept {
|
|
1541
1663
|
|
|
1542
1664
|
top.sort_ascending();
|
|
1543
1665
|
candidate_t* top_data = top.data();
|
|
@@ -1555,9 +1677,7 @@ class index_gt {
|
|
|
1555
1677
|
for (std::size_t idx = 0; idx < submitted_count; idx++) {
|
|
1556
1678
|
candidate_t submitted = top_data[idx];
|
|
1557
1679
|
node_ref_t submitted_node = node(submitted.second);
|
|
1558
|
-
distance_t inter_result_dist = context.
|
|
1559
|
-
submitted_node.vector, candidate_node.vector, //
|
|
1560
|
-
submitted_node.head.dim, candidate_node.head.dim);
|
|
1680
|
+
distance_t inter_result_dist = context.measure(submitted_node, candidate_node);
|
|
1561
1681
|
if (inter_result_dist < candidate_dist) {
|
|
1562
1682
|
good = false;
|
|
1563
1683
|
break;
|