Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 18 additions & 17 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,19 +151,21 @@ else()
set(DISABLE_EXCEPTIONS_FLAGS -fno-exceptions)
endif()

# Turn on assertions in the RelWithDebInfo build type.
foreach(NDEBUG_FLAG_STR IN ITEMS "/DNDEBUG" "/D NDEBUG" "-DNDEBUG")
string(REPLACE "${NDEBUG_FLAG_STR}" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO
"${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
endforeach()
string(STRIP "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}" CMAKE_CXX_FLAGS_RELWITHDEBINFO)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}"
CACHE STRING "Flags for RelWithDebInfo configuration." FORCE)

# Start with an empty value of CMAKE_CXX_FLAGS, not the value from the cache.
# It will not override any "default" flags -- those will come from
# per-build-type variables (CMAKE_CXX_FLAGS_${CMAKE_BUILD_TYPE}).
set(CMAKE_CXX_FLAGS "")
# RelWithDebInfo assertions apply only when this repo is the top-level
# project. Forcing the cache from an add_subdirectory() would mutate the
# parent build.
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
foreach(NDEBUG_FLAG_STR IN ITEMS "/DNDEBUG" "/D NDEBUG" "-DNDEBUG")
string(REPLACE "${NDEBUG_FLAG_STR}" "" CMAKE_CXX_FLAGS_RELWITHDEBINFO
"${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
endforeach()
string(STRIP "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}" CMAKE_CXX_FLAGS_RELWITHDEBINFO)
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO}"
CACHE STRING "Flags for RelWithDebInfo configuration." FORCE)
endif()

# Do not wipe or FORCE-write CMAKE_CXX_FLAGS. That erased caller-supplied
# flags even when HNSWLIB_EXAMPLES=OFF (add_subdirectory consumers).

if(HNSWLIB_EXAMPLES)
message("Building examples and tests")
Expand Down Expand Up @@ -274,7 +276,6 @@ if(HNSWLIB_EXAMPLES)
endforeach()
endif()

# Persist CMAKE_CXX_FLAGS in the cache for debuggability.
string(STRIP "${CMAKE_CXX_FLAGS}" CMAKE_CXX_FLAGS)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}"
CACHE STRING "Flags used by the CXX compiler during all build types." FORCE)
# Do not persist CMAKE_CXX_FLAGS into the CMake cache. A FORCE write from
# this directory would overwrite the parent project's (or the user's
# -DCMAKE_CXX_FLAGS=) value, including when examples are disabled.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ Header-only C++ HNSW implementation with python bindings, insertions and updates

**NEWS:**

**version 0.10.0**
**version 0.10.0rc2** (release candidate — not a stable PyPI upload)

* Package version is `0.10.0rc2` (PEP 440); do not upload as final `0.10.0` yet. Supersedes v0.10.0-rc.1.
* Stream `loadIndexNoExceptions` fails closed on an unopened/failed input without clearing a live index
* `searchKnnCloserFirst` is `const` again; `addPoint(..., int level)` is restored so integer levels are not treated as `replace_deleted`
* CMake no longer wipes caller `CMAKE_CXX_FLAGS` (including `add_subdirectory` / examples-off)
* `StatusOr` stores T inline and moves the result; `*NoExceptions` stream writes return `Status` instead of throwing on write failure
* Optional no-exceptions C++ API: `*NoExceptions` methods return `Status` / `StatusOr` so the headers can be compiled with `-fno-exceptions` (`-DHNSWLIB_ENABLE_EXCEPTIONS=OFF`). Throwing methods remain the default. (#619, #678) by [@michaelbautin](https://github.com/michaelbautin)
* Stream `saveIndex` / `loadIndex` over `std::ostream` / `std::istream`, plus `getInternalIdByLabel`
* CI covers exceptions on/off, Clang / GCC / MSVC, and ASAN / UBSAN
Expand Down
34 changes: 23 additions & 11 deletions hnswlib/bruteforce.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,28 +132,40 @@ class BruteforceSearch : public AlgorithmInterface<dist_t> {


Status saveIndexNoExceptions(std::ostream &output) {
writeBinaryPOD(output, maxelements_);
writeBinaryPOD(output, size_per_element_);
writeBinaryPOD(output, cur_element_count);
if (!output.good()) {
return Status("Failed writing index metadata");
}
StreamExceptionsOff guard(output);
return invokeWithoutStreamThrow([&]() -> Status {
if (!output) {
return Status("Cannot save index: output stream is not open or in a failed state");
}
writeBinaryPOD(output, maxelements_);
writeBinaryPOD(output, size_per_element_);
writeBinaryPOD(output, cur_element_count);
if (!output.good()) {
return Status("Failed writing index metadata");
}

output.write(data_, maxelements_ * size_per_element_);
if (!output.good()) {
return Status("Failed writing vector data");
}
return OkStatus();
output.write(data_, maxelements_ * size_per_element_);
if (!output.good()) {
return Status("Failed writing vector data");
}
return OkStatus();
});
}


Status saveIndexNoExceptions(const std::string &location) override {
std::ofstream output(location, std::ios::binary);
if (!output.is_open()) {
return Status("Cannot save index: failed to open output file");
}
return saveIndexNoExceptions(output);
}


void loadIndex(std::istream &input, SpaceInterface<dist_t> *s) {
if (!input) {
HNSWLIB_THROW_RUNTIME_ERROR("Cannot load index: input stream is not open or not readable");
}
readBinaryPOD(input, maxelements_);
readBinaryPOD(input, size_per_element_);
readBinaryPOD(input, cur_element_count);
Expand Down
118 changes: 83 additions & 35 deletions hnswlib/hnswalg.h
Original file line number Diff line number Diff line change
Expand Up @@ -722,45 +722,54 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
}

Status saveIndexNoExceptions(std::ostream &output) {
writeBinaryPOD(output, offsetLevel0_);
writeBinaryPOD(output, max_elements_);
writeBinaryPOD(output, cur_element_count);
writeBinaryPOD(output, size_data_per_element_);
writeBinaryPOD(output, label_offset_);
writeBinaryPOD(output, offsetData_);
writeBinaryPOD(output, maxlevel_);
writeBinaryPOD(output, enterpoint_node_);
writeBinaryPOD(output, maxM_);

writeBinaryPOD(output, maxM0_);
writeBinaryPOD(output, M_);
writeBinaryPOD(output, mult_);
writeBinaryPOD(output, ef_construction_);

if (!output.good()) {
return Status("Failed writing index metadata");
}

output.write(data_level0_memory_, cur_element_count * size_data_per_element_);
if (!output.good()) {
return Status("Failed writing level 0 memory block");
}
StreamExceptionsOff guard(output);
return invokeWithoutStreamThrow([&]() -> Status {
if (!output) {
return Status("Cannot save index: output stream is not open or in a failed state");
}
writeBinaryPOD(output, offsetLevel0_);
writeBinaryPOD(output, max_elements_);
writeBinaryPOD(output, cur_element_count);
writeBinaryPOD(output, size_data_per_element_);
writeBinaryPOD(output, label_offset_);
writeBinaryPOD(output, offsetData_);
writeBinaryPOD(output, maxlevel_);
writeBinaryPOD(output, enterpoint_node_);
writeBinaryPOD(output, maxM_);

writeBinaryPOD(output, maxM0_);
writeBinaryPOD(output, M_);
writeBinaryPOD(output, mult_);
writeBinaryPOD(output, ef_construction_);

for (size_t i = 0; i < cur_element_count; i++) {
unsigned int linkListSize = element_levels_[i] > 0 ? size_links_per_element_ * element_levels_[i] : 0;
writeBinaryPOD(output, linkListSize);
if (linkListSize) {
output.write(linkLists_[i], linkListSize);
if (!output.good()) {
return Status("Failed writing index metadata");
}

output.write(data_level0_memory_, cur_element_count * size_data_per_element_);
if (!output.good()) {
return Status("Failed writing link list elements");
return Status("Failed writing level 0 memory block");
}
}
return OkStatus();

for (size_t i = 0; i < cur_element_count; i++) {
unsigned int linkListSize = element_levels_[i] > 0 ? size_links_per_element_ * element_levels_[i] : 0;
writeBinaryPOD(output, linkListSize);
if (linkListSize) {
output.write(linkLists_[i], linkListSize);
}
if (!output.good()) {
return Status("Failed writing link list elements");
}
}
return OkStatus();
});
}

Status saveIndexNoExceptions(const std::string &location) override {
std::ofstream output(location, std::ios::binary);
if (!output.is_open()) {
return Status("Cannot save index: failed to open output file");
}
return saveIndexNoExceptions(output);
}

Expand All @@ -772,11 +781,31 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
}

Status loadIndexNoExceptions(std::istream &input, SpaceInterface<dist_t> *s, size_t max_elements_i = 0) {
clear();
// get file size:
// Must not destroy the live index until the stream is known to be readable.
// An unopened / failed stream used to seek to -1, skip the empty-index
// corruption loop, and return OkStatus() after clear().
if (!input) {
return Status("Cannot load index: input stream is not open or not readable");
}

StreamExceptionsOff guard(input);
return invokeWithoutStreamThrow([&]() -> Status {
// Default-constructed ifstreams are often still good() on libc++.
// If we cannot peek a byte, there is no index to load — leave the
// live index untouched.
if (input.peek() == std::char_traits<char>::eof()) {
return Status("Cannot load index: input stream is not open or not readable");
}

// get file size before mutating the in-memory index:
input.seekg(0, input.end);
std::streampos total_filesize = input.tellg();
input.seekg(0, input.beg);
if (!input || total_filesize < std::streampos(0)) {
return Status("Cannot load index: failed to determine stream size");
}

clear();

readBinaryPOD(input, offsetLevel0_);
readBinaryPOD(input, max_elements_);
Expand All @@ -797,6 +826,9 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
readBinaryPOD(input, M_);
readBinaryPOD(input, mult_);
readBinaryPOD(input, ef_construction_);
if (!input) {
return Status("Cannot load index: failed to read index header");
}

data_size_ = s->get_data_size();
fstdistfunc_ = s->get_dist_func();
Expand Down Expand Up @@ -870,6 +902,7 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
}

return OkStatus();
});
}


Expand Down Expand Up @@ -898,7 +931,7 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
if (!result.ok()) {
HNSWLIB_THROW_RUNTIME_ERROR(result.status().message());
}
return std::move(result.value());
return std::move(result).value();
}

template<typename data_t>
Expand Down Expand Up @@ -1057,6 +1090,21 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
return OkStatus();
}

// Keep the bool addPoint from AlgorithmInterface visible. Without this,
// the integer-level overload below would hide it and `addPoint(p, id, 1)`
// would bind to replace_deleted=true.
using AlgorithmInterface<dist_t>::addPoint;

// Historic overload: insert at an explicit graph level.
// `addPoint(data, label, 1)` must not become replace_deleted=true.
tableint addPoint(const void *data_point, labeltype label, int level) {
auto result = addPointWithLevel(data_point, label, level);
if (!result.ok()) {
HNSWLIB_THROW_RUNTIME_ERROR(result.status().message());
}
return std::move(result).value();
}


Status updatePoint(const void *dataPoint, tableint internalId, float updateNeighborProbability) {
// update the feature vector associated with existing point with new vector
Expand Down Expand Up @@ -1434,7 +1482,7 @@ class HierarchicalNSW : public AlgorithmInterface<dist_t> {
if (!result.ok()) {
HNSWLIB_THROW_RUNTIME_ERROR(result.status().message());
}
return std::move(result.value());
return std::move(result).value();
}

StatusOr<DistanceLabelVector>
Expand Down
Loading