blob: 46a96ff544fdec5eaeddebb8827880e42fdce091 [file] [log] [blame] [edit]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/omnibox/browser/autocomplete_controller.h"
#include <inttypes.h>
#include <limits.h>
#include <algorithm>
#include <cstddef>
#include <functional>
#include <map>
#include <memory>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <string>
#include <tuple>
#include <unordered_set>
#include <utility>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/feature_list.h"
#include "base/format_macros.h"
#include "base/functional/bind.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/observer_list.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "base/timer/elapsed_timer.h"
#include "base/trace_event/memory_dump_manager.h"
#include "build/build_config.h"
#include "components/history_embeddings/history_embeddings_features.h"
#include "components/lens/lens_features.h"
#include "components/omnibox/browser/actions/omnibox_action_in_suggest.h"
#include "components/omnibox/browser/actions/omnibox_answer_action.h"
#include "components/omnibox/browser/actions/omnibox_pedal_provider.h"
#include "components/omnibox/browser/autocomplete_enums.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/autocomplete_provider.h"
#include "components/omnibox/browser/autocomplete_provider_client.h"
#include "components/omnibox/browser/autocomplete_scoring_signals_annotator.h"
#include "components/omnibox/browser/bookmark_provider.h"
#include "components/omnibox/browser/bookmark_scoring_signals_annotator.h"
#include "components/omnibox/browser/builtin_provider.h"
#include "components/omnibox/browser/calculator_provider.h"
#include "components/omnibox/browser/clipboard_provider.h"
#include "components/omnibox/browser/contextual_search_provider.h"
#include "components/omnibox/browser/document_provider.h"
#include "components/omnibox/browser/enterprise_search_aggregator_provider.h"
#include "components/omnibox/browser/featured_search_provider.h"
#include "components/omnibox/browser/history_embeddings_provider.h"
#include "components/omnibox/browser/history_fuzzy_provider.h"
#include "components/omnibox/browser/history_quick_provider.h"
#include "components/omnibox/browser/history_scoring_signals_annotator.h"
#include "components/omnibox/browser/history_url_provider.h"
#include "components/omnibox/browser/keyword_provider.h"
#include "components/omnibox/browser/local_history_zero_suggest_provider.h"
#include "components/omnibox/browser/most_visited_sites_provider.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/browser/on_device_head_provider.h"
#include "components/omnibox/browser/open_tab_provider.h"
#include "components/omnibox/browser/page_classification_functions.h"
#include "components/omnibox/browser/recently_closed_tabs_provider.h"
#include "components/omnibox/browser/search_provider.h"
#include "components/omnibox/browser/search_scoring_signals_annotator.h"
#include "components/omnibox/browser/shortcuts_provider.h"
#include "components/omnibox/browser/tab_group_provider.h"
#include "components/omnibox/browser/unscoped_extension_provider.h"
#include "components/omnibox/browser/url_scoring_signals_annotator.h"
#include "components/omnibox/browser/voice_suggest_provider.h"
#include "components/omnibox/browser/zero_suggest_provider.h"
#include "components/omnibox/browser/zero_suggest_verbatim_match_provider.h"
#include "components/omnibox/common/omnibox_feature_configs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/open_from_clipboard/clipboard_recent_content.h"
#include "components/optimization_guide/machine_learning_tflite_buildflags.h"
#include "components/search_engines/search_engine_type.h"
#include "components/search_engines/template_url.h"
#include "components/search_engines/template_url_service.h"
#include "components/search_engines/template_url_starter_pack_data.h"
#include "components/strings/grit/components_strings.h"
#include "components/url_formatter/elide_url.h"
#include "net/http/http_util.h"
#include "third_party/metrics_proto/omnibox_focus_type.pb.h"
#include "third_party/metrics_proto/omnibox_scoring_signals.pb.h"
#include "third_party/omnibox_proto/chrome_searchbox_stats.pb.h"
#include "third_party/omnibox_proto/types.pb.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/url_canon.h"
#include "url/url_util.h"
#if !BUILDFLAG(IS_IOS)
#include "components/history_clusters/core/config.h" // nogncheck
#include "components/omnibox/browser/actions/history_clusters_action.h"
#include "components/omnibox/browser/history_cluster_provider.h"
#include "components/open_from_clipboard/clipboard_recent_content_generic.h"
#endif
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
#include "components/omnibox/browser/autocomplete_scoring_model_service.h"
#endif
constexpr bool kIsDesktop = !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS);
namespace {
using ScoringSignals = ::metrics::OmniboxScoringSignals;
using ProviderType = AutocompleteProvider::Type;
constexpr bool is_android = !!BUILDFLAG(IS_ANDROID);
void RecordMlScoreCoverage(size_t matches_with_non_null_scores,
size_t total_scored_matches) {
int percent_score_coverage =
matches_with_non_null_scores * 100 / total_scored_matches;
base::UmaHistogramPercentage(
"Omnibox.URLScoringModelExecuted.MLScoreCoverage",
percent_score_coverage);
}
// Records the coverage (i.e. null vs non-null values) for each of the
// `scoring_signals` associated with matches generated by the given `provider`.
void RecordScoringSignalCoverageForProvider(
const ScoringSignals& scoring_signals,
const AutocompleteProvider* provider) {
// Keep consistent:
// - omnibox_event.proto `ScoringSignals`
// - omnibox_scoring_signals.proto `OmniboxScoringSignals`
// - autocomplete_scoring_model_handler.cc
// `AutocompleteScoringModelHandler::ExtractInputFromScoringSignals()`
// - autocomplete_match.cc `AutocompleteMatch::MergeScoringSignals()`
// - autocomplete_controller.cc `RecordScoringSignalCoverageForProvider()`
// - omnibox_metrics_provider.cc `GetScoringSignalsForLogging()`
// - omnibox.mojom `struct Signals`
// - omnibox_page_handler.cc
// `TypeConverter<AutocompleteMatch::ScoringSignals, mojom::SignalsPtr>`
// - omnibox_page_handler.cc `TypeConverter<mojom::SignalsPtr,
// AutocompleteMatch::ScoringSignals>`
// - omnibox_util.ts `signalNames`
// - omnibox/histograms.xml
// `Omnibox.URLScoringModelExecuted.ScoringSignalCoverage`
if (!provider) {
return;
}
// Map from each scoring signal type to whether or not it has a non-null
// value.
std::vector<std::pair<std::string, bool>> scoring_signal_values{
{"typed_count", scoring_signals.has_typed_count()},
{"visit_count", scoring_signals.has_visit_count()},
{"elapsed_time_last_visit_secs",
scoring_signals.has_elapsed_time_last_visit_secs()},
{"shortcut_visit_count", scoring_signals.has_shortcut_visit_count()},
{"shortest_shortcut_len", scoring_signals.has_shortest_shortcut_len()},
{"elapsed_time_last_shortcut_visit_sec",
scoring_signals.has_elapsed_time_last_shortcut_visit_sec()},
{"is_host_only", scoring_signals.has_is_host_only()},
{"num_bookmarks_of_url", scoring_signals.has_num_bookmarks_of_url()},
{"first_bookmark_title_match_position",
scoring_signals.has_first_bookmark_title_match_position()},
{"total_bookmark_title_match_length",
scoring_signals.has_total_bookmark_title_match_length()},
{"num_input_terms_matched_by_bookmark_title",
scoring_signals.has_num_input_terms_matched_by_bookmark_title()},
{"first_url_match_position",
scoring_signals.has_first_url_match_position()},
{"total_url_match_length", scoring_signals.has_total_url_match_length()},
{"host_match_at_word_boundary",
scoring_signals.has_host_match_at_word_boundary()},
{"total_host_match_length",
scoring_signals.has_total_host_match_length()},
{"total_path_match_length",
scoring_signals.has_total_path_match_length()},
{"total_query_or_ref_match_length",
scoring_signals.has_total_query_or_ref_match_length()},
{"total_title_match_length",
scoring_signals.has_total_title_match_length()},
{"has_non_scheme_www_match",
scoring_signals.has_has_non_scheme_www_match()},
{"num_input_terms_matched_by_title",
scoring_signals.has_num_input_terms_matched_by_title()},
{"num_input_terms_matched_by_url",
scoring_signals.has_num_input_terms_matched_by_url()},
{"length_of_url", scoring_signals.has_length_of_url()},
{"site_engagement", scoring_signals.has_site_engagement()},
{"allowed_to_be_default_match",
scoring_signals.has_allowed_to_be_default_match()},
{"search_suggest_relevance",
scoring_signals.has_search_suggest_relevance()},
{"is_search_suggest_entity",
scoring_signals.has_is_search_suggest_entity()},
{"is_verbatim", scoring_signals.has_is_verbatim()},
{"is_navsuggest", scoring_signals.has_is_navsuggest()},
{"is_search_suggest_tail", scoring_signals.has_is_search_suggest_tail()},
{"is_answer_suggest", scoring_signals.has_is_answer_suggest()},
{"is_calculator_suggest", scoring_signals.has_is_calculator_suggest()},
};
const std::string provider_type = provider->GetName();
for (const auto& pair : scoring_signal_values) {
base::UmaHistogramBoolean(
"Omnibox.URLScoringModelExecuted.ScoringSignalCoverage." +
provider_type + "." + pair.first,
pair.second);
}
}
void RecordMlScoringElapsedTime(base::TimeDelta elapsed) {
UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
"Omnibox.URLScoringModelExecuted.ElapsedTime", elapsed,
base::Microseconds(1), base::Milliseconds(3), 100);
}
void RecordTotalMatchesScored(size_t num_scored) {
base::UmaHistogramCounts1000("Omnibox.URLScoringModelExecuted.Matches",
num_scored);
}
// Appends available autocompletion of the given type, subtype, and number to
// the existing available autocompletions string, encoding according to the
// spec.
std::string ConstructAvailableAutocompletion(
omnibox::SuggestType type,
const base::flat_set<omnibox::SuggestSubtype>& subtypes,
int count) {
std::ostringstream result;
result << int(type);
for (auto subtype : subtypes) {
result << 'i' << subtype;
}
if (count > 1) {
result << 'l' << count;
}
return result.str();
}
#if !BUILDFLAG(IS_ANDROID)
// Returns whether this match is provided by an extension in unscoped mode.
bool IsUnscopedExtensionMatch(const AutocompleteMatch& match) {
return match.provider && match.provider->type() ==
AutocompleteProvider::TYPE_UNSCOPED_EXTENSION;
}
#endif // !BUILDFLAG(IS_ANDROID)
// Returns which rich autocompletion type, if any, had (or would have had for
// counterfactual variations) an impact; i.e. whether the top scoring rich
// autocompleted suggestion outscores the top scoring default suggestion.
AutocompleteMatch::RichAutocompletionType TopMatchRichAutocompletionType(
const AutocompleteResult& result) {
// Trigger rich autocompletion logging if the highest scoring match has
// |rich_autocompletion_triggered| set indicating it is, or could have been
// rich autocompleted. It's not sufficient to check the default match since
// counterfactual variations will not allow rich autocompleted matches to be
// the default match.
if (result.empty()) {
return AutocompleteMatch::RichAutocompletionType::kNone;
}
auto get_sort_key = [](const AutocompleteMatch& match) {
return std::make_tuple(
match.allowed_to_be_default_match ||
match.rich_autocompletion_triggered !=
AutocompleteMatch::RichAutocompletionType::kNone,
match.relevance);
};
auto top_match = std::ranges::max_element(result, {}, get_sort_key);
return top_match->rich_autocompletion_triggered;
}
void RecordMatchDeletion(const AutocompleteMatch& match) {
if (match.deletable) {
// This formula combines provider and result type into a single enum as
// defined in OmniboxProviderAndResultType in enums.xml.
auto combined_type = match.provider->AsOmniboxEventProviderType() * 100 +
match.GetOmniboxEventResultType();
// This histogram is defined in the internal histograms.xml. This is because
// the vast majority of OmniboxProviderAndResultType histograms are
// generated by internal tools, and we wish to keep them together.
base::UmaHistogramSparse("Omnibox.SuggestionDeleted.ProviderAndResultType",
combined_type);
}
}
// Return if the default match from a previous pass should be preserved.
bool ShouldPreserveLastDefaultMatch(
AutocompleteController::UpdateType update_type,
const AutocompleteInput& input) {
// Don't preserve default in keyword mode to avoid e.g. the 'google.com'
// suggestion being preserved and kicking the user out of keyword mode when
// they type 'google.com '.
if (input.prefer_keyword()) {
return false;
}
// Preserve for all async updates, but only for longer inputs for sync
// updates. This mitigates aggressive scoring search suggestions getting
// 'stuck' as the default when short inputs provide low confidence.
if (update_type == AutocompleteController::UpdateType::kSyncPassOnly ||
update_type == AutocompleteController::UpdateType::kSyncPass) {
return input.text().length() >= 4;
} else {
return true;
}
}
// Helper function to retrieve domains that will be used to find a match between
// historical suggestions and a company entity suggestion. Matches of
// AutocompleteMatchType::HISTORY_URL type will return the domain of
// |destination_url| and those of AutocompleteMatchType::SEARCH_SUGGEST_ENTITY
// will return the domain of |website_uri|. For any other match types,
// GetDomain() should not be called.
std::u16string GetDomain(const AutocompleteMatch& match) {
DCHECK(match.type == AutocompleteMatchType::HISTORY_URL ||
match.type == AutocompleteMatchType::SEARCH_SUGGEST_ENTITY);
GURL url = match.type == AutocompleteMatchType::HISTORY_URL
? match.destination_url
: GURL(match.website_uri);
std::u16string url_host;
std::u16string url_domain;
url_formatter::SplitHost(url, &url_host, &url_domain, nullptr);
return url_domain;
}
std::string EncodeURIComponent(const std::string& component) {
url::RawCanonOutputT<char> encoded;
url::EncodeURIComponent(component, &encoded);
return std::string(encoded.view());
}
// Returns whether contextual suggestions can be shown to the user.
// If the page has a paywall signal, contextual suggestions cannot be shown.
// If the page paywall signal could not be determined (this `paywall_signal` is
// std::nullopt), a feature flag will be used to determine if contextual
// suggestions can be shown.
bool CanShowContextualSuggestions(std::optional<bool> paywall_signal) {
const auto& contextual_search_params =
omnibox_feature_configs::ContextualSearch::Get();
// If the feature flag to use the APC paywall signal is disabled, show
// contextual suggestions.
if (!contextual_search_params.use_apc_paywall_signal) {
return true;
}
// If the page has determined a signal, use it to determine if contextual
// suggestions should be shown.
if (paywall_signal.has_value()) {
// Negate since if `paywall_signal` is true, it means the page is paywalled
// and contextual suggestions should not be shown.
return !paywall_signal.value();
}
// Finally, if no signal was extracted from the page, fallback to either show
// or hide contextual suggestions based on the feature flag.
return contextual_search_params.show_suggestions_on_no_apc;
}
} // namespace
AutocompleteController::OldResult::OldResult(UpdateType update_type,
const AutocompleteInput& input,
AutocompleteResult* result) {
if (result->default_match()) {
last_default_match = *result->default_match();
if (last_default_match->associated_keyword) {
last_default_associated_keyword =
last_default_match->associated_keyword->keyword;
}
}
if (last_default_match &&
ShouldPreserveLastDefaultMatch(update_type, input)) {
default_match_to_preserve = last_default_match;
}
if (update_type == UpdateType::kSyncPass ||
update_type == UpdateType::kAsyncPass) {
matches_to_transfer.SwapMatchesWith(result);
} else {
result->ClearMatches();
}
}
AutocompleteController::OldResult::~OldResult() = default;
// static
std::string AutocompleteController::UpdateTypeToDebugString(
UpdateType update_type) {
switch (update_type) {
case UpdateType::kNone:
return "None";
case UpdateType::kSyncPassOnly:
return "Sync pass only";
case UpdateType::kSyncPass:
return "Sync pass";
case UpdateType::kAsyncPass:
return "Async pass";
case UpdateType::kLastAsyncPassExceptDoc:
return "Last async pass except doc";
case UpdateType::kExpirePass:
return "Expire pass";
case UpdateType::kLastAsyncPass:
return "Last async pass";
case UpdateType::kStop:
return "Stop";
case UpdateType::kMatchDeletion:
return "Match deletion";
}
NOTREACHED();
}
// static
void AutocompleteController::ExtendMatchSubtypes(
const AutocompleteMatch& match,
base::flat_set<omnibox::SuggestSubtype>* subtypes) {
// If provider is TYPE_ZERO_SUGGEST_LOCAL_HISTORY, TYPE_ZERO_SUGGEST, or
// TYPE_ON_DEVICE_HEAD, set the subtype accordingly.
if (match.provider) {
if (match.provider->type() == AutocompleteProvider::TYPE_ZERO_SUGGEST) {
// Make sure changes here are reflected in UpdateSearchboxStats()
// below in which the zero-prefix suggestions are counted.
// We abuse this subtype and use it to for zero-suggest suggestions that
// aren't personalized by the server. That is, it indicates either
// client-side most-likely URL suggestions or server-side suggestions
// that depend only on the URL as context.
if (match.type == AutocompleteMatchType::NAVSUGGEST) {
subtypes->emplace(omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_URLS);
subtypes->emplace(omnibox::SUBTYPE_URL_BASED);
} else if (match.type == AutocompleteMatchType::SEARCH_SUGGEST) {
subtypes->emplace(omnibox::SUBTYPE_URL_BASED);
}
} else if (match.provider->type() ==
AutocompleteProvider::TYPE_ON_DEVICE_HEAD) {
// This subtype indicates a match from an on-device head provider.
subtypes->emplace(omnibox::SUBTYPE_SUGGEST_2G_LITE);
// Make sure changes here are reflected in UpdateSearchboxStats()
// below in which the zero-prefix suggestions are counted.
} else if (match.provider->type() ==
AutocompleteProvider::TYPE_ZERO_SUGGEST_LOCAL_HISTORY) {
subtypes->emplace(omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_HISTORY);
}
}
switch (match.type) {
case AutocompleteMatchType::SEARCH_SUGGEST_PERSONALIZED: {
subtypes->emplace(omnibox::SUBTYPE_PERSONAL);
break;
}
case AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_ECHO_SEARCH);
break;
}
case AutocompleteMatchType::URL_WHAT_YOU_TYPED: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_ECHO_URL);
break;
}
case AutocompleteMatchType::SEARCH_HISTORY: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_HISTORY_SEARCH);
break;
}
case AutocompleteMatchType::HISTORY_URL: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_HISTORY_URL);
break;
}
case AutocompleteMatchType::HISTORY_TITLE: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_HISTORY_TITLE);
break;
}
case AutocompleteMatchType::HISTORY_BODY: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_HISTORY_BODY);
break;
}
case AutocompleteMatchType::HISTORY_KEYWORD: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_HISTORY_KEYWORD);
break;
}
case AutocompleteMatchType::BOOKMARK_TITLE: {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_BOOKMARK_TITLE);
break;
}
case AutocompleteMatchType::NAVSUGGEST_PERSONALIZED: {
subtypes->emplace(omnibox::SUBTYPE_PERSONAL);
break;
}
case AutocompleteMatchType::CLIPBOARD_URL: {
subtypes->emplace(omnibox::SUBTYPE_CLIPBOARD_URL);
break;
}
case AutocompleteMatchType::CLIPBOARD_TEXT: {
subtypes->emplace(omnibox::SUBTYPE_CLIPBOARD_TEXT);
break;
}
case AutocompleteMatchType::CLIPBOARD_IMAGE: {
subtypes->emplace(omnibox::SUBTYPE_CLIPBOARD_IMAGE);
break;
}
default: {
// This value indicates a native chrome suggestion with no named subtype
// (yet).
if (subtypes->empty()) {
subtypes->emplace(omnibox::SUBTYPE_OMNIBOX_OTHER);
}
}
}
}
// static
int AutocompleteController::ApplyPiecewiseScoringTransform(
double ml_score,
std::vector<std::pair<double, int>> break_points) {
// Start and end points for the line segment whose domain contains `ml_score`.
std::pair<double, int> start;
std::pair<double, int> end;
for (size_t i = 0; i < break_points.size() - 1; i++) {
start = break_points[i];
end = break_points[i + 1];
if (ml_score <= end.first) {
double m = (end.second - start.second) / (end.first - start.first);
double b = end.second - m * end.first;
return m * ml_score + b;
}
}
return 0;
}
AutocompleteController::AutocompleteController(
std::unique_ptr<AutocompleteProviderClient> provider_client,
int provider_types,
bool is_cros_launcher,
bool disable_ml)
: provider_client_(std::move(provider_client)),
bookmark_provider_(nullptr),
document_provider_(nullptr),
history_url_provider_(nullptr),
keyword_provider_(nullptr),
search_provider_(nullptr),
zero_suggest_provider_(nullptr),
on_device_head_provider_(nullptr),
history_fuzzy_provider_(nullptr),
contextual_search_provider_(nullptr),
stop_timer_duration_(kAutocompleteDefaultStopTimerDuration),
notify_changed_debouncer_(false, 200),
is_cros_launcher_(is_cros_launcher),
search_service_worker_signal_sent_(false),
disable_ml_(disable_ml),
template_url_service_(provider_client_->GetTemplateURLService()),
triggered_feature_service_(
provider_client_->GetOmniboxTriggeredFeatureService()),
steady_state_omnibox_position_(
metrics::OmniboxEventProto::UNKNOWN_POSITION) {
provider_types &= ~OmniboxFieldTrial::GetDisabledProviderTypes();
// Providers run in the order they're added. Async providers should run first
// so their async requests can be kicked off before waiting a few milliseconds
// for the other sync providers to complete.
InitializeAsyncProviders(provider_types);
InitializeSyncProviders(provider_types);
// Ideally, we'd check `IsApplicationLocaleSupportedByJourneys()` when
// constructing `provider_types`. But that's usually constructed in
// `AutocompleteClassifier::DefaultOmniboxProviders` which can't depend on the
// browser dir to detect locale. The alternative of piping in the locale from
// each call site seems too intrusive for a temporary condition (some call
// sites are also in the components dir). All callers of
// `DefaultOmniboxProviders` only use it to then construct
// `AutocompleteController`, so placing the check here instead has no behavior
// change.
// TODO(manukh): Move this to `InitializeAsyncProviders()`.
#if !BUILDFLAG(IS_IOS)
// HistoryClusters is not enabled on iOS.
if (provider_types & AutocompleteProvider::TYPE_HISTORY_CLUSTER_PROVIDER &&
history_clusters::IsApplicationLocaleSupportedByJourneys(
provider_client_->GetApplicationLocale()) &&
search_provider_ != nullptr && history_url_provider_ != nullptr &&
history_quick_provider_ != nullptr) {
providers_.push_back(new HistoryClusterProvider(
provider_client_.get(), this, search_provider_, history_url_provider_,
history_quick_provider_));
}
#endif
// Create URL scoring signal annotators.
if (OmniboxFieldTrial::IsPopulatingUrlScoringSignalsEnabled() &&
OmniboxFieldTrial::AreScoringSignalsAnnotatorsEnabled()) {
url_scoring_signals_annotators_.push_back(
std::make_unique<HistoryScoringSignalsAnnotator>(
provider_client_.get()));
url_scoring_signals_annotators_.push_back(
std::make_unique<BookmarkScoringSignalsAnnotator>(
provider_client_.get()));
url_scoring_signals_annotators_.push_back(
std::make_unique<UrlScoringSignalsAnnotator>());
url_scoring_signals_annotators_.push_back(
std::make_unique<SearchScoringSignalsAnnotator>());
}
base::trace_event::MemoryDumpManager::GetInstance()->RegisterDumpProvider(
this, "AutocompleteController",
base::SingleThreadTaskRunner::GetCurrentDefault());
}
AutocompleteController::AutocompleteController(
std::unique_ptr<AutocompleteProviderClient> provider_client,
int provider_types,
base::TimeDelta stop_timer_duration,
bool is_cros_launcher,
bool disable_ml)
: AutocompleteController(std::move(provider_client),
provider_types,
is_cros_launcher,
disable_ml) {
stop_timer_duration_ = stop_timer_duration;
}
AutocompleteController::~AutocompleteController() {
base::trace_event::MemoryDumpManager::GetInstance()->UnregisterDumpProvider(
this);
// Must stop providers because they may have unowned tasks that continue to
// run or hold refs to them; e.g. remote requests or DB reads. Don't bother
// notifying observers or using `kClobbered` to clear provider caches and
// state, since the observers and providers are being destroyed too.
Stop(AutocompleteStopReason::kInteraction);
}
void AutocompleteController::AddObserver(Observer* observer) {
observers_.AddObserver(observer);
}
void AutocompleteController::RemoveObserver(Observer* observer) {
observers_.RemoveObserver(observer);
}
void AutocompleteController::Start(const AutocompleteInput& input) {
TRACE_EVENT1("omnibox", "AutocompleteController::Start", "text",
base::UTF16ToUTF8(input.text()));
// Providers assume synchronous inputs (`omit_asynchronous_matches() ==
// true`) are not zero-suggest ones. See crbug.com/1339425.
DCHECK(!input.omit_asynchronous_matches() || !input.IsZeroSuggest());
// Use a zero-suggest input as the signal that zero-prefix suggestions could
// have been shown in the autocomplete session.
if (input.IsZeroSuggest()) {
internal_result_.set_zero_prefix_enabled_in_session(true);
}
triggered_feature_service_->ResetInput();
// When input.omit_asynchronous_matches() is true, the AutocompleteController
// is being used for text classification, which should not notify observers.
// TODO(manukh): This seems unnecessary; `AutocompleteClassifier` and
// `OmniboxController` use separate instances of `AutocompleteController`,
// the former doesn't add observers, the latter always uses
// `omit_asynchronous_matches()` set to false. Besides, if that weren't the
// case, e.g. the classifier did add an observer, then
// `AutocompleteController` should respect that, not assume it's a mistake
// and silently ignore the observer. Audit all call paths of `::Start()` to
// remove this check.
if (!input.omit_asynchronous_matches()) {
for (Observer& obs : observers_) {
obs.OnStart(this, input);
}
}
// Must be called before `expire_timer_.Stop()`, modifying `done_`, or
// modifying `AutocompleteProvider::done_` below. If the previous request has
// not completed, and therefore has not been logged yet, will log it now.
// Likewise, if the providers have not completed, and therefore have not been
// logged yet, will log them now.
metrics_.OnStart();
// See if we can avoid rerunning autocomplete when the query hasn't changed
// much. When the user presses or releases the ctrl key, the desired_tld
// changes, and when the user finishes an IME composition, inline autocomplete
// may no longer be prevented. In both these cases the text itself hasn't
// changed since the last query, and some providers can do much less work (and
// get matches back more quickly). Taking advantage of this reduces flicker.
//
// NOTE: This comes after constructing |input_| above since that construction
// can change the text string (e.g. by stripping off a leading '?').
const bool minimal_changes = (input_.text() == input.text()) &&
(input_.allow_exact_keyword_match() ==
input.allow_exact_keyword_match()) &&
(input_.omit_asynchronous_matches() ==
input.omit_asynchronous_matches()) &&
(input_.focus_type() == input.focus_type());
input_ = input;
// Start the new query.
last_update_type_ = UpdateType::kNone;
// Use `start_time` rather than `metrics.start_time_` for
// 'Omnibox.QueryTime2.*'. They differ by 3 μs, which though too small to be
// distinguished in the ms-scale buckets, is large enough to move the
// arithmetic mean.
base::TimeTicks start_time = base::TimeTicks::Now();
for (const auto& provider : providers_) {
// Starter Pack engines in keyword mode only run a subset of the providers,
// so call `ShouldRunProvider()` to determine which ones should run.
if (!ShouldRunProvider(provider.get())) {
continue;
}
base::TimeTicks provider_start_time = base::TimeTicks::Now();
provider->Start(input_, minimal_changes);
// `UmaHistogramTimes()` uses 1ms - 10s buckets, whereas this uses 1ms - 5s
// buckets.
// TODO(crbug.com/1340291|manukh): This isn't handled by `metrics_` yet. It
// will "automatically" move to `metrics_` if we make all providers async.
// Otherwise, if we decide not to make all providers async, move this
// there.
base::TimeTicks provider_end_time = base::TimeTicks::Now();
base::UmaHistogramCustomTimes(
std::string("Omnibox.ProviderTime2.") + provider->GetName(),
provider_end_time - provider_start_time, base::Milliseconds(1),
base::Seconds(5), 20);
}
if (!input.omit_asynchronous_matches()) {
auto elapsed_time = base::TimeTicks::Now() - start_time;
// `UmaHistogramTimes()` uses 1ms - 10s buckets, whereas this uses 1ms -
// 1s buckets.
// TODO(crbug.com/1340291|manukh): This isn't handled by `metrics_` yet.
// Do so after we decide whether to make all providers async.
base::UmaHistogramCustomTimes("Omnibox.QueryTime2", elapsed_time,
base::Milliseconds(1), base::Seconds(1), 50);
if (input.text().length() < 6) {
base::UmaHistogramCustomTimes(
"Omnibox.QueryTime2." + base::NumberToString(input.text().length()),
elapsed_time, base::Milliseconds(1), base::Seconds(1), 50);
}
}
base::UmaHistogramBoolean("Omnibox.Start.WantAsyncMatches",
!input.omit_asynchronous_matches());
// `done` will usually be false, unless all providers are finished after the
// synchronous pass just completed.
bool done = GetProviderDoneState() == ProviderDoneState::kAllDone;
DCHECK(!input_.omit_asynchronous_matches() || done);
UpdateResult(done ? UpdateType::kSyncPassOnly : UpdateType::kSyncPass);
// If the input looks like a query, send a signal predicting that the user is
// going to issue a search (either to the default search engine or to a
// keyword search engine, as indicated by the destination_url). This allows
// any associated service worker to start up early and reduce the latency of a
// resulting search. However, to avoid a potentially expensive operation, we
// only do this once per session. Additionally, a default match is expected to
// be available at this point but we check anyway to guard against an invalid
// dereference.
if (input.type() == metrics::OmniboxInputType::QUERY &&
!search_service_worker_signal_sent_ && internal_result_.default_match()) {
search_service_worker_signal_sent_ = true;
provider_client_->StartServiceWorker(
internal_result_.default_match()->destination_url);
}
}
void AutocompleteController::StartPrefetch(const AutocompleteInput& input) {
TRACE_EVENT1("omnibox", "AutocompleteController::StartPrefetch", "text",
base::UTF16ToUTF8(input.text()));
if (!OmniboxFieldTrial::IsZeroSuggestPrefetchingEnabledInContext(
input.current_page_classification()) &&
!omnibox_feature_configs::OmniboxUrlSuggestionsOnFocus::Get()
.MostVisitedPrefetchingEnabled() &&
!omnibox_feature_configs::ContextualSearch::Get()
.IsEnabledWithPrefetch()) {
return;
}
for (auto provider : providers_) {
if (!ShouldRunProvider(provider.get())) {
continue;
}
// Avoid starting a prefetch request if a non-prefetch request is in
// progress. Though explicitly discouraged as per documentation in
// `AutocompleteProvider::StartPrefetch()`, a provider may still cancel its
// in-flight non-prefetch request when a prefetch request is started. This
// may cause the provider to never get a chance to notify the controller of
// its status; resulting in the controller to remain in an invalid state.
if (!provider->done()) {
continue;
}
provider->StartPrefetch(input);
DCHECK(provider->done());
}
}
void AutocompleteController::Stop(AutocompleteStopReason stop_reason) {
// Must be called before `expire_timer_.Stop()`, modifying `done_`, or
// modifying `AutocompleteProvider::done_` below. If the current request has
// not completed, and therefore has not been logged yet, will log it now.
// Likewise, if the providers have not completed, and therefore have not been
// logged yet, will log them now.
metrics_.OnStop();
for (const auto& provider : providers_) {
if (!ShouldRunProvider(provider.get())) {
continue;
}
provider->Stop(stop_reason);
}
// Cancel any pending requests that may update the results. Otherwise, e.g.,
// the user's suggestion selection may be reset.
UpdateResult(UpdateType::kStop);
CancelNotifyChangedRequest();
const bool non_empty_result = !internal_result_.empty();
if (stop_reason == AutocompleteStopReason::kClobbered) {
internal_result_.Reset();
if (non_empty_result) {
// Pass `notify_default_match` as false to clear only the popup and not
// the edit. Passing true would, e.g., discard the selected suggestion
// when closing the omnibox.
RequestNotifyChanged(/*notify_default_match=*/false, /*delayed=*/false);
}
}
}
void AutocompleteController::DeleteMatch(const AutocompleteMatch& match) {
TRACE_EVENT0("omnibox", "AutocompleteController::DeleteMatch");
DCHECK(match.SupportsDeletion());
// Delete duplicate matches attached to the main match first.
for (const auto& duplicate_match : match.duplicate_matches) {
if (duplicate_match.deletable) {
duplicate_match.provider->DeleteMatch(duplicate_match);
}
}
if (match.deletable) {
RecordMatchDeletion(match);
match.provider->DeleteMatch(match);
}
// Removes deleted match. Does not re-score URLs so that we don't wait on the
// posted task, therefore notifying listeners as soon as possible.
UpdateResult(UpdateType::kMatchDeletion);
}
void AutocompleteController::DeleteMatchElement(const AutocompleteMatch& match,
size_t element_index) {
DCHECK(match.SupportsDeletion());
if (match.deletable) {
RecordMatchDeletion(match);
match.provider->DeleteMatchElement(match, element_index);
}
OnProviderUpdate(true, nullptr);
}
void AutocompleteController::OnProviderUpdate(
bool updated_matches,
const AutocompleteProvider* provider) {
TRACE_EVENT0("omnibox", "AutocompleteController::OnProviderUpdate");
// Should be called even if `sync_pass_done_` is false in order to include
// early exited async providers. If the provider is done, will log how long
// the provider took.
if (provider) {
metrics_.OnProviderUpdate(*provider);
}
// Providers should only call this method during the asynchronous pass.
// There's no reason to call this during the synchronous pass, since we
// call `UpdateResult()` after the sync pass anyways. This is not a DCHECK,
// because in the unusual case that a provider calls an asynchronous method,
// and that method early exits by calling the callback immediately, it's not
// necessarily a programmer error. We should just no-op.
if (last_update_type_ == UpdateType::kNone) {
return;
}
// Allow some providers to trigger updates after `stop_timer_` has fired.
// TODO(crbug.com/364303536) This is a temporary fix for allowing history
// embedding answers to `UpdateResults()` after `stop_timer_` has fired.
// TODO(crbug.com/408512535): This is a temporary fix for allowing the
// contextual search provider to `UpdateResults()` after `stop_timer_` has
// fired.
bool allow_post_done_updates =
provider &&
(provider->type() == AutocompleteProvider::TYPE_HISTORY_EMBEDDINGS ||
provider->type() == AutocompleteProvider::TYPE_UNSCOPED_EXTENSION ||
provider->type() == AutocompleteProvider::TYPE_CONTEXTUAL_SEARCH ||
provider->type() ==
AutocompleteProvider::TYPE_ENTERPRISE_SEARCH_AGGREGATOR);
// Providers shouldn't be running and calling `OnProviderUpdate()` after
// autocompletion has stopped.
DCHECK(!done() || allow_post_done_updates)
<< "last_update_type_: "
<< AutocompleteController::UpdateTypeToDebugString(last_update_type_)
<< ", provider: " << (provider ? provider->GetName() : "null");
auto done_state = GetProviderDoneState();
if (done_state == ProviderDoneState::kAllDone) {
UpdateResult(UpdateType::kLastAsyncPass, allow_post_done_updates);
} else if (done_state == ProviderDoneState::kAllExceptDocDone) {
UpdateResult(UpdateType::kLastAsyncPassExceptDoc, allow_post_done_updates);
} else if (updated_matches) {
UpdateResult(UpdateType::kAsyncPass, allow_post_done_updates);
}
if (done_state == ProviderDoneState::kAllDone) {
size_t calculator_count =
std::ranges::count_if(published_result_, [](const auto& match) {
return match.type == AutocompleteMatchType::CALCULATOR;
});
UMA_HISTOGRAM_COUNTS_100("Omnibox.NumCalculatorMatches", calculator_count);
}
}
void AutocompleteController::AddProviderAndTriggeringLogs(
OmniboxLog* logs) const {
TRACE_EVENT0("omnibox",
"AutocompleteController::AddProviderAndTriggeringLogs");
logs->providers_info.clear();
for (const auto& provider : providers_) {
if (!ShouldRunProvider(provider.get())) {
continue;
}
// Add per-provider info, if any.
provider->AddProviderInfo(&logs->providers_info);
// This is also a good place to put code to add info that you want to
// add for every provider.
}
logs->steady_state_omnibox_position = steady_state_omnibox_position_;
// Add any features that have been triggered.
triggered_feature_service_->RecordToLogs(
&logs->features_triggered, &logs->features_triggered_in_session);
}
void AutocompleteController::ResetSession() {
search_service_worker_signal_sent_ = false;
triggered_feature_service_->ResetSession();
}
void AutocompleteController::
UpdateMatchDestinationURLWithAdditionalSearchboxStats(
base::TimeDelta query_formulation_time,
AutocompleteMatch* match) const {
TRACE_EVENT0("omnibox",
"AutocompleteController::"
"UpdateMatchDestinationURLWithAdditionalSearchboxStats");
// The searchbox_stats is expected to have been previously set when this
// method is called. If that is not the case, this method is being called by
// mistake and searchbox_stats should not be updated with additional
// information.
if (!match->search_terms_args ||
match->search_terms_args->searchbox_stats.ByteSizeLong() == 0) {
return;
}
UpdateSearchTermsArgsWithAdditionalSearchboxStats(query_formulation_time,
*match->search_terms_args);
SetMatchDestinationURL(match);
}
void AutocompleteController::UpdateSearchTermsArgsWithAdditionalSearchboxStats(
base::TimeDelta query_formulation_time,
TemplateURLRef::SearchTermsArgs& search_terms_args) const {
// Append the query formulation time (time from when the user first typed a
// character into the omnibox to when the user selected a query), whether
// a field trial has triggered, and the current page classification to the
// searchbox stats parameter.
bool search_feature_triggered =
triggered_feature_service_->GetFeatureTriggeredInSession(
metrics::OmniboxEventProto_Feature_REMOTE_SEARCH_FEATURE) ||
triggered_feature_service_->GetFeatureTriggeredInSession(
metrics::OmniboxEventProto_Feature_REMOTE_ZERO_SUGGEST_FEATURE);
const std::string experiment_stats = base::StringPrintf(
"%" PRId64 "j%dj%d", query_formulation_time.InMilliseconds(),
search_feature_triggered, input_.current_page_classification());
// TODO(crbug.com/40197024): experiment_stats is a deprecated field. We should
// however continue to report it for the downstream consumers that expect this
// field. Eventually Chrome should start logging the substitute fields and
// the downstream consumers should migrate to using those fields before we
// can stop logging this deprecated field.
search_terms_args.searchbox_stats.set_experiment_stats(experiment_stats);
if (zero_suggest_provider_) {
// Append the ExperimentStatsV2 to the searchbox stats parameter to be
// logged in searchbox_stats.proto's `experiment_stats_v2` field.
for (const auto& experiment_stat_v2 :
zero_suggest_provider_->experiment_stats_v2s()) {
// The string value consists of suggestion type/subtype pairs delimited
// with colons. However, the SearchboxStats logging flow expects
// suggestion type/subtype pairs to be delimited with commas instead.
std::string value = experiment_stat_v2.string_value();
std::replace(value.begin(), value.end(), ':', ',');
auto* reported_experiment_stats_v2 =
search_terms_args.searchbox_stats.add_experiment_stats_v2();
reported_experiment_stats_v2->set_type_int(experiment_stat_v2.type_int());
reported_experiment_stats_v2->set_string_value(value);
}
}
#if BUILDFLAG(IS_IOS)
// Append the omnibox position when it's set to experiment_stats_v2.
if (steady_state_omnibox_position_ !=
metrics::OmniboxEventProto::UNKNOWN_POSITION) {
const auto omnibox_position_stat = GetOmniboxPositionExperimentStatsV2();
auto* reported_experiment_stats_v2 =
search_terms_args.searchbox_stats.add_experiment_stats_v2();
reported_experiment_stats_v2->set_type_int(
omnibox_position_stat.type_int());
reported_experiment_stats_v2->set_int_value(
omnibox_position_stat.int_value());
}
#endif
}
void AutocompleteController::SetMatchDestinationURL(
AutocompleteMatch* match) const {
TRACE_EVENT0("omnibox", "AutocompleteController::SetMatchDestinationURL");
// Convert search terms to UTF8 and URI-component encode the string.
const std::string encoded_search_terms = EncodeURIComponent(
base::UTF16ToUTF8(match->search_terms_args->search_terms));
// Append an extra header to navigations from the @gemini scope.
const TemplateURL* turl = match->GetTemplateURL(template_url_service_, false);
if (turl &&
turl->starter_pack_id() == template_url_starter_pack_data::kGemini &&
!encoded_search_terms.empty() &&
net::HttpUtil::IsValidHeaderValue(encoded_search_terms)) {
DCHECK(net::HttpUtil::IsValidHeaderName(kOmniboxGeminiHeader));
match->extra_headers =
base::StrCat({kOmniboxGeminiHeader, ":", encoded_search_terms});
}
auto url = ComputeURLFromSearchTermsArgs(turl, *match->search_terms_args);
if (url.is_valid()) {
match->destination_url = std::move(url);
}
#if BUILDFLAG(IS_ANDROID)
match->UpdateJavaDestinationUrl();
#endif
}
void AutocompleteController::GroupSuggestionsBySearchVsURL(size_t begin,
size_t end) {
if (begin == end) {
return;
}
TRACE_EVENT0("omnibox",
"AutocompleteController::GroupSuggestionsBySearchVsURL");
AutocompleteResult& result = const_cast<AutocompleteResult&>(this->result());
const size_t num_elements = result.size();
if (begin < 0 || end <= begin || end > num_elements) {
DCHECK(false) << "Range [" << begin << "; " << end
<< ") is not valid for grouping; accepted range: [0; "
<< num_elements << ").";
return;
}
AutocompleteResult::GroupSuggestionsBySearchVsURL(
std::next(result.begin(), begin), std::next(result.begin(), end));
}
bool AutocompleteController::ShouldRunProvider(
AutocompleteProvider* provider) const {
if (!provider) {
return false;
}
// If zero prefix suggest is disabled for the Lens contextual searchbox, only
// run the typed search provider. Else, will use the IsLensSearchbox check
// below.
if (omnibox::IsLensContextualSearchbox(
input_.current_page_classification()) &&
!lens::features::ShowContextualSearchboxZeroPrefixSuggest()) {
return provider->type() == AutocompleteProvider::TYPE_SEARCH;
}
// Only a subset of providers are run for the Lens searchboxes.
if (omnibox::IsLensSearchbox(input_.current_page_classification())) {
return provider->type() == AutocompleteProvider::TYPE_SEARCH ||
provider->type() == AutocompleteProvider::TYPE_ZERO_SUGGEST;
}
#if BUILDFLAG(IS_ANDROID)
if (omnibox::IsAndroidHub(input_.current_page_classification())) {
return provider->type() == AutocompleteProvider::TYPE_SEARCH ||
provider->type() == AutocompleteProvider::TYPE_OPEN_TAB ||
provider->type() == AutocompleteProvider::TYPE_BOOKMARK ||
provider->type() == AutocompleteProvider::TYPE_HISTORY_QUICK;
}
#endif
// Always let the `ContextualSearchProvider` generate the toolbelt match,
// even when in keyword modes. Note this comes after above checks
// only because Lens searchboxes don't yet fully support toolbelt UI.
if (omnibox_feature_configs::Toolbelt::Get().enabled &&
provider->type() == AutocompleteProvider::TYPE_CONTEXTUAL_SEARCH) {
return true;
}
if (input_.InKeywordMode()) {
// Only a subset of providers are run when we're in a starter pack keyword
// mode. Try to grab the TemplateURL to determine if we're in starter pack
// mode and whether this provider should be run.
AutocompleteInput keyword_input = input_;
const TemplateURL* keyword_turl =
AutocompleteInput::GetSubstitutingTemplateURLForInput(
template_url_service_, &keyword_input);
if (keyword_turl &&
(keyword_turl->starter_pack_id() > 0 ||
keyword_turl->policy_origin() ==
TemplateURLData::PolicyOrigin::kSearchAggregator)) {
if (keyword_turl->starter_pack_id() ==
template_url_starter_pack_data::kPage) {
return provider->type() == AutocompleteProvider::TYPE_CONTEXTUAL_SEARCH;
}
switch (provider->type()) {
// Keyword provider creates the suggestion attached to the keyword chip
// and search provider creates the SEARCH_OTHER_ENGINE suggestion
// required for keyword mode to work. These still need to be run or
// keyword mode breaks.
// search-what-you-typed suggestions from the DSE are usually provided
// by the search provider, but are skipped within the search provider
// logic when in keyword mode, so do not need to be handled here..
case AutocompleteProvider::TYPE_SEARCH:
case AutocompleteProvider::TYPE_KEYWORD:
return true;
// @Bookmarks starter pack scope - run only the bookmarks provider.
case AutocompleteProvider::TYPE_BOOKMARK:
return (keyword_turl->starter_pack_id() ==
template_url_starter_pack_data::kBookmarks);
// @History starter pack scope - run the history providers & featured
// search for embeddings IPH suggestions.
case AutocompleteProvider::TYPE_HISTORY_QUICK:
case AutocompleteProvider::TYPE_HISTORY_URL:
case AutocompleteProvider::TYPE_HISTORY_EMBEDDINGS:
case AutocompleteProvider::TYPE_FEATURED_SEARCH:
return (keyword_turl->starter_pack_id() ==
template_url_starter_pack_data::kHistory);
// @Tabs starter pack scope - run the open tab provider.
case AutocompleteProvider::TYPE_OPEN_TAB:
return (keyword_turl->starter_pack_id() ==
template_url_starter_pack_data::kTabs);
case AutocompleteProvider::TYPE_ENTERPRISE_SEARCH_AGGREGATOR:
return keyword_turl->policy_origin() ==
TemplateURLData::PolicyOrigin::kSearchAggregator;
// No other providers should run when in a starter pack scope.
default:
return false;
}
}
// Outside of the starter pack scopes, keyword mode should still restrict
// certain providers.
switch (provider->type()) {
// Don't run history cluster provider or on device head provider.
case AutocompleteProvider::TYPE_HISTORY_CLUSTER_PROVIDER:
case AutocompleteProvider::TYPE_ON_DEVICE_HEAD:
return false;
// Don't run document provider, except for Google Drive.
case AutocompleteProvider::TYPE_DOCUMENT:
return keyword_turl &&
base::StartsWith(keyword_turl->url(), "https://drive.google.com",
base::CompareCase::INSENSITIVE_ASCII);
// Don't run aggregator provider unless the user is in a aggregator scope,
// which is handled above.
case AutocompleteProvider::TYPE_ENTERPRISE_SEARCH_AGGREGATOR:
return false;
// Treat all other providers as usual.
default:
break;
}
}
// Some providers should only run in starter pack mode or in the CrOS
// launcher. If we reach here, we're not in starter pack mode.
bool should_run_search_aggregator_provider =
omnibox_feature_configs::SearchAggregatorProvider::Get().enabled &&
template_url_service_ &&
template_url_service_->GetEnterpriseSearchAggregatorEngine() &&
!template_url_service_->IsShortcutRequiredForSearchAggregatorEngine();
switch (provider->type()) {
case AutocompleteProvider::TYPE_ENTERPRISE_SEARCH_AGGREGATOR:
return should_run_search_aggregator_provider;
// Document provider suggestions are redundant with the enterprise search
// aggregator provider suggestions, which can be configured to provide
// Google Drive suggestions.
case AutocompleteProvider::TYPE_DOCUMENT:
return !omnibox_feature_configs::SearchAggregatorProvider::Get()
.disable_drive ||
!should_run_search_aggregator_provider;
case AutocompleteProvider::TYPE_OPEN_TAB:
return is_cros_launcher_;
#if !BUILDFLAG(IS_IOS)
case AutocompleteProvider::TYPE_HISTORY_EMBEDDINGS:
return history_embeddings::GetFeatureParameters().omnibox_unscoped;
#endif
default:
break;
}
// Otherwise, run all providers.
return true;
}
GURL AutocompleteController::ComputeURLFromSearchTermsArgs(
const TemplateURL* template_url,
const TemplateURLRef::SearchTermsArgs& search_terms_args) const {
if (!template_url) {
return GURL();
}
// Skip search term replacement when in the @gemini scope.
// TODO(crbug.com/41494524): Replace this logic with a proper fix to support
// keywords that do not do search term replacement in omnibox.
if (template_url->starter_pack_id() ==
template_url_starter_pack_data::kGemini) {
return GURL(OmniboxFieldTrial::kGeminiUrlOverride.Get());
}
return GURL(template_url->url_ref().ReplaceSearchTerms(
search_terms_args, template_url_service_->search_terms_data()));
}
void AutocompleteController::InitializeAsyncProviders(int provider_types) {
if (provider_types & AutocompleteProvider::TYPE_SEARCH) {
search_provider_ = new SearchProvider(provider_client_.get(), this);
providers_.push_back(search_provider_.get());
}
// Providers run in the order they're added. Add `HistoryURLProvider` after
// `SearchProvider` because:
// - `SearchProvider` synchronously queries the history database's
// keyword_search_terms and url table.
// - `HistoryUrlProvider` schedules a background task that also accesses the
// history database.
// If both db accesses happen concurrently, TSan complains. So put
// `HistoryURLProvider` later to make sure that `SearchProvider` is done
// doing its thing by the time the `HistoryURLProvider` task runs. (And hope
// that it completes before `AutocompleteController::Start()` is called the
// next time.)
if (provider_types & AutocompleteProvider::TYPE_HISTORY_URL) {
history_url_provider_ =
new HistoryURLProvider(provider_client_.get(), this);
providers_.push_back(history_url_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_DOCUMENT) {
document_provider_ = DocumentProvider::Create(provider_client_.get(), this);
providers_.push_back(document_provider_.get());
}
if (provider_types &
AutocompleteProvider::TYPE_ENTERPRISE_SEARCH_AGGREGATOR) {
providers_.push_back(
new EnterpriseSearchAggregatorProvider(provider_client_.get(), this));
}
if (provider_types & AutocompleteProvider::TYPE_ON_DEVICE_HEAD) {
on_device_head_provider_ =
OnDeviceHeadProvider::Create(provider_client_.get(), this);
providers_.push_back(on_device_head_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_CALCULATOR &&
search_provider_ != nullptr) {
providers_.push_back(
new CalculatorProvider(provider_client_.get(), this, search_provider_));
}
#if !BUILDFLAG(IS_IOS)
if (provider_types & AutocompleteProvider::TYPE_HISTORY_EMBEDDINGS) {
providers_.push_back(
new HistoryEmbeddingsProvider(provider_client_.get(), this));
}
#endif
if (provider_types & AutocompleteProvider::TYPE_UNSCOPED_EXTENSION) {
unscoped_extension_provider_ =
new UnscopedExtensionProvider(provider_client_.get(), this);
providers_.push_back(unscoped_extension_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_CONTEXTUAL_SEARCH) {
contextual_search_provider_ =
new ContextualSearchProvider(provider_client_.get(), this);
providers_.push_back(contextual_search_provider_.get());
}
}
void AutocompleteController::InitializeSyncProviders(int provider_types) {
if (provider_types & AutocompleteProvider::TYPE_BOOKMARK) {
bookmark_provider_ = new BookmarkProvider(provider_client_.get());
providers_.push_back(bookmark_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_BUILTIN) {
providers_.push_back(new BuiltinProvider(provider_client_.get()));
}
if (provider_types & AutocompleteProvider::TYPE_HISTORY_QUICK) {
history_quick_provider_ = new HistoryQuickProvider(provider_client_.get());
providers_.push_back(history_quick_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_KEYWORD) {
keyword_provider_ = new KeywordProvider(provider_client_.get(), this);
providers_.push_back(keyword_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_SHORTCUTS) {
providers_.push_back(new ShortcutsProvider(provider_client_.get()));
}
if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST) {
zero_suggest_provider_ =
ZeroSuggestProvider::Create(provider_client_.get(), this);
if (zero_suggest_provider_) {
providers_.push_back(zero_suggest_provider_.get());
}
}
if (provider_types & AutocompleteProvider::TYPE_ZERO_SUGGEST_LOCAL_HISTORY) {
providers_.push_back(
LocalHistoryZeroSuggestProvider::Create(provider_client_.get(), this));
}
if (provider_types & AutocompleteProvider::TYPE_MOST_VISITED_SITES) {
providers_.push_back(
new MostVisitedSitesProvider(provider_client_.get(), this));
}
if (provider_types & AutocompleteProvider::TYPE_VERBATIM_MATCH) {
providers_.push_back(
new ZeroSuggestVerbatimMatchProvider(provider_client_.get()));
}
if (provider_types & AutocompleteProvider::TYPE_CLIPBOARD) {
#if !BUILDFLAG(IS_IOS)
// On iOS, a global ClipboardRecentContent should've been created by now
// (if enabled). If none has been created (e.g., we're on a different
// platform), use the generic implementation, which AutocompleteController
// will own. Don't try to create a generic implementation on iOS because
// iOS doesn't want/need to link in the implementation and the libraries
// that would come with it.
if (!ClipboardRecentContent::GetInstance()) {
ClipboardRecentContent::SetInstance(
std::make_unique<ClipboardRecentContentGeneric>());
}
#endif
// ClipboardRecentContent can be null in iOS tests. For non-iOS, we
// create a ClipboardRecentContent as above (for both Chrome and tests).
if (ClipboardRecentContent::GetInstance()) {
clipboard_provider_ = new ClipboardProvider(
provider_client_.get(), this, ClipboardRecentContent::GetInstance());
providers_.push_back(clipboard_provider_.get());
}
}
if (provider_types & AutocompleteProvider::TYPE_VOICE_SUGGEST) {
voice_suggest_provider_ = new VoiceSuggestProvider(provider_client_.get());
providers_.push_back(voice_suggest_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_HISTORY_FUZZY) {
history_fuzzy_provider_ = new HistoryFuzzyProvider(provider_client_.get());
providers_.push_back(history_fuzzy_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_OPEN_TAB) {
open_tab_provider_ = new OpenTabProvider(provider_client_.get());
providers_.push_back(open_tab_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_FEATURED_SEARCH) {
featured_search_provider_ =
new FeaturedSearchProvider(provider_client_.get());
providers_.push_back(featured_search_provider_.get());
}
if (provider_types & AutocompleteProvider::TYPE_RECENTLY_CLOSED_TABS) {
providers_.push_back(
new RecentlyClosedTabsProvider(provider_client_.get(), this));
}
#if BUILDFLAG(IS_ANDROID)
if (provider_types & AutocompleteProvider::TYPE_TAB_GROUP) {
providers_.push_back(new TabGroupProvider(provider_client_.get()));
}
#endif
}
void AutocompleteController::UpdateResult(UpdateType update_type,
bool allow_post_done_updates) {
TRACE_EVENT0("omnibox", "AutocompleteController::UpdateResult");
SCOPED_UMA_HISTOGRAM_TIMER_MICROS("Omnibox.AutocompletionTime.UpdateResult");
#if DCHECK_IS_ON()
auto debug_string =
AutocompleteController::UpdateTypeToDebugString(last_update_type_) +
" -> " + AutocompleteController::UpdateTypeToDebugString(update_type);
switch (update_type) {
case UpdateType::kSyncPassOnly:
case UpdateType::kSyncPass:
DCHECK(last_update_type_ == UpdateType::kNone) << debug_string;
break;
case UpdateType::kAsyncPass:
case UpdateType::kLastAsyncPassExceptDoc:
DCHECK(
last_update_type_ == UpdateType::kSyncPass ||
last_update_type_ == UpdateType::kAsyncPass ||
last_update_type_ == UpdateType::kExpirePass ||
(last_update_type_ == UpdateType::kStop && allow_post_done_updates))
<< debug_string;
break;
case UpdateType::kExpirePass:
DCHECK(last_update_type_ == UpdateType::kSyncPass ||
last_update_type_ == UpdateType::kLastAsyncPassExceptDoc ||
last_update_type_ == UpdateType::kAsyncPass)
<< debug_string;
break;
case UpdateType::kLastAsyncPass:
DCHECK(
last_update_type_ == UpdateType::kSyncPass ||
last_update_type_ == UpdateType::kAsyncPass ||
last_update_type_ == UpdateType::kLastAsyncPassExceptDoc ||
last_update_type_ == UpdateType::kExpirePass ||
(last_update_type_ == UpdateType::kStop && allow_post_done_updates))
<< debug_string;
break;
case UpdateType::kMatchDeletion:
DCHECK(last_update_type_ != UpdateType::kNone) << debug_string;
break;
case UpdateType::kStop:
// All cases are valid.
break;
case UpdateType::kNone:
NOTREACHED();
}
#endif // DCHECK_IS_ON()
last_update_type_ = update_type;
if (update_type == UpdateType::kSyncPassOnly ||
update_type == UpdateType::kSyncPass ||
update_type == UpdateType::kLastAsyncPass ||
update_type == UpdateType::kStop) {
expire_timer_.Stop();
stop_timer_.Stop();
}
if (update_type == UpdateType::kStop) {
return;
}
OldResult old_result(update_type, input_, &internal_result_);
AggregateNewMatches();
MlRerank(old_result);
// If the entrypoints aren't visible, then Lens is active and contextual
// suggestions shouldn't be shown.
const bool is_lens_active =
!autocomplete_provider_client()->AreLensEntrypointsVisible();
const bool can_show_contextual_suggestions = CanShowContextualSuggestions(
autocomplete_provider_client()->IsPagePaywalled());
const bool mia_enabled =
omnibox_feature_configs::MiaZPS::Get().enabled &&
omnibox::IsAimAllowedByPolicy(provider_client_->GetPrefs());
if (update_type == UpdateType::kSyncPass ||
update_type == UpdateType::kAsyncPass ||
update_type == UpdateType::kLastAsyncPassExceptDoc) {
internal_result_.SortAndCull(input_, template_url_service_,
triggered_feature_service_, is_lens_active,
can_show_contextual_suggestions, mia_enabled,
old_result.default_match_to_preserve);
internal_result_.TransferOldMatches(input_,
&old_result.matches_to_transfer);
}
internal_result_.SortAndCull(input_, template_url_service_,
triggered_feature_service_, is_lens_active,
can_show_contextual_suggestions, mia_enabled,
old_result.default_match_to_preserve);
if (update_type == UpdateType::kSyncPass) {
StartExpireTimer();
StartStopTimer();
}
PostProcessMatches();
bool default_match_changed = CheckWhetherDefaultMatchChanged(
old_result.last_default_match,
old_result.last_default_associated_keyword);
// Pretend the default match changed for sync passes, because when the user
// types a character, the inline autocompletion selection must be updated
// even if the current match has the same URL as the last run's default match.
// Likewise, the controller doesn't know what's happened in the edit since the
// last time it ran autocomplete. The user might have selected all the text
// and hit delete, then typed a new character. The selection and delete won't
// send any signals to the controller so it doesn't realize that anything was
// cleared or changed. Even if the default match hasn't changed, we need the
// edit model to update the display.
default_match_changed = default_match_changed ||
update_type == UpdateType::kSyncPassOnly ||
update_type == UpdateType::kSyncPass;
bool immediate = update_type == UpdateType::kSyncPassOnly ||
update_type == UpdateType::kSyncPass ||
update_type == UpdateType::kLastAsyncPass ||
update_type == UpdateType::kMatchDeletion ||
update_type == UpdateType::kLastAsyncPassExceptDoc;
RequestNotifyChanged(default_match_changed, !immediate);
}
void AutocompleteController::AggregateNewMatches() {
for (const auto& provider : providers_) {
if (!ShouldRunProvider(provider.get())) {
continue;
}
// Append the new matches and conditionally set a swap bit. This logic
// was previously within `AppendMatches` but here is the only place
// where it's still needed, and even this should ideally be cleaned up.
size_t match_index = internal_result_.size();
internal_result_.AppendMatches(provider->matches());
for (; match_index < internal_result_.size(); match_index++) {
AutocompleteMatch* match = internal_result_.match_at(match_index);
if (!match->description.empty() &&
!AutocompleteMatch::IsSearchType(match->type) &&
match->type != AutocompleteMatchType::DOCUMENT_SUGGESTION) {
match->swap_contents_and_description = true;
}
if (omnibox_feature_configs::ForceAllowedToBeDefault::Get().enabled &&
!match->allowed_to_be_default_match && match->keyword.empty() &&
!input_.prevent_inline_autocomplete()) {
match->allowed_to_be_default_match = true;
match->RecordAdditionalInfo("force allowed to be default", "true");
}
}
internal_result_.MergeSuggestionGroupsMap(
provider->suggestion_groups_map());
}
}
void AutocompleteController::MlRerank(OldResult& old_result) {
// Annotate the eligible matches in `internal_result_` with additional scoring
// signals. The additional signals in `internal_result_` will be lost when
// `UpdateResult()` is called again. Currently, `internal_result_` is updated
// in each `UpdateResult()` call.
if (OmniboxFieldTrial::IsPopulatingUrlScoringSignalsEnabled() &&
OmniboxFieldTrial::AreScoringSignalsAnnotatorsEnabled()) {
for (const auto& annotator : url_scoring_signals_annotators_) {
annotator->AnnotateResult(input_, &internal_result_);
}
}
if (internal_result_.empty()) {
return;
}
if (!OmniboxFieldTrial::IsMlUrlScoringEnabled()) {
return;
}
if (!provider_client_->GetAutocompleteScoringModelService()) {
return;
}
if (disable_ml_) {
return;
}
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
if (OmniboxFieldTrial::GetMLConfig().piecewise_mapped_search_blending) {
RunBatchUrlScoringModelPiecewiseMappedSearchBlending(old_result);
} else if (OmniboxFieldTrial::GetMLConfig().mapped_search_blending) {
RunBatchUrlScoringModelMappedSearchBlending(old_result);
} else {
RunBatchUrlScoringModel(old_result);
}
#else
NOTREACHED();
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
}
void AutocompleteController::PostProcessMatches() {
#if DCHECK_IS_ON()
internal_result_.Validate();
#endif // DCHECK_IS_ON()
AttachActions();
UpdateKeywordDescriptions(&internal_result_);
UpdateAssociatedKeywords(&internal_result_);
UpdateSearchboxStats(&internal_result_);
UpdateShownInSession(&internal_result_);
UpdateTailSuggestPrefix(&internal_result_);
MaybeRemoveCompanyEntityImages(&internal_result_);
MaybeCleanSuggestionsForKeywordMode(input_, &internal_result_);
MaybeCleanIphSuggestions(&internal_result_);
// Notify providers which of their matches were shown. If we end up with more
// providers to notify, we should add `RegisterDisplayedMatches()` to the
// `AutocompleteProvider` interface and iterate all providers here.
if (search_provider_) {
search_provider_->RegisterDisplayedAnswers(internal_result_);
}
// `featured_search_provider_` isn't interested in "invisible" autocomplete
// runs, e.g. when text is copied.
if (featured_search_provider_ && !input_.omit_asynchronous_matches()) {
featured_search_provider_->RegisterDisplayedMatches(internal_result_);
}
// Mark the rich autocompletion feature triggered if the top match, or
// would-be-top-match if rich autocompletion is counterfactual enabled, is
// rich autocompleted.
const auto top_match_rich_autocompletion_type =
TopMatchRichAutocompletionType(internal_result_);
triggered_feature_service_->RichAutocompletionTypeTriggered(
top_match_rich_autocompletion_type);
if (top_match_rich_autocompletion_type !=
AutocompleteMatch::RichAutocompletionType::kNone) {
triggered_feature_service_->FeatureTriggered(
metrics::OmniboxEventProto_Feature_RICH_AUTOCOMPLETION);
}
}
bool AutocompleteController::CheckWhetherDefaultMatchChanged(
std::optional<AutocompleteMatch> last_default_match,
const std::u16string& last_default_associated_keyword) {
const bool default_is_valid = internal_result_.default_match();
std::u16string default_associated_keyword;
if (default_is_valid &&
internal_result_.default_match()->associated_keyword) {
default_associated_keyword =
internal_result_.default_match()->associated_keyword->keyword;
}
// We've gotten async results. Send notification that the default match
// updated if:
// * fill_into_edit differs.
// * icon_url differs.
// * associated_keyword differs.
// This can change when Chrome starts and the keyword database
// finishes loading while processing this request.
// * keyword differs.
// This can change if the interpretation of the input switches
// between a search, which gets labeled with the default search
// provider's keyword, to a URL.
// The URL is NOT checked for changes because it might be updated for the
// default match even if fill_into_edit remains the same (see SearchProvider
// for an example).
const bool notify_default_match =
(last_default_match.has_value() != default_is_valid) ||
(last_default_match &&
((internal_result_.default_match()->fill_into_edit !=
last_default_match->fill_into_edit) ||
(internal_result_.default_match()->icon_url !=
last_default_match->icon_url) ||
(default_associated_keyword != last_default_associated_keyword) ||
(internal_result_.default_match()->keyword !=
last_default_match->keyword)));
if (notify_default_match) {
last_time_default_match_changed_ = base::TimeTicks::Now();
}
return notify_default_match;
}
void AutocompleteController::AttachActions() {
// No actions should be attached for lens searchboxes.
if (omnibox::IsLensSearchbox(input_.current_page_classification())) {
return;
}
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
if (omnibox_feature_configs::ContextualSearch::Get()
.contextual_zero_suggest_lens_fulfillment &&
input_.IsZeroSuggest()) {
internal_result_.AttachContextualSearchFulfillmentActionToMatches();
} else if (input_.InKeywordMode()) {
AutocompleteInput keyword_input = input_;
const TemplateURL* keyword_turl =
AutocompleteInput::GetSubstitutingTemplateURLForInput(
template_url_service_, &keyword_input);
// Attach the contextual search fulfillment actions in the @page keyword
// mode.
if (keyword_turl && keyword_turl->starter_pack_id() ==
template_url_starter_pack_data::kPage) {
internal_result_.AttachContextualSearchFulfillmentActionToMatches();
return;
}
}
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
// TabMatcher should run for ZPS for the Hub since open tab suggestions are
// shown there.
if (!input_.IsZeroSuggest() ||
omnibox::IsAndroidHub(input_.current_page_classification())) {
// Do not look for matching tabs on Android unless we collected all the
// suggestions. Tab matching is an expensive process with multiple JNI calls
// involved. Run it only when all the suggestions are collected.
bool perform_tab_match = is_android ? done() : true;
if (perform_tab_match) {
internal_result_.ConvertOpenTabMatches(provider_client_.get(), &input_);
}
internal_result_.AttachPedalsToMatches(input_, *provider_client_);
#if !BUILDFLAG(IS_IOS)
// HistoryClusters is not enabled on iOS.
AttachHistoryClustersActions(provider_client_->GetHistoryClustersService(),
internal_result_);
#endif
}
internal_result_.TrimOmniboxActions(input_.IsZeroSuggest());
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
internal_result_.SplitActionsToSuggestions();
#endif
}
void AutocompleteController::UpdateAssociatedKeywords(
AutocompleteResult* result) {
if (!keyword_provider_) {
return;
}
// The keyword matching the user's input.
std::u16string input_text_keyword = keyword_provider_->GetKeywordForText(
input_.text(), template_url_service_);
TemplateURL* input_text_keyword_turl =
template_url_service_->GetTemplateURLForKeyword(input_text_keyword);
// Cache added keywords to avoid showing the tab-to-search hint for the same
// keyword on different matches.
std::set<std::u16string> added_keywords;
auto add_keyword = [&](AutocompleteMatch& match,
const std::u16string& keyword_text,
const std::u16string& keyword) {
// There shouldn't be duplicate keywords.
if (added_keywords.count(keyword)) {
std::string debug_string = base::StringPrintf(
"Input [%s]. Duplicate keyword attached, "
"([contents] [description] [provider] [keyword]) : "
"([%s] [%s] [%s] [%s]). "
"Existing matches and keywords are: ",
base::UTF16ToUTF8(input_.text()).c_str(),
base::UTF16ToUTF8(match.contents).c_str(),
base::UTF16ToUTF8(match.description).c_str(),
match.provider ? match.provider->GetName() : "null",
base::UTF16ToUTF8(keyword).c_str());
for (const AutocompleteMatch& m : *result) {
debug_string += base::StringPrintf(
"([%s] [%s] [%s] [%s]), ", base::UTF16ToUTF8(m.contents).c_str(),
base::UTF16ToUTF8(m.description).c_str(),
m.provider ? m.provider->GetName() : "null",
base::UTF16ToUTF8(m.fill_into_edit).c_str());
}
CHECK(!added_keywords.count(keyword)) << debug_string;
}
added_keywords.insert(keyword);
match.associated_keyword = std::make_unique<AutocompleteMatch>(
keyword_provider_->CreateVerbatimMatch(keyword_text, keyword, input_));
};
for (AutocompleteMatch& match : *result) {
// In order to keep zero suggest UI minimal, zero suggest should never have
// attached keywords. The matches eligible for an associated keywords
// should be treated as URL suggestions.
if (input_.IsZeroSuggest()) {
return;
}
// Clear any keyword the match may have from previous passes.
match.associated_keyword.reset();
// If this match is in keyword mode (e.g. the user tabbed into a keyword
// then continued typing), don't attach a keyword chip to it.
std::u16string explicit_keyword(
match.GetSubstitutingExplicitlyInvokedKeyword(template_url_service_));
if (!explicit_keyword.empty()) {
// Also prevent other matches showing a keyword chip for the keyword the
// user is already in.
added_keywords.insert(explicit_keyword);
continue;
}
// When the input text matches a keyword, even if the match inline-
// autocompletes to a different keyword, offer tab-to-search keyword hint
// on the match, except for starter packed keywords or those featured by
// policy.
if (!input_text_keyword.empty() &&
!added_keywords.count(input_text_keyword) &&
input_text_keyword_turl->starter_pack_id() == 0 &&
!input_text_keyword_turl->featured_by_policy()) {
add_keyword(match, input_text_keyword, input_text_keyword);
continue;
}
// The keyword for the match text.
std::u16string match_text_keyword = keyword_provider_->GetKeywordForText(
match.fill_into_edit, template_url_service_);
TemplateURL* match_text_keyword_turl =
template_url_service_->GetTemplateURLForKeyword(match_text_keyword);
// Featured keyword matches should always have their corresponding keyword
// or they won't work.
if (AutocompleteMatch::IsFeaturedSearchType(match.type)) {
CHECK(!match_text_keyword.empty());
add_keyword(match, match.fill_into_edit, match_text_keyword);
continue;
}
// Add keyword hints for non-FeaturedKeyword matches.
if (!match_text_keyword.empty() &&
!added_keywords.count(match_text_keyword) &&
match_text_keyword_turl->starter_pack_id() == 0 &&
!match_text_keyword_turl->featured_by_policy()) {
add_keyword(match, match.fill_into_edit, match_text_keyword);
}
}
}
void AutocompleteController::UpdateKeywordDescriptions(
AutocompleteResult* result) {
// No need to update the description on Android since description for plain
// text match is not allowed.
#if !BUILDFLAG(IS_ANDROID)
// The Lens searchbox does not require the search engine name description
// label since all suggestions will be from a single source.
// TODO(crbug.com/338094774): Remove this Lens-specific change and implement a
// general solution.
if (omnibox::IsLensSearchbox(input_.current_page_classification())) {
return;
}
std::u16string last_keyword;
bool last_contextual = false;
for (auto i(result->begin()); i != result->end(); ++i) {
if (AutocompleteMatch::IsSearchType(i->type)) {
if (i->HasCustomDescription() || IsUnscopedExtensionMatch(*i)) {
continue;
}
i->description.clear();
i->description_class.clear();
DCHECK(!i->keyword.empty());
bool is_contextual = i->IsContextualSearchSuggestion();
if (i->keyword != last_keyword || is_contextual != last_contextual) {
const TemplateURL* template_url =
i->GetTemplateURL(template_url_service_, false);
if (template_url) {
// The search keyword description is applied except in these cases:
// - For extension keywords, the description is the extension name.
// - For contextual search matches, the description indicates the
// alternative UX because they're opened in the side panel.
i->description = template_url->AdjustedShortNameForLocaleDirection();
if (is_contextual) {
i->description = l10n_util::GetStringUTF16(
IDS_AUTOCOMPLETE_SEARCH_IN_SIDE_PANEL_DESCRIPTION);
} else if (template_url->is_ask_starter_pack()) {
i->description = l10n_util::GetStringFUTF16(
IDS_AUTOCOMPLETE_ASK_DESCRIPTION, i->description);
} else if (template_url->type() !=
TemplateURL::OMNIBOX_API_EXTENSION) {
i->description = l10n_util::GetStringFUTF16(
IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, i->description);
}
i->description_class.push_back(
ACMatchClassification(0, ACMatchClassification::DIM));
}
last_keyword = i->keyword;
last_contextual = is_contextual;
}
} else {
last_keyword.clear();
}
}
#endif // !BUILDFLAG(IS_ANDROID)
}
void AutocompleteController::UpdateSearchboxStats(AutocompleteResult* result) {
using omnibox::metrics::ChromeSearchboxStats;
if (result->empty()) {
return;
}
ChromeSearchboxStats searchbox_stats;
searchbox_stats.set_client_name("chrome");
int count = 0;
int num_zero_prefix_suggestions_shown = 0;
std::optional<omnibox::SuggestType> last_type;
base::flat_set<omnibox::SuggestSubtype> last_subtypes = {};
omnibox::GroupId previous_group_id = omnibox::GROUP_INVALID;
std::vector<size_t> match_index_to_position(result->size());
size_t match_position = 0;
std::vector<bool> match_index_belongs_to_horizontal_render_group(
result->size());
std::vector<size_t> match_index_to_aqs_slot(result->size());
std::vector<std::string> aqs;
aqs.reserve(result->size());
for (size_t index = 0; index < result->size(); ++index) {
AutocompleteMatch* match = result->match_at(index);
// Consider all AutocompleteMatches belonging to a Horizontal render group
// as a single element. If suggestion group ID has not changed, and the
// group render type is horizontal, we won't create a separate entry for
// this suggestion.
omnibox::GroupId group_id =
match->suggestion_group_id.value_or(omnibox::GROUP_INVALID);
omnibox::GroupConfig_RenderType render_type =
result->GetRenderTypeForSuggestionGroup(group_id);
bool match_belongs_to_horizontal_render_group =
render_type == omnibox::GroupConfig_RenderType_HORIZONTAL;
match_index_belongs_to_horizontal_render_group[index] =
match_belongs_to_horizontal_render_group;
if (group_id == previous_group_id &&
match_belongs_to_horizontal_render_group) {
// All elements in a Horizontal Render Group share the same index
// and AQS slot.
match_index_to_position[index] = match_position - 1;
match_index_to_aqs_slot[index] = aqs.size();
continue;
}
previous_group_id = group_id;
omnibox::SuggestType type = match->suggest_type;
auto subtypes = match->subtypes;
ExtendMatchSubtypes(*match, &subtypes);
if (input_.IsZeroSuggest()) {
// Count the zero-prefix suggestions in the result set.
if (subtypes.contains(omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_HISTORY) ||
subtypes.contains(omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_URLS) ||
subtypes.contains(
omnibox::SUBTYPE_ZERO_PREFIX_LOCAL_FREQUENT_QUERIES) ||
subtypes.contains(omnibox::SUBTYPE_ZERO_PREFIX) ||
subtypes.contains(omnibox::SUBTYPE_CLIPBOARD_IMAGE) ||
subtypes.contains(omnibox::SUBTYPE_CLIPBOARD_TEXT) ||
subtypes.contains(omnibox::SUBTYPE_CLIPBOARD_URL)) {
num_zero_prefix_suggestions_shown++;
}
}
auto* available_suggestion = searchbox_stats.add_available_suggestions();
available_suggestion->set_index(match_position);
available_suggestion->set_type(type);
match_index_to_position[index] = match_position;
for (const auto subtype : subtypes) {
available_suggestion->add_subtypes(subtype);
}
if (last_type.has_value() &&
(type != last_type || subtypes != last_subtypes)) {
aqs.push_back(
ConstructAvailableAutocompletion(*last_type, last_subtypes, count));
count = 1;
} else {
count++;
}
match_index_to_aqs_slot[index] = aqs.size();
last_type = type;
last_subtypes = subtypes;
match_position++;
}
if (last_type.has_value()) {
aqs.push_back(
ConstructAvailableAutocompletion(*last_type, last_subtypes, count));
}
// If zero-prefix suggestions are offered multiple times, log the most recent
// count.
if (num_zero_prefix_suggestions_shown > 0) {
result->set_num_zero_prefix_suggestions_shown_in_session(
num_zero_prefix_suggestions_shown);
}
searchbox_stats.set_num_zero_prefix_suggestions_shown(
result->num_zero_prefix_suggestions_shown_in_session());
searchbox_stats.set_zero_prefix_enabled(
result->zero_prefix_enabled_in_session());
// Append the GWS event ID hashes to the searchbox stats parameter to be
// logged in searchbox_stats.proto's `gws_event_id_hash` field.
if (zero_suggest_provider_) {
for (const auto& gws_event_id_hash :
zero_suggest_provider_->gws_event_id_hashes()) {
result->add_gws_event_id_hash_in_session(gws_event_id_hash);
}
}
for (const auto& gws_event_id_hash :
result->gws_event_id_hashes_in_session()) {
searchbox_stats.add_gws_event_id_hash(gws_event_id_hash);
}
// Go over all matches and set searchbox stats if the match supports it.
for (size_t index = 0; index < result->size(); ++index) {
AutocompleteMatch* match = result->match_at(index);
const TemplateURL* template_url =
match->GetTemplateURL(template_url_service_, false);
if (!template_url || !match->search_terms_args) {
continue;
}
match->search_terms_args->searchbox_stats = searchbox_stats;
// Prevent trivial suggestions from getting credit for being selected.
if (!match->IsTrivialAutocompletion()) {
match_position = match_index_to_position[index];
DCHECK_LT(static_cast<int>(match_position),
match->search_terms_args->searchbox_stats
.available_suggestions_size());
auto* selected_suggestion =
match->search_terms_args->searchbox_stats
.mutable_available_suggestions(match_position);
DCHECK_EQ(static_cast<int>(match_position), selected_suggestion->index());
selected_suggestion->set_type(match->suggest_type);
match->search_terms_args->searchbox_stats.mutable_assisted_query_info()
->MergeFrom(*selected_suggestion);
// Reconstruct AQS for items sharing the slot (e.g. elements in the
// carousel).
if (match_index_belongs_to_horizontal_render_group[index]) {
aqs[match_index_to_aqs_slot[index]] = ConstructAvailableAutocompletion(
match->suggest_type, match->subtypes, 1);
}
}
// Duplicate searchbox stats for eligible ActionsInSuggest.
// TODO(crbug.com/40257536): rather than computing the `action_uri`, keep
// the updated search_terms_args, and apply the query formulation time the
// moment the action is selected.
for (auto& scoped_action : match->actions) {
auto* action_in_suggest =
OmniboxActionInSuggest::FromAction(scoped_action.get());
auto* answer_action =
OmniboxAnswerAction::FromAction(scoped_action.get());
TemplateURLRef::SearchTermsArgs* search_terms_args;
if (action_in_suggest == nullptr ||
!action_in_suggest->search_terms_args.has_value()) {
if (answer_action == nullptr) {
continue;
}
search_terms_args = &answer_action->search_terms_args;
} else {
search_terms_args = &action_in_suggest->search_terms_args.value();
}
search_terms_args->searchbox_stats.MergeFrom(
match->search_terms_args->searchbox_stats);
if (action_in_suggest != nullptr) {
action_in_suggest->action_info.set_action_uri(
ComputeURLFromSearchTermsArgs(
match->GetTemplateURL(template_url_service_, false),
*search_terms_args)
.spec());
}
}
}
}
// TODO(crbug.com/402519775): Merge the logic in this function into
// `UpdateSearchboxStats()` once we've rolled all session-related data into a
// single `SessionData` property on matches.
void AutocompleteController::UpdateShownInSession(AutocompleteResult* result) {
// Currently, `AutocompleteClassifier::Classify()` is the only place where
// `omit_asynchronous_matches` is set to `true`. Therefore, this check is
// essentially asking "Is `UpdateShownInSession()` being invoked via
// `AutocompleteClassifier::Classify()`?"
//
// The internal `AutocompleteResult` generated during the match
// classification process is not actually shown to the user in any way, so it
// doesn't make sense to record session-based "suggestion shown" metrics
// in this particular case.
if (input_.omit_asynchronous_matches()) {
return;
}
for (auto& match : *result) {
result->set_suggestions_shown_in_session(input_.IsZeroSuggest(), match);
}
for (auto& match : *result) {
match.session = result->session();
}
}
void AutocompleteController::UpdateTailSuggestPrefix(
AutocompleteResult* result) {
const auto common_prefix = result->GetCommonPrefix();
if (!common_prefix.empty()) {
for (auto& match : *result) {
if (match.type == AutocompleteMatchType::SEARCH_SUGGEST_TAIL) {
match.tail_suggest_common_prefix = common_prefix;
}
}
}
}
void AutocompleteController::NotifyChanged() {
TRACE_EVENT0("omnibox", "AutocompleteController::NotifyChanged");
// Will log metrics for how many matches changed. Will also log timing metrics
// for the current request if it's complete; otherwise, will just update
// timestamps of when the last update changed any or the default suggestion.
metrics_.OnNotifyChanged(last_result_for_logging_,
internal_result_.GetMatchDedupComparators());
// Swap matches from `internal_result_` to `published_result_` and copy them
// back from `published_result_` to `internal_result_`. This allows
// `published_result_` to retain `java_match_` and the computed
// `matching_java_tab_` which otherwise would have been lost if
// `internal_result_` simply copied matches from `internal_result_`.
published_result_.SwapMatchesWith(&internal_result_);
internal_result_.CopyMatchesFrom(published_result_);
last_result_for_logging_ = internal_result_.GetMatchDedupComparators();
for (Observer& obs : observers_) {
obs.OnResultChanged(this, notify_changed_default_match_);
}
CancelNotifyChangedRequest();
}
void AutocompleteController::RequestNotifyChanged(bool notify_default_match,
bool delayed) {
if (notify_default_match) {
notify_changed_default_match_ = true;
}
notify_changed_debouncer_.RequestRun(base::BindOnce(
&AutocompleteController::NotifyChanged, base::Unretained(this)));
if (!delayed) {
notify_changed_debouncer_.FlushRequest();
}
}
void AutocompleteController::CancelNotifyChangedRequest() {
notify_changed_debouncer_.CancelRequest();
notify_changed_default_match_ = false;
}
AutocompleteController::ProviderDoneState
AutocompleteController::GetProviderDoneState() {
bool doc_not_done = false;
for (const auto& provider : providers_) {
if (!ShouldRunProvider(provider.get()) || provider->done()) {
continue;
}
if (provider->type() != AutocompleteProvider::TYPE_DOCUMENT) {
return ProviderDoneState::kNotDone;
} else {
doc_not_done = true;
}
}
return doc_not_done ? ProviderDoneState::kAllExceptDocDone
: ProviderDoneState::kAllDone;
}
void AutocompleteController::StartExpireTimer() {
// Amount of time (in ms) between when the user stops typing and
// when we remove any copied entries. We do this from the time the
// user stopped typing as some providers (such as SearchProvider)
// wait for the user to stop typing before they initiate a query.
const int kExpireTimeMS = 500;
if (internal_result_.HasCopiedMatches()) {
expire_timer_.Start(
FROM_HERE, base::Milliseconds(kExpireTimeMS),
base::BindOnce(&AutocompleteController::UpdateResult,
base::Unretained(this), UpdateType::kExpirePass,
/*allow_post_done_updates=*/false));
}
}
void AutocompleteController::StartStopTimer() {
stop_timer_.Start(
FROM_HERE, stop_timer_duration_,
base::BindOnce(&AutocompleteController::OnStopTimerTriggered,
base::Unretained(this)));
}
void AutocompleteController::OnStopTimerTriggered() {
Stop(AutocompleteStopReason::kInactivity);
for (Observer& obs : observers_) {
obs.OnAutocompleteStopTimerTriggered(input_);
}
}
bool AutocompleteController::OnMemoryDump(
const base::trace_event::MemoryDumpArgs& args,
base::trace_event::ProcessMemoryDump* process_memory_dump) {
size_t res = 0;
// provider_client_ seems to be small enough to ignore it.
// TODO(dyaroshev): implement memory estimation for scoped_refptr in
// base::trace_event.
res += std::accumulate(providers_.begin(), providers_.end(), 0u,
[](size_t sum, const auto& provider) {
return sum + sizeof(AutocompleteProvider) +
provider->EstimateMemoryUsage();
});
res += input_.EstimateMemoryUsage();
res += internal_result_.EstimateMemoryUsage();
auto* dump = process_memory_dump->CreateAllocatorDump(
base::StringPrintf("omnibox/autocomplete_controller/0x%" PRIXPTR,
reinterpret_cast<uintptr_t>(this)));
dump->AddScalar(base::trace_event::MemoryAllocatorDump::kNameSize,
base::trace_event::MemoryAllocatorDump::kUnitsBytes, res);
return true;
}
void AutocompleteController::SetStartStopTimerDurationForTesting(
base::TimeDelta duration) {
stop_timer_duration_ = duration;
}
size_t AutocompleteController::InjectAdHocMatch(AutocompleteMatch match) {
size_t index = internal_result_.size();
// Append the match exactly as it is provided, with no change to
// `swap_contents_and_description`.
internal_result_.AppendMatches({std::move(match)});
RequestNotifyChanged(false, false);
return index;
}
void AutocompleteController::SetSteadyStateOmniboxPosition(
metrics::OmniboxEventProto::OmniboxPosition position) {
steady_state_omnibox_position_ = position;
}
const omnibox::metrics::ChromeSearchboxStats::ExperimentStatsV2
AutocompleteController::GetOmniboxPositionExperimentStatsV2() const {
// Field number of the omnibox position in
// SearchboxStats::ExperimentStatsV2::StatType.
constexpr int kOmniboxPositionFieldNumber = 95;
// Value of the enum in SearchboxStats::OmniboxPosition.
constexpr int kTopOmniboxValue = 1;
constexpr int kBottomOmniboxValue = 2;
omnibox::metrics::ChromeSearchboxStats::ExperimentStatsV2 experiment_stats_v2;
experiment_stats_v2.set_type_int(kOmniboxPositionFieldNumber);
switch (steady_state_omnibox_position_) {
case metrics::OmniboxEventProto::TOP_POSITION:
experiment_stats_v2.set_int_value(kTopOmniboxValue);
break;
case metrics::OmniboxEventProto::BOTTOM_POSITION:
experiment_stats_v2.set_int_value(kBottomOmniboxValue);
break;
default:
break;
}
return experiment_stats_v2;
}
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
void AutocompleteController::RunBatchUrlScoringModel(OldResult& old_result) {
TRACE_EVENT0("omnibox", "AutocompleteController::RunBatchUrlScoringModel");
// Dedupe matches; otherwise, e.g., duplicate bookmark and history matches
// would be scored independently with their partial signals.
internal_result_.DeduplicateMatches(input_, template_url_service_);
size_t eligible_matches_count = std::ranges::count_if(
internal_result_.matches_,
[](const auto& match) { return match.IsMlScoringEligible(); });
if (eligible_matches_count == 0) {
return;
}
// Run the model for the eligible matches. Keep a reference to those matches
// to later redistribute their relevance scores based on the model output.
std::vector<const ScoringSignals*> batch_scoring_signals;
batch_scoring_signals.reserve(eligible_matches_count);
std::vector<ACMatches::iterator> eligible_match_itrs;
for (auto match_itr = internal_result_.begin();
match_itr != internal_result_.end(); ++match_itr) {
if (!match_itr->IsMlScoringEligible()) {
continue;
}
RecordScoringSignalCoverageForProvider(match_itr->scoring_signals.value(),
match_itr->provider.get());
batch_scoring_signals.push_back(&match_itr->scoring_signals.value());
eligible_match_itrs.push_back(match_itr);
}
auto elapsed_timer = base::ElapsedTimer();
const auto ml_scores = provider_client_->GetAutocompleteScoringModelService()
->BatchScoreAutocompleteUrlMatchesSync(
std::move(batch_scoring_signals));
if (ml_scores.empty()) {
return;
}
if (ml_scores.size() != eligible_match_itrs.size()) {
NOTREACHED();
}
// Record how many eligible matches the model was executed for.
RecordTotalMatchesScored(ml_scores.size());
// Record how long it took to execute the model for all eligible matches.
RecordMlScoringElapsedTime(elapsed_timer.Elapsed());
// Record whether the model was executed for at least one eligible match.
provider_client_->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
metrics::OmniboxEventProto_Feature_ML_URL_SCORING);
// The goal is to redistribute the existing relevance scores among the
// eligible matches according to the model prediction scores.
// `relevance_heap` is a max heap containing the (legacy) relevance scores,
// while `prediction_and_match_itr_heap` is a max heap containing tuples of
// the form (ml_score, legacy_score, match_itr). If two matches have the same
// ML score (e.g. two remote document suggestions w/o local scoring signals),
// then the legacy score will be used to break ties.
std::priority_queue<int> relevance_heap;
std::priority_queue<std::tuple<float, int, AutocompleteResult::iterator>>
prediction_and_match_itr_heap;
size_t score_coverage_count = 0;
// Likewise, keep the same number of shortcut boosted suggestions but reassign
// them to the highest scoring suggestions.
size_t boosted_shortcut_count = 0;
for (size_t index = 0; index < ml_scores.size(); index++) {
const auto& prediction = ml_scores[index];
if (!prediction.has_value()) {
continue;
}
score_coverage_count++;
auto match_itr = eligible_match_itrs[index];
relevance_heap.emplace(match_itr->relevance);
prediction_and_match_itr_heap.emplace(prediction.value(),
match_itr->relevance, match_itr);
if (match_itr->shortcut_boosted) {
boosted_shortcut_count++;
}
}
// Record the percentage of matches that were assigned non-null scores by
// the ML scoring model.
RecordMlScoreCoverage(score_coverage_count, ml_scores.size());
while (!relevance_heap.empty()) {
// If not in the counterfactual treatment, assign the highest relevance
// score to the match with the highest respective model prediction score.
if (!OmniboxFieldTrial::IsMlUrlScoringCounterfactual()) {
auto match_itr = std::get<2>(prediction_and_match_itr_heap.top());
match_itr->RecordAdditionalInfo("ml legacy relevance",
match_itr->relevance);
match_itr->RecordAdditionalInfo(
"ml model output", std::get<0>(prediction_and_match_itr_heap.top()));
match_itr->relevance = relevance_heap.top();
if (boosted_shortcut_count) {
match_itr->RecordAdditionalInfo("ML shortcut boosted", "true");
match_itr->shortcut_boosted = true;
boosted_shortcut_count--;
} else {
match_itr->shortcut_boosted = false;
}
}
relevance_heap.pop();
prediction_and_match_itr_heap.pop();
}
for (Observer& obs : observers_) {
obs.OnMlScored(this, internal_result_);
}
}
void AutocompleteController::RunBatchUrlScoringModelMappedSearchBlending(
OldResult& old_result) {
TRACE_EVENT0(
"omnibox",
"AutocompleteController::RunBatchUrlScoringModelMappedSearchBlending");
// Sort according to traditional scores.
// This is needed in order to ensure that the relevance score assignment logic
// can properly break ties when two (or more) URL suggestions have the same ML
// score.
internal_result_.Sort(input_, template_url_service_,
old_result.default_match_to_preserve);
// Run the model for the eligible matches.
std::vector<const ScoringSignals*> batch_scoring_signals;
std::vector<size_t> scored_positions;
for (size_t i = 0; i < internal_result_.size(); ++i) {
const auto& match = internal_result_.matches_[i];
if (!match.IsMlScoringEligible()) {
continue;
}
RecordScoringSignalCoverageForProvider(match.scoring_signals.value(),
match.provider.get());
batch_scoring_signals.push_back(&match.scoring_signals.value());
scored_positions.push_back(i);
}
if (batch_scoring_signals.empty()) {
return;
}
auto elapsed_timer = base::ElapsedTimer();
const auto ml_scores = provider_client_->GetAutocompleteScoringModelService()
->BatchScoreAutocompleteUrlMatchesSync(
std::move(batch_scoring_signals));
if (ml_scores.empty()) {
return;
}
if (ml_scores.size() != scored_positions.size()) {
NOTREACHED();
}
// Record how many eligible matches the model was executed for.
RecordTotalMatchesScored(ml_scores.size());
// Record how long it took to execute the model for all eligible matches.
RecordMlScoringElapsedTime(elapsed_timer.Elapsed());
// Record whether the model was executed for at least one eligible match.
provider_client_->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
metrics::OmniboxEventProto_Feature_ML_URL_SCORING);
if (OmniboxFieldTrial::IsMlUrlScoringCounterfactual()) {
return;
}
const int min = OmniboxFieldTrial::GetMLConfig().mapped_search_blending_min;
const int max = OmniboxFieldTrial::GetMLConfig().mapped_search_blending_max;
const int grouping_threshold = OmniboxFieldTrial::GetMLConfig()
.mapped_search_blending_grouping_threshold;
size_t score_coverage_count = 0;
for (size_t i = 0; i < ml_scores.size(); ++i) {
const auto& prediction = ml_scores[i];
float p_value = prediction.value_or(0);
if (prediction.has_value()) {
score_coverage_count++;
}
auto& match = internal_result_.matches_[scored_positions[i]];
match.RecordAdditionalInfo("ml legacy relevance", match.relevance);
match.RecordAdditionalInfo("ml model output", p_value);
match.relevance = min + p_value * (max - min);
match.shortcut_boosted = match.relevance > grouping_threshold;
}
// Record the percentage of matches that were assigned non-null scores by
// the ML scoring model.
RecordMlScoreCoverage(score_coverage_count, ml_scores.size());
// Following the initial relevance assignment, build a sorted list of
// values which will contain the finalized set of relevance scores for URL
// suggestions.
std::vector<int> scores_pool;
for (size_t i = 0; i < internal_result_.size(); ++i) {
const auto& match = internal_result_.matches_[i];
if (!match.IsMlScoringEligible()) {
continue;
}
scores_pool.push_back(match.relevance);
}
std::ranges::sort(scores_pool, std::greater<>());
// Avoid duplicate scores by ensuring that no two URL suggestions are assigned
// the same score.
int max_score = INT_MAX;
for (auto& score : scores_pool) {
score = std::min(score, max_score - 1);
max_score = score;
}
std::vector<std::pair<float, size_t>> prediction_and_position_heap;
for (size_t i = 0; i < ml_scores.size(); ++i) {
const auto& prediction = ml_scores[i];
prediction_and_position_heap.push_back(
{prediction.value_or(0), scored_positions[i]});
}
std::ranges::stable_sort(prediction_and_position_heap, std::greater<>(),
[](const auto& pair) { return pair.first; });
// Assign the finalized relevance scores to each URL suggestion in order of
// priority (i.e. ML score).
for (size_t i = 0; i < prediction_and_position_heap.size(); ++i) {
auto& match =
internal_result_.matches_[prediction_and_position_heap[i].second];
match.relevance = scores_pool[i];
}
for (Observer& obs : observers_) {
obs.OnMlScored(this, internal_result_);
}
}
void AutocompleteController::
RunBatchUrlScoringModelPiecewiseMappedSearchBlending(
OldResult& old_result) {
TRACE_EVENT0("omnibox",
"AutocompleteController::"
"RunBatchUrlScoringModelPiecewiseMappedSearchBlending");
using PiecewiseMappingVariant = OmniboxFieldTrial::PiecewiseMappingVariant;
const auto break_points = OmniboxFieldTrial::GetPiecewiseMappingBreakPoints();
if (break_points.empty()) {
return;
}
const auto break_points_verbatim_url =
OmniboxFieldTrial::GetPiecewiseMappingBreakPoints(
PiecewiseMappingVariant::kVerbatimUrl);
const auto break_points_search =
OmniboxFieldTrial::GetPiecewiseMappingBreakPoints(
PiecewiseMappingVariant::kSearch);
// Sort according to traditional scores.
// This is needed in order to ensure that the relevance score assignment logic
// can properly break ties when two (or more) URL suggestions have the same ML
// score.
internal_result_.Sort(input_, template_url_service_,
old_result.default_match_to_preserve);
// Run the model for the eligible matches.
std::vector<const ScoringSignals*> batch_scoring_signals;
std::vector<size_t> scored_positions;
for (size_t i = 0; i < internal_result_.size(); ++i) {
const auto& match = internal_result_.matches_[i];
if (!match.IsMlScoringEligible()) {
continue;
}
RecordScoringSignalCoverageForProvider(match.scoring_signals.value(),
match.provider.get());
batch_scoring_signals.push_back(&match.scoring_signals.value());
scored_positions.push_back(i);
}
if (batch_scoring_signals.empty()) {
return;
}
auto elapsed_timer = base::ElapsedTimer();
const auto ml_scores = provider_client_->GetAutocompleteScoringModelService()
->BatchScoreAutocompleteUrlMatchesSync(
std::move(batch_scoring_signals));
if (ml_scores.empty()) {
return;
}
if (ml_scores.size() != scored_positions.size()) {
NOTREACHED();
}
// Record how many eligible matches the model was executed for.
RecordTotalMatchesScored(ml_scores.size());
// Record how long it took to execute the model for all eligible matches.
RecordMlScoringElapsedTime(elapsed_timer.Elapsed());
// Record whether the model was executed for at least one eligible match.
provider_client_->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
metrics::OmniboxEventProto_Feature_ML_URL_SCORING);
if (OmniboxFieldTrial::IsMlUrlScoringCounterfactual()) {
return;
}
const int grouping_threshold =
OmniboxFieldTrial::GetMLConfig()
.piecewise_mapped_search_blending_grouping_threshold;
const int relevance_bias =
OmniboxFieldTrial::GetMLConfig()
.piecewise_mapped_search_blending_relevance_bias;
size_t score_coverage_count = 0;
for (size_t i = 0; i < ml_scores.size(); ++i) {
const auto& prediction = ml_scores[i];
float p_value = prediction.value_or(0);
if (prediction.has_value()) {
score_coverage_count++;
}
auto& match = internal_result_.matches_[scored_positions[i]];
match.RecordAdditionalInfo("ml legacy relevance", match.relevance);
match.RecordAdditionalInfo("ml model output", p_value);
const auto* break_points_for_transform = &break_points;
if (match.IsVerbatimUrlSuggestion() && !break_points_verbatim_url.empty()) {
break_points_for_transform = &break_points_verbatim_url;
} else if (AutocompleteMatch::IsSearchType(match.type) &&
!break_points_search.empty()) {
break_points_for_transform = &break_points_search;
}
match.relevance =
ApplyPiecewiseScoringTransform(p_value, *break_points_for_transform) +
relevance_bias;
// Shortcut boosting should only be applied to shortcut suggestions that
// have been used (visited) more than once. This logic will also overwrite
// whatever value was originally set by ShortcutsProvider for the
// `shortcut_boosted` property.
const bool is_shortcut = match.provider && match.provider->type() ==
ProviderType::TYPE_SHORTCUTS;
const bool has_enough_visits = match.scoring_signals.has_value() &&
match.scoring_signals->has_visit_count() &&
match.scoring_signals->visit_count() >= 2;
match.shortcut_boosted = is_shortcut && has_enough_visits &&
match.relevance > grouping_threshold;
}
// Record the percentage of matches that were assigned non-null scores by
// the ML scoring model.
RecordMlScoreCoverage(score_coverage_count, ml_scores.size());
// Following the initial relevance assignment, build a sorted list of
// values which will contain the finalized set of relevance scores for URL
// suggestions.
std::vector<int> scores_pool;
for (size_t i = 0; i < internal_result_.size(); ++i) {
const auto& match = internal_result_.matches_[i];
if (!match.IsMlScoringEligible()) {
continue;
}
scores_pool.push_back(match.relevance);
}
std::ranges::sort(scores_pool, std::greater<>());
// Avoid duplicate scores by ensuring that no two URL suggestions are assigned
// the same score.
int max_score = INT_MAX;
for (auto& score : scores_pool) {
score = std::min(score, max_score - 1);
max_score = score;
}
std::vector<std::pair<float, size_t>> prediction_and_position_heap;
for (size_t i = 0; i < ml_scores.size(); ++i) {
const auto& prediction = ml_scores[i];
prediction_and_position_heap.push_back(
{prediction.value_or(0), scored_positions[i]});
}
std::ranges::stable_sort(prediction_and_position_heap, std::greater<>(),
[](const auto& pair) { return pair.first; });
// Assign the finalized relevance scores to each URL suggestion in order of
// priority (i.e. ML score).
for (size_t i = 0; i < prediction_and_position_heap.size(); ++i) {
auto& match =
internal_result_.matches_[prediction_and_position_heap[i].second];
match.relevance = scores_pool[i];
}
for (Observer& obs : observers_) {
obs.OnMlScored(this, internal_result_);
}
}
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
void AutocompleteController::MaybeRemoveCompanyEntityImages(
AutocompleteResult* result) {
if (result->size() == 0) {
return;
}
std::u16string history_domain;
// First match must be of history URL type to ablate entity image.
if (result->match_at(0)->type == AutocompleteMatchType::HISTORY_URL) {
history_domain = GetDomain(*result->match_at(0));
}
auto iter = std::ranges::find_if(result->matches_, [](const auto& match) {
return match.type == AutocompleteMatchType::SEARCH_SUGGEST_ENTITY;
});
if (iter == result->matches_.end()) {
return;
}
bool image_ablated = false;
if (!history_domain.empty()) {
for (auto it = iter; it != result->matches_.end(); it++) {
// Do not attempt to change image to search loupe if not an entity
// suggestion.
if (it->type != AutocompleteMatchType::SEARCH_SUGGEST_ENTITY) {
continue;
}
// Check that the entity domain matches the history domain.
if (history_domain == GetDomain(*it)) {
it->image_url = GURL();
it->image_dominant_color.clear();
image_ablated = true;
}
}
}
base::UmaHistogramBoolean("Omnibox.CompanyEntityImageAblated", image_ablated);
}
void AutocompleteController::MaybeCleanSuggestionsForKeywordMode(
const AutocompleteInput& input,
AutocompleteResult* result) {
if (!kIsDesktop || input.current_page_classification() ==
metrics::OmniboxEventProto::NTP_REALBOX) {
// Realbox doesn't support keyword mode yet, so keep original list intact.
return;
}
if (input.GetFeaturedKeywordMode() ==
AutocompleteInput::FeaturedKeywordMode::kExact) {
result->EraseMatchesWhere([](const AutocompleteMatch& match) {
// When the input is '@' exactly, keep only the trivial search, starter
// pack, and featured enterprise suggestions.
return match.contents != u"@" && !match.associated_keyword &&
!match.IsToolbelt();
});
if (omnibox_feature_configs::Toolbelt::Get().enabled) {
// Sort is needed to restore verbatim '@' search as top/default match
// because a different default, e.g. "@hill", might have previously
// occupied the top spot while '@' was demoted below others. The
// comparison here achieves this without direct vector manipulation,
// and also considers `GetSortingOrder` to put toolbelt match last.
std::sort(
result->begin(), result->end(),
[](const AutocompleteMatch& match1, const AutocompleteMatch& match2) {
return std::forward_as_tuple(
match1.type !=
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED,
match1.GetSortingOrder(), -match1.relevance,
match1.contents) <
std::forward_as_tuple(
match2.type !=
AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED,
match2.GetSortingOrder(), -match2.relevance,
match2.contents);
});
} else {
// Simple sort is needed to restore verbatim '@' search as top/default
// match because a different default, e.g. "@hill", might have previously
// occupied the top spot while '@' was demoted below others.
std::sort(result->begin(), result->end(),
AutocompleteMatch::MoreRelevant);
// Put first defaultable match in top position since relevance
// ranking alone doesn't guarantee it.
auto default_match = std::find_if(
result->begin(), result->end(),
[](const auto& m) { return m.allowed_to_be_default_match; });
if (default_match != result->begin() && default_match != result->end()) {
std::rotate(result->begin(), default_match, default_match + 1);
}
}
}
if (input.GetFeaturedKeywordMode() !=
AutocompleteInput::FeaturedKeywordMode::kFalse) {
// Intentionally avoid actions and remove button on first suggestion
// which may interfere with keyword mode refresh.
if (result->size() > 1 &&
AutocompleteMatch::IsFeaturedSearchType(result->match_at(1)->type)) {
result->match_at(0)->actions.clear();
result->match_at(0)->deletable = false;
for (AutocompleteMatch& duplicate :
result->match_at(0)->duplicate_matches) {
duplicate.deletable = false;
}
}
// Eliminate tab switch on instant keyword matches for clean appearance.
for (size_t i = 0; i < result->size(); i++) {
if (result->match_at(i)->HasInstantKeyword(template_url_service_)) {
result->match_at(i)->actions.clear();
}
}
}
}
void AutocompleteController::MaybeCleanIphSuggestions(
AutocompleteResult* result) {
bool has_toolbelt = std::ranges::any_of(result->begin(), result->end(),
&AutocompleteMatch::IsToolbelt);
if (has_toolbelt) {
result->EraseMatchesWhere([](const auto& match) {
return match.IsIphSuggestion() &&
match.iph_type != IphType::kHistoryEmbeddingsDisclaimer;
});
}
}