usearch 2.10.5 → 2.11.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/c/lib.cpp CHANGED
@@ -6,6 +6,13 @@ extern "C" {
6
6
  #include "usearch.h"
7
7
  }
8
8
 
9
+ // Check if NDEBUG is defined to determine if it's a release build
10
+ #ifdef NDEBUG
11
+ #define USEARCH_ASSERT(expression) (void)(expression)
12
+ #else
13
+ #define USEARCH_ASSERT(expression) assert(expression)
14
+ #endif
15
+
9
16
  using namespace unum::usearch;
10
17
  using namespace unum;
11
18
 
@@ -91,22 +98,38 @@ std::size_t get_(index_dense_t* index, usearch_key_t key, size_t count, void* ve
91
98
  }
92
99
  }
93
100
 
94
- search_result_t search_(index_dense_t* index, void const* vector, scalar_kind_t kind, size_t n) {
101
+ template <typename predicate_at = dummy_predicate_t>
102
+ search_result_t search_(index_dense_t* index, void const* vector, scalar_kind_t kind, size_t n,
103
+ predicate_at&& predicate = predicate_at{}) {
95
104
  switch (kind) {
96
- case scalar_kind_t::f32_k: return index->search((f32_t const*)vector, n);
97
- case scalar_kind_t::f64_k: return index->search((f64_t const*)vector, n);
98
- case scalar_kind_t::f16_k: return index->search((f16_t const*)vector, n);
99
- case scalar_kind_t::i8_k: return index->search((i8_t const*)vector, n);
100
- case scalar_kind_t::b1x8_k: return index->search((b1x8_t const*)vector, n);
105
+ case scalar_kind_t::f32_k:
106
+ return index->filtered_search((f32_t const*)vector, n, std::forward<predicate_at>(predicate));
107
+ case scalar_kind_t::f64_k:
108
+ return index->filtered_search((f64_t const*)vector, n, std::forward<predicate_at>(predicate));
109
+ case scalar_kind_t::f16_k:
110
+ return index->filtered_search((f16_t const*)vector, n, std::forward<predicate_at>(predicate));
111
+ case scalar_kind_t::i8_k:
112
+ return index->filtered_search((i8_t const*)vector, n, std::forward<predicate_at>(predicate));
113
+ case scalar_kind_t::b1x8_k:
114
+ return index->filtered_search((b1x8_t const*)vector, n, std::forward<predicate_at>(predicate));
101
115
  default: return search_result_t().failed("Unknown scalar kind!");
102
116
  }
103
117
  }
104
118
 
105
119
  extern "C" {
106
120
 
121
+ USEARCH_EXPORT char const* usearch_version(void) {
122
+ int major = USEARCH_VERSION_MAJOR;
123
+ int minor = USEARCH_VERSION_MINOR;
124
+ int patch = USEARCH_VERSION_PATCH;
125
+ static char version[32];
126
+ sprintf(version, "%d.%d.%d", major, minor, patch);
127
+ return version;
128
+ }
129
+
107
130
  USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, usearch_error_t* error) {
108
131
 
109
- assert(options && error && "Missing arguments");
132
+ USEARCH_ASSERT(options && error && "Missing arguments");
110
133
 
111
134
  index_dense_config_t config(options->connectivity, options->expansion_add, options->expansion_search);
112
135
  config.multi = options->multi;
@@ -114,11 +137,11 @@ USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, use
114
137
  scalar_kind_t scalar_kind = scalar_kind_to_cpp(options->quantization);
115
138
 
116
139
  metric_punned_t metric = //
117
- !options->metric ? metric_punned_t(options->dimensions, metric_kind, scalar_kind)
118
- : metric_punned_t(options->dimensions, //
119
- reinterpret_cast<std::uintptr_t>(options->metric), //
120
- metric_punned_signature_t::array_array_k, //
121
- metric_kind, scalar_kind);
140
+ !options->metric ? metric_punned_t::builtin(options->dimensions, metric_kind, scalar_kind)
141
+ : metric_punned_t::stateless(options->dimensions, //
142
+ reinterpret_cast<std::uintptr_t>(options->metric), //
143
+ metric_punned_signature_t::array_array_k, //
144
+ metric_kind, scalar_kind);
122
145
  if (metric.missing()) {
123
146
  *error = "Unknown metric kind!";
124
147
  return NULL;
@@ -136,13 +159,13 @@ USEARCH_EXPORT void usearch_free(usearch_index_t index, usearch_error_t*) {
136
159
  }
137
160
 
138
161
  USEARCH_EXPORT size_t usearch_serialized_length(usearch_index_t index, usearch_error_t*) {
139
- assert(index && "Missing arguments");
162
+ USEARCH_ASSERT(index && "Missing arguments");
140
163
  return reinterpret_cast<index_dense_t*>(index)->serialized_length();
141
164
  }
142
165
 
143
166
  USEARCH_EXPORT void usearch_save(usearch_index_t index, char const* path, usearch_error_t* error) {
144
167
 
145
- assert(index && path && error && "Missing arguments");
168
+ USEARCH_ASSERT(index && path && error && "Missing arguments");
146
169
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->save(path);
147
170
  if (!result)
148
171
  *error = result.error.release();
@@ -150,7 +173,7 @@ USEARCH_EXPORT void usearch_save(usearch_index_t index, char const* path, usearc
150
173
 
151
174
  USEARCH_EXPORT void usearch_load(usearch_index_t index, char const* path, usearch_error_t* error) {
152
175
 
153
- assert(index && path && error && "Missing arguments");
176
+ USEARCH_ASSERT(index && path && error && "Missing arguments");
154
177
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->load(path);
155
178
  if (!result)
156
179
  *error = result.error.release();
@@ -158,7 +181,7 @@ USEARCH_EXPORT void usearch_load(usearch_index_t index, char const* path, usearc
158
181
 
159
182
  USEARCH_EXPORT void usearch_view(usearch_index_t index, char const* path, usearch_error_t* error) {
160
183
 
161
- assert(index && path && error && "Missing arguments");
184
+ USEARCH_ASSERT(index && path && error && "Missing arguments");
162
185
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->view(path);
163
186
  if (!result)
164
187
  *error = result.error.release();
@@ -166,7 +189,7 @@ USEARCH_EXPORT void usearch_view(usearch_index_t index, char const* path, usearc
166
189
 
167
190
  USEARCH_EXPORT void usearch_metadata(char const* path, usearch_init_options_t* options, usearch_error_t* error) {
168
191
 
169
- assert(path && options && error && "Missing arguments");
192
+ USEARCH_ASSERT(path && options && error && "Missing arguments");
170
193
  index_dense_metadata_result_t result = index_dense_metadata_from_path(path);
171
194
  if (!result)
172
195
  *error = result.error.release();
@@ -184,7 +207,7 @@ USEARCH_EXPORT void usearch_metadata(char const* path, usearch_init_options_t* o
184
207
 
185
208
  USEARCH_EXPORT void usearch_save_buffer(usearch_index_t index, void* buffer, size_t length, usearch_error_t* error) {
186
209
 
187
- assert(index && buffer && length && error && "Missing arguments");
210
+ USEARCH_ASSERT(index && buffer && length && error && "Missing arguments");
188
211
  memory_mapped_file_t memory_map((byte_t*)buffer, length);
189
212
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->save(std::move(memory_map));
190
213
  if (!result)
@@ -194,7 +217,7 @@ USEARCH_EXPORT void usearch_save_buffer(usearch_index_t index, void* buffer, siz
194
217
  USEARCH_EXPORT void usearch_load_buffer(usearch_index_t index, void const* buffer, size_t length,
195
218
  usearch_error_t* error) {
196
219
 
197
- assert(index && buffer && length && error && "Missing arguments");
220
+ USEARCH_ASSERT(index && buffer && length && error && "Missing arguments");
198
221
  memory_mapped_file_t memory_map((byte_t*)buffer, length);
199
222
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->load(std::move(memory_map));
200
223
  if (!result)
@@ -204,7 +227,7 @@ USEARCH_EXPORT void usearch_load_buffer(usearch_index_t index, void const* buffe
204
227
  USEARCH_EXPORT void usearch_view_buffer(usearch_index_t index, void const* buffer, size_t length,
205
228
  usearch_error_t* error) {
206
229
 
207
- assert(index && buffer && length && error && "Missing arguments");
230
+ USEARCH_ASSERT(index && buffer && length && error && "Missing arguments");
208
231
  memory_mapped_file_t memory_map((byte_t*)buffer, length);
209
232
  serialization_result_t result = reinterpret_cast<index_dense_t*>(index)->view(std::move(memory_map));
210
233
  if (!result)
@@ -214,7 +237,7 @@ USEARCH_EXPORT void usearch_view_buffer(usearch_index_t index, void const* buffe
214
237
  USEARCH_EXPORT void usearch_metadata_buffer(void const* buffer, size_t length, usearch_init_options_t* options,
215
238
  usearch_error_t* error) {
216
239
 
217
- assert(buffer && length && options && error && "Missing arguments");
240
+ USEARCH_ASSERT(buffer && length && options && error && "Missing arguments");
218
241
  index_dense_metadata_result_t result =
219
242
  index_dense_metadata_from_buffer(memory_mapped_file_t((byte_t*)(buffer), length));
220
243
  if (!result)
@@ -231,24 +254,80 @@ USEARCH_EXPORT void usearch_metadata_buffer(void const* buffer, size_t length, u
231
254
  options->metric = NULL;
232
255
  }
233
256
 
234
- USEARCH_EXPORT size_t usearch_size(usearch_index_t index, usearch_error_t*) { //
257
+ USEARCH_EXPORT size_t usearch_size(usearch_index_t index, usearch_error_t* error) {
258
+ USEARCH_ASSERT(index && error && "Missing arguments");
235
259
  return reinterpret_cast<index_dense_t*>(index)->size();
236
260
  }
237
261
 
238
- USEARCH_EXPORT size_t usearch_capacity(usearch_index_t index, usearch_error_t*) {
262
+ USEARCH_EXPORT size_t usearch_capacity(usearch_index_t index, usearch_error_t* error) {
263
+ USEARCH_ASSERT(index && error && "Missing arguments");
239
264
  return reinterpret_cast<index_dense_t*>(index)->capacity();
240
265
  }
241
266
 
242
- USEARCH_EXPORT size_t usearch_dimensions(usearch_index_t index, usearch_error_t*) {
267
+ USEARCH_EXPORT size_t usearch_dimensions(usearch_index_t index, usearch_error_t* error) {
268
+ USEARCH_ASSERT(index && error && "Missing arguments");
243
269
  return reinterpret_cast<index_dense_t*>(index)->dimensions();
244
270
  }
245
271
 
246
- USEARCH_EXPORT size_t usearch_connectivity(usearch_index_t index, usearch_error_t*) {
272
+ USEARCH_EXPORT size_t usearch_connectivity(usearch_index_t index, usearch_error_t* error) {
273
+ USEARCH_ASSERT(index && error && "Missing arguments");
247
274
  return reinterpret_cast<index_dense_t*>(index)->connectivity();
248
275
  }
249
276
 
277
+ USEARCH_EXPORT size_t usearch_expansion_add(usearch_index_t index, usearch_error_t* error) {
278
+ USEARCH_ASSERT(index && error && "Missing arguments");
279
+ return reinterpret_cast<index_dense_t*>(index)->expansion_add();
280
+ }
281
+
282
+ USEARCH_EXPORT size_t usearch_expansion_search(usearch_index_t index, usearch_error_t* error) {
283
+ USEARCH_ASSERT(index && error && "Missing arguments");
284
+ return reinterpret_cast<index_dense_t*>(index)->expansion_search();
285
+ }
286
+
287
+ USEARCH_EXPORT size_t usearch_memory_usage(usearch_index_t index, usearch_error_t* error) {
288
+ USEARCH_ASSERT(index && error && "Missing arguments");
289
+ return reinterpret_cast<index_dense_t*>(index)->memory_usage();
290
+ }
291
+
292
+ USEARCH_EXPORT char const* usearch_hardware_acceleration(usearch_index_t index, usearch_error_t* error) {
293
+ USEARCH_ASSERT(index && error && "Missing arguments");
294
+ return reinterpret_cast<index_dense_t*>(index)->metric().isa_name();
295
+ }
296
+
297
+ USEARCH_EXPORT void usearch_change_expansion_add(usearch_index_t index, size_t expansion, usearch_error_t* error) {
298
+ USEARCH_ASSERT(index && error && "Missing arguments");
299
+ reinterpret_cast<index_dense_t*>(index)->change_expansion_add(expansion);
300
+ }
301
+
302
+ USEARCH_EXPORT void usearch_change_expansion_search(usearch_index_t index, size_t expansion, usearch_error_t* error) {
303
+ USEARCH_ASSERT(index && error && "Missing arguments");
304
+ reinterpret_cast<index_dense_t*>(index)->change_expansion_search(expansion);
305
+ }
306
+
307
+ USEARCH_EXPORT void usearch_change_metric_kind(usearch_index_t index, usearch_metric_kind_t kind,
308
+ usearch_error_t* error) {
309
+ USEARCH_ASSERT(index && error && "Missing arguments");
310
+ auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
311
+ index_dense.change_metric(
312
+ metric_punned_t::builtin(index_dense.dimensions(), metric_kind_to_cpp(kind), index_dense.scalar_kind()));
313
+ }
314
+
315
+ USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_t metric, void* state,
316
+ usearch_metric_kind_t kind, usearch_error_t* error) {
317
+ USEARCH_ASSERT(index && error && "Missing arguments");
318
+ auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
319
+ auto metric_punned =
320
+ state ? metric_punned_t::statefull(reinterpret_cast<std::uintptr_t>(metric),
321
+ reinterpret_cast<std::uintptr_t>(state), metric_kind_to_cpp(kind),
322
+ index_dense.scalar_kind())
323
+ : metric_punned_t::stateless(index_dense.dimensions(), reinterpret_cast<std::uintptr_t>(metric),
324
+ metric_punned_signature_t::array_array_k, metric_kind_to_cpp(kind),
325
+ index_dense.scalar_kind());
326
+ index_dense.change_metric(std::move(metric_punned));
327
+ }
328
+
250
329
  USEARCH_EXPORT void usearch_reserve(usearch_index_t index, size_t capacity, usearch_error_t* error) {
251
- assert(index && error && "Missing arguments");
330
+ USEARCH_ASSERT(index && error && "Missing arguments");
252
331
  if (!reinterpret_cast<index_dense_t*>(index)->reserve(capacity))
253
332
  *error = "Out of memory!";
254
333
  }
@@ -257,29 +336,47 @@ USEARCH_EXPORT void usearch_add(
257
336
  usearch_index_t index, usearch_key_t key, void const* vector, usearch_scalar_kind_t kind, //
258
337
  usearch_error_t* error) {
259
338
 
260
- assert(index && vector && error && "Missing arguments");
339
+ USEARCH_ASSERT(index && vector && error && "Missing arguments");
261
340
  add_result_t result = add_(reinterpret_cast<index_dense_t*>(index), key, vector, scalar_kind_to_cpp(kind));
262
341
  if (!result)
263
342
  *error = result.error.release();
264
343
  }
265
344
 
266
345
  USEARCH_EXPORT bool usearch_contains(usearch_index_t index, usearch_key_t key, usearch_error_t*) {
267
- assert(index && "Missing arguments");
346
+ USEARCH_ASSERT(index && "Missing arguments");
268
347
  return reinterpret_cast<index_dense_t*>(index)->contains(key);
269
348
  }
270
349
 
271
350
  USEARCH_EXPORT size_t usearch_count(usearch_index_t index, usearch_key_t key, usearch_error_t*) {
272
- assert(index && "Missing arguments");
351
+ USEARCH_ASSERT(index && "Missing arguments");
273
352
  return reinterpret_cast<index_dense_t*>(index)->count(key);
274
353
  }
275
354
 
276
- USEARCH_EXPORT size_t usearch_search( //
277
- usearch_index_t index, void const* vector, usearch_scalar_kind_t kind, size_t results_limit, //
355
+ USEARCH_EXPORT size_t usearch_search( //
356
+ usearch_index_t index, void const* query, usearch_scalar_kind_t query_kind, size_t results_limit, //
357
+ usearch_key_t* found_keys, usearch_distance_t* found_distances, usearch_error_t* error) {
358
+
359
+ USEARCH_ASSERT(index && query && error && "Missing arguments");
360
+ search_result_t result =
361
+ search_(reinterpret_cast<index_dense_t*>(index), query, scalar_kind_to_cpp(query_kind), results_limit);
362
+ if (!result) {
363
+ *error = result.error.release();
364
+ return 0;
365
+ }
366
+
367
+ return result.dump_to(found_keys, found_distances);
368
+ }
369
+
370
+ USEARCH_EXPORT size_t usearch_filtered_search( //
371
+ usearch_index_t index, //
372
+ void const* query, usearch_scalar_kind_t query_kind, size_t results_limit, //
373
+ int (*filter)(usearch_key_t key, void* filter_state), void* filter_state, //
278
374
  usearch_key_t* found_keys, usearch_distance_t* found_distances, usearch_error_t* error) {
279
375
 
280
- assert(index && vector && error && "Missing arguments");
376
+ USEARCH_ASSERT(index && query && filter && error && "Missing arguments");
281
377
  search_result_t result =
282
- search_(reinterpret_cast<index_dense_t*>(index), vector, scalar_kind_to_cpp(kind), results_limit);
378
+ search_(reinterpret_cast<index_dense_t*>(index), query, scalar_kind_to_cpp(query_kind), results_limit,
379
+ [=](usearch_key_t key) noexcept { return filter(key, filter_state); });
283
380
  if (!result) {
284
381
  *error = result.error.release();
285
382
  return 0;
@@ -292,13 +389,13 @@ USEARCH_EXPORT size_t usearch_get( //
292
389
  usearch_index_t index, usearch_key_t key, size_t count, //
293
390
  void* vectors, usearch_scalar_kind_t kind, usearch_error_t*) {
294
391
 
295
- assert(index && vectors);
392
+ USEARCH_ASSERT(index && vectors);
296
393
  return get_(reinterpret_cast<index_dense_t*>(index), key, count, vectors, scalar_kind_to_cpp(kind));
297
394
  }
298
395
 
299
396
  USEARCH_EXPORT size_t usearch_remove(usearch_index_t index, usearch_key_t key, usearch_error_t* error) {
300
397
 
301
- assert(index && error && "Missing arguments");
398
+ USEARCH_ASSERT(index && error && "Missing arguments");
302
399
  labeling_result_t result = reinterpret_cast<index_dense_t*>(index)->remove(key);
303
400
  if (!result)
304
401
  *error = result.error.release();
@@ -308,7 +405,7 @@ USEARCH_EXPORT size_t usearch_remove(usearch_index_t index, usearch_key_t key, u
308
405
  USEARCH_EXPORT size_t usearch_rename( //
309
406
  usearch_index_t index, usearch_key_t from, usearch_key_t to, usearch_error_t* error) {
310
407
 
311
- assert(index && error && "Missing arguments");
408
+ USEARCH_ASSERT(index && error && "Missing arguments");
312
409
  labeling_result_t result = reinterpret_cast<index_dense_t*>(index)->rename(from, to);
313
410
  if (!result)
314
411
  *error = result.error.release();
@@ -334,7 +431,7 @@ USEARCH_EXPORT void usearch_exact_search( //
334
431
  usearch_distance_t* distances, size_t distances_stride, //
335
432
  usearch_error_t* error) {
336
433
 
337
- assert(dataset && queries && keys && distances && error && "Missing arguments");
434
+ USEARCH_ASSERT(dataset && queries && keys && distances && error && "Missing arguments");
338
435
 
339
436
  metric_punned_t metric(dimensions, metric_kind_to_cpp(metric_kind), scalar_kind_to_cpp(scalar_kind));
340
437
  executor_default_t executor(threads);
package/c/test.c CHANGED
@@ -72,6 +72,8 @@ void test_init(size_t const collection_size, size_t const dimensions) {
72
72
  ASSERT(usearch_capacity(index, &error) == collection_size, error);
73
73
  ASSERT(usearch_dimensions(index, &error) == dimensions, error);
74
74
  ASSERT(usearch_connectivity(index, &error) == opts.connectivity, error);
75
+ ASSERT(usearch_hardware_acceleration(index, &error), error);
76
+ ASSERT(usearch_memory_usage(index, &error), error);
75
77
 
76
78
  usearch_free(index, &error);
77
79
  ASSERT(!error, error);
@@ -361,6 +363,8 @@ void test_view(size_t const collection_size, size_t const dimensions) {
361
363
  }
362
364
 
363
365
  int main(int argc, char const* argv[]) {
366
+ printf("Running tests...\n");
367
+ printf("USearch version: %s\n", usearch_version());
364
368
 
365
369
  size_t collection_sizes[] = {11, 512};
366
370
  size_t dimensions[] = {83, 2}; // Not all distance functions make sense for 1 dimensional data
package/c/usearch.h CHANGED
@@ -97,6 +97,12 @@ USEARCH_EXPORT typedef struct usearch_init_options_t {
97
97
  bool multi;
98
98
  } usearch_init_options_t;
99
99
 
100
+ /**
101
+ * @brief Retrieves the version of the library.
102
+ * @return The version of the library.
103
+ */
104
+ USEARCH_EXPORT char const* usearch_version(void);
105
+
100
106
  /**
101
107
  * @brief Initializes a new instance of the index.
102
108
  * @param options Pointer to the `usearch_init_options_t` structure containing initialization options.
@@ -112,6 +118,22 @@ USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, use
112
118
  */
113
119
  USEARCH_EXPORT void usearch_free(usearch_index_t index, usearch_error_t* error);
114
120
 
121
+ /**
122
+ * @brief Reports the memory usage of the index.
123
+ * @param[in] index The handle to the USearch index to be queried.
124
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
125
+ * @return Number of bytes used by the index.
126
+ */
127
+ USEARCH_EXPORT size_t usearch_memory_usage(usearch_index_t index, usearch_error_t* error);
128
+
129
+ /**
130
+ * @brief Reports the SIMD capabilities used by the index on the current CPU.
131
+ * @param[in] index The handle to the USearch index to be queried.
132
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
133
+ * @return The codename of the SIMD instruction set used by the index.
134
+ */
135
+ USEARCH_EXPORT char const* usearch_hardware_acceleration(usearch_index_t index, usearch_error_t* error);
136
+
115
137
  /**
116
138
  * @brief Reports expected file size after serialization.
117
139
  * @param[in] index The handle to the USearch index to be serialized.
@@ -227,6 +249,58 @@ USEARCH_EXPORT size_t usearch_connectivity(usearch_index_t index, usearch_error_
227
249
  */
228
250
  USEARCH_EXPORT void usearch_reserve(usearch_index_t index, size_t capacity, usearch_error_t* error);
229
251
 
252
+ /**
253
+ * @brief Retrieves the expansion value used during index creation.
254
+ * @param[in] index The handle to the USearch index to be queried.
255
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
256
+ * @return The expansion value used during index creation.
257
+ */
258
+ USEARCH_EXPORT size_t usearch_expansion_add(usearch_index_t index, usearch_error_t* error);
259
+
260
+ /**
261
+ * @brief Retrieves the expansion value used during search.
262
+ * @param[in] index The handle to the USearch index to be queried.
263
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
264
+ * @return The expansion value used during search.
265
+ */
266
+ USEARCH_EXPORT size_t usearch_expansion_search(usearch_index_t index, usearch_error_t* error);
267
+
268
+ /**
269
+ * @brief Updates the expansion value used during index creation. Rarely used.
270
+ * @param[in] index The handle to the USearch index to be queried.
271
+ * @param[in] expansion The new expansion value.
272
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
273
+ */
274
+ USEARCH_EXPORT void usearch_change_expansion_add(usearch_index_t index, size_t expansion, usearch_error_t* error);
275
+
276
+ /**
277
+ * @brief Updates the expansion value used during search. Rarely used.
278
+ * @param[in] index The handle to the USearch index to be queried.
279
+ * @param[in] expansion The new expansion value.
280
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
281
+ */
282
+ USEARCH_EXPORT void usearch_change_expansion_search(usearch_index_t index, size_t expansion, usearch_error_t* error);
283
+
284
+ /**
285
+ * @brief Updates the metric kind used for distance calculation between vectors.
286
+ * @param[in] index The handle to the USearch index to be queried.
287
+ * @param[in] kind The metric kind used for distance calculation between vectors.
288
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
289
+ */
290
+ USEARCH_EXPORT void usearch_change_metric_kind(usearch_index_t index, usearch_metric_kind_t kind,
291
+ usearch_error_t* error);
292
+
293
+ /**
294
+ * @brief Updates the custom metric function used for distance calculation between vectors.
295
+ * @param[in] index The handle to the USearch index to be queried.
296
+ * @param[in] metric The custom metric function used for distance calculation between vectors.
297
+ * @param[in] state The @b optional state pointer to be passed to the custom metric function.
298
+ * @param[in] kind The metric kind used for distance calculation between vectors. Needed for serialization.
299
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
300
+ */
301
+ USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_t metric, void* state,
302
+ usearch_metric_kind_t kind, usearch_error_t* error);
303
+
230
304
  /**
231
305
  * @brief Adds a vector with a key to the index.
232
306
  * @param[inout] index The handle to the USearch index to be populated.
@@ -268,10 +342,31 @@ USEARCH_EXPORT size_t usearch_count(usearch_index_t index, usearch_key_t, usearc
268
342
  * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
269
343
  * @return Number of found matches.
270
344
  */
271
- USEARCH_EXPORT size_t usearch_search( //
272
- usearch_index_t index, //
273
- void const* query_vector, usearch_scalar_kind_t query_kind, //
274
- size_t count, usearch_key_t* keys, usearch_distance_t* distances, usearch_error_t* error);
345
+ USEARCH_EXPORT size_t usearch_search( //
346
+ usearch_index_t index, //
347
+ void const* query_vector, usearch_scalar_kind_t query_kind, size_t count, //
348
+ usearch_key_t* keys, usearch_distance_t* distances, usearch_error_t* error);
349
+
350
+ /**
351
+ * @brief Performs k-Approximate Nearest Neighbors (kANN) Search for closest vectors to query,
352
+ * predicated on a custom function that returns `true` for vectors to be included.
353
+ *
354
+ * @param[in] index The handle to the USearch index to be queried.
355
+ * @param[in] query_vector Pointer to the query vector data.
356
+ * @param[in] query_kind The scalar type used in the query vector data.
357
+ * @param[in] count Upper bound on the number of neighbors to search, the "k" in "kANN".
358
+ * @param[in] filter The custom filter function that returns `true` for vectors to be included.
359
+ * @param[in] filter_state The @b optional state pointer to be passed to the custom filter function.
360
+ * @param[out] keys Output buffer for up to `count` nearest neighbors keys.
361
+ * @param[out] distances Output buffer for up to `count` distances to nearest neighbors.
362
+ * @param[out] error Pointer to a string where the error message will be stored, if an error occurs.
363
+ * @return Number of found matches.
364
+ */
365
+ USEARCH_EXPORT size_t usearch_filtered_search( //
366
+ usearch_index_t index, //
367
+ void const* query_vector, usearch_scalar_kind_t query_kind, size_t count, //
368
+ int (*filter)(usearch_key_t key, void* filter_state), void* filter_state, //
369
+ usearch_key_t* keys, usearch_distance_t* distances, usearch_error_t* error);
275
370
 
276
371
  /**
277
372
  * @brief Retrieves the vector associated with the given key from the index.
package/cpp/README.md CHANGED
@@ -71,8 +71,19 @@ Instead of spawning additional threads within USearch, we focus on the thread sa
71
71
 
72
72
  During initialization, we allocate enough temporary memory for all the cores on the machine.
73
73
  On the call, the user can supply the identifier of the current thread, making this library easy to integrate with OpenMP and similar tools.
74
+ Here is how parallel indexing may look like, when dealing with the low-level engine:
74
75
 
75
- Moreover, you can take advantage of one of the provided "executors" to parallelize the search:
76
+ ```cpp
77
+ std::size_t executor_threads = std::thread::hardware_concurrency() * 4;
78
+ executor_default_t executor(executor_threads);
79
+
80
+ index.reserve(index_limits_t {vectors.size(), executor.size()});
81
+ executor.fixed(vectors.size(), [&](std::size_t thread, std::size_t task) {
82
+ index.add(task, vectors[task + 3].data(), index_update_config_t { .thread = thread });
83
+ });
84
+ ```
85
+
86
+ Aside from the `executor_default_t`, you can take advantage of one of the provided "executors" to parallelize the search:
76
87
 
77
88
  - `executor_openmp_t`, that would use OpenMP under the hood.
78
89
  - `executor_stl_t`, that will spawn `std::thread` instances.
package/cpp/test.cpp CHANGED
@@ -65,6 +65,17 @@ void test_cosine(index_at& index, std::vector<std::vector<scalar_at>> const& vec
65
65
 
66
66
  // Perform exact search
67
67
  matched_count = index.search(vector_first, 5, args...).dump_to(matched_keys, matched_distances);
68
+ expect(matched_count != 0);
69
+
70
+ // Perform filtered exact search, keeping only odd values
71
+ if constexpr (punned_ak) {
72
+ auto is_odd = [](vector_key_t key) -> bool { return (key & 1) != 0; };
73
+ matched_count =
74
+ index.filtered_search(vector_first, 5, is_odd, args...).dump_to(matched_keys, matched_distances);
75
+ expect(matched_count != 0);
76
+ for (std::size_t i = 0; i < matched_count; i++)
77
+ expect(is_odd(matched_keys[i]));
78
+ }
68
79
 
69
80
  // Validate scans
70
81
  std::size_t count = 0;
@@ -103,8 +114,9 @@ void test_cosine(index_at& index, std::vector<std::vector<scalar_at>> const& vec
103
114
  expect(std::equal(vector_second, vector_second + dimensions, vec_recovered_from_load.data()));
104
115
  }
105
116
 
106
- // Try batch requests
107
- executor_default_t executor;
117
+ // Try batch requests, heavily obersubscribing the CPU cores
118
+ std::size_t executor_threads = std::thread::hardware_concurrency() * 4;
119
+ executor_default_t executor(executor_threads);
108
120
  index.reserve({vectors.size(), executor.size()});
109
121
  executor.fixed(vectors.size() - 3, [&](std::size_t thread, std::size_t task) {
110
122
  if constexpr (punned_ak) {
@@ -10,8 +10,8 @@
10
10
  #define UNUM_USEARCH_HPP
11
11
 
12
12
  #define USEARCH_VERSION_MAJOR 2
13
- #define USEARCH_VERSION_MINOR 10
14
- #define USEARCH_VERSION_PATCH 5
13
+ #define USEARCH_VERSION_MINOR 11
14
+ #define USEARCH_VERSION_PATCH 1
15
15
 
16
16
  // Inferring C++ version
17
17
  // https://stackoverflow.com/a/61552074
@@ -3497,9 +3497,12 @@ class index_gt {
3497
3497
 
3498
3498
  distance_t radius = context.measure(query, citerator_at(start_slot), metric);
3499
3499
  next.insert_reserved({-radius, static_cast<compressed_slot_t>(start_slot)});
3500
- top.insert_reserved({radius, static_cast<compressed_slot_t>(start_slot)});
3501
3500
  visits.set(static_cast<compressed_slot_t>(start_slot));
3502
3501
 
3502
+ // Don't populate the top list if the predicate is not satisfied
3503
+ if (is_dummy<predicate_at>() || predicate(member_cref_t{node_at_(start_slot).ckey(), start_slot}))
3504
+ top.insert_reserved({radius, static_cast<compressed_slot_t>(start_slot)});
3505
+
3503
3506
  while (!next.empty()) {
3504
3507
 
3505
3508
  candidate_t candidate = next.top();
@@ -3529,12 +3532,9 @@ class index_gt {
3529
3532
  if (top.size() < top_limit || successor_dist < radius) {
3530
3533
  // This can substantially grow our priority queue:
3531
3534
  next.insert({-successor_dist, successor_slot});
3532
- if (!is_dummy<predicate_at>())
3533
- if (!predicate(member_cref_t{node_at_(successor_slot).ckey(), successor_slot}))
3534
- continue;
3535
-
3536
- // This will automatically evict poor matches:
3537
- top.insert({successor_dist, successor_slot}, top_limit);
3535
+ if (is_dummy<predicate_at>() ||
3536
+ predicate(member_cref_t{node_at_(successor_slot).ckey(), successor_slot}))
3537
+ top.insert({successor_dist, successor_slot}, top_limit);
3538
3538
  radius = top.top().distance;
3539
3539
  }
3540
3540
  }