usearch 0.22.1 → 0.22.3

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.
@@ -200,44 +200,38 @@ class index_punned_dense_gt {
200
200
  using stats_t = typename index_t::stats_t;
201
201
  using match_t = typename index_t::match_t;
202
202
 
203
- struct labeling_result_t {
204
- error_t error{};
205
- std::size_t completed{};
206
-
207
- explicit operator bool() const noexcept { return !error; }
208
- labeling_result_t failed(error_t message) noexcept {
209
- error = std::move(message);
210
- return std::move(*this);
211
- }
212
- };
213
-
214
- struct compaction_result_t {
215
- error_t error{};
216
- std::size_t pruned_edges{};
217
-
218
- explicit operator bool() const noexcept { return !error; }
219
- compaction_result_t failed(error_t message) noexcept {
220
- error = std::move(message);
221
- return std::move(*this);
222
- }
223
- };
224
-
225
203
  index_punned_dense_gt() = default;
226
- index_punned_dense_gt(index_punned_dense_gt&& other) { swap(other); }
204
+ index_punned_dense_gt(index_punned_dense_gt&& other)
205
+ : dimensions_(std::move(other.dimensions_)), //
206
+ scalar_words_(std::move(other.scalar_words_)), //
207
+ expansion_add_(std::move(other.expansion_add_)), //
208
+ expansion_search_(std::move(other.expansion_search_)), //
209
+ typed_(exchange(other.typed_, nullptr)), //
210
+ casted_vector_bytes_(std::move(other.casted_vector_bytes_)), //
211
+ cast_buffer_(std::move(other.cast_buffer_)), //
212
+ casts_(std::move(other.casts_)), //
213
+ root_metric_(std::move(other.root_metric_)), //
214
+ available_threads_(std::move(other.available_threads_)), //
215
+ labeled_lookup_(std::move(other.labeled_lookup_)), //
216
+ free_ids_(std::move(other.free_ids_)), //
217
+ free_label_(std::move(other.free_label_)) {} //
218
+
227
219
  index_punned_dense_gt& operator=(index_punned_dense_gt&& other) {
228
220
  swap(other);
229
221
  return *this;
230
222
  }
231
223
 
232
- ~index_punned_dense_gt() { index_allocator_t{}.deallocate(typed_, 1); }
233
-
224
+ /**
225
+ * @brief Swaps the contents of this index with another index.
226
+ * @param other The other index to swap with.
227
+ */
234
228
  void swap(index_punned_dense_gt& other) {
235
229
  std::swap(dimensions_, other.dimensions_);
236
230
  std::swap(scalar_words_, other.scalar_words_);
237
231
  std::swap(expansion_add_, other.expansion_add_);
238
232
  std::swap(expansion_search_, other.expansion_search_);
239
- std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
240
233
  std::swap(typed_, other.typed_);
234
+ std::swap(casted_vector_bytes_, other.casted_vector_bytes_);
241
235
  std::swap(cast_buffer_, other.cast_buffer_);
242
236
  std::swap(casts_, other.casts_);
243
237
  std::swap(root_metric_, other.root_metric_);
@@ -247,8 +241,65 @@ class index_punned_dense_gt {
247
241
  std::swap(free_label_, other.free_label_);
248
242
  }
249
243
 
244
+ ~index_punned_dense_gt() {
245
+ if (typed_)
246
+ typed_->~index_t();
247
+ index_allocator_t{}.deallocate(typed_, 1);
248
+ typed_ = nullptr;
249
+ }
250
+
250
251
  static index_config_t optimize(index_config_t config) { return index_t::optimize(config); }
251
252
 
253
+ /**
254
+ * @brief Constructs an instance of ::index_punned_dense_gt.
255
+ * @param[in] dimensions The of dimensions per vector.
256
+ * @param[in] metric One of the default supported metric @b kinds for distance measurements.
257
+ * @param[in] config The index configuration (optional).
258
+ * @param[in] accuracy The scalar kind used for internal representations (optional).
259
+ * @param[in] expansion_add The expansion factor for adding vectors (optional).
260
+ * @param[in] expansion_search The expansion factor for searching vectors (optional).
261
+ * @param[in] free_label The label used for freed vectors (optional).
262
+ * @return An instance of ::index_punned_dense_gt.
263
+ */
264
+ static index_punned_dense_gt make( //
265
+ std::size_t dimensions, metric_kind_t metric, //
266
+ index_config_t config = {}, //
267
+ scalar_kind_t accuracy = scalar_kind_t::f32_k, //
268
+ std::size_t expansion_add = default_expansion_add(), //
269
+ std::size_t expansion_search = default_expansion_search(), //
270
+ label_t free_label = default_free_value<label_t>()) {
271
+
272
+ return make_( //
273
+ dimensions, accuracy, //
274
+ config, expansion_add, expansion_search, //
275
+ make_metric_(metric, dimensions, accuracy), make_casts_(accuracy), free_label);
276
+ }
277
+
278
+ /**
279
+ * @brief Constructs an instance of ::index_punned_dense_gt.
280
+ * @param[in] dimensions The of dimensions per vector.
281
+ * @param[in] metric The @b ad-hoc metric wrapped for third-party distance measures.
282
+ * @param[in] config The index configuration (optional).
283
+ * @param[in] accuracy The scalar kind used for internal representations (optional).
284
+ * @param[in] expansion_add The expansion factor for adding vectors (optional).
285
+ * @param[in] expansion_search The expansion factor for searching vectors (optional).
286
+ * @param[in] free_label The label used for freed vectors (optional).
287
+ * @return An instance of ::index_punned_dense_gt.
288
+ */
289
+ static index_punned_dense_gt make( //
290
+ std::size_t dimensions, metric_t metric, //
291
+ index_config_t config = {}, //
292
+ scalar_kind_t accuracy = scalar_kind_t::f32_k, //
293
+ std::size_t expansion_add = default_expansion_add(), //
294
+ std::size_t expansion_search = default_expansion_search(), //
295
+ label_t free_label = default_free_value<label_t>()) {
296
+
297
+ return make_( //
298
+ dimensions, accuracy, //
299
+ config, expansion_add, expansion_search, //
300
+ metric_t(metric), make_casts_(accuracy), free_label);
301
+ }
302
+
252
303
  std::size_t dimensions() const { return dimensions_; }
253
304
  std::size_t scalar_words() const { return scalar_words_; }
254
305
  std::size_t connectivity() const { return typed_->connectivity(); }
@@ -274,6 +325,50 @@ class index_punned_dense_gt {
274
325
  stats_t stats() const { return typed_->stats(); }
275
326
  stats_t stats(std::size_t level) const { return typed_->stats(level); }
276
327
 
328
+ std::size_t memory_usage() const {
329
+ return typed_->memory_usage(0) + //
330
+ typed_->tape_allocator().total_wasted() + //
331
+ typed_->tape_allocator().total_reserved();
332
+ }
333
+
334
+ // clang-format off
335
+ add_result_t add(label_t label, b1x8_t const* vector) { return add_(label, vector, casts_.from_b1x8); }
336
+ add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
337
+ add_result_t add(label_t label, f16_t const* vector) { return add_(label, vector, casts_.from_f16); }
338
+ add_result_t add(label_t label, f32_t const* vector) { return add_(label, vector, casts_.from_f32); }
339
+ add_result_t add(label_t label, f64_t const* vector) { return add_(label, vector, casts_.from_f64); }
340
+
341
+ add_result_t add(label_t label, b1x8_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_b1x8); }
342
+ add_result_t add(label_t label, f8_bits_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f8); }
343
+ add_result_t add(label_t label, f16_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f16); }
344
+ add_result_t add(label_t label, f32_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f32); }
345
+ add_result_t add(label_t label, f64_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f64); }
346
+
347
+ search_result_t search(b1x8_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_b1x8); }
348
+ search_result_t search(f8_bits_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f8); }
349
+ search_result_t search(f16_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f16); }
350
+ search_result_t search(f32_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f32); }
351
+ search_result_t search(f64_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f64); }
352
+
353
+ search_result_t search(b1x8_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_b1x8); }
354
+ search_result_t search(f8_bits_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f8); }
355
+ search_result_t search(f16_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f16); }
356
+ search_result_t search(f32_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f32); }
357
+ search_result_t search(f64_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f64); }
358
+
359
+ bool get(label_t label, b1x8_t* vector) const { return get_(label, vector, casts_.to_b1x8); }
360
+ bool get(label_t label, f8_bits_t* vector) const { return get_(label, vector, casts_.to_f8); }
361
+ bool get(label_t label, f16_t* vector) const { return get_(label, vector, casts_.to_f16); }
362
+ bool get(label_t label, f32_t* vector) const { return get_(label, vector, casts_.to_f32); }
363
+ bool get(label_t label, f64_t* vector) const { return get_(label, vector, casts_.to_f64); }
364
+ // clang-format on
365
+
366
+ search_result_t empty_search_result() const { return search_result_t{*typed_}; }
367
+
368
+ /**
369
+ * @brief Reserves memory for the index and the labeled lookup.
370
+ * @return `true` if the memory reservation was successful, `false` otherwise.
371
+ */
277
372
  bool reserve(index_limits_t limits) {
278
373
  {
279
374
  unique_lock_t lock(labeled_lookup_mutex_);
@@ -282,6 +377,9 @@ class index_punned_dense_gt {
282
377
  return typed_->reserve(limits);
283
378
  }
284
379
 
380
+ /**
381
+ * @brief Clears the whole index, reclaiming the memory.
382
+ */
285
383
  void clear() {
286
384
  unique_lock_t lookup_lock(labeled_lookup_mutex_);
287
385
  std::unique_lock<std::mutex> free_lock(free_ids_mutex_);
@@ -290,8 +388,18 @@ class index_punned_dense_gt {
290
388
  free_ids_.clear();
291
389
  }
292
390
 
391
+ /**
392
+ * @brief Saves the index to a file.
393
+ * @param[in] path The path to the file.
394
+ * @return Outcome descriptor explictly convertable to boolean.
395
+ */
293
396
  serialization_result_t save(char const* path) const { return typed_->save(path); }
294
397
 
398
+ /**
399
+ * @brief Parses the index from file to RAM.
400
+ * @param[in] path The path to the file.
401
+ * @return Outcome descriptor explictly convertable to boolean.
402
+ */
295
403
  serialization_result_t load(char const* path) {
296
404
  serialization_result_t result = typed_->load(path);
297
405
  if (result)
@@ -299,6 +407,11 @@ class index_punned_dense_gt {
299
407
  return result;
300
408
  }
301
409
 
410
+ /**
411
+ * @brief Parses the index from file, without loading it into RAM.
412
+ * @param[in] path The path to the file.
413
+ * @return Outcome descriptor explictly convertable to boolean.
414
+ */
302
415
  serialization_result_t view(char const* path) {
303
416
  serialization_result_t result = typed_->view(path);
304
417
  if (result)
@@ -306,58 +419,34 @@ class index_punned_dense_gt {
306
419
  return result;
307
420
  }
308
421
 
309
- std::size_t memory_usage(std::size_t allocator_entry_bytes = default_allocator_entry_bytes()) const {
310
- return typed_->memory_usage(allocator_entry_bytes);
311
- }
312
-
422
+ /**
423
+ * @brief Checks if a vector with specidied label is present.
424
+ * @return `true` if the label is present in the index, `false` otherwise.
425
+ */
313
426
  bool contains(label_t label) const {
314
427
  shared_lock_t lock(labeled_lookup_mutex_);
315
428
  return labeled_lookup_.contains(label);
316
429
  }
317
430
 
318
- void export_labels(label_t* labels, std::size_t offset, std::size_t limit) const {
319
- shared_lock_t lock(labeled_lookup_mutex_);
320
- auto it = labeled_lookup_.begin();
321
- offset = (std::min)(offset, labeled_lookup_.size());
322
- std::advance(it, offset);
323
- for (; it != labeled_lookup_.end() && limit; ++it, ++labels, --limit)
324
- *labels = it->first;
325
- }
326
-
327
- // clang-format off
328
- add_result_t add(label_t label, b1x8_t const* vector) { return add_(label, vector, casts_.from_b1x8); }
329
- add_result_t add(label_t label, f8_bits_t const* vector) { return add_(label, vector, casts_.from_f8); }
330
- add_result_t add(label_t label, f16_t const* vector) { return add_(label, vector, casts_.from_f16); }
331
- add_result_t add(label_t label, f32_t const* vector) { return add_(label, vector, casts_.from_f32); }
332
- add_result_t add(label_t label, f64_t const* vector) { return add_(label, vector, casts_.from_f64); }
333
-
334
- add_result_t add(label_t label, b1x8_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_b1x8); }
335
- add_result_t add(label_t label, f8_bits_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f8); }
336
- add_result_t add(label_t label, f16_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f16); }
337
- add_result_t add(label_t label, f32_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f32); }
338
- add_result_t add(label_t label, f64_t const* vector, add_config_t config) { return add_(label, vector, config, casts_.from_f64); }
339
-
340
- search_result_t search(b1x8_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_b1x8); }
341
- search_result_t search(f8_bits_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f8); }
342
- search_result_t search(f16_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f16); }
343
- search_result_t search(f32_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f32); }
344
- search_result_t search(f64_t const* vector, std::size_t wanted) const { return search_(vector, wanted, casts_.from_f64); }
345
-
346
- search_result_t search(b1x8_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_b1x8); }
347
- search_result_t search(f8_bits_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f8); }
348
- search_result_t search(f16_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f16); }
349
- search_result_t search(f32_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f32); }
350
- search_result_t search(f64_t const* vector, std::size_t wanted, search_config_t config) const { return search_(vector, wanted, config, casts_.from_f64); }
351
-
352
- search_result_t empty_search_result() const { return search_result_t{*typed_}; }
431
+ struct labeling_result_t {
432
+ error_t error{};
433
+ std::size_t completed{};
353
434
 
354
- bool get(label_t label, b1x8_t* vector) const { return get_(label, vector, casts_.to_b1x8); }
355
- bool get(label_t label, f8_bits_t* vector) const { return get_(label, vector, casts_.to_f8); }
356
- bool get(label_t label, f16_t* vector) const { return get_(label, vector, casts_.to_f16); }
357
- bool get(label_t label, f32_t* vector) const { return get_(label, vector, casts_.to_f32); }
358
- bool get(label_t label, f64_t* vector) const { return get_(label, vector, casts_.to_f64); }
359
- // clang-format on
435
+ explicit operator bool() const noexcept { return !error; }
436
+ labeling_result_t failed(error_t message) noexcept {
437
+ error = std::move(message);
438
+ return std::move(*this);
439
+ }
440
+ };
360
441
 
442
+ /**
443
+ * @brief Removes an entry with the specified label from the index.
444
+ * @param[in] label The label of the entry to remove.
445
+ * @return The ::labeling_result_t indicating the result of the removal operation.
446
+ * If the removal was successful, `result.completed` will be `true`.
447
+ * If the label was not found in the index, `result.completed` will be `false`.
448
+ * If an error occurred during the removal operation, `result.error` will contain an error message.
449
+ */
361
450
  labeling_result_t remove(label_t label) {
362
451
  labeling_result_t result;
363
452
 
@@ -385,6 +474,14 @@ class index_punned_dense_gt {
385
474
  return result;
386
475
  }
387
476
 
477
+ /**
478
+ * @brief Removes multiple entries with the specified labels from the index.
479
+ * @param[in] labels_begin The beginning of the labels range.
480
+ * @param[in] labels_end The ending of the labels range.
481
+ * @return The ::labeling_result_t indicating the result of the removal operation.
482
+ * `result.completed` will contain the number of labels that were successfully removed.
483
+ * `result.error` will contain an error message if an error occurred during the removal operation.
484
+ */
388
485
  template <typename labels_iterator_at>
389
486
  labeling_result_t remove(labels_iterator_at&& labels_begin, labels_iterator_at&& labels_end) {
390
487
 
@@ -418,20 +515,14 @@ class index_punned_dense_gt {
418
515
  return result;
419
516
  }
420
517
 
421
- template <typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
422
- compaction_result_t compact(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
423
- compaction_result_t result;
424
- std::atomic<std::size_t> pruned_edges;
425
- auto disallow = [&](member_cref_t const& member) noexcept {
426
- bool freed = member.label == free_label_;
427
- pruned_edges += freed;
428
- return freed;
429
- };
430
- typed_->isolate(disallow, std::forward<executor_at>(executor), std::forward<progress_at>(progress));
431
- result.pruned_edges = pruned_edges;
432
- return result;
433
- }
434
-
518
+ /**
519
+ * @brief Renames an entry with the specified label to a new label.
520
+ * @param[in] from The current label of the entry to rename.
521
+ * @param[in] to The new label to assign to the entry.
522
+ * @return The ::labeling_result_t indicating the result of the rename operation.
523
+ * If the rename was successful, `result.completed` will be `true`.
524
+ * If the entry with the current label was not found, `result.completed` will be `false`.
525
+ */
435
526
  labeling_result_t rename(label_t from, label_t to) {
436
527
  labeling_result_t result;
437
528
  unique_lock_t lookup_lock(labeled_lookup_mutex_);
@@ -447,32 +538,52 @@ class index_punned_dense_gt {
447
538
  return result;
448
539
  }
449
540
 
450
- static index_punned_dense_gt make( //
451
- std::size_t dimensions, metric_kind_t metric, //
452
- index_config_t config = {}, //
453
- scalar_kind_t accuracy = scalar_kind_t::f32_k, //
454
- std::size_t expansion_add = default_expansion_add(), //
455
- std::size_t expansion_search = default_expansion_search(), //
456
- label_t free_label = default_free_value<label_t>()) {
457
-
458
- return make_( //
459
- dimensions, accuracy, //
460
- config, expansion_add, expansion_search, //
461
- make_metric_(metric, dimensions, accuracy), make_casts_(accuracy), free_label);
541
+ /**
542
+ * @brief Exports a range of labels for the vectors present in the index.
543
+ * @param[out] labels Pointer to the array where the labels will be exported.
544
+ * @param[in] offset The number of labels to skip. Useful for pagination.
545
+ * @param[in] limit The maximum number of labels to export, that can fit in ::labels.
546
+ */
547
+ void export_labels(label_t* labels, std::size_t offset, std::size_t limit) const {
548
+ shared_lock_t lock(labeled_lookup_mutex_);
549
+ auto it = labeled_lookup_.begin();
550
+ offset = (std::min)(offset, labeled_lookup_.size());
551
+ std::advance(it, offset);
552
+ for (; it != labeled_lookup_.end() && limit; ++it, ++labels, --limit)
553
+ *labels = it->first;
462
554
  }
463
555
 
464
- static index_punned_dense_gt make( //
465
- std::size_t dimensions, metric_t metric, //
466
- index_config_t config = {}, //
467
- scalar_kind_t accuracy = scalar_kind_t::f32_k, //
468
- std::size_t expansion_add = default_expansion_add(), //
469
- std::size_t expansion_search = default_expansion_search(), //
470
- label_t free_label = default_free_value<label_t>()) {
556
+ /**
557
+ * @brief Adapts the Male-Optimal Stable Marriage algorithm for unequal sets
558
+ * to perform fast one-to-one matching between two large collections
559
+ * of vectors, using approximate nearest neighbors search.
560
+ *
561
+ * @param[inout] first_to_second Container to map ::first labels to ::second.
562
+ * @param[inout] second_to_first Container to map ::second labels to ::first.
563
+ * @param[in] executor Thread-pool to execute the job in parallel.
564
+ * @param[in] progress Callback to report the execution progress.
565
+ */
566
+ template < //
567
+ typename first_to_second_at = dummy_label_to_label_mapping_t, //
568
+ typename second_to_first_at = dummy_label_to_label_mapping_t, //
569
+ typename executor_at = dummy_executor_t, //
570
+ typename progress_at = dummy_progress_t //
571
+ >
572
+ static join_result_t join( //
573
+ index_punned_dense_gt const& first, //
574
+ index_punned_dense_gt const& second, //
575
+ join_config_t config = {}, //
576
+ first_to_second_at&& first_to_second = first_to_second_at{}, //
577
+ second_to_first_at&& second_to_first = second_to_first_at{}, //
578
+ executor_at&& executor = executor_at{}, //
579
+ progress_at&& progress = progress_at{}) noexcept {
471
580
 
472
- return make_( //
473
- dimensions, accuracy, //
474
- config, expansion_add, expansion_search, //
475
- metric_t(metric), make_casts_(accuracy), free_label);
581
+ return index_t::join( //
582
+ *first.typed_, *second.typed_, config, //
583
+ std::forward<first_to_second_at>(first_to_second), //
584
+ std::forward<second_to_first_at>(second_to_first), //
585
+ std::forward<executor_at>(executor), //
586
+ std::forward<progress_at>(progress));
476
587
  }
477
588
 
478
589
  struct copy_result_t {
@@ -486,6 +597,11 @@ class index_punned_dense_gt {
486
597
  }
487
598
  };
488
599
 
600
+ /**
601
+ * @brief Copies the ::index_punned_dense_gt @b with all the data in it.
602
+ * @param config The copy configuration (optional).
603
+ * @return A copy of the ::index_punned_dense_gt instance.
604
+ */
489
605
  copy_result_t copy(copy_config_t config = {}) const {
490
606
  copy_result_t result = fork();
491
607
  if (!result)
@@ -503,6 +619,10 @@ class index_punned_dense_gt {
503
619
  return result;
504
620
  }
505
621
 
622
+ /**
623
+ * @brief Copies the ::index_punned_dense_gt model @b without any data.
624
+ * @return A similarly configured ::index_punned_dense_gt instance.
625
+ */
506
626
  copy_result_t fork() const {
507
627
  copy_result_t result;
508
628
  index_punned_dense_gt& other = result.index;
@@ -527,27 +647,37 @@ class index_punned_dense_gt {
527
647
  return result;
528
648
  }
529
649
 
530
- template < //
531
- typename first_to_second_at = dummy_label_to_label_mapping_t, //
532
- typename second_to_first_at = dummy_label_to_label_mapping_t, //
533
- typename executor_at = dummy_executor_t, //
534
- typename progress_at = dummy_progress_t //
535
- >
536
- static join_result_t join( //
537
- index_punned_dense_gt const& first, //
538
- index_punned_dense_gt const& second, //
539
- join_config_t config = {}, //
540
- first_to_second_at&& first_to_second = first_to_second_at{}, //
541
- second_to_first_at&& second_to_first = second_to_first_at{}, //
542
- executor_at&& executor = executor_at{}, //
543
- progress_at&& progress = progress_at{}) noexcept {
650
+ struct compaction_result_t {
651
+ error_t error{};
652
+ std::size_t pruned_edges{};
544
653
 
545
- return index_t::join( //
546
- *first.typed_, *second.typed_, config, //
547
- std::forward<first_to_second_at>(first_to_second), //
548
- std::forward<second_to_first_at>(second_to_first), //
549
- std::forward<executor_at>(executor), //
550
- std::forward<progress_at>(progress));
654
+ explicit operator bool() const noexcept { return !error; }
655
+ compaction_result_t failed(error_t message) noexcept {
656
+ error = std::move(message);
657
+ return std::move(*this);
658
+ }
659
+ };
660
+
661
+ /**
662
+ * @brief Performs compaction on the index, pruning links to removed entries.
663
+ * @param executor The executor parallel processing. Default ::dummy_executor_t single-threaded.
664
+ * @param progress The progress tracker instance to use. Default ::dummy_progress_t reports nothing.
665
+ * @return The ::compaction_result_t indicating the result of the compaction operation.
666
+ * `result.pruned_edges` will contain the number of edges that were removed.
667
+ * `result.error` will contain an error message if an error occurred during the compaction operation.
668
+ */
669
+ template <typename executor_at = dummy_executor_t, typename progress_at = dummy_progress_t>
670
+ compaction_result_t compact(executor_at&& executor = executor_at{}, progress_at&& progress = progress_at{}) {
671
+ compaction_result_t result;
672
+ std::atomic<std::size_t> pruned_edges;
673
+ auto disallow = [&](member_cref_t const& member) noexcept {
674
+ bool freed = member.label == free_label_;
675
+ pruned_edges += freed;
676
+ return freed;
677
+ };
678
+ typed_->isolate(disallow, std::forward<executor_at>(executor), std::forward<progress_at>(progress));
679
+ result.pruned_edges = pruned_edges;
680
+ return result;
551
681
  }
552
682
 
553
683
  private: