Skip to content
Open
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
46 changes: 39 additions & 7 deletions be/src/exprs/function/like.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@

#include <fmt/format.h>
#include <hs/hs_compile.h>
#include <re2/stringpiece.h>

#include <cstddef>
#include <ostream>
Expand All @@ -34,8 +33,10 @@
#include "core/column/column_vector.h"
#include "core/string_ref.h"
#include "exprs/function/simple_function_factory.h"
#include "util/hyperscan_util.h"

namespace doris {

// A regex to match any regex pattern is equivalent to a substring search.
static const RE2 SUBSTRING_RE(R"((?:\.\*)*([^\.\^\{\[\(\|\)\]\}\+\*\?\$\\]*)(?:\.\*)*)");

Expand Down Expand Up @@ -183,8 +184,9 @@ struct VectorEndsWithSearchState : public VectorPatternSearchState {
}
};

Status LikeSearchState::clone(LikeSearchState& cloned) {
Status LikeSearchState::clone(LikeSearchState& cloned) const {
cloned.set_search_string(search_string);
cloned.enable_hyperscan_fallback = enable_hyperscan_fallback;

std::string re_pattern;
FunctionLike::convert_like_pattern(this, pattern_str, &re_pattern);
Expand Down Expand Up @@ -452,7 +454,8 @@ Status FunctionLikeBase::regexp_fn(const LikeSearchState* state, const ColumnStr

hs_database_t* database = nullptr;
hs_scratch_t* scratch = nullptr;
if (hs_prepare(nullptr, re_pattern.c_str(), &database, &scratch).ok()) { // use hyperscan
auto hs_status = hs_prepare(nullptr, re_pattern.c_str(), &database, &scratch);
if (hs_status.ok()) { // use hyperscan
auto sz = val.size();
for (size_t i = 0; i < sz; i++) {
const auto& str_ref = val.get_data_at(i);
Expand All @@ -467,6 +470,9 @@ Status FunctionLikeBase::regexp_fn(const LikeSearchState* state, const ColumnStr
hs_free_scratch(scratch);
hs_free_database(database);
} else { // fallback to re2
if (!state->enable_hyperscan_fallback) {
return hs_status;
}
RE2::Options opts;
opts.set_never_nl(false);
opts.set_dot_nl(true);
Expand All @@ -487,8 +493,19 @@ Status FunctionLikeBase::regexp_fn(const LikeSearchState* state, const ColumnStr
}

// hyperscan compile expression to database and allocate scratch space
bool FunctionLikeBase::should_fallback_to_re2(std::string_view regexp) {
Comment thread
HappenLee marked this conversation as resolved.
return is_hyperscan_regexp_expensive(regexp);
}

Status FunctionLikeBase::hs_prepare(FunctionContext* context, const char* expression,
hs_database_t** database, hs_scratch_t** scratch) {
if (should_fallback_to_re2(expression)) {
Comment thread
HappenLee marked this conversation as resolved.
*database = nullptr;
*scratch = nullptr;
// Callers either fall back to RE2 or return this status based on the session variable.
return Status::RuntimeError<false>(HYPERSCAN_BOUNDED_REPEAT_ERROR);
}

hs_compile_error_t* compile_err;
auto res = hs_compile(expression, HS_FLAG_DOTALL | HS_FLAG_ALLOWEMPTY | HS_FLAG_UTF8,
HS_MODE_BLOCK, nullptr, database, &compile_err);
Expand All @@ -497,7 +514,7 @@ Status FunctionLikeBase::hs_prepare(FunctionContext* context, const char* expres
*database = nullptr;
std::string error_message = compile_err->message;
hs_free_compile_error(compile_err);
// Do not call FunctionContext::set_error here, since we do not want to cancel the query here.
// Callers either fall back to RE2 or return this status based on the session variable.
return Status::RuntimeError<false>("hs_compile regex pattern error:" + error_message);
}
hs_free_compile_error(compile_err);
Expand All @@ -506,7 +523,7 @@ Status FunctionLikeBase::hs_prepare(FunctionContext* context, const char* expres
hs_free_database(*database);
*database = nullptr;
*scratch = nullptr;
// Do not call FunctionContext::set_error here, since we do not want to cancel the query here.
// Callers either fall back to RE2 or return this status based on the session variable.
return Status::RuntimeError<false>("hs_alloc_scratch allocate scratch space error");
}

Expand Down Expand Up @@ -942,12 +959,19 @@ Status FunctionLike::construct_like_const_state(FunctionContext* context, const

hs_database_t* database = nullptr;
hs_scratch_t* scratch = nullptr;
if (try_hyperscan && hs_prepare(context, re_pattern.c_str(), &database, &scratch).ok()) {
Status hs_status;
if (try_hyperscan) {
hs_status = hs_prepare(context, re_pattern.c_str(), &database, &scratch);
}
if (try_hyperscan && hs_status.ok()) {
// use hyperscan
state->search_state.hs_database.reset(database);
state->search_state.hs_scratch.reset(scratch);
} else {
// fallback to re2
if (try_hyperscan && !state->search_state.enable_hyperscan_fallback) {
return hs_status;
}
// reset hs_database to nullptr to indicate not use hyperscan
state->search_state.hs_database.reset();
state->search_state.hs_scratch.reset();
Expand All @@ -974,6 +998,8 @@ Status FunctionLike::open(FunctionContext* context, FunctionContext::FunctionSta
}
std::shared_ptr<LikeState> state = std::make_shared<LikeState>();
state->is_like_pattern = true;
state->search_state.enable_hyperscan_fallback =
context->state()->query_options().enable_hyperscan_fallback;
state->function = like_fn;
state->scalar_function = like_fn_scalar;
if (context->is_col_constant(2)) {
Expand Down Expand Up @@ -1004,6 +1030,8 @@ Status FunctionRegexpLike::open(FunctionContext* context,
std::shared_ptr<LikeState> state = std::make_shared<LikeState>();
context->set_function_state(scope, state);
state->is_like_pattern = false;
state->search_state.enable_hyperscan_fallback =
context->state()->query_options().enable_hyperscan_fallback;
state->function = regexp_fn;
state->scalar_function = regexp_fn_scalar;
if (context->is_col_constant(1)) {
Expand Down Expand Up @@ -1035,12 +1063,16 @@ Status FunctionRegexpLike::open(FunctionContext* context,
} else {
hs_database_t* database = nullptr;
hs_scratch_t* scratch = nullptr;
if (hs_prepare(context, pattern_str.c_str(), &database, &scratch).ok()) {
auto hs_status = hs_prepare(context, pattern_str.c_str(), &database, &scratch);
if (hs_status.ok()) {
// use hyperscan
state->search_state.hs_database.reset(database);
state->search_state.hs_scratch.reset(scratch);
} else {
// fallback to re2
if (!state->search_state.enable_hyperscan_fallback) {
return hs_status;
}
// reset hs_database to nullptr to indicate not use hyperscan
state->search_state.hs_database.reset();
state->search_state.hs_scratch.reset();
Expand Down
7 changes: 6 additions & 1 deletion be/src/exprs/function/like.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <functional>
#include <memory>
#include <string>
#include <string_view>

#include "common/status.h"
#include "core/block/column_numbers.h"
Expand Down Expand Up @@ -182,6 +183,8 @@ struct LikeSearchState {

std::string pattern_str;

bool enable_hyperscan_fallback = true;

/// Used for LIKE predicates if the pattern is a constant argument, and is either a
/// constant string or has a constant string at the beginning or end of the pattern.
/// This will be set in order to check for that pattern in the corresponding part of
Expand Down Expand Up @@ -226,7 +229,7 @@ struct LikeSearchState {

LikeSearchState() = default;

Status clone(LikeSearchState& cloned);
Status clone(LikeSearchState& cloned) const;

void set_search_string(const std::string& search_string_arg) {
search_string = search_string_arg;
Expand Down Expand Up @@ -292,6 +295,8 @@ class FunctionLikeBase : public IFunction {
friend struct VectorEndsWithSearchState;

protected:
static bool should_fallback_to_re2(std::string_view regexp);

Status vector_const(const ColumnString& values, const StringRef* pattern_val,
ColumnUInt8::Container& result, const LikeFn& function,
LikeSearchState* search_state) const;
Expand Down
5 changes: 5 additions & 0 deletions be/src/exprs/function/match.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "storage/index/index_reader_helper.h"
#include "storage/index/inverted/analyzer/analyzer.h"
#include "util/debug_points.h"
#include "util/hyperscan_util.h"

namespace doris {

Expand Down Expand Up @@ -507,6 +508,10 @@ Status FunctionMatchRegexp::execute_match(FunctionContext* context, const std::s
hs_compile_error_t* compile_err = nullptr;
hs_scratch_t* scratch = nullptr;

if (is_hyperscan_regexp_expensive(pattern)) {
return Status::Error<ErrorCode::INDEX_INVALID_PARAMETERS>(HYPERSCAN_BOUNDED_REPEAT_ERROR);
}

if (hs_compile(pattern.data(), HS_FLAG_DOTALL | HS_FLAG_ALLOWEMPTY | HS_FLAG_UTF8,
HS_MODE_BLOCK, nullptr, &database, &compile_err) != HS_SUCCESS) {
std::string err_message = "hyperscan compilation failed: ";
Expand Down
4 changes: 4 additions & 0 deletions be/src/exprs/function/regexps.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@

#include "common/exception.h"
#include "core/string_ref.h"
#include "util/hyperscan_util.h"

namespace doris::multiregexps {

Expand Down Expand Up @@ -144,6 +145,9 @@ Regexps constructRegexps(const std::vector<String>& str_patterns,

for (auto& pattern : patterns) {
LOG(INFO) << "pattern: " << pattern << "\n";
if (is_hyperscan_regexp_expensive(pattern)) {
throw doris::Exception(Status::InvalidArgument(HYPERSCAN_BOUNDED_REPEAT_ERROR));
}
}

hs_error_t err;
Expand Down
4 changes: 4 additions & 0 deletions be/src/storage/index/inverted/query/regexp_query.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include "common/logging.h"
#include "util/debug_points.h"
#include "util/hyperscan_util.h"

namespace doris::segment_v2 {

Expand All @@ -39,6 +40,9 @@ void RegexpQuery::add(const InvertedIndexQueryInfo& query_info) {
}

const std::string& pattern = query_info.term_infos[0].get_single_term();
if (is_hyperscan_regexp_expensive(pattern)) {
throw Exception(ErrorCode::INVALID_ARGUMENT, HYPERSCAN_BOUNDED_REPEAT_ERROR);
}
auto prefix = get_regex_prefix(pattern);

hs_database_t* database = nullptr;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "storage/index/inverted/query_v2/nullable_scorer.h"
#include "storage/index/inverted/query_v2/segment_postings.h"
#include "storage/index/inverted/util/string_helper.h"
#include "util/hyperscan_util.h"

CL_NS_USE(index)

Expand Down Expand Up @@ -70,6 +71,9 @@ ScorerPtr RegexpWeight::scorer(const QueryExecutionContext& context,

ScorerPtr RegexpWeight::regexp_scorer(const QueryExecutionContext& context,
const std::string& binding_key) {
if (is_hyperscan_regexp_expensive(_pattern)) {
throw Exception(ErrorCode::INVALID_ARGUMENT, HYPERSCAN_BOUNDED_REPEAT_ERROR);
}
auto prefix = get_regex_prefix(_pattern);

hs_database_t* database = nullptr;
Expand Down
139 changes: 139 additions & 0 deletions be/src/util/hyperscan_util.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

#include "util/hyperscan_util.h"

#include <re2/re2.h>
#include <re2/stringpiece.h>

#include <charconv>
#include <string>

namespace doris {
namespace {

bool is_larger_than_fifty(std::string_view str) {
int number = 0;
auto [_, error] = std::from_chars(str.data(), str.data() + str.size(), number);
return error == std::errc() && number > 50;
}

std::string mask_escaped_characters_and_character_classes(std::string_view regexp) {
std::string masked_regexp(regexp);
bool escaped = false;
bool in_character_class = false;
bool character_class_can_close = false;
bool character_class_can_negate = false;
for (char& masked_character : masked_regexp) {
const char current = masked_character;
if (escaped) {
masked_character = ' ';
escaped = false;
if (in_character_class) {
character_class_can_close = true;
character_class_can_negate = false;
}
continue;
}
if (current == '\\') {
masked_character = ' ';
escaped = true;
continue;
}
if (in_character_class) {
masked_character = ' ';
if (current == ']' && character_class_can_close) {
in_character_class = false;
} else if (current == '^' && character_class_can_negate) {
character_class_can_negate = false;
} else {
character_class_can_close = true;
character_class_can_negate = false;
}
continue;
}
if (current == '[') {
masked_character = ' ';
in_character_class = true;
character_class_can_close = false;
character_class_can_negate = true;
}
}
return masked_regexp;
}

class SlowWithHyperscanChecker {
public:
SlowWithHyperscanChecker()
: _searcher_one_repeat(R"(\{\s*([\d]+)\s*,?\s*})"),
_searcher_two_repeats(R"(\{\s*([\d]+)\s*,\s*([\d]+)\s*\})") {}

bool is_slow(std::string_view regexp) const {
const std::string masked_regexp = mask_escaped_characters_and_character_classes(regexp);
return is_slow_one_repeat(masked_regexp) || is_slow_two_repeats(masked_regexp);
}

private:
bool is_slow_one_repeat(std::string_view regexp) const {
re2::StringPiece haystack(regexp.data(), regexp.size());
re2::StringPiece matches[2];
size_t start_pos = 0;
while (start_pos < haystack.size()) {
if (!_searcher_one_repeat.Match(haystack, start_pos, haystack.size(),
re2::RE2::Anchor::UNANCHORED, matches, 2)) {
break;
}

start_pos = matches[0].data() - haystack.data() + matches[0].size();
if (is_larger_than_fifty({matches[1].data(), matches[1].size()})) {
return true;
}
}
return false;
}

bool is_slow_two_repeats(std::string_view regexp) const {
re2::StringPiece haystack(regexp.data(), regexp.size());
re2::StringPiece matches[3];
size_t start_pos = 0;
while (start_pos < haystack.size()) {
if (!_searcher_two_repeats.Match(haystack, start_pos, haystack.size(),
re2::RE2::Anchor::UNANCHORED, matches, 3)) {
break;
}

start_pos = matches[0].data() - haystack.data() + matches[0].size();
if (is_larger_than_fifty({matches[1].data(), matches[1].size()}) ||
is_larger_than_fifty({matches[2].data(), matches[2].size()})) {
return true;
}
}
return false;
}

re2::RE2 _searcher_one_repeat;
re2::RE2 _searcher_two_repeats;
};

} // namespace

bool is_hyperscan_regexp_expensive(std::string_view regexp) {
static const SlowWithHyperscanChecker slow_with_hyperscan_checker;
return slow_with_hyperscan_checker.is_slow(regexp);
}

} // namespace doris
Loading
Loading