Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b296566
Fix: Eager-reserve thread contexts in `index_dense_gt::make` (#755)
desertfury May 23, 2026
d544745
Fix: Resize cast buffer in `change_metric` for new bytes-per-vector
ashvardanian May 23, 2026
b779c47
Fix: Bounded probe in `equal_iterator_gt::operator++`
ashvardanian May 23, 2026
ad24056
Fix: Report OOM from C-ABI thread-limit changers
ashvardanian May 23, 2026
34889ee
Fix: Stop C-ABI metadata readers on failure
ashvardanian May 23, 2026
2aa0070
Fix: Stop `usearch_init` on `make` failure
ashvardanian May 23, 2026
f3e1052
Fix: Stop JavaScript `Remove` loop after exception throw
ashvardanian May 23, 2026
18c44ee
Fix: Restore `ring_gt::try_push` return value
ashvardanian May 23, 2026
d8be67d
Improve: Release Python GIL during long index operations
ashvardanian May 23, 2026
a598493
Improve: Test GIL-release contract and progress-callback path
ashvardanian May 23, 2026
47528b5
Improve: Serialize concurrent same-`Index` Python access with a mutex
ashvardanian May 23, 2026
9d77be5
Fix: Keep `vectors_lookup_` capacity after `clear()` #759
desertfury May 24, 2026
830f31a
Fix: Short-circuit self-renames (#761)
desertfury May 24, 2026
490c1b2
Fix: Refuse missing metrics in C change-metric API (#760)
desertfury May 24, 2026
0c903f4
Fix: Guard quantized casts against zero-magnitude inputs (#758)
desertfury May 24, 2026
3cf843b
Fix: Preserve hash lookup capacity across thread reserves (#765)
desertfury May 24, 2026
89da7c5
Fix: Checked arithmetic for allocation sizes (#763)
desertfury May 24, 2026
b8d3403
Fix: Refuse operations without reserved thread contexts (#757)
desertfury May 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions c/lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,14 @@ USEARCH_EXPORT usearch_index_t usearch_init(usearch_init_options_t* options, use

using state_result_t = typename index_dense_t::state_result_t;
state_result_t state = index_dense_t::make(metric, config);
if (!state)
if (!state) {
*error = state.error.release();
index_dense_t* result_ptr = new index_dense_t(std::move(state.index));
return NULL;
}
index_dense_t* result_ptr = new (std::nothrow) index_dense_t(std::move(state.index));
if (!result_ptr)
*error = "Out of memory!";

// Let's immediately make it usable by reserving enough threads for this machine:
if (!result_ptr->try_reserve(index_limits_t()))
*error = "Out of memory when preparing contexts!";

return result_ptr;
}

Expand Down Expand Up @@ -236,8 +234,10 @@ USEARCH_EXPORT void usearch_metadata(char const* path, usearch_init_options_t* o

USEARCH_ASSERT(path && options && error && "Missing arguments");
index_dense_metadata_result_t result = index_dense_metadata_from_path(path);
if (!result)
if (!result) {
*error = result.error.release();
return;
}

options->metric_kind = metric_kind_to_c(result.head.kind_metric);
options->quantization = scalar_kind_to_c(result.head.kind_scalar);
Expand Down Expand Up @@ -285,8 +285,10 @@ USEARCH_EXPORT void usearch_metadata_buffer(void const* buffer, size_t length, u
USEARCH_ASSERT(buffer && length && options && error && "Missing arguments");
index_dense_metadata_result_t result =
index_dense_metadata_from_buffer(memory_mapped_file_t((byte_t*)(buffer), length));
if (!result)
if (!result) {
*error = result.error.release();
return;
}

options->metric_kind = metric_kind_to_c(result.head.kind_metric);
options->quantization = scalar_kind_to_c(result.head.kind_scalar);
Expand Down Expand Up @@ -354,23 +356,31 @@ USEARCH_EXPORT void usearch_change_threads_add(usearch_index_t index, size_t thr
auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
index_limits_t limits = index_dense.limits();
limits.threads_add = threads;
index_dense.try_reserve(limits);
if (!index_dense.try_reserve(limits))
*error = "Out of memory!";
}

USEARCH_EXPORT void usearch_change_threads_search(usearch_index_t index, size_t threads, usearch_error_t* error) {
USEARCH_ASSERT(index && error && "Missing arguments");
auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
index_limits_t limits = index_dense.limits();
limits.threads_search = threads;
index_dense.try_reserve(limits);
if (!index_dense.try_reserve(limits))
*error = "Out of memory!";
}

USEARCH_EXPORT void usearch_change_metric_kind(usearch_index_t index, usearch_metric_kind_t kind,
usearch_error_t* error) {
USEARCH_ASSERT(index && error && "Missing arguments");
auto& index_dense = *reinterpret_cast<index_dense_t*>(index);
index_dense.change_metric(
metric_punned_t::builtin(index_dense.dimensions(), metric_kind_to_cpp(kind), index_dense.scalar_kind()));
auto metric_punned =
metric_punned_t::builtin(index_dense.dimensions(), metric_kind_to_cpp(kind), index_dense.scalar_kind());
if (metric_punned.missing()) {
*error = "Unsupported metric for this index's dimensions and scalar kind!";
return;
}
if (!index_dense.try_change_metric(std::move(metric_punned)))
*error = "Failed to grow cast buffer for the new metric!";
}

USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_t metric, void* state,
Expand All @@ -384,7 +394,12 @@ USEARCH_EXPORT void usearch_change_metric(usearch_index_t index, usearch_metric_
: metric_punned_t::stateless(index_dense.dimensions(), reinterpret_cast<std::uintptr_t>(metric),
metric_punned_signature_t::array_array_k, metric_kind_to_cpp(kind),
index_dense.scalar_kind());
index_dense.change_metric(std::move(metric_punned));
if (metric_punned.missing()) {
*error = "Unsupported metric for this index's dimensions and scalar kind!";
return;
}
if (!index_dense.try_change_metric(std::move(metric_punned)))
*error = "Failed to grow cast buffer for the new metric!";
}

USEARCH_EXPORT void usearch_reserve(usearch_index_t index, size_t capacity, usearch_error_t* error) {
Expand Down
99 changes: 99 additions & 0 deletions cpp/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <csignal> // `std::signal`, `SIGSEGV`, ...
#include <cstdio> // `std::fprintf`
#include <cstdlib> // `std::_Exit`
#include <limits> // `std::numeric_limits`

#include <algorithm> // `std::shuffle`
#include <random> // `std::default_random_engine`
Expand Down Expand Up @@ -216,6 +217,47 @@ void test_uint40() {
expect_eq(u40_default, uint40_t(0u));
}

void test_checked_size_arithmetic() {

std::printf("Testing checked size arithmetic\n");
std::size_t max = (std::numeric_limits<std::size_t>::max)();

checked_size_result_t cast = checked_size_from_u64(42);
expect(cast);
expect_eq(cast.value, static_cast<std::size_t>(42));
if (sizeof(std::size_t) < sizeof(std::uint64_t))
expect(!checked_size_from_u64((std::numeric_limits<std::uint64_t>::max)()));

checked_size_result_t sum = checked_add(max - 1, 1);
expect(sum);
expect_eq(sum.value, max);
expect(!checked_add(max, 1));

checked_size_result_t product = checked_mul(max / 2, 2);
expect(product);
expect_eq(product.value, max - 1);
expect(!checked_mul(max / 2 + 1, 2));

checked_size_result_t fused = checked_mul_add(10, 20, 30);
expect(fused);
expect_eq(fused.value, static_cast<std::size_t>(230));
expect(!checked_mul_add(max / 2 + 1, 2, 0));
expect(!checked_mul_add(max / 2, 2, 2));

checked_size_result_t rounded = checked_round_up(17, 8);
expect(rounded);
expect_eq(rounded.value, static_cast<std::size_t>(24));
expect(!checked_round_up(max, 8));

checked_size_result_t power = checked_ceil2(17);
expect(power);
expect_eq(power.value, static_cast<std::size_t>(32));
checked_size_result_t zero_power = checked_ceil2(0);
expect(zero_power);
expect_eq(zero_power.value, static_cast<std::size_t>(0));
expect(!checked_ceil2(max));
}

/**
* @brief Tests the functionality of the custom float16_t type ensuring consistent.
*/
Expand Down Expand Up @@ -1264,6 +1306,61 @@ static void install_crash_handlers() {
std::signal(signal_number, &usearch_crash_handler);
}

/**
* @brief Regression test: `make(metric, config)` must return an index that is
* immediately usable - no explicit `reserve` required before `load` /
* `view` / `search`. Previously the typed graph's `{0, 0}` thread
* limits leaked into `load_from_stream`, leaving `available_threads_`
* empty and making the first `search` throw "No available threads to
* lock".
*/
void test_load_after_metric_make() {
std::printf("Testing load and view into a metric-made index\n");

using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
std::size_t const dimensions = 32;
std::size_t const collection = 64;

std::default_random_engine rng(7);
std::uniform_real_distribution<float> distribution(-1.f, 1.f);
std::vector<std::vector<float>> data(collection);
for (auto& vector : data) {
vector.resize(dimensions);
for (auto& value : vector)
value = distribution(rng);
}

metric_punned_t metric(dimensions, metric_kind_t::cos_k, scalar_kind<f32_t>());
index_dense_config_t config(16);

// Build an index and persist it to disk.
index_t::state_result_t built = index_t::make(metric, config);
expect(built);
expect(built.index.try_reserve(collection));
for (std::size_t i = 0; i != collection; ++i)
expect(built.index.add(static_cast<std::int64_t>(i), data[i].data()));
char const* path = "tmp_metric_make.usearch";
expect(built.index.save(path));

// Load into a fresh, metric-made index that was never explicitly reserved.
// The first `search` must not throw "No available threads to lock".
index_t::state_result_t loaded = index_t::make(metric, config);
expect(loaded);
expect(loaded.index.load(path));
expect_eq(loaded.index.size(), collection);
std::int64_t found[8];
expect(loaded.index.search(data[0].data(), 5).dump_to(found) != 0);

// Same check for the memory-mapped `view` path.
index_t::state_result_t viewed = index_t::make(metric, config);
expect(viewed);
expect(viewed.index.view(path));
expect_eq(viewed.index.size(), collection);
expect(viewed.index.search(data[0].data(), 5).dump_to(found) != 0);

std::remove(path);
}

int main(int, char**) {
install_crash_handlers();

Expand All @@ -1272,6 +1369,7 @@ int main(int, char**) {

// Non-default floating-point types may result in many compilation & rounding issues.
test_uint40();
test_checked_size_arithmetic();
test_cosine<f32_t, std::int64_t, uint40_t>(10, 10);
test_cosine<bf16_t, std::int64_t, uint40_t>(10, 10);
test_cosine<f16_t, std::int64_t, uint40_t>(10, 10);
Expand Down Expand Up @@ -1354,5 +1452,6 @@ int main(int, char**) {

test_filtered_search();
test_isolate();
test_load_after_metric_make();
return 0;
}
Loading
Loading