usearch 2.17.1 → 2.19.5

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.
Files changed (47) hide show
  1. package/README.md +6 -2
  2. package/c/lib.cpp +13 -2
  3. package/c/module.modulemap +4 -0
  4. package/c/usearch.h +13 -4
  5. package/cpp/README.md +1 -1
  6. package/cpp/bench.cpp +2 -2
  7. package/cpp/test.cpp +110 -16
  8. package/include/usearch/index.hpp +91 -35
  9. package/include/usearch/index_dense.hpp +34 -19
  10. package/include/usearch/index_plugins.hpp +29 -7
  11. package/javascript/README.md +8 -6
  12. package/javascript/dist/cjs/usearch.d.ts +9 -4
  13. package/javascript/dist/cjs/usearch.js +23 -8
  14. package/javascript/dist/esm/usearch.d.ts +9 -4
  15. package/javascript/dist/esm/usearch.js +21 -7
  16. package/javascript/lib.cpp +137 -59
  17. package/javascript/usearch.test.js +359 -192
  18. package/javascript/usearch.ts +35 -11
  19. package/package.json +1 -1
  20. package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
  21. package/prebuilds/linux-arm64/usearch.node +0 -0
  22. package/prebuilds/linux-x64/usearch.node +0 -0
  23. package/simsimd/CONTRIBUTING.md +23 -8
  24. package/simsimd/Cargo.lock +1 -1
  25. package/simsimd/Cargo.toml +1 -1
  26. package/simsimd/README.md +49 -9
  27. package/simsimd/build.rs +7 -3
  28. package/simsimd/c/lib.c +6 -1
  29. package/simsimd/include/simsimd/binary.h +3 -3
  30. package/simsimd/include/simsimd/dot.h +2 -2
  31. package/simsimd/include/simsimd/elementwise.h +26 -17
  32. package/simsimd/include/simsimd/simsimd.h +125 -49
  33. package/simsimd/include/simsimd/sparse.h +30 -29
  34. package/simsimd/include/simsimd/spatial.h +4 -3
  35. package/simsimd/include/simsimd/types.h +19 -8
  36. package/simsimd/javascript/lib.c +0 -1
  37. package/simsimd/package-lock.json +411 -362
  38. package/simsimd/package.json +9 -9
  39. package/simsimd/rust/lib.rs +63 -29
  40. package/simsimd/scripts/bench.cxx +13 -0
  41. package/simsimd/scripts/bench_cosine.rs +8 -0
  42. package/simsimd/scripts/bench_native.rs +4 -0
  43. package/simsimd/scripts/bench_sqeuclidean.rs +8 -0
  44. package/simsimd/scripts/test.c +22 -0
  45. package/simsimd/scripts/test.py +80 -4
  46. package/simsimd/swift/SimSIMD.swift +17 -0
  47. package/simsimd/swift/Test.swift +36 -0
@@ -229,10 +229,12 @@ export class Index {
229
229
  * If a single key is provided, it is associated with all provided vectors.
230
230
  * @param {Float32Array|Float64Array|Int8Array} vectors - Input matrix representing vectors,
231
231
  * matrix of size n * d, where n is the number of vectors, and d is their dimensionality.
232
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for indexing.
233
+ * If set to 0, the number of threads is determined automatically.
232
234
  * @throws Will throw an error if the length of keys doesn't match the number of vectors
233
235
  * or if it's not a single key.
234
236
  */
235
- add(keys, vectors) {
237
+ add(keys, vectors, threads = 0) {
236
238
  let normalizedKeys = normalizeKeys(keys);
237
239
  let normalizedVectors = normalizeVectors(vectors, this.#compiledIndex.dimensions());
238
240
  let countVectors = normalizedVectors.length / this.#compiledIndex.dimensions();
@@ -244,8 +246,11 @@ export class Index {
244
246
  else if (normalizedKeys.length !== countVectors) {
245
247
  throw new Error(`The length of keys (${normalizedKeys.length}) must match the number of vectors (${countVectors}) or be a single key.`);
246
248
  }
249
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
250
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
251
+ }
247
252
  // Call the compiled method
248
- this.#compiledIndex.add(normalizedKeys, normalizedVectors);
253
+ this.#compiledIndex.add(normalizedKeys, normalizedVectors, threads);
249
254
  }
250
255
  /**
251
256
  * Perform a k-nearest neighbor search on the index.
@@ -264,17 +269,21 @@ export class Index {
264
269
  *
265
270
  * @param {Float32Array|Float64Array|Int8Array|Array<Array<number>>} vectors - Input matrix representing query vectors, can be a TypedArray or an array of TypedArray.
266
271
  * @param {number} k - The number of nearest neighbors to search for each query vector.
272
+ * @param {number} [threads=0] - Optional, default is 0. Number of threads to use for searching. If set to 0, the number of threads is determined automatically.
267
273
  * @return {Matches|BatchMatches} - Search results for one or more queries, containing keys, distances, and counts of the matches found.
268
274
  * @throws Will throw an error if `k` is not a positive integer or if the size of the vectors is not a multiple of dimensions.
269
275
  * @throws Will throw an error if `vectors` is not a valid input type (TypedArray or an array of TypedArray) or if its flattened size is not a multiple of dimensions.
270
276
  */
271
- search(vectors, k) {
277
+ search(vectors, k, threads = 0) {
272
278
  if ((!Number.isNaN(k) && typeof k !== "number") || k <= 0) {
273
279
  throw new Error("`k` must be a positive integer representing the number of nearest neighbors to search for.");
274
280
  }
281
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
282
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
283
+ }
275
284
  const normalizedVectors = normalizeVectors(vectors, this.#compiledIndex.dimensions());
276
285
  // Call the compiled method and create Matches or BatchMatches object with the result
277
- const result = this.#compiledIndex.search(normalizedVectors, k);
286
+ const result = this.#compiledIndex.search(normalizedVectors, k, threads);
278
287
  const countInQueries = normalizedVectors.length / Number(this.#compiledIndex.dimensions());
279
288
  const batchMatches = new BatchMatches(...result, k);
280
289
  if (countInQueries === 1) {
@@ -402,6 +411,7 @@ export class Index {
402
411
  * @param {number} dimensions - The dimensionality of the vectors in both the dataset and the queries. It defines the number of elements in each vector.
403
412
  * @param {number} count - The number of nearest neighbors to return for each query. If the dataset contains fewer vectors than the specified count, the result will contain only the available vectors.
404
413
  * @param {MetricKind} metric - The distance metric to be used for the search.
414
+ * @param {number} [threads=0] - Optional, default is 0. The number of threads to use for the search. If set to 0, the number of threads is determined automatically.
405
415
  * @return {Matches|BatchMatches} - Returns a `Matches` or `BatchMatches` object containing the results of the search.
406
416
  * @throws Will throw an error if `dimensions` and `count` are not positive integers.
407
417
  * @throws Will throw an error if `metric` is not a valid MetricKind.
@@ -415,21 +425,25 @@ export class Index {
415
425
  * const dimensions = 2; // The number of elements in each vector.
416
426
  * const count = 1; // The number of nearest neighbors to return for each query.
417
427
  * const metric = MetricKind.IP; // Using the Inner Product distance metric.
428
+ * const threads = 0; // How many threads to use to perform the search.
418
429
  *
419
- * const result = exactSearch(dataset, queries, dimensions, count, metric);
430
+ * const result = exactSearch(dataset, queries, dimensions, count, metric, threads);
420
431
  * // result might be:
421
432
  * // {
422
433
  * // keys: BigUint64Array [ 1n ],
423
434
  * // distances: Float32Array [ some_value ],
424
435
  * // }
425
436
  */
426
- function exactSearch(dataset, queries, dimensions, count, metric) {
437
+ export function exactSearch(dataset, queries, dimensions, count, metric, threads = 0) {
427
438
  // Validate and normalize the dimensions and count
428
439
  dimensions = Number(dimensions);
429
440
  count = Number(count);
430
441
  if (count <= 0 || dimensions <= 0) {
431
442
  throw new Error("Dimensions and count must be positive integers.");
432
443
  }
444
+ if ((!Number.isNaN(threads) && typeof threads !== "number") || threads < 0) {
445
+ throw new Error("`threads` must be a non-negative integer representing the number of threads to use for searching.");
446
+ }
433
447
  // Validate metric
434
448
  if (!Object.values(MetricKind).includes(metric)) {
435
449
  throw new Error(`Invalid metric: ${metric}. It must be one of: ${Object.values(MetricKind).join(", ")}`);
@@ -450,7 +464,7 @@ function exactSearch(dataset, queries, dimensions, count, metric) {
450
464
  throw new Error("Count must be equal or smaller than the number of vectors in the dataset.");
451
465
  }
452
466
  // Call the compiled function with the normalized input
453
- const result = compiled.exactSearch(dataset, queries, dimensions, count, metric);
467
+ const result = compiled.exactSearch(dataset, queries, dimensions, count, metric, threads);
454
468
  // Create and return a Matches or BatchMatches object with the result
455
469
  if (countInQueries == 1) {
456
470
  return new Matches(result[0], result[1]);
@@ -9,7 +9,8 @@
9
9
  * @see NodeJS docs: https://nodejs.org/api/addons.html#hello-world
10
10
  *
11
11
  */
12
- #include <new> // `std::bad_alloc`
12
+ #include <new> // `std::bad_alloc`
13
+ #include <thread> // `std::thread::hardware_concurrency()`
13
14
 
14
15
  #define NAPI_CPP_EXCEPTIONS
15
16
  #include <napi.h>
@@ -20,7 +21,10 @@
20
21
  using namespace unum::usearch;
21
22
  using namespace unum;
22
23
 
24
+ using index_error_t = usearch::error_t;
23
25
  using add_result_t = typename index_dense_t::add_result_t;
26
+ using search_result_t = typename index_dense_t::search_result_t;
27
+ using state_result_t = typename index_dense_t::state_result_t;
24
28
 
25
29
  class CompiledIndex : public Napi::ObjectWrap<CompiledIndex> {
26
30
  public:
@@ -44,6 +48,7 @@ class CompiledIndex : public Napi::ObjectWrap<CompiledIndex> {
44
48
  Napi::Value Count(Napi::CallbackInfo const& ctx);
45
49
 
46
50
  std::unique_ptr<index_dense_t> native_;
51
+ std::mutex mtx;
47
52
  };
48
53
 
49
54
  Napi::Object CompiledIndex::Init(Napi::Env env, Napi::Object exports) {
@@ -77,7 +82,6 @@ std::size_t napi_argument_to_size(Napi::Value v) {
77
82
  }
78
83
 
79
84
  CompiledIndex::CompiledIndex(Napi::CallbackInfo const& ctx) : Napi::ObjectWrap<CompiledIndex>(ctx) {
80
-
81
85
  // Directly assign the parameters without checks
82
86
  std::size_t dimensions = napi_argument_to_size(ctx[0]);
83
87
  metric_kind_t metric_kind = metric_from_name(ctx[1].As<Napi::String>().Utf8Value().c_str());
@@ -95,7 +99,12 @@ CompiledIndex::CompiledIndex(Napi::CallbackInfo const& ctx) : Napi::ObjectWrap<C
95
99
 
96
100
  index_dense_config_t config(connectivity, expansion_add, expansion_search);
97
101
  config.multi = multi;
98
- native_.reset(new index_dense_t(index_dense_t::make(metric, config)));
102
+ state_result_t result = index_dense_t::make(metric, config);
103
+ if (!result) {
104
+ Napi::TypeError::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
105
+ return;
106
+ }
107
+ native_.reset(new index_dense_t(std::move(result.index)));
99
108
  if (!native_)
100
109
  Napi::Error::New(ctx.Env(), "Out of memory!").ThrowAsJavaScriptException();
101
110
  }
@@ -155,45 +164,92 @@ void CompiledIndex::View(Napi::CallbackInfo const& ctx) {
155
164
  }
156
165
 
157
166
  void CompiledIndex::Add(Napi::CallbackInfo const& ctx) {
167
+ Napi::Env env = ctx.Env();
168
+
169
+ // Check the number of arguments
170
+ if (ctx.Length() != 3) {
171
+ Napi::TypeError::New(env, "`Add` expects 3 arguments: keys, vectors[, threads]").ThrowAsJavaScriptException();
172
+ return;
173
+ }
174
+
158
175
  // Extract keys and vectors from arguments
159
176
  Napi::BigUint64Array keys = ctx[0].As<Napi::BigUint64Array>();
160
- std::size_t tasks = keys.ElementLength();
177
+ Napi::TypedArray vectors = ctx[1].As<Napi::TypedArray>();
178
+
179
+ // Optional arguments
180
+ std::size_t threads = napi_argument_to_size(ctx[2]);
181
+ if (threads == 0)
182
+ threads = std::thread::hardware_concurrency();
161
183
 
162
- // Ensure there is enough capacity
184
+ // Ensure there is enough capacity and memory
185
+ std::size_t tasks = keys.ElementLength();
163
186
  if (native_->size() + tasks >= native_->capacity())
164
- native_->reserve(ceil2(native_->size() + tasks));
187
+ if (!native_->try_reserve({ceil2(native_->size() + tasks), threads})) {
188
+ Napi::TypeError::New(env, "Failed to reserve memory").ThrowAsJavaScriptException();
189
+ return;
190
+ }
165
191
 
166
- // Create an instance of the executor with the default number of threads
192
+ // Run insertions concurrently
167
193
  auto run_parallel = [&](auto vectors) {
168
- executor_stl_t executor;
194
+ // Errors can be set only from the main thread, so before spawning workers
195
+ // we need temporary space to keep the message
196
+ index_error_t first_error{};
197
+ std::atomic<bool> failed{false};
198
+ executor_default_t executor{threads};
169
199
  executor.fixed(tasks, [&](std::size_t /*thread_idx*/, std::size_t task_idx) {
170
- add_result_t result = native_->add(static_cast<default_key_t>(keys[task_idx]),
171
- vectors + task_idx * native_->dimensions());
172
- if (!result)
173
- Napi::Error::New(ctx.Env(), result.error.release()).ThrowAsJavaScriptException();
200
+ if (failed.load())
201
+ return;
202
+ auto key = static_cast<default_key_t>(keys[task_idx]);
203
+ auto vector = vectors + task_idx * native_->dimensions();
204
+ add_result_t result = native_->add(key, vector);
205
+ if (!result) {
206
+ if (!failed.exchange(true)) {
207
+ first_error = std::move(result.error);
208
+ } else {
209
+ result.error.release();
210
+ }
211
+ }
174
212
  });
213
+ if (failed)
214
+ Napi::TypeError::New(env, first_error.release()).ThrowAsJavaScriptException();
175
215
  };
176
216
 
177
- Napi::TypedArray vectors = ctx[1].As<Napi::TypedArray>();
178
- if (vectors.TypedArrayType() == napi_float32_array) {
179
- run_parallel(vectors.As<Napi::Float32Array>().Data());
180
- } else if (vectors.TypedArrayType() == napi_float64_array) {
181
- run_parallel(vectors.As<Napi::Float64Array>().Data());
182
- } else if (vectors.TypedArrayType() == napi_int8_array) {
183
- run_parallel(vectors.As<Napi::Int8Array>().Data());
184
- } else {
185
- Napi::TypeError::New(ctx.Env(),
186
- "Unsupported TypedArray. Supported types are Float32Array, Float64Array, and Int8Array.")
187
- .ThrowAsJavaScriptException();
217
+ // Dispatch the parallel tasks based on the `TypedArray` type
218
+ try {
219
+ if (vectors.TypedArrayType() == napi_float32_array) {
220
+ run_parallel(vectors.As<Napi::Float32Array>().Data());
221
+ } else if (vectors.TypedArrayType() == napi_float64_array) {
222
+ run_parallel(vectors.As<Napi::Float64Array>().Data());
223
+ } else if (vectors.TypedArrayType() == napi_int8_array) {
224
+ run_parallel(vectors.As<Napi::Int8Array>().Data());
225
+ } else {
226
+ Napi::TypeError::New(
227
+ env, "Unsupported TypedArray. Supported types are Float32Array, Float64Array, and Int8Array.")
228
+ .ThrowAsJavaScriptException();
229
+ }
230
+ } catch (...) {
231
+ Napi::TypeError::New(env, "Insertion failed").ThrowAsJavaScriptException();
188
232
  }
189
233
  }
190
234
 
191
235
  Napi::Value CompiledIndex::Search(Napi::CallbackInfo const& ctx) {
192
236
  Napi::Env env = ctx.Env();
237
+
238
+ // Check the number of arguments
239
+ if (ctx.Length() != 3) {
240
+ Napi::TypeError::New(env, "`Search` expects 3 arguments: queries, k[, threads]").ThrowAsJavaScriptException();
241
+ return env.Null();
242
+ }
243
+
244
+ // Extract mandatory arguments
193
245
  Napi::TypedArray queries = ctx[0].As<Napi::TypedArray>();
194
- std::size_t tasks = queries.ElementLength() / native_->dimensions();
195
246
  std::size_t wanted = napi_argument_to_size(ctx[1]);
247
+ std::size_t threads = napi_argument_to_size(ctx[2]);
248
+ if (threads == 0)
249
+ threads = std::thread::hardware_concurrency();
196
250
 
251
+ // Run queries concurrently
252
+ std::size_t tasks = queries.ElementLength() / native_->dimensions();
197
253
  auto run_parallel = [&](auto vectors) -> Napi::Value {
198
254
  Napi::Array result_js = Napi::Array::New(env, 3);
199
255
  Napi::BigUint64Array matches_js = Napi::BigUint64Array::New(env, tasks * wanted);
@@ -204,29 +260,30 @@ Napi::Value CompiledIndex::Search(Napi::CallbackInfo const& ctx) {
204
260
  auto distances_data = distances_js.Data();
205
261
  auto counts_data = counts_js.Data();
206
262
 
207
- try {
208
- bool failed = false;
209
- executor_stl_t executor;
210
- executor.fixed(tasks, [&](std::size_t /*thread_idx*/, std::size_t task_idx) {
211
- auto result = native_->search(vectors + task_idx * native_->dimensions(), wanted);
212
- if (!result) {
213
- failed = true;
214
- Napi::TypeError::New(env, result.error.release()).ThrowAsJavaScriptException();
263
+ // Errors can be set only from the main thread, so before spawning workers
264
+ // we need temporary space to keep the message
265
+ index_error_t first_error{};
266
+ std::atomic<bool> failed{false};
267
+ executor_default_t executor{threads};
268
+ executor.fixed(tasks, [&](std::size_t /*thread_idx*/, std::size_t task_idx) {
269
+ if (failed.load())
270
+ return;
271
+ auto vector = vectors + task_idx * native_->dimensions();
272
+ search_result_t result = native_->search(vector, wanted);
273
+ if (!result) {
274
+ if (!failed.exchange(true)) {
275
+ first_error = std::move(result.error);
215
276
  } else {
216
- counts_data[task_idx] = result.dump_to(matches_data + task_idx * native_->dimensions(),
217
- distances_data + task_idx * native_->dimensions());
277
+ result.error.release();
218
278
  }
219
- });
220
-
221
- if (failed)
222
- return env.Null();
223
-
224
- } catch (std::bad_alloc const&) {
225
- Napi::TypeError::New(env, "Out of memory").ThrowAsJavaScriptException();
226
- return env.Null();
227
-
228
- } catch (...) {
229
- Napi::TypeError::New(env, "Search failed").ThrowAsJavaScriptException();
279
+ } else {
280
+ auto matches = matches_data + task_idx * wanted;
281
+ auto distances = distances_data + task_idx * wanted;
282
+ counts_data[task_idx] = result.dump_to(matches, distances, wanted);
283
+ }
284
+ });
285
+ if (failed) {
286
+ Napi::TypeError::New(env, first_error.release()).ThrowAsJavaScriptException();
230
287
  return env.Null();
231
288
  }
232
289
 
@@ -236,16 +293,22 @@ Napi::Value CompiledIndex::Search(Napi::CallbackInfo const& ctx) {
236
293
  return result_js;
237
294
  };
238
295
 
239
- if (queries.TypedArrayType() == napi_float32_array) {
240
- return run_parallel(queries.As<Napi::Float32Array>().Data());
241
- } else if (queries.TypedArrayType() == napi_float64_array) {
242
- return run_parallel(queries.As<Napi::Float64Array>().Data());
243
- } else if (queries.TypedArrayType() == napi_int8_array) {
244
- return run_parallel(queries.As<Napi::Int8Array>().Data());
245
- } else {
246
- Napi::TypeError::New(env,
247
- "Unsupported TypedArray. Supported types are Float32Array, Float64Array, and Int8Array.")
248
- .ThrowAsJavaScriptException();
296
+ // Dispatch the parallel tasks based on the `TypedArray` type
297
+ try {
298
+ if (queries.TypedArrayType() == napi_float32_array) {
299
+ return run_parallel(queries.As<Napi::Float32Array>().Data());
300
+ } else if (queries.TypedArrayType() == napi_float64_array) {
301
+ return run_parallel(queries.As<Napi::Float64Array>().Data());
302
+ } else if (queries.TypedArrayType() == napi_int8_array) {
303
+ return run_parallel(queries.As<Napi::Int8Array>().Data());
304
+ } else {
305
+ Napi::TypeError::New(
306
+ env, "Unsupported TypedArray. Supported types are Float32Array, Float64Array, and Int8Array.")
307
+ .ThrowAsJavaScriptException();
308
+ return env.Null();
309
+ }
310
+ } catch (...) {
311
+ Napi::TypeError::New(env, "Search failed").ThrowAsJavaScriptException();
249
312
  return env.Null();
250
313
  }
251
314
  }
@@ -287,21 +350,36 @@ Napi::Value CompiledIndex::Count(Napi::CallbackInfo const& ctx) {
287
350
  Napi::Value exactSearch(Napi::CallbackInfo const& ctx) {
288
351
  Napi::Env env = ctx.Env();
289
352
 
353
+ // Check the number of arguments
354
+ if (ctx.Length() != 6) {
355
+ Napi::TypeError::New(env,
356
+ "`exactSearch` expects 6 arguments: dataset, queries, dimensions, k, metric[, threads].")
357
+ .ThrowAsJavaScriptException();
358
+ return env.Null();
359
+ }
360
+
290
361
  // Extracting parameters directly without additional type checks.
291
362
  Napi::TypedArray dataset = ctx[0].As<Napi::TypedArray>();
292
363
  Napi::ArrayBuffer datasetBuffer = dataset.ArrayBuffer();
293
364
  Napi::TypedArray queries = ctx[1].As<Napi::TypedArray>();
294
365
  Napi::ArrayBuffer queriesBuffer = queries.ArrayBuffer();
295
- std::uint64_t dimensions = napi_argument_to_size(ctx[2]);
296
- std::uint64_t wanted = napi_argument_to_size(ctx[3]);
366
+ std::size_t dimensions = napi_argument_to_size(ctx[2]);
367
+ std::size_t wanted = napi_argument_to_size(ctx[3]);
297
368
  metric_kind_t metric_kind = metric_from_name(ctx[4].As<Napi::String>().Utf8Value().c_str());
369
+ std::size_t threads = napi_argument_to_size(ctx[5]);
370
+ if (threads == 0)
371
+ threads = std::thread::hardware_concurrency();
298
372
 
373
+ // Check the types used
299
374
  scalar_kind_t quantization;
300
375
  std::size_t bytes_per_scalar;
301
376
  switch (queries.TypedArrayType()) {
302
377
  case napi_float64_array: quantization = scalar_kind_t::f64_k, bytes_per_scalar = 8; break;
378
+ case napi_float32_array: quantization = scalar_kind_t::f32_k, bytes_per_scalar = 4; break;
303
379
  case napi_int8_array: quantization = scalar_kind_t::i8_k, bytes_per_scalar = 1; break;
304
- default: quantization = scalar_kind_t::f32_k, bytes_per_scalar = 4; break;
380
+ default:
381
+ Napi::TypeError::New(env, "Unsupported TypedArray for queries.").ThrowAsJavaScriptException();
382
+ return env.Null();
305
383
  }
306
384
 
307
385
  metric_punned_t metric(dimensions, metric_kind, quantization);
@@ -310,7 +388,7 @@ Napi::Value exactSearch(Napi::CallbackInfo const& ctx) {
310
388
  return env.Null();
311
389
  }
312
390
 
313
- executor_default_t executor;
391
+ executor_default_t executor{threads};
314
392
  exact_search_t search;
315
393
 
316
394
  // Performing the exact search.