usearch 2.26.0 → 2.26.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/README.md +2 -2
- package/cpp/test.cpp +66 -3
- package/include/usearch/index.hpp +5 -1
- package/include/usearch/index_dense.hpp +16 -5
- package/package.json +1 -1
- package/prebuilds/darwin-arm64+x64/usearch.node +0 -0
- package/prebuilds/linux-arm64/usearch.node +0 -0
- package/prebuilds/linux-x64/usearch.node +0 -0
- package/prebuilds/win32-x64/usearch.node +0 -0
package/README.md
CHANGED
|
@@ -545,11 +545,11 @@ index = Index(ndim=ndim, metric=CompiledMetric(
|
|
|
545
545
|
|
|
546
546
|
```bibtex
|
|
547
547
|
@software{Vardanian_USearch,
|
|
548
|
-
doi = {10.5281/zenodo.
|
|
548
|
+
doi = {10.5281/zenodo.7949415},
|
|
549
549
|
author = {Vardanian, Ash},
|
|
550
550
|
title = {{USearch by Unum Cloud}},
|
|
551
551
|
url = {https://github.com/unum-cloud/USearch},
|
|
552
|
-
version = {2.26.
|
|
552
|
+
version = {2.26.1},
|
|
553
553
|
year = {2026},
|
|
554
554
|
}
|
|
555
555
|
```
|
package/cpp/test.cpp
CHANGED
|
@@ -1112,9 +1112,6 @@ template <typename key_at, typename slot_at> void test_strings() {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
}
|
|
1114
1114
|
|
|
1115
|
-
/**
|
|
1116
|
-
* @brief Tests replacing and updating entries in index_dense_gt to ensure consistency after modifications.
|
|
1117
|
-
*/
|
|
1118
1115
|
/**
|
|
1119
1116
|
* @brief Churns a tightly-reserved index, so `slot_lookup_` accumulates tombstones.
|
|
1120
1117
|
*
|
|
@@ -1181,6 +1178,10 @@ template <typename key_at, typename slot_at> void test_slot_lookup_churn() {
|
|
|
1181
1178
|
expect(copy.contains(key));
|
|
1182
1179
|
}
|
|
1183
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* @brief Tests replacing and updating entries in `index_dense_gt`, to ensure consistency
|
|
1183
|
+
* after modifications.
|
|
1184
|
+
*/
|
|
1184
1185
|
template <typename key_at, typename slot_at> void test_replacing_update() {
|
|
1185
1186
|
|
|
1186
1187
|
using vector_key_t = key_at;
|
|
@@ -1427,6 +1428,67 @@ void test_load_after_metric_make() {
|
|
|
1427
1428
|
std::remove(path);
|
|
1428
1429
|
}
|
|
1429
1430
|
|
|
1431
|
+
/**
|
|
1432
|
+
* @brief Feeds `view` a truncated index file.
|
|
1433
|
+
*
|
|
1434
|
+
* The matrix dimensions and the node count come from the file itself, so a short file
|
|
1435
|
+
* describes a matrix and a level array that reach past the mapping. Both must be rejected
|
|
1436
|
+
* rather than walked, and a caller-supplied offset past the end must fail the same way.
|
|
1437
|
+
*/
|
|
1438
|
+
void test_view_of_truncated_file() {
|
|
1439
|
+
std::printf("Testing view of a truncated index file\n");
|
|
1440
|
+
|
|
1441
|
+
using index_t = index_dense_gt<std::int64_t, std::uint32_t>;
|
|
1442
|
+
std::size_t const dimensions = 64;
|
|
1443
|
+
std::size_t const collection = 2048;
|
|
1444
|
+
char const* path = "tmp_truncated.usearch";
|
|
1445
|
+
|
|
1446
|
+
metric_punned_t metric(dimensions, metric_kind_t::l2sq_k, scalar_kind<f32_t>());
|
|
1447
|
+
index_t::state_result_t built = index_t::make(metric);
|
|
1448
|
+
expect(built);
|
|
1449
|
+
expect(built.index.try_reserve(collection));
|
|
1450
|
+
std::vector<float> vector(dimensions);
|
|
1451
|
+
for (std::size_t i = 0; i != collection; ++i) {
|
|
1452
|
+
for (std::size_t d = 0; d != dimensions; ++d)
|
|
1453
|
+
vector[d] = static_cast<float>((i * 31 + d) % 997) / 997.f;
|
|
1454
|
+
expect(built.index.add(static_cast<std::int64_t>(i), vector.data()));
|
|
1455
|
+
}
|
|
1456
|
+
expect(built.index.save(path));
|
|
1457
|
+
|
|
1458
|
+
std::vector<char> prefix;
|
|
1459
|
+
{
|
|
1460
|
+
std::FILE* file = std::fopen(path, "rb");
|
|
1461
|
+
expect(file != nullptr);
|
|
1462
|
+
std::fseek(file, 0, SEEK_END);
|
|
1463
|
+
long const bytes = std::ftell(file);
|
|
1464
|
+
std::fseek(file, 0, SEEK_SET);
|
|
1465
|
+
prefix.resize(static_cast<std::size_t>(bytes) / 8);
|
|
1466
|
+
expect(std::fread(prefix.data(), 1, prefix.size(), file) == prefix.size());
|
|
1467
|
+
std::fclose(file);
|
|
1468
|
+
}
|
|
1469
|
+
{
|
|
1470
|
+
std::FILE* file = std::fopen(path, "wb");
|
|
1471
|
+
expect(file != nullptr);
|
|
1472
|
+
std::fwrite(prefix.data(), 1, prefix.size(), file);
|
|
1473
|
+
std::fclose(file);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
index_t::state_result_t viewed = index_t::make(metric);
|
|
1477
|
+
expect(viewed);
|
|
1478
|
+
serialization_result_t truncated = viewed.index.view(path);
|
|
1479
|
+
expect(!truncated);
|
|
1480
|
+
truncated.error.release();
|
|
1481
|
+
|
|
1482
|
+
// A caller-supplied offset past the end must fail the same way.
|
|
1483
|
+
index_t::state_result_t offset_viewed = index_t::make(metric);
|
|
1484
|
+
expect(offset_viewed);
|
|
1485
|
+
serialization_result_t beyond = offset_viewed.index.view(memory_mapped_file_t(path), prefix.size() * 4);
|
|
1486
|
+
expect(!beyond);
|
|
1487
|
+
beyond.error.release();
|
|
1488
|
+
|
|
1489
|
+
std::remove(path);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1430
1492
|
int main(int, char**) {
|
|
1431
1493
|
install_crash_handlers();
|
|
1432
1494
|
|
|
@@ -1523,5 +1585,6 @@ int main(int, char**) {
|
|
|
1523
1585
|
test_filtered_search();
|
|
1524
1586
|
test_isolate();
|
|
1525
1587
|
test_load_after_metric_make();
|
|
1588
|
+
test_view_of_truncated_file();
|
|
1526
1589
|
return 0;
|
|
1527
1590
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
#define USEARCH_VERSION_MAJOR 2
|
|
11
11
|
#define USEARCH_VERSION_MINOR 26
|
|
12
|
-
#define USEARCH_VERSION_PATCH
|
|
12
|
+
#define USEARCH_VERSION_PATCH 1
|
|
13
13
|
|
|
14
14
|
// Inferring C++ version
|
|
15
15
|
// https://stackoverflow.com/a/61552074
|
|
@@ -3970,6 +3970,10 @@ class index_gt {
|
|
|
3970
3970
|
: checked_size_overflow();
|
|
3971
3971
|
if (!first_offset)
|
|
3972
3972
|
return result.failed("Index is too large");
|
|
3973
|
+
if (file.size() < first_offset.value) {
|
|
3974
|
+
reset();
|
|
3975
|
+
return result.failed("File is corrupted and can't fit the node levels");
|
|
3976
|
+
}
|
|
3973
3977
|
offsets[0u] = first_offset.value;
|
|
3974
3978
|
for (std::size_t i = 1; i < header_size.value; ++i) {
|
|
3975
3979
|
checked_size_result_t next_offset = checked_add(offsets[i - 1], node_bytes_(levels[i - 1]));
|
|
@@ -1050,17 +1050,18 @@ class index_dense_gt {
|
|
|
1050
1050
|
*/
|
|
1051
1051
|
bool try_reserve(index_limits_t limits) {
|
|
1052
1052
|
|
|
1053
|
+
// Reserving is monotonic: reducing thread limits must not shrink member-addressed storage.
|
|
1054
|
+
limits.members = (std::max)(limits.members, typed_->capacity());
|
|
1055
|
+
limits.members = (std::max)(limits.members, vectors_lookup_.size());
|
|
1056
|
+
|
|
1053
1057
|
// The slot lookup system will generally prefer power-of-two sizes.
|
|
1054
1058
|
if (config_.enable_key_lookups) {
|
|
1055
1059
|
unique_lock_t lock(slot_lookup_mutex_);
|
|
1056
1060
|
if (!slot_lookup_.try_reserve(limits.members))
|
|
1057
1061
|
return false;
|
|
1058
|
-
limits.members = slot_lookup_.capacity();
|
|
1059
1062
|
}
|
|
1060
1063
|
|
|
1061
|
-
//
|
|
1062
|
-
// target for the `vectors_lookup_` to synchronize allocations and
|
|
1063
|
-
// expensive index re-organizations.
|
|
1064
|
+
// Hash-table load-factor slack does not need corresponding vector or graph slots.
|
|
1064
1065
|
if (limits.members != vectors_lookup_.size()) {
|
|
1065
1066
|
vectors_lookup_t new_vectors_lookup(limits.members);
|
|
1066
1067
|
if (!new_vectors_lookup)
|
|
@@ -1358,6 +1359,11 @@ class index_dense_gt {
|
|
|
1358
1359
|
if (!result)
|
|
1359
1360
|
return result;
|
|
1360
1361
|
|
|
1362
|
+
// `offset` is caller-supplied, so every `file.size() - offset` below would
|
|
1363
|
+
// otherwise underflow into a huge span instead of failing.
|
|
1364
|
+
if (offset > file.size())
|
|
1365
|
+
return result.failed("File is corrupted and lacks matrix dimensions");
|
|
1366
|
+
|
|
1361
1367
|
// Infer the new index size
|
|
1362
1368
|
std::uint64_t matrix_rows = 0;
|
|
1363
1369
|
std::uint64_t matrix_cols = 0;
|
|
@@ -1383,7 +1389,12 @@ class index_dense_gt {
|
|
|
1383
1389
|
matrix_cols = dimensions[1];
|
|
1384
1390
|
offset += sizeof(dimensions);
|
|
1385
1391
|
}
|
|
1386
|
-
|
|
1392
|
+
// bound the vectors matrix span against the file (overflow-safe; offset <= file.size() holds here)
|
|
1393
|
+
checked_size_result_t vectors_bytes =
|
|
1394
|
+
checked_mul(static_cast<std::size_t>(matrix_rows), static_cast<std::size_t>(matrix_cols));
|
|
1395
|
+
if (!vectors_bytes || file.size() - offset < vectors_bytes.value)
|
|
1396
|
+
return result.failed("File is corrupted: vectors matrix exceeds file size");
|
|
1397
|
+
vectors_buffer = {file.data() + offset, vectors_bytes.value};
|
|
1387
1398
|
offset += vectors_buffer.size();
|
|
1388
1399
|
}
|
|
1389
1400
|
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|