-
Notifications
You must be signed in to change notification settings - Fork 112
fix(hgraph): backport duplicate handling to v0.18 #2615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 0.18
Are you sure you want to change the base?
Changes from 8 commits
9874492
0c7edee
1f46ea5
34bfc8a
31b32f8
7b796f2
8a4a409
27a9ff4
306bc89
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,8 @@ | |
|
|
||
| #include "hgraph_parameter.h" | ||
|
|
||
| #include <nlohmann/json.hpp> | ||
|
|
||
| #include "datacell/extra_info_datacell_parameter.h" | ||
| #include "datacell/flatten_datacell_parameter.h" | ||
| #include "datacell/graph_datacell_parameter.h" | ||
|
|
@@ -223,6 +225,22 @@ HGraphSearchParameters::FromJson(const std::string& json_string) { | |
| if (params[INDEX_TYPE_HGRAPH].Contains("min_distance")) { | ||
| obj.min_distance = params[INDEX_TYPE_HGRAPH]["min_distance"].GetFloat(); | ||
| } | ||
| if (params[INDEX_TYPE_HGRAPH].Contains(HGRAPH_PARAMETER_MAX_DUPLICATES_PER_GROUP)) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The if (ef_search_json.IsNumberUnsigned()) {
CHECK_ARGUMENT(ef_search_json.GetUint64() <= ...);
}Including Suggested fix: remove the |
||
| const auto& max_duplicates = | ||
| params[INDEX_TYPE_HGRAPH][HGRAPH_PARAMETER_MAX_DUPLICATES_PER_GROUP]; | ||
| CHECK_ARGUMENT(max_duplicates.IsNumberInteger(), | ||
| "max_duplicates_per_group must be an integer"); | ||
| const auto* max_duplicates_json = max_duplicates.GetInnerJson(); | ||
| if (max_duplicates_json->is_number_unsigned()) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] The unsigned overflow check via |
||
| CHECK_ARGUMENT(max_duplicates_json->get<uint64_t>() <= | ||
| static_cast<uint64_t>(std::numeric_limits<int64_t>::max()), | ||
| "max_duplicates_per_group exceeds int64_t range"); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] Direct use of nlohmann::json internals via GetInnerJson() bypasses the project's JsonType wrapper abstraction. The #include <nlohmann/json.hpp> is added solely for is_number_unsigned() and get<uint64_t>(). Consider adding IsUnsignedInteger() / GetUint64() to JsonType to keep the JSON library dependency encapsulated, or use GetInt() with a separate range check for negative values. |
||
| obj.max_duplicates_per_group = max_duplicates.GetInt(); | ||
| CHECK_ARGUMENT(obj.max_duplicates_per_group >= -1, | ||
| fmt::format("max_duplicates_per_group({}) must be >= -1", | ||
| obj.max_duplicates_per_group)); | ||
| } | ||
|
|
||
| return obj; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| #include <mutex> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] In Consider adding an explicit |
||
| #include <tuple> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #include "storage/stream_reader.h" | ||
| #include "storage/stream_writer.h" | ||
|
|
@@ -284,23 +285,28 @@ class LabelTable { | |
| Serialize(StreamWriter& writer) const { | ||
| StreamWriter::WriteVector(writer, label_table_); | ||
| if (compress_duplicate_data_) { | ||
| StreamWriter::WriteObj(writer, duplicate_count_); | ||
| for (InnerIdType i = 0; i < label_table_.size(); ++i) { | ||
| if (duplicate_records_[i] != nullptr) { | ||
| StreamWriter::WriteObj(writer, i); | ||
| Vector<InnerIdType> id_list(allocator_); | ||
| for (const auto& duplicate_id : duplicate_records_[i]->duplicate_ids) { | ||
| id_list.push_back(duplicate_id); | ||
| } | ||
| StreamWriter::WriteVector(writer, id_list); | ||
| } | ||
| } | ||
| this->SerializeDuplicateRecords(writer); | ||
| } | ||
| if (support_tombstone_) { | ||
| StreamWriter::WriteObj(writer, deleted_ids_); | ||
| } | ||
| } | ||
|
|
||
| void | ||
| SerializeDuplicateRecords(StreamWriter& writer) const { | ||
| StreamWriter::WriteObj(writer, duplicate_count_); | ||
| for (InnerIdType i = 0; i < label_table_.size(); ++i) { | ||
| if (duplicate_records_[i] != nullptr) { | ||
| StreamWriter::WriteObj(writer, i); | ||
| Vector<InnerIdType> id_list(allocator_); | ||
| for (const auto& duplicate_id : duplicate_records_[i]->duplicate_ids) { | ||
| id_list.push_back(duplicate_id); | ||
| } | ||
| StreamWriter::WriteVector(writer, id_list); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void | ||
| Deserialize(lvalue_or_rvalue<StreamReader> reader) { | ||
| StreamReader::ReadVector(reader, label_table_); | ||
|
|
@@ -312,25 +318,107 @@ class LabelTable { | |
| } | ||
| } | ||
| if (compress_duplicate_data_) { | ||
| StreamReader::ReadObj(reader, duplicate_count_); | ||
| duplicate_records_.resize(label_table_.size(), nullptr); | ||
| for (InnerIdType i = 0; i < duplicate_count_; ++i) { | ||
| InnerIdType id; | ||
| StreamReader::ReadObj<InnerIdType>(reader, id); | ||
| duplicate_records_[id] = allocator_->New<DuplicateRecord>(allocator_); | ||
| Vector<InnerIdType> id_list(allocator_); | ||
| StreamReader::ReadVector(reader, id_list); | ||
| for (const auto& duplicate_id : id_list) { | ||
| duplicate_records_[id]->duplicate_ids.insert(duplicate_id); | ||
| } | ||
| } | ||
| this->DeserializeDuplicateRecords(reader, label_table_.size()); | ||
|
jac0626 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The native-format HGraph resizes The v0.14 path already does this correctly: const auto logical_element_count = static_cast<uint64_t>(this->label_table_->GetTotalCount());
this->label_table_->DeserializeDuplicateRecords(reader, logical_element_count);Consider using
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P2] Native path passes max_capacity_ instead of logical element count In the native (v0.15+) deserialization path, The v0.14 path correctly uses This is a defense-in-depth concern.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] The native-format deserialization path in This means duplicate validation in the native path accepts IDs up to Consider either:
|
||
| } | ||
| if (support_tombstone_) { | ||
| StreamReader::ReadObj(reader, deleted_ids_); | ||
| } | ||
| this->total_count_.store(label_table_.size()); | ||
| } | ||
|
|
||
| void | ||
| DeserializeDuplicateRecords(lvalue_or_rvalue<StreamReader> reader, | ||
| uint64_t logical_element_count) { | ||
| if (logical_element_count > label_table_.size()) { | ||
| throw VsagException(ErrorType::INVALID_BINARY, | ||
| fmt::format("logical element count {} exceeds label capacity {}", | ||
| logical_element_count, | ||
| label_table_.size())); | ||
| } | ||
|
|
||
| uint64_t duplicate_count = 0; | ||
| StreamReader::ReadObj(reader, duplicate_count); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] The validation |
||
| if (duplicate_count > logical_element_count / 2) { | ||
| throw VsagException( | ||
| ErrorType::INVALID_BINARY, | ||
| fmt::format("duplicate group count {} exceeds logical element limit {}", | ||
| duplicate_count, | ||
| logical_element_count / 2)); | ||
| } | ||
|
|
||
| std::vector<std::pair<InnerIdType, Vector<InnerIdType>>> duplicate_groups; | ||
|
jac0626 marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The While this is a local temporary that is immediately consumed and moved into allocator-managed storage, using
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The |
||
| duplicate_groups.reserve(duplicate_count); | ||
| UnorderedSet<InnerIdType> assigned_ids(allocator_); | ||
| for (uint64_t i = 0; i < duplicate_count; ++i) { | ||
| InnerIdType id; | ||
| StreamReader::ReadObj<InnerIdType>(reader, id); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] This is not a blocker — the final |
||
| if (id >= logical_element_count) { | ||
| throw VsagException(ErrorType::INVALID_BINARY, | ||
| fmt::format("duplicate head id {} exceeds logical element " | ||
| "count {}", | ||
| id, | ||
| logical_element_count)); | ||
| } | ||
| if (not assigned_ids.insert(id).second) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] This is acceptable since |
||
| throw VsagException(ErrorType::INVALID_BINARY, | ||
| fmt::format("id {} belongs to multiple duplicate groups", id)); | ||
| } | ||
|
|
||
| uint64_t member_count = 0; | ||
| StreamReader::ReadObj(reader, member_count); | ||
| const auto assigned_count = static_cast<uint64_t>(assigned_ids.size()); | ||
| if (member_count == 0 or member_count > logical_element_count - assigned_count) { | ||
| throw VsagException( | ||
| ErrorType::INVALID_BINARY, | ||
| fmt::format("duplicate member count {} exceeds remaining logical element " | ||
| "count {}", | ||
| member_count, | ||
| logical_element_count - assigned_count)); | ||
| } | ||
|
|
||
| Vector<InnerIdType> id_list(allocator_); | ||
| id_list.resize(member_count); | ||
| reader->Read(reinterpret_cast<char*>(id_list.data()), | ||
| member_count * sizeof(InnerIdType)); | ||
| for (const auto duplicate_id : id_list) { | ||
| if (duplicate_id >= logical_element_count) { | ||
| throw VsagException( | ||
| ErrorType::INVALID_BINARY, | ||
| fmt::format("duplicate member id {} exceeds logical element count {}", | ||
| duplicate_id, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] The catch block calls |
||
| logical_element_count)); | ||
| } | ||
| if (not assigned_ids.insert(duplicate_id).second) { | ||
| throw VsagException( | ||
| ErrorType::INVALID_BINARY, | ||
| fmt::format("id {} belongs to multiple duplicate groups", duplicate_id)); | ||
| } | ||
| } | ||
| duplicate_groups.emplace_back(id, std::move(id_list)); | ||
| } | ||
|
|
||
| Vector<DuplicateRecord*> restored_records(label_table_.size(), nullptr, allocator_); | ||
| try { | ||
| for (auto& [id, id_list] : duplicate_groups) { | ||
| restored_records[id] = allocator_->New<DuplicateRecord>(allocator_); | ||
| for (const auto& duplicate_id : id_list) { | ||
| restored_records[id]->duplicate_ids.insert(duplicate_id); | ||
| } | ||
| } | ||
| } catch (...) { | ||
| for (auto* record : restored_records) { | ||
| allocator_->Delete(record); | ||
| } | ||
| throw; | ||
| } | ||
|
|
||
| for (auto* record : duplicate_records_) { | ||
| allocator_->Delete(record); | ||
| } | ||
| duplicate_records_ = std::move(restored_records); | ||
| duplicate_count_ = duplicate_count; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] |
||
| } | ||
|
|
||
| void | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [note] In Consider adding an explicit |
||
| Resize(uint64_t new_size) { | ||
| if (new_size < total_count_) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[note] Test - please ignore