blob: cebd2052527441f251f2a7c978dd5e9093da26f1 [file]
// Copyright (C) 2021 Google LLC
//
// Licensed 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 "icing/query/suggestion-processor.h"
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "icing/text_classifier/lib3/utils/base/statusor.h"
#include "icing/absl_ports/canonical_errors.h"
#include "icing/absl_ports/str_cat.h"
#include "icing/feature-flags.h"
#include "icing/index/embed/embedding-index.h"
#include "icing/index/index.h"
#include "icing/index/iterator/doc-hit-info-iterator.h"
#include "icing/index/numeric/numeric-index.h"
#include "icing/index/term-metadata.h"
#include "icing/proto/search.pb.h"
#include "icing/query/query-processor.h"
#include "icing/query/query-results.h"
#include "icing/schema/schema-store.h"
#include "icing/schema/section.h"
#include "icing/store/document-filter-data.h"
#include "icing/store/document-id.h"
#include "icing/store/document-store.h"
#include "icing/store/namespace-id.h"
#include "icing/store/suggestion-result-checker-impl.h"
#include "icing/tokenization/language-segmenter.h"
#include "icing/transform/normalizer.h"
#include "icing/util/clock.h"
#include "icing/util/status-macros.h"
namespace icing {
namespace lib {
libtextclassifier3::StatusOr<std::unique_ptr<SuggestionProcessor>>
SuggestionProcessor::Create(Index* index,
const NumericIndex<int64_t>* numeric_index,
const EmbeddingIndex* embedding_index,
const LanguageSegmenter* language_segmenter,
const Normalizer* normalizer,
const DocumentStore* document_store,
const SchemaStore* schema_store, const Clock* clock,
const FeatureFlags* feature_flags) {
ICING_RETURN_ERROR_IF_NULL(index);
ICING_RETURN_ERROR_IF_NULL(numeric_index);
ICING_RETURN_ERROR_IF_NULL(embedding_index);
ICING_RETURN_ERROR_IF_NULL(language_segmenter);
ICING_RETURN_ERROR_IF_NULL(normalizer);
ICING_RETURN_ERROR_IF_NULL(document_store);
ICING_RETURN_ERROR_IF_NULL(schema_store);
ICING_RETURN_ERROR_IF_NULL(clock);
ICING_RETURN_ERROR_IF_NULL(feature_flags);
return std::unique_ptr<SuggestionProcessor>(new SuggestionProcessor(
index, numeric_index, embedding_index, language_segmenter, normalizer,
document_store, schema_store, clock, feature_flags));
}
libtextclassifier3::StatusOr<
std::unordered_map<NamespaceId, std::unordered_set<DocumentId>>>
PopulateDocumentIdFilters(
const DocumentStore* document_store,
const icing::lib::SuggestionSpecProto& suggestion_spec,
const std::unordered_set<NamespaceId>& namespace_ids) {
std::unordered_map<NamespaceId, std::unordered_set<DocumentId>>
document_id_filter_map;
document_id_filter_map.reserve(suggestion_spec.document_uri_filters_size());
for (const NamespaceDocumentUriGroup& namespace_document_uri_group :
suggestion_spec.document_uri_filters()) {
auto namespace_id_or = document_store->GetNamespaceId(
namespace_document_uri_group.namespace_());
if (!namespace_id_or.ok()) {
// The current namespace doesn't exist.
continue;
}
NamespaceId namespace_id = namespace_id_or.ValueOrDie();
if (!namespace_ids.empty() &&
namespace_ids.find(namespace_id) == namespace_ids.end()) {
// The current namespace doesn't appear in the namespace filter.
return absl_ports::InvalidArgumentError(absl_ports::StrCat(
"The namespace : ", namespace_document_uri_group.namespace_(),
" appears in the document uri filter, but doesn't appear in the "
"namespace filter."));
}
if (namespace_document_uri_group.document_uris().empty()) {
// Client should use namespace filter to filter out all document under
// a namespace.
return absl_ports::InvalidArgumentError(absl_ports::StrCat(
"The namespace : ", namespace_document_uri_group.namespace_(),
" has empty document uri in the document uri filter. Please use the "
"namespace filter to exclude a namespace instead of the document uri "
"filter."));
}
// Translate namespace document Uris into document_ids
std::unordered_set<DocumentId> target_document_ids;
target_document_ids.reserve(
namespace_document_uri_group.document_uris_size());
for (std::string_view document_uri :
namespace_document_uri_group.document_uris()) {
auto document_id_or = document_store->GetDocumentId(
namespace_document_uri_group.namespace_(), document_uri);
if (!document_id_or.ok()) {
continue;
}
target_document_ids.insert(document_id_or.ValueOrDie());
}
document_id_filter_map.insert({namespace_id, target_document_ids});
}
return document_id_filter_map;
}
libtextclassifier3::StatusOr<std::unordered_map<SchemaTypeId, SectionIdMask>>
PopulatePropertyFilters(
const SchemaStore* schema_store,
const icing::lib::SuggestionSpecProto& suggestion_spec,
const std::unordered_set<SchemaTypeId>& schema_type_ids) {
std::unordered_map<SchemaTypeId, SectionIdMask> property_filter_map;
property_filter_map.reserve(suggestion_spec.type_property_filters_size());
for (const TypePropertyMask& type_field_mask :
suggestion_spec.type_property_filters()) {
auto schema_type_id_or =
schema_store->GetSchemaTypeId(type_field_mask.schema_type());
if (!schema_type_id_or.ok()) {
// The current schema doesn't exist
continue;
}
SchemaTypeId schema_type_id = schema_type_id_or.ValueOrDie();
if (!schema_type_ids.empty() &&
schema_type_ids.find(schema_type_id) == schema_type_ids.end()) {
// The current schema type doesn't appear in the schema type filter.
return absl_ports::InvalidArgumentError(absl_ports::StrCat(
"The schema : ", type_field_mask.schema_type(),
" appears in the property filter, but doesn't appear in the schema"
" type filter."));
}
if (type_field_mask.paths().empty()) {
return absl_ports::InvalidArgumentError(absl_ports::StrCat(
"The schema type : ", type_field_mask.schema_type(),
" has empty path in the property filter. Please use the schema type"
" filter to exclude a schema type instead of the property filter."));
}
// Translate property paths into section id mask
SectionIdMask section_mask = kSectionIdMaskNone;
auto section_metadata_list_or =
schema_store->GetSectionMetadata(type_field_mask.schema_type());
if (!section_metadata_list_or.ok()) {
// The current schema doesn't has section metadata.
continue;
}
std::unordered_set<std::string> target_property_paths;
target_property_paths.reserve(type_field_mask.paths_size());
for (const std::string& target_property_path : type_field_mask.paths()) {
target_property_paths.insert(target_property_path);
}
const std::vector<SectionMetadata>* section_metadata_list =
section_metadata_list_or.ValueOrDie();
for (const SectionMetadata& section_metadata : *section_metadata_list) {
if (target_property_paths.find(section_metadata.path) !=
target_property_paths.end()) {
section_mask |= UINT64_C(1) << section_metadata.id;
}
}
property_filter_map.insert({schema_type_id, section_mask});
}
return property_filter_map;
}
libtextclassifier3::StatusOr<std::vector<TermMetadata>>
SuggestionProcessor::QuerySuggestions(
const icing::lib::SuggestionSpecProto& suggestion_spec,
int64_t current_time_ms) {
// We use query tokenizer to tokenize the give prefix, and we only use the
// last token to be the suggestion prefix.
// Populate target namespace filter.
std::unordered_set<NamespaceId> namespace_ids;
namespace_ids.reserve(suggestion_spec.namespace_filters_size());
for (std::string_view name_space : suggestion_spec.namespace_filters()) {
auto namespace_id_or = document_store_.GetNamespaceId(name_space);
if (!namespace_id_or.ok()) {
// The current namespace doesn't exist.
continue;
}
namespace_ids.insert(namespace_id_or.ValueOrDie());
}
if (namespace_ids.empty() && !suggestion_spec.namespace_filters().empty()) {
// None of desired namespace exists, we should return directly.
return std::vector<TermMetadata>();
}
// Populate target document id filter.
auto document_id_filter_map_or = PopulateDocumentIdFilters(
&document_store_, suggestion_spec, namespace_ids);
if (!document_id_filter_map_or.ok()) {
return std::move(document_id_filter_map_or).status();
}
std::unordered_map<NamespaceId, std::unordered_set<DocumentId>>
document_id_filter_map = document_id_filter_map_or.ValueOrDie();
if (document_id_filter_map.empty() &&
!suggestion_spec.document_uri_filters().empty()) {
// None of desired DocumentId exists, we should return directly.
return std::vector<TermMetadata>();
}
// Populate target schema type filter.
std::unordered_set<SchemaTypeId> schema_type_ids;
schema_type_ids.reserve(suggestion_spec.schema_type_filters_size());
for (std::string_view schema_type : suggestion_spec.schema_type_filters()) {
auto schema_type_id_or = schema_store_.GetSchemaTypeId(schema_type);
if (!schema_type_id_or.ok()) {
continue;
}
schema_type_ids.insert(schema_type_id_or.ValueOrDie());
}
if (schema_type_ids.empty() &&
!suggestion_spec.schema_type_filters().empty()) {
// None of desired schema type exists, we should return directly.
return std::vector<TermMetadata>();
}
// Populate target properties filter.
auto property_filter_map_or =
PopulatePropertyFilters(&schema_store_, suggestion_spec, schema_type_ids);
if (!property_filter_map_or.ok()) {
return std::move(property_filter_map_or).status();
}
std::unordered_map<SchemaTypeId, SectionIdMask> property_filter_map =
property_filter_map_or.ValueOrDie();
ICING_ASSIGN_OR_RETURN(
std::unique_ptr<QueryProcessor> query_processor,
QueryProcessor::Create(
&index_, &numeric_index_, &embedding_index_, &language_segmenter_,
&normalizer_, &document_store_, &schema_store_,
/*join_children_fetcher=*/nullptr, &clock_, &feature_flags_));
SearchSpecProto search_spec;
search_spec.set_query(suggestion_spec.prefix());
search_spec.set_term_match_type(
suggestion_spec.scoring_spec().scoring_match_type());
for (const PropertyProto::VectorProto& vector :
suggestion_spec.embedding_query_vectors()) {
*search_spec.add_embedding_query_vectors() = vector;
}
search_spec.set_embedding_query_metric_type(
suggestion_spec.embedding_query_metric_type());
for (const std::string& query_parameter_string :
suggestion_spec.query_parameter_strings()) {
search_spec.add_query_parameter_strings(query_parameter_string);
}
for (const std::string& enabled_feature :
suggestion_spec.enabled_features()) {
search_spec.add_enabled_features(enabled_feature);
}
ICING_ASSIGN_OR_RETURN(
QueryResults query_results,
query_processor->ParseSearch(search_spec,
ScoringSpecProto::RankingStrategy::NONE,
current_time_ms));
ICING_ASSIGN_OR_RETURN(
DocHitInfoIterator::TrimmedNode trimmed_node,
std::move(*query_results.root_iterator).TrimRightMostNode());
// If the position of the last token is not the end of the prefix, it means
// there should be some operator tokens after it and are ignored by the
// tokenizer.
bool is_last_token =
trimmed_node.term_start_index_ + trimmed_node.unnormalized_term_length_ >=
suggestion_spec.prefix().length();
if (!is_last_token || trimmed_node.term_.empty()) {
// We don't have a valid last token, return early.
return std::vector<TermMetadata>();
}
// Populate the search base in document ids.
// Suggestions are only generated for the very last term,
// trimmed_node.iterator_ tracks search results for all previous terms. If it
// is null means there is no pervious term and we are generating suggetion for
// a single term.
std::unordered_set<DocumentId> search_base;
if (trimmed_node.iterator_ != nullptr) {
while (trimmed_node.iterator_->Advance().ok()) {
search_base.insert(trimmed_node.iterator_->doc_hit_info().document_id());
}
if (search_base.empty()) {
// Nothing matches the previous terms in the query. There are no valid
// suggestions to make, we should return directly.
return std::vector<TermMetadata>();
}
}
// Create result checker based on given filters.
SuggestionResultCheckerImpl suggestion_result_checker_impl(
&document_store_, &schema_store_, std::move(namespace_ids),
std::move(document_id_filter_map), std::move(schema_type_ids),
std::move(property_filter_map), std::move(trimmed_node.target_section_),
std::move(search_base), current_time_ms);
// TODO(b/228240987) support generate suggestion and append suffix for advance
// query and function call.
std::string query_prefix =
suggestion_spec.prefix().substr(0, trimmed_node.term_start_index_);
// Run suggestion based on given SuggestionSpec.
// Normalize token text to lowercase since all tokens in the lexicon are
// lowercase.
ICING_ASSIGN_OR_RETURN(
std::vector<TermMetadata> terms,
index_.FindTermsByPrefix(
trimmed_node.term_, suggestion_spec.num_to_return(),
suggestion_spec.scoring_spec().scoring_match_type(),
suggestion_spec.scoring_spec().rank_by(),
&suggestion_result_checker_impl));
for (TermMetadata& term : terms) {
term.content = query_prefix + term.content;
}
return terms;
}
SuggestionProcessor::SuggestionProcessor(
Index* index, const NumericIndex<int64_t>* numeric_index,
const EmbeddingIndex* embedding_index,
const LanguageSegmenter* language_segmenter, const Normalizer* normalizer,
const DocumentStore* document_store, const SchemaStore* schema_store,
const Clock* clock, const FeatureFlags* feature_flags)
: index_(*index),
numeric_index_(*numeric_index),
embedding_index_(*embedding_index),
language_segmenter_(*language_segmenter),
normalizer_(*normalizer),
document_store_(*document_store),
schema_store_(*schema_store),
clock_(*clock),
feature_flags_(*feature_flags) {}
} // namespace lib
} // namespace icing