blob: 469f5ee2d6921acf9eefa85c3b78992733d9fa5a [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/analyzer/custom_analyzer.h"
#include <algorithm>
#include <string_view>
#include "common/status.h"
#include "runtime/exec_env.h"
#include "storage/index/inverted/analysis_factory_mgr.h"
#include "storage/index/inverted/common_grams/common_word_set.h"
#include "storage/index/inverted/token_filter/common_grams_filter_factory.h"
#include "storage/index/inverted/token_stream.h"
#include "util/sha.h"
namespace doris::segment_v2::inverted_index {
namespace {
bool config_uses_common_grams(const ImmutableCustomAnalyzerConfigPtr& config) {
DORIS_CHECK(config != nullptr);
const auto& filter_configs = config->get_token_filter_configs();
return std::any_of(filter_configs.begin(), filter_configs.end(),
[](const auto& entry) { return entry->get_name() == "common_grams"; });
}
void append_canonical_value(std::string_view value, std::string* output) {
output->append(std::to_string(value.size()));
output->push_back(':');
output->append(value);
}
void append_component(std::string_view role, const ComponentConfigPtr& component,
std::string* output) {
append_canonical_value(role, output);
append_canonical_value(component->get_name(), output);
const auto entries = component->get_params().sorted_entries();
append_canonical_value(std::to_string(entries.size()), output);
for (const auto& [key, value] : entries) {
append_canonical_value(key, output);
append_canonical_value(value, output);
}
}
std::string sha256(std::string_view value) {
SHA256Digest digest;
digest.reset(value.data(), value.size());
return std::string(digest.digest());
}
std::string calculate_base_analyzer_fingerprint_impl(
const ImmutableCustomAnalyzerConfigPtr& config,
const std::map<std::string, std::string>& outer_char_filter_map) {
DORIS_CHECK(config != nullptr);
std::string base;
append_canonical_value("doris-common-grams-base-analyzer:v1", &base);
append_canonical_value("outer_char_filter", &base);
append_canonical_value(std::to_string(outer_char_filter_map.size()), &base);
for (const auto& [key, value] : outer_char_filter_map) {
append_canonical_value(key, &base);
append_canonical_value(value, &base);
}
append_component("tokenizer", config->get_tokenizer_config(), &base);
const auto char_filters = config->get_char_filter_configs();
append_canonical_value(std::to_string(char_filters.size()), &base);
for (const auto& char_filter : char_filters) {
append_component("char_filter", char_filter, &base);
}
const auto token_filters = config->get_token_filter_configs();
const auto base_token_filter_count = std::count_if(
token_filters.begin(), token_filters.end(),
[](const auto& token_filter) { return token_filter->get_name() != "common_grams"; });
append_canonical_value(std::to_string(base_token_filter_count), &base);
for (const auto& token_filter : token_filters) {
if (token_filter->get_name() != "common_grams") {
append_component("token_filter", token_filter, &base);
}
}
return sha256(base);
}
CommonGramsQueryIdentity build_common_grams_identity(std::string dictionary_identity,
std::string base_analyzer_fingerprint) {
std::string common_grams;
append_canonical_value("doris-common-grams:v1", &common_grams);
append_canonical_value(std::to_string(COMMON_GRAMS_SEMANTICS_VERSION_V1), &common_grams);
append_canonical_value(std::to_string(COMMON_GRAMS_KEY_VERSION_V1), &common_grams);
append_canonical_value(WORDSET_FORMAT_V1, &common_grams);
append_canonical_value(dictionary_identity, &common_grams);
return {.common_grams_dictionary_identity = std::move(dictionary_identity),
.base_analyzer_fingerprint = std::move(base_analyzer_fingerprint),
.common_grams_fingerprint = sha256(common_grams)};
}
std::array<std::shared_ptr<lucene::analysis::Analyzer>, 5> build_purpose_analyzers(
const ImmutableCustomAnalyzerConfigPtr& config,
const std::shared_ptr<const CommonWordSet>& common_words) {
DORIS_CHECK(config != nullptr);
const bool has_common_grams = config_uses_common_grams(config);
if (!has_common_grams) {
auto analyzer = CustomAnalyzer::build_custom_analyzer(config);
return {analyzer, analyzer, analyzer, analyzer, analyzer};
}
return {CustomAnalyzer::build_custom_analyzer(config, AnalysisPurpose::kIndex, common_words),
CustomAnalyzer::build_custom_analyzer(config, AnalysisPurpose::kSniiTransientIndex,
common_words),
CustomAnalyzer::build_custom_analyzer(config, AnalysisPurpose::kPlainQuery,
common_words),
CustomAnalyzer::build_custom_analyzer(config, AnalysisPurpose::kExactPhraseQuery,
common_words),
CustomAnalyzer::build_custom_analyzer(config, AnalysisPurpose::kPhrasePrefixQuery,
common_words)};
}
} // namespace
CustomAnalyzer::CustomAnalyzer(Builder* builder) {
_tokenizer = builder->_tokenizer;
_char_filters = builder->_char_filters;
_token_filters = builder->_token_filters;
}
TokenStream* CustomAnalyzer::tokenStream(const TCHAR* fieldName, lucene::util::Reader* reader) {
throw Exception(ErrorCode::INVERTED_INDEX_NOT_SUPPORTED,
"CustomAnalyzer::tokenStream not supported");
}
TokenStream* CustomAnalyzer::reusableTokenStream(const TCHAR* fieldName,
lucene::util::Reader* reader) {
throw Exception(ErrorCode::INVERTED_INDEX_NOT_SUPPORTED,
"CustomAnalyzer::reusableTokenStream not supported");
}
TokenStream* CustomAnalyzer::tokenStream(const TCHAR* fieldName, const ReaderPtr& reader) {
auto r = init_reader(reader);
auto token_stream = create_components();
token_stream->set_reader(r);
token_stream->get_token_stream()->reset();
return new TokenStreamWrapper(token_stream->get_token_stream());
}
TokenStream* CustomAnalyzer::reusableTokenStream(const TCHAR* fieldName, const ReaderPtr& reader) {
auto r = init_reader(reader);
if (_reuse_token_stream == nullptr) {
_reuse_token_stream = create_components();
}
_reuse_token_stream->set_reader(r);
return _reuse_token_stream->get_token_stream().get();
}
ReaderPtr CustomAnalyzer::init_reader(ReaderPtr reader) {
for (const auto& filter : _char_filters) {
reader = filter->create(reader);
}
return reader;
}
TokenStreamComponentsPtr CustomAnalyzer::create_components() {
auto tk = _tokenizer->create();
TokenStreamPtr ts = tk;
for (const auto& filter : _token_filters) {
ts = filter->create(ts);
}
return std::make_shared<TokenStreamComponents>(tk, ts);
}
CustomAnalyzerPtr CustomAnalyzer::build_custom_analyzer(
const ImmutableCustomAnalyzerConfigPtr& config) {
if (config == nullptr) {
throw Exception(ErrorCode::ILLEGAL_STATE, "Null configuration detected.");
}
CustomAnalyzer::Builder builder;
for (const auto& filter_config : config->get_char_filter_configs()) {
builder.add_char_filter(filter_config->get_name(), filter_config->get_params());
}
builder.with_tokenizer(config->get_tokenizer_config()->get_name(),
config->get_tokenizer_config()->get_params());
for (const auto& filter_config : config->get_token_filter_configs()) {
builder.add_token_filter(filter_config->get_name(), filter_config->get_params());
}
return builder.build();
}
CustomAnalyzerPtr CustomAnalyzer::build_custom_analyzer(
const ImmutableCustomAnalyzerConfigPtr& config, AnalysisPurpose purpose) {
return build_custom_analyzer(config, purpose, CommonWordSet::default_word_set());
}
CustomAnalyzerPtr CustomAnalyzer::build_custom_analyzer(
const ImmutableCustomAnalyzerConfigPtr& config, AnalysisPurpose purpose,
const std::shared_ptr<const CommonWordSet>& common_words) {
if (config == nullptr) {
throw Exception(ErrorCode::ILLEGAL_STATE, "Null configuration detected.");
}
CustomAnalyzer::Builder builder;
for (const auto& filter_config : config->get_char_filter_configs()) {
builder.add_char_filter(filter_config->get_name(), filter_config->get_params());
}
builder.with_tokenizer(config->get_tokenizer_config()->get_name(),
config->get_tokenizer_config()->get_params());
const auto filter_configs = config->get_token_filter_configs();
const size_t common_grams_count =
std::count_if(filter_configs.begin(), filter_configs.end(),
[](const auto& entry) { return entry->get_name() == "common_grams"; });
if (common_grams_count == 0) {
for (const auto& filter_config : filter_configs) {
builder.add_token_filter(filter_config->get_name(), filter_config->get_params());
}
return builder.build();
}
if (common_grams_count != 1 || filter_configs.back()->get_name() != "common_grams") {
throw Exception(ErrorCode::INVERTED_INDEX_ANALYZER_ERROR,
"common_grams must appear exactly once as the terminal token filter");
}
if (builder._tokenizer->position_capability() != PositionCapability::kAlwaysUnitIncrement) {
throw Exception(ErrorCode::INVERTED_INDEX_ANALYZER_ERROR,
"CommonGrams tokenizer does not guarantee unit position increments");
}
for (size_t i = 0; i + 1 < filter_configs.size(); ++i) {
auto factory = AnalysisFactoryMgr::instance().create<TokenFilterFactory>(
filter_configs[i]->get_name(), filter_configs[i]->get_params());
if (factory->position_capability() != PositionCapability::kAlwaysUnitIncrement) {
throw Exception(ErrorCode::INVERTED_INDEX_ANALYZER_ERROR,
"CommonGrams token filter '{}' does not guarantee unit position "
"increments",
filter_configs[i]->get_name());
}
builder._token_filters.push_back(std::move(factory));
}
auto common_grams = AnalysisFactoryMgr::instance().create<TokenFilterFactory>(
filter_configs.back()->get_name(), filter_configs.back()->get_params());
auto common_grams_factory = std::dynamic_pointer_cast<CommonGramsFilterFactory>(common_grams);
DORIS_CHECK(common_grams_factory != nullptr);
common_grams_factory->set_common_words(common_words);
switch (purpose) {
case AnalysisPurpose::kIndex:
common_grams_factory->set_output_mode(CommonGramsOutputMode::kEscapedV1Index);
builder._token_filters.push_back(std::move(common_grams));
break;
case AnalysisPurpose::kSniiTransientIndex:
common_grams_factory->set_output_mode(CommonGramsOutputMode::kEscapedV1SpimiIndex);
builder._token_filters.push_back(std::move(common_grams));
break;
case AnalysisPurpose::kPlainQuery: {
auto factory = std::make_shared<CommonGramsPositionFilterFactory>();
factory->initialize({});
builder._token_filters.push_back(std::move(factory));
break;
}
case AnalysisPurpose::kExactPhraseQuery: {
builder._token_filters.push_back(std::move(common_grams));
auto factory = std::make_shared<CommonGramsQueryFilterFactory>(common_words);
factory->initialize({});
builder._token_filters.push_back(std::move(factory));
break;
}
case AnalysisPurpose::kPhrasePrefixQuery: {
builder._token_filters.push_back(std::move(common_grams));
auto factory = std::make_shared<CommonGramsPhrasePrefixFilterFactory>(common_words);
factory->initialize({});
builder._token_filters.push_back(std::move(factory));
break;
}
}
return builder.build();
}
CustomAnalyzerProvider::CustomAnalyzerProvider(
ImmutableCustomAnalyzerConfigPtr config,
std::map<std::string, std::string> outer_char_filter_map)
: _config(std::move(config)),
_base_analyzer_fingerprint(
calculate_base_analyzer_fingerprint(_config, outer_char_filter_map)),
_uses_common_grams(config_uses_common_grams(_config)) {
_common_words = CommonWordSet::default_word_set();
_analyzers = build_purpose_analyzers(_config, _common_words);
if (_uses_common_grams) {
// Content-derived, so a BE reading a segment grammed against a different word list sees a
// mismatched identity and falls back to the plain plan instead of trusting its grams.
_common_grams_identity =
build_common_grams_identity(_common_words->identity(), _base_analyzer_fingerprint);
}
}
std::string CustomAnalyzerProvider::calculate_base_analyzer_fingerprint(
const ImmutableCustomAnalyzerConfigPtr& config,
const std::map<std::string, std::string>& outer_char_filter_map) {
return calculate_base_analyzer_fingerprint_impl(config, outer_char_filter_map);
}
std::shared_ptr<lucene::analysis::Analyzer> CustomAnalyzerProvider::get_analyzer(
AnalysisPurpose purpose) const {
switch (purpose) {
case AnalysisPurpose::kIndex:
return _analyzers[0];
case AnalysisPurpose::kSniiTransientIndex:
return _analyzers[1];
case AnalysisPurpose::kPlainQuery:
return _analyzers[2];
case AnalysisPurpose::kExactPhraseQuery:
return _analyzers[3];
case AnalysisPurpose::kPhrasePrefixQuery:
return _analyzers[4];
}
__builtin_unreachable();
}
void CustomAnalyzer::Builder::with_tokenizer(const std::string& name, const Settings& params) {
_tokenizer = AnalysisFactoryMgr::instance().create<TokenizerFactory>(name, params);
}
void CustomAnalyzer::Builder::add_char_filter(const std::string& name, const Settings& params) {
_char_filters.push_back(AnalysisFactoryMgr::instance().create<CharFilterFactory>(name, params));
}
void CustomAnalyzer::Builder::add_token_filter(const std::string& name, const Settings& params) {
_token_filters.push_back(
AnalysisFactoryMgr::instance().create<TokenFilterFactory>(name, params));
}
CustomAnalyzerPtr CustomAnalyzer::Builder::build() {
if (_tokenizer == nullptr) {
throw Exception(ErrorCode::ILLEGAL_STATE, "You have to set at least a tokenizer.");
}
return std::make_shared<CustomAnalyzer>(this);
}
void TokenStreamComponents::set_reader(const ReaderPtr& reader) {
_source->set_reader(reader);
}
TokenStreamPtr TokenStreamComponents::get_token_stream() {
return _sink;
}
TokenizerPtr TokenStreamComponents::get_source() {
return _source;
}
} // namespace doris::segment_v2::inverted_index