blob: fe9e3666ea547e1017400d271d85eea5dd36c3e0 [file]
// 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 "storage/index/inverted/query/regexp_query.h"
#include <CLucene/config/repl_wchar.h>
#include <optional>
#include "common/logging.h"
#include "util/debug_points.h"
#include "util/hyperscan_util.h"
namespace doris::segment_v2 {
RegexpQuery::RegexpQuery(SearcherPtr searcher, IndexQueryContextPtr context)
: _searcher(std::move(searcher)),
_context(std::move(context)),
_query(_searcher, _context) {
_max_expansions = _context->runtime_state->query_options().inverted_index_max_expansions;
}
void RegexpQuery::add(const InvertedIndexQueryInfo& query_info) {
if (query_info.term_infos.empty()) {
throw Exception(ErrorCode::INVALID_ARGUMENT, "term_infos cannot be empty");
}
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;
hs_compile_error_t* compile_err = nullptr;
hs_scratch_t* scratch = nullptr;
if (hs_compile(pattern.data(), HS_FLAG_DOTALL | HS_FLAG_ALLOWEMPTY | HS_FLAG_UTF8,
HS_MODE_BLOCK, nullptr, &database, &compile_err) != HS_SUCCESS) {
LOG(ERROR) << "hyperscan compilation failed: " << compile_err->message;
hs_free_compile_error(compile_err);
return;
}
if (hs_alloc_scratch(database, &scratch) != HS_SUCCESS) {
LOG(ERROR) << "hyperscan could not allocate scratch space.";
hs_free_database(database);
return;
}
std::vector<std::string> terms;
try {
collect_matching_terms(query_info.field_name, terms, database, scratch, prefix);
}
_CLFINALLY({
hs_free_scratch(scratch);
hs_free_database(database);
})
if (terms.empty()) {
return;
}
InvertedIndexQueryInfo new_query_info;
new_query_info.field_name = query_info.field_name;
new_query_info.term_infos.emplace_back(std::move(terms), query_info.term_infos[0].position);
_query.add(new_query_info);
}
void RegexpQuery::search(roaring::Roaring& roaring) {
_query.search(roaring);
}
std::optional<std::string> RegexpQuery::get_regex_prefix(const std::string& pattern) {
DBUG_EXECUTE_IF("RegexpQuery.get_regex_prefix", { return std::nullopt; });
if (pattern.empty() || pattern[0] != '^') {
return std::nullopt;
}
re2::RE2 re(pattern);
if (!re.ok()) {
return std::nullopt;
}
std::string min_prefix, max_prefix;
if (!re.PossibleMatchRange(&min_prefix, &max_prefix, 256)) {
return std::nullopt;
}
if (min_prefix.empty() || max_prefix.empty() || min_prefix[0] != max_prefix[0]) {
return std::nullopt;
}
auto [it1, it2] = std::mismatch(min_prefix.begin(), min_prefix.end(), max_prefix.begin(),
max_prefix.end());
const size_t common_len = std::distance(min_prefix.begin(), it1);
if (common_len == 0) {
return std::nullopt;
}
return min_prefix.substr(0, common_len);
}
void RegexpQuery::collect_matching_terms(const std::wstring& field_name,
std::vector<std::string>& terms, hs_database_t* database,
hs_scratch_t* scratch,
const std::optional<std::string>& prefix) {
auto on_match = [](unsigned int id, unsigned long long from, unsigned long long to,
unsigned int flags, void* context) -> int {
*((bool*)context) = true;
return 0;
};
int32_t count = 0;
Term* term = nullptr;
TermEnum* enumerator = nullptr;
try {
if (prefix) {
std::wstring ws_prefix = StringUtil::string_to_wstring(*prefix);
Term prefix_term(field_name.c_str(), ws_prefix.c_str());
enumerator = _searcher->getReader()->terms(&prefix_term, _context->io_ctx);
} else {
enumerator = _searcher->getReader()->terms(nullptr, _context->io_ctx);
enumerator->next();
}
do {
term = enumerator->term();
if (term != nullptr) {
std::string input = lucene_wcstoutf8string(term->text(), term->textLength());
if (prefix) {
if (!input.starts_with(*prefix)) {
break;
}
}
bool is_match = false;
if (hs_scan(database, input.data(), static_cast<uint32_t>(input.size()), 0, scratch,
on_match, (void*)&is_match) != HS_SUCCESS) {
LOG(ERROR) << "hyperscan match failed: " << input;
break;
}
if (is_match) {
if (_max_expansions > 0 && count >= _max_expansions) {
break;
}
terms.emplace_back(std::move(input));
count++;
}
} else {
break;
}
_CLDECDELETE(term);
} while (enumerator->next());
}
_CLFINALLY({
_CLDECDELETE(term);
enumerator->close();
_CLDELETE(enumerator);
})
}
} // namespace doris::segment_v2