usearch 0.19.3 → 0.20.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/Cargo.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "usearch"
3
- version = "0.19.3"
3
+ version = "0.20.0"
4
4
  authors = ["Ashot Vardanian <1983160+ashvardanian@users.noreply.github.com>"]
5
5
  description = "Smaller & Faster Single-File Vector Search Engine from Unum"
6
6
  edition = "2021"
package/README.md CHANGED
@@ -167,7 +167,7 @@ index.add(/* label: */ 42, /* vector: */ {&vec[0], 3});
167
167
  auto results = index.search(/* query: */ {&vec[0], 3}, 5 /* neighbors */);
168
168
 
169
169
  for (std::size_t i = 0; i != results.size(); ++i)
170
- results[i].element.label, results[i].element.vector, results[i].distance;
170
+ results[i].member.label, results[i].member.vector, results[i].member.id, results[i].distance;
171
171
  ```
172
172
 
173
173
  The `add` is thread-safe for concurrent index construction.
package/c/test.c CHANGED
@@ -1,182 +1,294 @@
1
- #include <assert.h>
1
+ #include <stdio.h>
2
2
  #include <errno.h>
3
3
  #include <stdio.h>
4
4
  #include <stdlib.h>
5
- #include <string.h>
6
5
  #include <sys/stat.h>
7
- #include <time.h>
8
6
 
9
7
  #include "usearch.h"
10
8
 
11
- float* read_fvecs_file(const char* filename, size_t* num_vectors, size_t* vector_dim) {
12
- struct stat st;
13
- FILE* file;
14
- size_t cnt;
15
- float skip;
16
-
17
- ;
18
- if (stat(filename, &st) == -1) {
19
- printf("Failed to read file: %s err: %d: %s\n", filename, errno, strerror(errno));
20
- return NULL;
9
+ #define ASSERT(must_be_true, message) \
10
+ if (!(must_be_true)) { \
11
+ printf("Assert: %s\n", message); \
12
+ exit(-1); \
21
13
  }
22
14
 
23
- file = fopen(filename, "rb");
24
- if (file == NULL) {
25
- printf("Failed to open file: %s\n", filename);
26
- return NULL;
27
- }
28
15
 
29
- // Read the number of vectors and vector dimension from the file
30
- cnt = fread(vector_dim, sizeof(int), 1, file);
31
- assert(cnt == 1);
32
- if (st.st_size % ((*vector_dim + 1) * sizeof(float)) != 0) {
33
- printf("File does not contain a whole number of vectors.\n Vector dim: %ld, file size: %ld\n", *vector_dim,
34
- st.st_size);
35
- fclose(file);
36
- return NULL;
37
- }
16
+ void parse_vectors_from_file(char const* file_path, size_t* count, size_t* dimension, float** data) {
38
17
 
39
- *num_vectors = st.st_size / ((*vector_dim + 1) * sizeof(float));
18
+ // Open the file
19
+ struct stat st = {};
20
+ ASSERT(stat(file_path, &st) == 0, "Failed to get metadata of the file");
21
+ FILE* file = fopen(file_path, "rb");
22
+ ASSERT(file, "Failed to open the file");
40
23
 
41
- // Allocate memory for the array
42
- float* data = (float*)malloc(*num_vectors * *vector_dim * sizeof(float));
43
- if (data == NULL) {
44
- printf("Memory allocation failed.\n");
45
- fclose(file);
46
- return NULL;
47
- }
24
+ // Read vectors dimension and calculate count
25
+ int dim = 0;
26
+ size_t read = fread(&dim, sizeof(int), 1, file);
27
+ ASSERT(read == 1, "Failed to read from the file");
28
+ ASSERT(st.st_size % ((dim + 1) * sizeof(float)) == 0, "File does not contain a whole number of vectors");
29
+ *dimension = (size_t)dim;
30
+ *count = st.st_size / ((dim + 1) * sizeof(float));
48
31
 
49
- // Read the data from the file into the array
50
- for (size_t i = 0; i < *num_vectors; i++) {
51
- cnt = fread(data + i * *vector_dim, sizeof(float), *vector_dim, file);
52
- assert(cnt == *vector_dim);
53
- if (i == *num_vectors - 1) {
32
+ // Allocate memory for the vectors' data
33
+ *data = (float*)malloc(*count * *dimension * sizeof(float));
34
+ ASSERT(*data, "Failed to allocate memory");
35
+
36
+ // Read the data
37
+ for (size_t i = 0; i < *count; ++i) {
38
+ read = fread(*data + i * *dimension, sizeof(float), *dimension, file);
39
+ ASSERT(read == *dimension, "Failed to read from the file");
40
+ if (i == *count - 1)
54
41
  break;
55
- }
56
- cnt = fread(&skip, sizeof(float), 1, file);
57
- assert(cnt == 1);
42
+
43
+ // Skip
44
+ float skip;
45
+ read = fread(&skip, sizeof(float), 1, file);
46
+ ASSERT(read == 1, "Failed to read from the file");
58
47
  }
59
48
 
60
49
  fclose(file);
61
- return data;
62
50
  }
63
51
 
64
- // log every this many iterations in test loops
65
- const int LOG_GRANULARITY = 10000;
66
-
67
- void test_index(float* data, size_t num_vectors, size_t vector_dim) {
52
+ usearch_init_options_t create_options(size_t vector_dimension) {
68
53
  usearch_init_options_t opts;
69
- usearch_index_t idx;
70
- usearch_error_t error = NULL;
71
- clock_t start;
72
54
  opts.connectivity = 2; // 32 in faiss
73
- opts.dimensions = vector_dim;
55
+ opts.dimensions = vector_dimension;
74
56
  opts.expansion_add = 40; // 40 in faiss
75
57
  opts.expansion_search = 16; // 10 in faiss
76
58
  opts.metric_kind = usearch_metric_ip_k;
77
59
  opts.metric = NULL;
78
60
  opts.quantization = usearch_scalar_f32_k;
61
+ return opts;
62
+ }
63
+
64
+ void test_init(size_t vectors_count, size_t vector_dimension) {
65
+ printf("Test: Index Initialization...\n");
66
+
67
+ // Init index
68
+ usearch_index_t idx = NULL;
69
+ usearch_error_t error = NULL;
70
+ usearch_init_options_t opts = create_options(vector_dimension);
71
+ idx = usearch_init(&opts, &error);
72
+ ASSERT(!error, error);
73
+ usearch_free(idx, &error);
74
+ ASSERT(!error, error);
75
+
76
+ // Init second time
77
+ idx = usearch_init(&opts, &error);
78
+ ASSERT(!error, error);
79
+
80
+ ASSERT(usearch_size(idx, &error) == 0, error);
81
+ ASSERT(usearch_capacity(idx, &error) == 0, error);
82
+ ASSERT(usearch_dimensions(idx, &error) == vector_dimension, error);
83
+ ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
84
+
85
+ // Reserve
86
+ usearch_reserve(idx, vectors_count, &error);
87
+ ASSERT(!error, error);
88
+ ASSERT(usearch_size(idx, &error) == 0, error);
89
+ ASSERT(usearch_capacity(idx, &error) == vectors_count, error);
90
+ ASSERT(usearch_dimensions(idx, &error) == vector_dimension, error);
91
+ ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
92
+
93
+ usearch_free(idx, &error);
94
+ ASSERT(!error, error);
95
+
96
+ printf("Test: Index Initialization - PASSED\n");
97
+ }
79
98
 
80
- // results arrays
81
- int k = 10;
82
- num_vectors /= 10;
83
- usearch_label_t* labels = (usearch_label_t*)calloc(k, sizeof(usearch_label_t));
84
- float* distances = (float*)calloc(k, sizeof(float));
99
+ void test_add_vector(size_t vectors_count, size_t vector_dimension, float const* data) {
100
+ printf("Test: Add Vector...\n");
85
101
 
86
- fprintf(stderr, "read %ld vectors of dimension %ld\n", num_vectors, vector_dim);
87
- fprintf(stderr, "initializing usearch...\n");
102
+ usearch_index_t idx = NULL;
103
+ usearch_error_t error = NULL;
104
+ usearch_init_options_t opts = create_options(vector_dimension);
88
105
  idx = usearch_init(&opts, &error);
89
- if (error != NULL) {
90
- fprintf(stderr, "usearch error: %s\n", error);
91
- assert(false);
106
+ usearch_reserve(idx, vectors_count, &error);
107
+
108
+ // Add vectors
109
+ for (size_t i = 0; i < vectors_count; ++i) {
110
+ usearch_label_t label = i;
111
+ usearch_add(idx, label, data + i * vector_dimension, usearch_scalar_f32_k, &error);
112
+ ASSERT(!error, error);
92
113
  }
93
114
 
94
- fprintf(stderr, "reserving capacity...\n");
95
- usearch_reserve(idx, num_vectors, &error);
96
- assert(!error);
115
+ ASSERT(usearch_size(idx, &error) == vectors_count, error);
116
+ ASSERT(usearch_capacity(idx, &error) == vectors_count, error);
97
117
 
98
- fprintf(stderr, "adding vectors...\n");
99
- for (size_t i = 0; i < num_vectors; i++) {
100
- if (i % LOG_GRANULARITY == 0) {
101
- fprintf(stderr, "added %ld vectors...\n", i);
102
- }
118
+ // Check vectors in the index
119
+ for (size_t i = 0; i < vectors_count; ++i) {
103
120
  usearch_label_t label = i;
104
- usearch_add(idx, label, data + i * vector_dim, usearch_scalar_f32_k, &error);
105
- assert(!error);
121
+ ASSERT(usearch_contains(idx, label, &error), error);
106
122
  }
123
+ ASSERT(!usearch_contains(idx, -1, &error), error); // Non existing label
124
+
125
+ usearch_free(idx, &error);
126
+ printf("Test: Add Vector - PASSED\n");
127
+ }
128
+
129
+ void test_find_vector(size_t vectors_count, size_t vector_dimension, float const* data) {
130
+ printf("Test: Find Vector...\n");
107
131
 
108
- fprintf(stderr, "checking containment of vectors...\n");
109
- for (size_t i = 0; i < num_vectors; i++) {
110
- if (i % LOG_GRANULARITY == 0) {
111
- fprintf(stderr, "checked %ld vectors...\n", i);
112
- }
132
+ usearch_index_t idx = NULL;
133
+ usearch_error_t error = NULL;
134
+ usearch_init_options_t opts = create_options(vector_dimension);
135
+ idx = usearch_init(&opts, &error);
136
+ usearch_reserve(idx, vectors_count, &error);
137
+
138
+ // Create result buffers
139
+ int results_count = 10;
140
+ usearch_label_t* labels = (usearch_label_t*)malloc(results_count * sizeof(usearch_label_t));
141
+ float* distances = (float*)malloc(results_count * sizeof(float));
142
+ ASSERT(labels && distances, "Failed to allocate memory");
143
+
144
+ // Add vectors
145
+ for (size_t i = 0; i < vectors_count; ++i) {
113
146
  usearch_label_t label = i;
114
- assert(usearch_contains(idx, label, &error));
115
- assert(!error);
147
+ usearch_add(idx, label, data + i * vector_dimension, usearch_scalar_f32_k, &error);
148
+ ASSERT(!error, error);
116
149
  }
117
150
 
118
- assert(!usearch_contains(idx, -1, &error));
119
- assert(!error);
120
-
121
- fprintf(stderr, "searching vectors...\n");
122
- for (size_t i = 0; i < num_vectors; i++) {
123
- if (i % LOG_GRANULARITY == 0) {
124
- fprintf(stderr, "searched %ld vectors...\n", i);
125
- }
126
- const void *query_vector = data + i * vector_dim;
127
- usearch_search(idx, query_vector, usearch_scalar_f32_k, k, labels, distances, &error);
128
- assert(!error);
151
+ // Find the vectors
152
+ for (size_t i = 0; i < vectors_count; i++) {
153
+ const void *query_vector = data + i * vector_dimension;
154
+ size_t found_count = usearch_search(idx, query_vector, usearch_scalar_f32_k, results_count, labels, distances, &error);
155
+ ASSERT(!error, error);
156
+ ASSERT(found_count = results_count, "Vector is missing");
129
157
  }
130
158
 
131
- fprintf(stderr, "saving the index...\n");
132
- start = clock();
133
- usearch_save(idx, "usearch_index.bin", &error);
134
- assert(!error);
135
- fprintf(stderr, "saving took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
159
+ usearch_free(idx, &error);
160
+ printf("Test: Find Vector - PASSED\n");
161
+ }
162
+
163
+ void test_remove_vector(size_t vectors_count, size_t vector_dimension, float const* data) {
164
+ printf("Test: Remove Vector...\n");
165
+
166
+ usearch_index_t idx = NULL;
167
+ usearch_error_t error = NULL;
168
+ usearch_init_options_t opts = create_options(vector_dimension);
169
+ idx = usearch_init(&opts, &error);
170
+ usearch_reserve(idx, vectors_count, &error);
171
+
172
+ // Add vectors
173
+ for (size_t i = 0; i < vectors_count; ++i) {
174
+ usearch_label_t label = i;
175
+ usearch_add(idx, label, data + i * vector_dimension, usearch_scalar_f32_k, &error);
176
+ ASSERT(!error, error);
177
+ }
178
+
179
+ // Remove the vectors
180
+ for (size_t i = 0; i < vectors_count; i++) {
181
+ usearch_label_t label = i;
182
+ usearch_remove(idx, label, &error);
183
+ ASSERT(error, "Currently, Remove is not supported");
184
+ }
185
+
186
+ usearch_free(idx, &error);
187
+ printf("Test: Remove Vector - PASSED\n");
188
+ }
136
189
 
137
- fprintf(stderr, "destroying the index...\n");
138
- start = clock();
190
+ void test_save_load(size_t vectors_count, size_t vector_dimension, float const* data) {
191
+ printf("Test: Save/Load...\n");
192
+
193
+ usearch_index_t idx = NULL;
194
+ usearch_error_t error = NULL;
195
+ usearch_init_options_t opts = create_options(vector_dimension);
196
+ idx = usearch_init(&opts, &error);
197
+ usearch_reserve(idx, vectors_count, &error);
198
+
199
+ // Add vectors
200
+ for (size_t i = 0; i < vectors_count; ++i) {
201
+ usearch_label_t label = i;
202
+ usearch_add(idx, label, data + i * vector_dimension, usearch_scalar_f32_k, &error);
203
+ ASSERT(!error, error);
204
+ }
205
+
206
+ // Save and free the index
207
+ usearch_save(idx, "usearch_index.bin", &error);
208
+ ASSERT(!error, error);
139
209
  usearch_free(idx, &error);
140
- assert(!error);
141
- fprintf(stderr, "destroying the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
210
+ ASSERT(!error, error);
142
211
 
143
- fprintf(stderr, "loading the index...\n");
144
- start = clock();
212
+ // Reinit
145
213
  idx = usearch_init(&opts, &error);
146
- assert(!error);
147
- assert(usearch_size(idx, &error) == 0);
148
- assert(!error);
214
+ ASSERT(!error, error);
215
+ ASSERT(usearch_size(idx, &error) == 0, error);
216
+
217
+ // Load
149
218
  usearch_load(idx, "usearch_index.bin", &error);
150
- assert(!error);
151
- fprintf(stderr, "loading the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
152
- assert(usearch_size(idx, &error) == num_vectors);
153
- assert(!error);
219
+ ASSERT(!error, error);
220
+ ASSERT(usearch_size(idx, &error) == vectors_count, error);
221
+ ASSERT(usearch_capacity(idx, &error) == vectors_count, error);
222
+ ASSERT(usearch_dimensions(idx, &error) == vector_dimension, error);
223
+ ASSERT(usearch_connectivity(idx, &error) == opts.connectivity, error);
224
+
225
+ // Check vectors in the index
226
+ for (size_t i = 0; i < vectors_count; ++i) {
227
+ usearch_label_t label = i;
228
+ ASSERT(usearch_contains(idx, label, &error), error);
229
+ }
230
+
154
231
  usearch_free(idx, &error);
232
+ printf("Test: Save/Load - PASSED\n");
233
+ }
234
+
235
+ void test_view(size_t vectors_count, size_t vector_dimension, float const* data) {
236
+ printf("Test: View...\n");
155
237
 
156
- fprintf(stderr, "viewing the index...\n");
157
- start = clock();
238
+ usearch_index_t idx = NULL;
239
+ usearch_error_t error = NULL;
240
+ usearch_init_options_t opts = create_options(vector_dimension);
158
241
  idx = usearch_init(&opts, &error);
159
- assert(!error);
160
- assert(usearch_size(idx, &error) == 0);
161
- assert(!error);
162
- usearch_view(idx, "usearch_index.bin", &error);
163
- assert(!error);
164
- fprintf(stderr, "viewing the index took %.2f ms\n", ((double)(clock() - start)) / CLOCKS_PER_SEC * 1000);
165
- assert(usearch_size(idx, &error) == num_vectors);
166
- assert(!error);
242
+ usearch_reserve(idx, vectors_count, &error);
243
+
244
+ // Add vectors
245
+ for (size_t i = 0; i < vectors_count; ++i) {
246
+ usearch_label_t label = i;
247
+ usearch_add(idx, label, data + i * vector_dimension, usearch_scalar_f32_k, &error);
248
+ ASSERT(!error, error);
249
+ }
250
+
251
+ // Save and free the index
252
+ usearch_save(idx, "usearch_index.bin", &error);
253
+ ASSERT(!error, error);
167
254
  usearch_free(idx, &error);
255
+ ASSERT(!error, error);
256
+
257
+ // Reinit
258
+ idx = usearch_init(&opts, &error);
259
+ ASSERT(!error, error);
260
+
261
+ // View
262
+ usearch_view(idx, "usearch_index.bin", &error);
263
+ ASSERT(!error, error);
168
264
 
265
+ usearch_free(idx, &error);
266
+ printf("Test: View - PASSED\n");
169
267
  }
170
268
 
171
- int main() {
172
- const char* filename = "sift/sift_base.fvecs";
173
- size_t num_vectors = 0, vector_dim = 0;
174
- float* data = read_fvecs_file(filename, &num_vectors, &vector_dim);
175
- if (data != NULL) {
269
+ int main(int argc, char* argv[]) {
270
+ // Resolve file path
271
+ char const* filename = "sample.fvecs";
272
+ if (argc == 2)
273
+ filename = argv[1];
176
274
 
177
- test_index(data, num_vectors, vector_dim);
275
+ // Read data from the file
276
+ float* data = NULL;
277
+ size_t vectors_count = 0;
278
+ size_t vector_dimension = 0;
279
+ parse_vectors_from_file(filename, &vectors_count, &vector_dimension, &data);
280
+ vectors_count = vectors_count > 100000 ? 100000 : vectors_count; // Just limit vectors
281
+
282
+ // Test
283
+ test_init(vectors_count, vector_dimension);
284
+ test_add_vector(vectors_count, vector_dimension, data);
285
+ test_find_vector(vectors_count, vector_dimension, data);
286
+ test_remove_vector(vectors_count, vector_dimension, data);
287
+ test_save_load(vectors_count, vector_dimension, data);
288
+ test_view(vectors_count, vector_dimension, data);
289
+
290
+ // Free vectors' buffer
291
+ free(data);
178
292
 
179
- free(data); // Remember to free the dynamically allocated memory
180
- }
181
293
  return 0;
182
294
  }
package/conanfile.py CHANGED
@@ -4,7 +4,7 @@ from conans import ConanFile
4
4
  class USearchConan(ConanFile):
5
5
 
6
6
  name = 'USearch'
7
- version = '0.19.3'
7
+ version = '0.20.0'
8
8
  license = 'Apache 2.0'
9
9
  url = 'https://github.com/unum-cloud/usearch'
10
10
  description = 'Smaller & Faster Single-File Vector Search Engine from Unum'
package/cpp/test.cpp CHANGED
@@ -9,8 +9,6 @@
9
9
  using namespace unum::usearch;
10
10
  using namespace unum;
11
11
 
12
- using point_id_t = std::int64_t;
13
-
14
12
  void expect(bool must_be_true) {
15
13
  if (!must_be_true)
16
14
  throw std::runtime_error("Failed!");
@@ -22,6 +20,7 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
22
20
  using view_t = span_gt<scalar_t const>;
23
21
  using index_t = typename std::remove_reference<index_at>::type;
24
22
  using distance_t = typename index_t::distance_t;
23
+ using label_t = typename index_t::label_t;
25
24
 
26
25
  scalar_t vec[3] = {10, 20, 15};
27
26
  scalar_t vec_a[3] = {15, 16, 17};
@@ -31,7 +30,7 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
31
30
  index.add(42, view_t{&vec[0], 3ul});
32
31
 
33
32
  // Default approximate search
34
- point_id_t matched_labels[10] = {0};
33
+ label_t matched_labels[10] = {0};
35
34
  distance_t matched_distances[10] = {0};
36
35
  std::size_t matched_count = index.search(view_t{&vec[0], 3ul}, 5).dump_to(matched_labels, matched_distances);
37
36
 
@@ -52,7 +51,7 @@ template <typename scalar_at, typename index_at> void test3d(index_at&& index) {
52
51
  // Validate scans
53
52
  std::size_t count = 0;
54
53
  for (auto member : index) {
55
- point_id_t id = member.label;
54
+ label_t id = member.label;
56
55
  expect(id >= 42 && id <= 44);
57
56
  count++;
58
57
  }
@@ -146,11 +145,12 @@ int main(int, char**) {
146
145
  // static_assert(!std::is_same<index_gt<pearson_correlation_gt<>>::value_type, std::true_type>());
147
146
  // static_assert(!std::is_same<index_gt<haversine_gt<>>::value_type, std::true_type>());
148
147
 
149
- test3d<float>(index_gt<cos_gt<float>, point_id_t, std::uint32_t>{});
150
- test3d<float>(index_gt<l2sq_gt<float>, point_id_t, std::uint32_t>{});
148
+ using big_point_id_t = std::int64_t;
149
+ test3d<float>(index_gt<cos_gt<float>, big_point_id_t, std::uint32_t>{});
150
+ test3d<float>(index_gt<l2sq_gt<float>, big_point_id_t, std::uint32_t>{});
151
151
 
152
- test3d<double>(index_gt<cos_gt<double>, point_id_t, std::uint32_t>{});
153
- test3d<double>(index_gt<l2sq_gt<double>, point_id_t, std::uint32_t>{});
152
+ test3d<double>(index_gt<cos_gt<double>, big_point_id_t, std::uint32_t>{});
153
+ test3d<double>(index_gt<l2sq_gt<double>, big_point_id_t, std::uint32_t>{});
154
154
 
155
155
  test3d<float>(punned_small_t::make(3, metric_kind_t::cos_k));
156
156
  test3d<float>(punned_small_t::make(3, metric_kind_t::l2sq_k));
@@ -161,11 +161,11 @@ int main(int, char**) {
161
161
  test3d_punned<float>(punned_small_t::make(3, metric_kind_t::cos_k));
162
162
  test3d_punned<float>(punned_small_t::make(3, metric_kind_t::l2sq_k));
163
163
 
164
- test_sets(index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t>{});
165
- test_sets(index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t>{});
164
+ test_sets(index_gt<jaccard_gt<std::int32_t, float>, big_point_id_t, std::uint32_t>{});
165
+ test_sets(index_gt<jaccard_gt<std::int64_t, float>, big_point_id_t, std::uint32_t>{});
166
166
 
167
- test_sets_moved<index_gt<jaccard_gt<std::int32_t, float>, point_id_t, std::uint32_t>>();
168
- test_sets_moved<index_gt<jaccard_gt<std::int64_t, float>, point_id_t, std::uint32_t>>();
167
+ test_sets_moved<index_gt<jaccard_gt<std::int32_t, float>, big_point_id_t, std::uint32_t>>();
168
+ test_sets_moved<index_gt<jaccard_gt<std::int64_t, float>, big_point_id_t, std::uint32_t>>();
169
169
 
170
170
  return 0;
171
171
  }
@@ -162,6 +162,10 @@ template <std::size_t multiple_ak> std::size_t divide_round_up(std::size_t num)
162
162
  return (num + multiple_ak - 1) / multiple_ak;
163
163
  }
164
164
 
165
+ inline std::size_t divide_round_up(std::size_t num, std::size_t denominator) noexcept {
166
+ return (num + denominator - 1) / denominator;
167
+ }
168
+
165
169
  inline std::size_t ceil2(std::size_t v) noexcept {
166
170
  v--;
167
171
  v |= v >> 1;
@@ -174,12 +178,16 @@ inline std::size_t ceil2(std::size_t v) noexcept {
174
178
  return v;
175
179
  }
176
180
 
177
- template <typename at> void misaligned_store(void* ptr, at v) noexcept { std::memcpy(ptr, &v, sizeof(v)); }
181
+ template <typename at> void misaligned_store(void* ptr, at v) noexcept {
182
+ static_assert(!std::is_reference<at>::value);
183
+ std::memcpy(ptr, &v, sizeof(at));
184
+ }
178
185
 
179
186
  /// @brief Simply dereferencing misaligned pointers can be dangerous.
180
187
  template <typename at> at misaligned_load(void* ptr) noexcept {
188
+ static_assert(!std::is_reference<at>::value);
181
189
  at v;
182
- std::memcpy(&v, ptr, sizeof(v));
190
+ std::memcpy(&v, ptr, sizeof(at));
183
191
  return v;
184
192
  }
185
193
 
@@ -640,7 +648,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
640
648
 
641
649
  void reset() noexcept {
642
650
  if (slots_)
643
- allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t)), slots_ = nullptr, count_ = 0;
651
+ allocator_t{}.deallocate((byte_t*)slots_, count_ * sizeof(slot_t));
652
+ slots_ = nullptr;
653
+ count_ = 0;
644
654
  }
645
655
 
646
656
  bool resize(std::size_t capacity) noexcept {
@@ -661,10 +671,8 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
661
671
  }
662
672
 
663
673
  visits_bitset_gt(visits_bitset_gt&& other) noexcept {
664
- slots_ = other.slots_;
665
- count_ = other.count_;
666
- other.slots_ = nullptr;
667
- other.count_ = 0;
674
+ slots_ = exchange(other.slots_, nullptr);
675
+ count_ = exchange(other.count_, 0);
668
676
  }
669
677
 
670
678
  visits_bitset_gt& operator=(visits_bitset_gt&& other) noexcept {
@@ -673,6 +681,9 @@ template <typename allocator_at = std::allocator<char>> class visits_bitset_gt {
673
681
  return *this;
674
682
  }
675
683
 
684
+ visits_bitset_gt(visits_bitset_gt const&) = delete;
685
+ visits_bitset_gt& operator=(visits_bitset_gt const&) = delete;
686
+
676
687
  inline bool test(std::size_t i) const noexcept { return slots_[i / bits_per_slot()] & (1ul << (i & bits_mask())); }
677
688
  inline void set(std::size_t i) noexcept { slots_[i / bits_per_slot()] |= (1ul << (i & bits_mask())); }
678
689
 
@@ -911,17 +922,18 @@ class sorted_buffer_gt {
911
922
  if (!new_elements)
912
923
  return false;
913
924
 
914
- if (size_) {
925
+ if (size_)
915
926
  std::memcpy(new_elements, elements_, size_ * sizeof(element_t));
927
+ if (elements_)
916
928
  allocator.deallocate(elements_, capacity_);
917
- }
929
+
918
930
  elements_ = new_elements;
919
931
  capacity_ = new_capacity;
920
932
  return true;
921
933
  }
922
934
 
923
935
  inline void insert_reserved(element_t&& element) noexcept {
924
- std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
936
+ std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
925
937
  std::size_t to_move = size_ - slot;
926
938
  element_t* source = elements_ + size_ - 1;
927
939
  for (; to_move; --to_move, --source)
@@ -934,7 +946,7 @@ class sorted_buffer_gt {
934
946
  * @return `true` if the entry was added, `false` if it wasn't relevant enough.
935
947
  */
936
948
  inline bool insert(element_t&& element, std::size_t limit) noexcept {
937
- std::size_t slot = std::lower_bound(elements_, elements_ + size_, element, &less) - elements_;
949
+ std::size_t slot = size_ ? std::lower_bound(elements_, elements_ + size_, element, &less) - elements_ : 0;
938
950
  if (slot == limit)
939
951
  return false;
940
952
  std::size_t to_move = size_ - slot - (size_ == limit);
@@ -975,10 +987,10 @@ class usearch_pack_m uint40_t {
975
987
 
976
988
  public:
977
989
  inline uint40_t() noexcept { std::memset(octets, 0, 5); }
978
- inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets + 1, (char*)&n, 4), octets[0] = 0; }
979
- inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n + 3, 5); }
990
+ inline uint40_t(std::uint32_t n) noexcept { std::memcpy(octets, (char*)&n, 4); }
991
+ inline uint40_t(std::uint64_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
980
992
  #if defined(USEARCH_DEFINED_CLANG) && defined(USEARCH_DEFINED_APPLE)
981
- inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n + 3, 5); }
993
+ inline uint40_t(std::size_t n) noexcept { std::memcpy(octets, (char*)&n, 5); }
982
994
  #endif
983
995
 
984
996
  uint40_t(uint40_t&&) = default;
@@ -986,24 +998,40 @@ class usearch_pack_m uint40_t {
986
998
  uint40_t& operator=(uint40_t&&) = default;
987
999
  uint40_t& operator=(uint40_t const&) = default;
988
1000
 
989
- inline uint40_t& operator+=(std::uint32_t i) noexcept {
990
- std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets + 1);
991
- octets[0] += static_cast<unsigned char>((tail + i) < tail);
992
- tail += i;
1001
+ inline uint40_t& operator+=(std::uint32_t n) noexcept {
1002
+ std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1003
+ octets[4] += static_cast<unsigned char>((tail + n) < tail);
1004
+ tail += n;
1005
+ return *this;
1006
+ }
1007
+
1008
+ inline uint40_t& operator+=(std::size_t n) noexcept {
1009
+ unsigned char* n_octets = reinterpret_cast<unsigned char*>(&n);
1010
+ std::uint32_t& n_tail = *reinterpret_cast<std::uint32_t*>(&n);
1011
+ std::uint32_t& tail = *reinterpret_cast<std::uint32_t*>(octets);
1012
+ octets[4] += static_cast<unsigned char>((tail + n_tail) < tail);
1013
+ tail += n_tail;
1014
+ octets[4] += n_octets[4];
993
1015
  return *this;
994
1016
  }
995
1017
 
1018
+ inline uint40_t operator+(std::size_t n) noexcept {
1019
+ uint40_t other(*this);
1020
+ other += n;
1021
+ return other;
1022
+ }
1023
+
996
1024
  inline operator std::size_t() const noexcept {
997
1025
  std::size_t result = 0;
998
- std::memcpy((char*)&result + 3, octets, 5);
1026
+ std::memcpy((char*)&result, octets, 5);
999
1027
  return result;
1000
1028
  }
1001
1029
 
1002
- inline uint40_t& operator++() noexcept { return *this += 1; }
1030
+ inline uint40_t& operator++() noexcept { return *this += 1u; }
1003
1031
 
1004
1032
  inline uint40_t operator++(int) noexcept {
1005
1033
  uint40_t old = *this;
1006
- *this += 1;
1034
+ *this += 1u;
1007
1035
  return old;
1008
1036
  }
1009
1037
  };
@@ -1317,11 +1345,13 @@ class index_gt {
1317
1345
  struct member_ref_t {
1318
1346
  misaligned_ref_gt<label_t> label;
1319
1347
  vector_view_t vector;
1348
+ id_t id;
1320
1349
  };
1321
1350
 
1322
1351
  struct member_cref_t {
1323
1352
  label_t label;
1324
1353
  vector_view_t vector;
1354
+ id_t id;
1325
1355
  };
1326
1356
 
1327
1357
  template <typename ref_at, typename index_at> class member_iterator_gt {
@@ -1341,7 +1371,11 @@ class index_gt {
1341
1371
  using pointer = void;
1342
1372
  using reference = ref_t;
1343
1373
 
1344
- reference operator*() const noexcept { return ref_t(index_->node_with_id_(offset_)); }
1374
+ reference operator*() const noexcept {
1375
+ node_t node = index_->node_with_id_(offset_);
1376
+ return {node.label(), node.vector_view(), static_cast<id_t>(offset_)};
1377
+ }
1378
+
1345
1379
  member_iterator_gt operator++(int) noexcept { return member_iterator_gt(index_, offset_ + 1); }
1346
1380
  member_iterator_gt operator--(int) noexcept { return member_iterator_gt(index_, offset_ - 1); }
1347
1381
  member_iterator_gt operator+(difference_type d) noexcept { return member_iterator_gt(index_, offset_ + d); }
@@ -1453,9 +1487,9 @@ class index_gt {
1453
1487
  node_t(node_t const&) = default;
1454
1488
  node_t& operator=(node_t const&) = default;
1455
1489
 
1456
- label_t label() const noexcept { return misaligned_load<label_t>(tape_); }
1457
- dim_t dim() const noexcept { return misaligned_load<dim_t>(tape_ + sizeof(label_t)); }
1458
- level_t level() const noexcept { return misaligned_load<level_t>(tape_ + sizeof(label_t) + sizeof(dim_t)); }
1490
+ misaligned_ref_gt<label_t> label() const noexcept { return {tape_}; }
1491
+ misaligned_ref_gt<dim_t> dim() const noexcept { return {tape_ + sizeof(label_t)}; }
1492
+ misaligned_ref_gt<level_t> level() const noexcept { return {tape_ + sizeof(label_t) + sizeof(dim_t)}; }
1459
1493
 
1460
1494
  void label(label_t v) noexcept { return misaligned_store<label_t>(tape_, v); }
1461
1495
  void dim(dim_t v) noexcept { return misaligned_store<dim_t>(tape_ + sizeof(label_t), v); }
@@ -1605,8 +1639,11 @@ class index_gt {
1605
1639
 
1606
1640
  if (nodes_)
1607
1641
  node_allocator_t{}.deallocate(exchange(nodes_, nullptr), limits_.elements);
1608
- if (contexts_)
1642
+ if (contexts_) {
1643
+ for (std::size_t i = 0; i != limits_.threads(); ++i)
1644
+ contexts_[i].~context_t();
1609
1645
  context_allocator_t{}.deallocate(exchange(contexts_, nullptr), limits_.threads());
1646
+ }
1610
1647
  limits_ = index_limits_t{0, 0};
1611
1648
  capacity_ = 0;
1612
1649
  reset_view_();
@@ -1730,7 +1767,7 @@ class index_gt {
1730
1767
  };
1731
1768
 
1732
1769
  struct match_t {
1733
- member_cref_t element;
1770
+ member_cref_t member;
1734
1771
  distance_t distance;
1735
1772
  };
1736
1773
 
@@ -1763,14 +1800,23 @@ class index_gt {
1763
1800
 
1764
1801
  inline operator std::size_t() const noexcept { return count; }
1765
1802
  inline std::size_t size() const noexcept { return count; }
1766
- inline match_t operator[](std::size_t i) const noexcept {
1803
+ inline match_t operator[](std::size_t i) const noexcept { return at(i); }
1804
+ inline bool contains(label_t label) const noexcept {
1805
+ for (std::size_t i = 0; i != count; ++i)
1806
+ if (at(i).member.label == label)
1807
+ return true;
1808
+ return false;
1809
+ }
1810
+ inline match_t at(std::size_t i) const noexcept {
1767
1811
  candidate_t const* top_ordered = top_.data();
1768
- return {member_cref_t(index_.node_with_id_(top_ordered[i].id)), top_ordered[i].distance};
1812
+ candidate_t candidate = top_ordered[i];
1813
+ node_t node = index_.node_with_id_(candidate.id);
1814
+ return {member_cref_t{node.label(), node.vector_view(), candidate.id}, candidate.distance};
1769
1815
  }
1770
1816
  inline std::size_t dump_to(label_t* labels, distance_t* distances) const noexcept {
1771
1817
  for (std::size_t i = 0; i != count; ++i) {
1772
1818
  match_t result = operator[](i);
1773
- labels[i] = result.element.label;
1819
+ labels[i] = result.member.label;
1774
1820
  distances[i] = result.distance;
1775
1821
  }
1776
1822
  return count;
@@ -1778,7 +1824,7 @@ class index_gt {
1778
1824
  inline std::size_t dump_to(label_t* labels) const noexcept {
1779
1825
  for (std::size_t i = 0; i != count; ++i) {
1780
1826
  match_t result = operator[](i);
1781
- labels[i] = result.element.label;
1827
+ labels[i] = result.member.label;
1782
1828
  }
1783
1829
  return count;
1784
1830
  }
@@ -1849,6 +1895,7 @@ class index_gt {
1849
1895
  // TODO: Handle out of memory conditions
1850
1896
  search_to_insert_(closest_id, vector, level, config.expansion, context);
1851
1897
  closest_id = connect_new_node_(new_id, level, context);
1898
+ reconnect_neighbor_nodes_(new_id, level, context);
1852
1899
  }
1853
1900
 
1854
1901
  // Normalize stats
@@ -1863,15 +1910,23 @@ class index_gt {
1863
1910
  return result;
1864
1911
  }
1865
1912
 
1913
+ struct dummy_predicate_t {
1914
+ constexpr bool operator()(match_t const&) const noexcept { return true; }
1915
+ };
1916
+
1866
1917
  /**
1867
1918
  * @brief Searches for the closest elements to the given ::query. Thread-safe.
1868
1919
  *
1869
1920
  * @param[in] query Contiguous range of scalars forming a vector view.
1870
1921
  * @param[in] wanted The upper bound for the number of results to return.
1871
1922
  * @param[in] config Configuration options for this specific operation.
1923
+ * @param[in] predicate Optional filtering predicate for `member_cref_t`.
1872
1924
  * @return Smart object referencing temporary memory. Valid until next `search()` or `add()`.
1873
1925
  */
1874
- search_result_t search(vector_view_t query, std::size_t wanted, search_config_t config = {}) const noexcept {
1926
+ template <typename predicate_at = dummy_predicate_t>
1927
+ search_result_t search( //
1928
+ vector_view_t query, std::size_t wanted, search_config_t config = {},
1929
+ predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
1875
1930
 
1876
1931
  context_t& context = contexts_[config.thread];
1877
1932
  top_candidates_t& top = context.top_candidates;
@@ -1886,7 +1941,7 @@ class index_gt {
1886
1941
  if (config.exact) {
1887
1942
  if (!top.reserve(wanted))
1888
1943
  return result.failed("Out of memory!");
1889
- search_exact_(query, wanted, context);
1944
+ search_exact_(query, wanted, context, std::forward<predicate_at>(predicate));
1890
1945
  } else {
1891
1946
  next_candidates_t& next = context.next_candidates;
1892
1947
  std::size_t expansion = (std::max)(config.expansion, wanted);
@@ -1897,7 +1952,7 @@ class index_gt {
1897
1952
 
1898
1953
  id_t closest_id = search_for_one_(entry_id_, query, max_level_, 0, context);
1899
1954
  // For bottom layer we need a more optimized procedure
1900
- if (!search_to_find_in_base_(closest_id, query, expansion, context))
1955
+ if (!search_to_find_in_base_(closest_id, query, expansion, context, std::forward<predicate_at>(predicate)))
1901
1956
  return result.failed("Out of memory!");
1902
1957
  }
1903
1958
 
@@ -1911,9 +1966,10 @@ class index_gt {
1911
1966
  return result;
1912
1967
  }
1913
1968
 
1969
+ template <typename predicate_at = dummy_predicate_t>
1914
1970
  search_result_t search_around( //
1915
1971
  id_t hint, vector_view_t query, std::size_t wanted, //
1916
- search_config_t config = {}) const noexcept {
1972
+ search_config_t config = {}, predicate_at&& predicate = dummy_predicate_t{}) const noexcept {
1917
1973
 
1918
1974
  context_t& context = contexts_[config.thread];
1919
1975
  top_candidates_t& top = context.top_candidates;
@@ -1933,7 +1989,7 @@ class index_gt {
1933
1989
  result.measurements = context.measurements_count;
1934
1990
  result.cycles = context.iteration_cycles;
1935
1991
 
1936
- search_to_find_in_base_(hint, query, expansion, context);
1992
+ search_to_find_in_base_(hint, query, expansion, context, std::forward<predicate_at>(predicate));
1937
1993
  top.sort_ascending();
1938
1994
  top.shrink(wanted);
1939
1995
 
@@ -1944,6 +2000,10 @@ class index_gt {
1944
2000
  return result;
1945
2001
  }
1946
2002
 
2003
+ #pragma endregion
2004
+
2005
+ #pragma region Metadata
2006
+
1947
2007
  struct stats_t {
1948
2008
  std::size_t nodes;
1949
2009
  std::size_t edges;
@@ -1958,7 +2018,7 @@ class index_gt {
1958
2018
  node_t node = node_with_id_(i);
1959
2019
  std::size_t max_edges = node.level() * config_.connectivity + base_level_multiple_() * config_.connectivity;
1960
2020
  std::size_t edges = 0;
1961
- for (level_t level = 0; level != node.level(); ++level)
2021
+ for (level_t level = 0; level <= node.level(); ++level)
1962
2022
  edges += neighbors_(node, level).size();
1963
2023
 
1964
2024
  result.allocated_bytes += node_bytes_(node);
@@ -2007,6 +2067,8 @@ class index_gt {
2007
2067
  return total;
2008
2068
  }
2009
2069
 
2070
+ std::size_t memory_usage_per_node(dim_t dim, level_t level) const noexcept { return node_bytes_(dim, level); }
2071
+
2010
2072
  void change_metric(metric_t const& m) noexcept {
2011
2073
  metric_ = m;
2012
2074
  for (std::size_t i = 0; i != limits_.threads(); ++i)
@@ -2285,7 +2347,6 @@ class index_gt {
2285
2347
  std::size_t node_vector_bytes = dim * sizeof(scalar_t);
2286
2348
  nodes_[i] = node_t{tape, (scalar_t*)(tape + node_bytes - node_vector_bytes)};
2287
2349
  progress += node_bytes;
2288
- max_level_ = (std::max)(max_level_, level);
2289
2350
  }
2290
2351
 
2291
2352
  return {};
@@ -2396,7 +2457,6 @@ class index_gt {
2396
2457
 
2397
2458
  node_t new_node = node_with_id_(new_id);
2398
2459
  top_candidates_t& top = context.top_candidates;
2399
- std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
2400
2460
 
2401
2461
  // Outgoing links from `new_id`:
2402
2462
  neighbors_ref_t new_neighbors = neighbors_(new_node, level);
@@ -2411,7 +2471,17 @@ class index_gt {
2411
2471
  }
2412
2472
  }
2413
2473
 
2474
+ return new_neighbors[0];
2475
+ }
2476
+
2477
+ void reconnect_neighbor_nodes_(id_t new_id, level_t level, context_t& context) usearch_noexcept_m {
2478
+
2479
+ node_t new_node = node_with_id_(new_id);
2480
+ top_candidates_t& top = context.top_candidates;
2481
+ neighbors_ref_t new_neighbors = neighbors_(new_node, level);
2482
+
2414
2483
  // Reverse links from the neighbors:
2484
+ std::size_t const connectivity_max = level ? config_.connectivity : pre_.connectivity_max_base;
2415
2485
  for (id_t close_id : new_neighbors) {
2416
2486
  node_t close_node = node_with_id_(close_id);
2417
2487
  node_lock_t close_lock = node_lock_(close_id);
@@ -2441,8 +2511,6 @@ class index_gt {
2441
2511
  for (std::size_t idx = 0; idx != top_view.size(); idx++)
2442
2512
  close_header.push_back(top_view[idx].id);
2443
2513
  }
2444
-
2445
- return new_neighbors[0];
2446
2514
  }
2447
2515
 
2448
2516
  level_t choose_random_level_(std::default_random_engine& level_generator) const noexcept {
@@ -2538,8 +2606,10 @@ class index_gt {
2538
2606
  * Doesn't lock any nodes, assuming read-only simultaneous access.
2539
2607
  * @return `true` if procedure succeeded, `false` if run out of memory.
2540
2608
  */
2609
+ template <typename predicate_at>
2541
2610
  bool search_to_find_in_base_( //
2542
- id_t start_id, vector_view_t query, std::size_t expansion, context_t& context) const noexcept {
2611
+ id_t start_id, vector_view_t query, std::size_t expansion, context_t& context,
2612
+ predicate_at&& predicate) const noexcept {
2543
2613
 
2544
2614
  visits_bitset_t& visits = context.visits;
2545
2615
  next_candidates_t& next = context.next_candidates; // pop min, push
@@ -2573,14 +2643,19 @@ class index_gt {
2573
2643
  continue;
2574
2644
 
2575
2645
  visits.set(successor_id);
2576
- distance_t successor_dist = context.measure(query, node_with_id_(successor_id));
2646
+ node_t successor = node_with_id_(successor_id);
2647
+ distance_t successor_dist = context.measure(query, successor);
2577
2648
 
2578
2649
  if (top.size() < top_limit || successor_dist < radius) {
2579
2650
  // This can substantially grow our priority queue:
2580
2651
  next.insert({-successor_dist, successor_id});
2581
- // This will automatically evict poor matches:
2582
- top.insert({successor_dist, successor_id}, top_limit);
2583
- radius = top.top().distance;
2652
+ if (predicate( //
2653
+ match_t{member_cref_t{successor.label(), successor.vector_view(), successor_id},
2654
+ successor_dist})) {
2655
+ // This will automatically evict poor matches:
2656
+ top.insert({successor_dist, successor_id}, top_limit);
2657
+ radius = top.top().distance;
2658
+ }
2584
2659
  }
2585
2660
  }
2586
2661
  }
@@ -2591,12 +2666,21 @@ class index_gt {
2591
2666
  /**
2592
2667
  * @brief Iterates through all managed vectors, without actually touching the index.
2593
2668
  */
2594
- void search_exact_(vector_view_t query, std::size_t count, context_t& context) const noexcept {
2669
+ template <typename predicate_at>
2670
+ void search_exact_( //
2671
+ vector_view_t query, std::size_t count, context_t& context, //
2672
+ predicate_at&& predicate) const noexcept {
2673
+
2595
2674
  top_candidates_t& top = context.top_candidates;
2596
2675
  top.clear();
2597
2676
  top.reserve(count);
2598
- for (std::size_t i = 0; i != size(); ++i)
2599
- top.insert({context.measure(query, node_with_id_(i)), static_cast<id_t>(i)}, count);
2677
+ for (std::size_t i = 0; i != size(); ++i) {
2678
+ id_t id = static_cast<id_t>(i);
2679
+ node_t node = node_with_id_(i);
2680
+ distance_t distance = context.measure(query, node);
2681
+ if (predicate(match_t{member_cref_t{node.label(), node.vector_view(), id}, distance}))
2682
+ top.insert(candidate_t{distance, id}, count);
2683
+ }
2600
2684
  }
2601
2685
 
2602
2686
  void prefetch_neighbors_(neighbors_ref_t, visits_bitset_t const&) const noexcept {}
@@ -295,6 +295,8 @@ class f8_bits_t {
295
295
  inline explicit operator std::int32_t() const noexcept { return int8_; }
296
296
  inline explicit operator std::int64_t() const noexcept { return int8_; }
297
297
 
298
+ inline f8_bits_t(f16_t v)
299
+ : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
298
300
  inline f8_bits_t(float v)
299
301
  : int8_(usearch::clamp<std::int8_t>(static_cast<std::int8_t>(v * divisor_k), min_k, max_k)) {}
300
302
  inline f8_bits_t(double v)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usearch",
3
- "version": "0.19.3",
3
+ "version": "0.20.0",
4
4
  "description": "Smaller & Faster Single-File Vector Search Engine from Unum",
5
5
  "author": "Ashot Vardanian",
6
6
  "license": "Apache 2.0",