Repository navigation
feat(clp-s): Add support for querying kv-pairs in the archive range index. #929
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
Changes from 12 commits
fb938c6
b28870f
3fc921a
62c1cf5
6ad77a8
af18885
29640b9
d224032
79f9bd8
8f697bb
6f11a52
d1b6dbc
f2c4b85
a6153d3
2cd7b0b
3996851
6b5e610
e09b667
eb25c27
5d6ff7c
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 |
|---|---|---|
| @@ -0,0 +1,292 @@ | ||
| #include "EvaluateMetadataFilters.hpp" | ||
|
|
||
| #include <cstdint> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <string> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #include <nlohmann/json.hpp> | ||
|
|
||
| #include "../../clp/ffi/encoding_methods.hpp" | ||
| #include "../../clp/ffi/ir_stream/search/utils.hpp" | ||
| #include "../../clp/ffi/Value.hpp" | ||
| #include "../../clp/ir/EncodedTextAst.hpp" | ||
| #include "../../clp/ir/types.hpp" | ||
| #include "../archive_constants.hpp" | ||
| #include "../ArchiveReaderAdaptor.hpp" | ||
| #include "ast/AndExpr.hpp" | ||
| #include "ast/ColumnDescriptor.hpp" | ||
| #include "ast/ConstantProp.hpp" | ||
| #include "ast/EmptyExpr.hpp" | ||
| #include "ast/Expression.hpp" | ||
| #include "ast/FilterExpr.hpp" | ||
| #include "ast/Integral.hpp" | ||
| #include "ast/OrExpr.hpp" | ||
| #include "ast/OrOfAndForm.hpp" | ||
|
|
||
| using clp::ffi::ir_stream::search::evaluate_filter_against_literal_type_value_pair; | ||
|
|
||
| namespace clp_s::search { | ||
| namespace { | ||
| /** | ||
| * Gets the four byte encoded text AST for a given input string. | ||
| * @param text | ||
| * @return The four byte encoded text AST corresponding to the input string. | ||
| */ | ||
| auto get_encoded_text_ast(std::string_view text) | ||
| -> clp::ir::EncodedTextAst<clp::ir::four_byte_encoded_variable_t> { | ||
| std::string logtype; | ||
| std::vector<clp::ir::four_byte_encoded_variable_t> encoded_vars; | ||
| std::vector<int32_t> dict_var_bounds; | ||
| clp::ffi::encode_message(text, logtype, encoded_vars, dict_var_bounds); | ||
| std::vector<std::string> dict_vars; | ||
| for (size_t i{0}; i < dict_var_bounds.size(); i += 2) { | ||
| auto const begin_pos{static_cast<size_t>(dict_var_bounds[i])}; | ||
| auto const end_pos{static_cast<size_t>(dict_var_bounds[i + 1])}; | ||
| dict_vars.emplace_back(text.cbegin() + begin_pos, text.cbegin() + end_pos); | ||
| } | ||
| return clp::ir::EncodedTextAst<clp::ir::four_byte_encoded_variable_t>{ | ||
| logtype, | ||
| dict_vars, | ||
| encoded_vars | ||
| }; | ||
| } | ||
| } // namespace | ||
|
|
||
| auto EvaluateMetadataFilters::run(std::shared_ptr<ast::Expression>& expr) | ||
| -> std::shared_ptr<ast::Expression> { | ||
| bool must_renormalize{false}; | ||
| std::vector<std::pair<ast::Expression*, std::optional<ast::OpList::iterator>>> work_list; | ||
| work_list.emplace_back(expr.get(), std::nullopt); | ||
| while (false == work_list.empty()) { | ||
| auto const [cur_expr, parent_it] = work_list.back(); | ||
| work_list.pop_back(); | ||
| if (cur_expr->has_only_expression_operands()) { | ||
| for (auto it = cur_expr->op_begin(); it != cur_expr->op_end(); ++it) { | ||
| work_list.emplace_back(static_cast<ast::Expression*>(it->get()), it); | ||
| } | ||
| } else if (auto filter_expr = dynamic_cast<ast::FilterExpr*>(cur_expr); | ||
| nullptr != filter_expr) | ||
| { | ||
| if (constants::cRangeIndexNamespace == filter_expr->get_column()->get_namespace()) { | ||
| evaluate_and_rewrite_filter(filter_expr, parent_it, expr); | ||
| must_renormalize = true; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (must_renormalize) { | ||
| ast::OrOfAndForm standardize_pass; | ||
| expr = standardize_pass.run(expr); | ||
| ast::ConstantProp constant_prop; | ||
| expr = constant_prop.run(expr); | ||
| } | ||
| return expr; | ||
| } | ||
|
|
||
| void EvaluateMetadataFilters::evaluate_and_rewrite_filter( | ||
| ast::FilterExpr* filter_expr, | ||
| std::optional<ast::OpList::iterator> parent_it, | ||
| std::shared_ptr<ast::Expression>& ast_root | ||
| ) const { | ||
| std::vector<std::pair<size_t, size_t>> matching_ranges; | ||
| for (auto const& range : m_range_index) { | ||
| if (evaluate_filter(filter_expr, range.fields)) { | ||
| matching_ranges.emplace_back(range.start_index, range.end_index); | ||
| } | ||
| } | ||
|
|
||
| auto replacement_expr{ast::EmptyExpr::create()}; | ||
| if (false == matching_ranges.empty()) { | ||
| auto log_event_idx_col{ast::ColumnDescriptor::create_from_escaped_tokens( | ||
| {std::string{constants::cLogEventIdxName}}, | ||
| constants::cDefaultNamespace | ||
| )}; | ||
| log_event_idx_col->set_subtree_type(std::string{constants::cMetadataSubtreeType}); | ||
| log_event_idx_col->set_matching_type(ast::LiteralType::IntegerT); | ||
| replacement_expr = ast::OrExpr::create(); | ||
|
|
||
| auto add_range_to_filter = [&](std::pair<size_t, size_t> const& range) { | ||
| auto begin_literal{ast::Integral::create_from_int(range.first)}; | ||
| auto end_literal{ast::Integral::create_from_int(range.second)}; | ||
| auto begin_filter{ast::FilterExpr::create( | ||
| log_event_idx_col, | ||
| ast::FilterOperation::GTE, | ||
| begin_literal | ||
| )}; | ||
| auto end_filter{ast::FilterExpr::create( | ||
| log_event_idx_col, | ||
| ast::FilterOperation::LT, | ||
| end_literal | ||
| )}; | ||
| auto range_filter{ast::AndExpr::create(begin_filter, end_filter)}; | ||
| range_filter->copy_append(replacement_expr.get()); | ||
| }; | ||
|
|
||
| std::optional<std::pair<size_t, size_t>> cur_range; | ||
| for (auto const& matching_range : matching_ranges) { | ||
| if (false == cur_range.has_value()) { | ||
| cur_range.emplace(matching_range); | ||
| continue; | ||
| } | ||
|
|
||
| if (cur_range.value().second == matching_range.first) { | ||
| cur_range.value().second = matching_range.second; | ||
| continue; | ||
| } | ||
|
|
||
| add_range_to_filter(cur_range.value()); | ||
| cur_range.emplace(matching_range); | ||
| } | ||
|
|
||
| if (cur_range.has_value()) { | ||
| add_range_to_filter(cur_range.value()); | ||
| } | ||
| } | ||
|
|
||
| if (false == parent_it.has_value()) { | ||
| ast_root = replacement_expr; | ||
| } else { | ||
| replacement_expr->copy_replace(filter_expr->get_parent(), parent_it.value()); | ||
| } | ||
| } | ||
|
|
||
| auto EvaluateMetadataFilters::evaluate_filter( | ||
| ast::FilterExpr* filter_expr, | ||
| nlohmann::json const& fields | ||
| ) const -> bool { | ||
| auto const col{filter_expr->get_column()}; | ||
| auto const operand{filter_expr->get_operand()}; | ||
| std::vector<std::pair<ast::DescriptorList::iterator, nlohmann::json const&>> work_list; | ||
| work_list.emplace_back(col->descriptor_begin(), fields); | ||
| // Allow prefix wildcard to match zero tokens. | ||
| if (col->descriptor_begin() != col->descriptor_end() && col->descriptor_begin()->wildcard()) { | ||
| work_list.emplace_back(++col->descriptor_begin(), fields); | ||
| } | ||
|
|
||
| auto evaluate_expr | ||
| = [&](ast::LiteralType type, std::optional<clp::ffi::Value> const& value) -> bool { | ||
| auto ret{evaluate_filter_against_literal_type_value_pair( | ||
|
Contributor
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. Just curious, why are we using clp::ffi methods to evaluate filter? Is it because we don't expose similar methods in clp_s?
Contributor
Author
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. That is part of it, but also
|
||
| filter_expr, | ||
| type, | ||
| value, | ||
| m_case_sensitive_match | ||
| )}; | ||
| return false == ret.has_error() && filter_expr->is_inverted() != ret.value(); | ||
| }; | ||
|
|
||
| while (false == work_list.empty()) { | ||
|
Contributor
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. I'm a bit confused about the purpose of this while loop. Is it because we want to handle nested column in the future? and are we planning to store nested structures in the RangeIndex fields?
Contributor
Author
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. Yes, it is for that and for handling wildcards tokens in the column descriptor. |
||
| auto [cur_it, cur_field] = work_list.back(); | ||
| work_list.pop_back(); | ||
| if (col->descriptor_end() == cur_it) { | ||
| switch (cur_field.type()) { | ||
| case nlohmann::json::value_t::boolean: | ||
| if (col->matches_type(ast::LiteralType::BooleanT)) { | ||
| std::optional<clp::ffi::Value> bool_value{ | ||
| clp::ffi::Value{cur_field.template get<bool>()} | ||
| }; | ||
| if (evaluate_expr(ast::LiteralType::BooleanT, bool_value)) { | ||
| return true; | ||
| } | ||
| } | ||
| break; | ||
| case nlohmann::json::value_t::number_integer: | ||
| if (col->matches_type(ast::LiteralType::IntegerT)) { | ||
| std::optional<clp::ffi::Value> int_value{ | ||
| clp::ffi::Value{cur_field.template get<int64_t>()} | ||
| }; | ||
| if (evaluate_expr(ast::LiteralType::IntegerT, int_value)) { | ||
| return true; | ||
| } | ||
| } | ||
| break; | ||
| case nlohmann::json::value_t::number_unsigned: | ||
| if (col->matches_type(ast::LiteralType::IntegerT)) { | ||
| // TODO: Remove static cast once we add full support for large unsigned | ||
| // values. | ||
| std::optional<clp::ffi::Value> int_value{clp::ffi::Value{ | ||
| static_cast<int64_t>(cur_field.template get<uint64_t>()) | ||
| }}; | ||
| if (evaluate_expr(ast::LiteralType::IntegerT, int_value)) { | ||
| return true; | ||
| } | ||
| } | ||
|
gibber9809 marked this conversation as resolved.
Resolved
|
||
| break; | ||
| case nlohmann::json::value_t::number_float: | ||
| if (col->matches_type(ast::LiteralType::FloatT)) { | ||
| std::optional<clp::ffi::Value> float_value{ | ||
| clp::ffi::Value{cur_field.template get<double>()} | ||
| }; | ||
| if (evaluate_expr(ast::LiteralType::FloatT, float_value)) { | ||
| return true; | ||
| } | ||
| } | ||
| break; | ||
| case nlohmann::json::value_t::string: { | ||
| if (false | ||
| == col->matches_any( | ||
| ast::LiteralType::VarStringT | ast::LiteralType::ClpStringT | ||
| )) | ||
| { | ||
| break; | ||
| } | ||
| auto tmp_string{cur_field.template get<std::string>()}; | ||
| bool contains_space{std::string::npos != tmp_string.find(' ')}; | ||
| if (false == contains_space) { | ||
| std::optional<clp::ffi::Value> str_value{ | ||
| clp::ffi::Value{std::move(tmp_string)} | ||
| }; | ||
| if (evaluate_expr(ast::LiteralType::VarStringT, str_value)) { | ||
| return true; | ||
| } | ||
| } else { | ||
| std::optional<clp::ffi::Value> str_value{ | ||
| clp::ffi::Value{get_encoded_text_ast(tmp_string)} | ||
| }; | ||
| if (evaluate_expr(ast::LiteralType::ClpStringT, str_value)) { | ||
| return true; | ||
| } | ||
| } | ||
| } break; | ||
| case nlohmann::json::value_t::null: | ||
| if (col->matches_type(ast::LiteralType::NullT)) { | ||
| std::optional<clp::ffi::Value> null_value{clp::ffi::Value{}}; | ||
| if (evaluate_expr(ast::LiteralType::NullT, null_value)) { | ||
| return true; | ||
| } | ||
| } | ||
| break; | ||
| case nlohmann::json::value_t::array: | ||
| // TODO: Add array search call once | ||
| // `evaluate_filter_against_literal_type_value_pair` adds support for array | ||
| // search. | ||
|
gibber9809 marked this conversation as resolved.
Resolved
|
||
| case nlohmann::json::value_t::object: | ||
| // TODO: Add object filter once we add `LiteralType::ObjectT`. | ||
| case nlohmann::json::value_t::discarded: | ||
| default: | ||
| break; | ||
| } | ||
| } else if (cur_field.is_object() && cur_it->wildcard()) { | ||
| auto cur_it_tmp = cur_it++; | ||
| // Allow wildcard to continue matching tokens or continue on to next descriptor token. | ||
| for (auto const& field : cur_field) { | ||
| work_list.emplace_back(cur_it_tmp, field); | ||
| work_list.emplace_back(cur_it, field); | ||
| } | ||
| } else if (cur_field.is_object()) { | ||
| if (cur_field.contains(cur_it->get_token())) { | ||
|
Contributor
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. Can we merge these two into one condition |
||
| auto const& next_field(cur_field.at(cur_it->get_token())); | ||
| work_list.emplace_back(++cur_it, next_field); | ||
|
|
||
| // Allow wildcard to match zero tokens. | ||
| if (col->descriptor_end() != cur_it && cur_it->wildcard()) { | ||
| work_list.emplace_back(++cur_it, next_field); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
| } // namespace clp_s::search | ||
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.
Can we reserve the size?