[omnibox][ml] Implement piecewise-mapped ML scoring variant.
Linear-mapped ML scoring provides a simple way to map from ML scores to
relevance scores by specifying only three params (min/max/
grouping-threshold). However, based on various Omnibox metrics
(SearchVsUrl, Usage, Precision, etc.), it was determined during
experimentation that this solution may be less than ideal with respect
to current Omnibox usage patterns.
Therefore, in order to better align with such usage patterns, this CL
introduces a new piecewise-mapped ML scoring variant which aims to
provide greater fidelity to the current Omnibox experience while
enabling a more flexible mechanism (via piecewise break points) than
was possible before using a single set of min/max params.
Unresolved-Comment-Reason: Planning to land this CL before M127 branch and fast-follow with patch to address non-blocking review feedback.
Bug: 40062540
Change-Id: I4421ba9b3b016995a002690f86e79a68f8e491b7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5610413
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Commit-Queue: Khalid Peer <khalidpeer@chromium.org>
Reviewed-by: manuk hovanesian <manukh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1312951}
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index 88b9ed3..866c5d6 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -1410,6 +1410,45 @@
std::size(kOmniboxMlUrlScoringMaxMatchesByProvider10), nullptr},
};
+const FeatureEntry::FeatureParam kMlUrlPiecewiseMappedSearchBlendingNoBias[] = {
+ {"MlUrlPiecewiseMappedSearchBlending", "true"},
+ {"MlUrlPiecewiseMappedSearchBlending_BreakPoints",
+ "0,550;0.018,1300;0.14,1398;1,1422"},
+ {"MlUrlPiecewiseMappedSearchBlending_GroupingThreshold", "1400"},
+ {"MlUrlPiecewiseMappedSearchBlending_RelevanceBias", "0"}};
+const FeatureEntry::FeatureParam
+ kMlUrlPiecewiseMappedSearchBlendingSmallBias[] = {
+ {"MlUrlPiecewiseMappedSearchBlending", "true"},
+ {"MlUrlPiecewiseMappedSearchBlending_BreakPoints",
+ "0,550;0.018,1300;0.14,1398;1,1422"},
+ {"MlUrlPiecewiseMappedSearchBlending_GroupingThreshold", "1350"},
+ {"MlUrlPiecewiseMappedSearchBlending_RelevanceBias", "-50"}};
+const FeatureEntry::FeatureParam
+ kMlUrlPiecewiseMappedSearchBlendingMediumBias[] = {
+ {"MlUrlPiecewiseMappedSearchBlending", "true"},
+ {"MlUrlPiecewiseMappedSearchBlending_BreakPoints",
+ "0,550;0.018,1300;0.14,1398;1,1422"},
+ {"MlUrlPiecewiseMappedSearchBlending_GroupingThreshold", "1300"},
+ {"MlUrlPiecewiseMappedSearchBlending_RelevanceBias", "-100"}};
+const FeatureEntry::FeatureParam
+ kMlUrlPiecewiseMappedSearchBlendingLargeBias[] = {
+ {"MlUrlPiecewiseMappedSearchBlending", "true"},
+ {"MlUrlPiecewiseMappedSearchBlending_BreakPoints",
+ "0,550;0.018,1300;0.14,1398;1,1422"},
+ {"MlUrlPiecewiseMappedSearchBlending_GroupingThreshold", "1250"},
+ {"MlUrlPiecewiseMappedSearchBlending_RelevanceBias", "-150"}};
+const FeatureEntry::FeatureVariation
+ kMlUrlPiecewiseMappedSearchBlendingVariations[] = {
+ {"no bias", kMlUrlPiecewiseMappedSearchBlendingNoBias,
+ std::size(kMlUrlPiecewiseMappedSearchBlendingNoBias), nullptr},
+ {"small bias", kMlUrlPiecewiseMappedSearchBlendingSmallBias,
+ std::size(kMlUrlPiecewiseMappedSearchBlendingSmallBias), nullptr},
+ {"medium bias", kMlUrlPiecewiseMappedSearchBlendingMediumBias,
+ std::size(kMlUrlPiecewiseMappedSearchBlendingMediumBias), nullptr},
+ {"large bias", kMlUrlPiecewiseMappedSearchBlendingLargeBias,
+ std::size(kMlUrlPiecewiseMappedSearchBlendingLargeBias), nullptr},
+};
+
const FeatureEntry::FeatureParam kMlUrlSearchBlendingStable[] = {
{"MlUrlSearchBlending_StableSearchBlending", "true"},
{"MlUrlSearchBlending_MappedSearchBlending", "false"},
@@ -6244,6 +6283,14 @@
flag_descriptions::kOmniboxMlLogUrlScoringSignalsName,
flag_descriptions::kOmniboxMlLogUrlScoringSignalsDescription, kOsDesktop,
FEATURE_VALUE_TYPE(omnibox::kLogUrlScoringSignals)},
+ {"omnibox-ml-url-piecewise-mapped-search-blending",
+ flag_descriptions::kOmniboxMlUrlPiecewiseMappedSearchBlendingName,
+ flag_descriptions::kOmniboxMlUrlPiecewiseMappedSearchBlendingDescription,
+ kOsDesktop,
+ FEATURE_WITH_PARAMS_VALUE_TYPE(
+ omnibox::kMlUrlPiecewiseMappedSearchBlending,
+ kMlUrlPiecewiseMappedSearchBlendingVariations,
+ "MlUrlPiecewiseMappedSearchBlending")},
{"omnibox-ml-url-score-caching",
flag_descriptions::kOmniboxMlUrlScoreCachingName,
flag_descriptions::kOmniboxMlUrlScoreCachingDescription, kOsDesktop,
diff --git a/chrome/browser/flag-metadata.json b/chrome/browser/flag-metadata.json
index 3f5428b..c9f2285 100644
--- a/chrome/browser/flag-metadata.json
+++ b/chrome/browser/flag-metadata.json
@@ -6466,6 +6466,11 @@
"expiry_milestone": 130
},
{
+ "name": "omnibox-ml-url-piecewise-mapped-search-blending",
+ "owners": ["khalidpeer@chromium.org", "junzou@chromium.org", "manukh@chromium.org", "chrome-desktop-search@google.com"],
+ "expiry_milestone": 130
+ },
+ {
"name": "omnibox-ml-url-score-caching",
"owners": [ "khalidpeer@chromium.org", "junzou@chromium.org", "manukh@chromium.org", "chrome-desktop-search@google.com" ],
"expiry_milestone": 130
diff --git a/chrome/browser/flag_descriptions.cc b/chrome/browser/flag_descriptions.cc
index 0250e84..783585f 100644
--- a/chrome/browser/flag_descriptions.cc
+++ b/chrome/browser/flag_descriptions.cc
@@ -2558,6 +2558,12 @@
const char kOmniboxMlLogUrlScoringSignalsDescription[] =
"Enables Omnibox to log scoring signals of URL suggestions.";
+const char kOmniboxMlUrlPiecewiseMappedSearchBlendingName[] =
+ "Omnibox ML Scoring with Piecewise Score Mapping";
+const char kOmniboxMlUrlPiecewiseMappedSearchBlendingDescription[] =
+ "Specifies how to blend URL ML scores and search traditional scores using "
+ "a piecewise ML score mapping function.";
+
const char kOmniboxMlUrlScoreCachingName[] = "Omnibox ML URL Score Caching";
const char kOmniboxMlUrlScoreCachingDescription[] =
"Enables in-memory caching of ML URL scores.";
diff --git a/chrome/browser/flag_descriptions.h b/chrome/browser/flag_descriptions.h
index 6d7e273..414313d 100644
--- a/chrome/browser/flag_descriptions.h
+++ b/chrome/browser/flag_descriptions.h
@@ -1439,6 +1439,9 @@
extern const char kOmniboxMlLogUrlScoringSignalsName[];
extern const char kOmniboxMlLogUrlScoringSignalsDescription[];
+extern const char kOmniboxMlUrlPiecewiseMappedSearchBlendingName[];
+extern const char kOmniboxMlUrlPiecewiseMappedSearchBlendingDescription[];
+
extern const char kOmniboxMlUrlScoreCachingName[];
extern const char kOmniboxMlUrlScoreCachingDescription[];
diff --git a/components/omnibox/browser/autocomplete_controller.cc b/components/omnibox/browser/autocomplete_controller.cc
index 439ec9a..cf49347 100644
--- a/components/omnibox/browser/autocomplete_controller.cc
+++ b/components/omnibox/browser/autocomplete_controller.cc
@@ -1284,7 +1284,9 @@
return;
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
- if (OmniboxFieldTrial::GetMLConfig().mapped_search_blending) {
+ if (OmniboxFieldTrial::GetMLConfig().piecewise_mapped_search_blending) {
+ RunBatchUrlScoringModelPiecewiseMappedSearchBlending(old_result);
+ } else if (OmniboxFieldTrial::GetMLConfig().mapped_search_blending) {
RunBatchUrlScoringModelMappedSearchBlending(old_result);
} else {
RunBatchUrlScoringModel(old_result);
@@ -2069,8 +2071,6 @@
// 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.
- // TODO(crbug.com/40062540): Replace `Sort` with `DeduplicateMatches` for
- // better stability of matches.
internal_result_.Sort(input_, template_url_service_,
old_result.default_match_to_preserve);
@@ -2095,15 +2095,17 @@
scored_positions.push_back(i);
}
- if (batch_scoring_signals.empty())
+ if (batch_scoring_signals.empty()) {
return;
+ }
auto elapsed_timer = base::ElapsedTimer();
const auto results = provider_client_->GetAutocompleteScoringModelService()
->BatchScoreAutocompleteUrlMatchesSync(
std::move(batch_scoring_signals));
- if (results.empty())
+ if (results.empty()) {
return;
+ }
// Record how many eligible matches the model was executed for.
base::UmaHistogramCounts1000("Omnibox.URLScoringModelExecuted.Matches",
@@ -2118,8 +2120,9 @@
provider_client_->GetOmniboxTriggeredFeatureService()->FeatureTriggered(
metrics::OmniboxEventProto_Feature_ML_URL_SCORING);
- if (OmniboxFieldTrial::IsMlUrlScoringCounterfactual())
+ if (OmniboxFieldTrial::IsMlUrlScoringCounterfactual()) {
return;
+ }
const int min = OmniboxFieldTrial::GetMLConfig().mapped_search_blending_min;
const int max = OmniboxFieldTrial::GetMLConfig().mapped_search_blending_max;
@@ -2184,6 +2187,164 @@
match.relevance = scores_pool[i];
}
+ for (Observer& obs : observers_) {
+ obs.OnMlScored(this, internal_result_);
+ }
+}
+
+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;
+}
+
+void AutocompleteController::
+ RunBatchUrlScoringModelPiecewiseMappedSearchBlending(
+ OldResult& old_result) {
+ TRACE_EVENT0("omnibox",
+ "AutocompleteController::"
+ "RunBatchUrlScoringModelPiecewiseMappedSearchBlending");
+
+ const auto break_points = OmniboxFieldTrial::GetPiecewiseMappingBreakPoints();
+ if (break_points.empty()) {
+ return;
+ }
+
+ // 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];
+ // Do not attempt to score matches that are generally ineligible for ML
+ // scoring nor any stale suggestions sourced from the DocumentProvider
+ // cache.
+ if (!match.IsUrlScoringEligible() ||
+ (match.type == AutocompleteMatchType::DOCUMENT_SUGGESTION &&
+ match.relevance == 0)) {
+ 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 results = provider_client_->GetAutocompleteScoringModelService()
+ ->BatchScoreAutocompleteUrlMatchesSync(
+ std::move(batch_scoring_signals));
+ if (results.empty()) {
+ return;
+ }
+
+ // Record how many eligible matches the model was executed for.
+ base::UmaHistogramCounts1000("Omnibox.URLScoringModelExecuted.Matches",
+ results.size());
+
+ // Record how long it took to execute the model for all eligible matches.
+ UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
+ "Omnibox.URLScoringModelExecuted.ElapsedTime", elapsed_timer.Elapsed(),
+ base::Microseconds(1), base::Milliseconds(3), 100);
+
+ // 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;
+
+ int score_coverage_count = 0;
+ for (size_t i = 0; i < results.size(); ++i) {
+ const auto& prediction = results[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 =
+ ApplyPiecewiseScoringTransform(p_value, break_points) + relevance_bias;
+ 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, results.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.IsUrlScoringEligible() ||
+ (match.type == AutocompleteMatchType::DOCUMENT_SUGGESTION &&
+ match.relevance == 0)) {
+ continue;
+ }
+ scores_pool.push_back(match.relevance);
+ }
+ base::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 < results.size(); ++i) {
+ const auto& prediction = results[i];
+ prediction_and_position_heap.push_back(
+ {prediction.value_or(0), scored_positions[i]});
+ }
+ base::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_);
}
diff --git a/components/omnibox/browser/autocomplete_controller.h b/components/omnibox/browser/autocomplete_controller.h
index f4a4486..a6d18229 100644
--- a/components/omnibox/browser/autocomplete_controller.h
+++ b/components/omnibox/browser/autocomplete_controller.h
@@ -428,9 +428,18 @@
bool ShouldRunProvider(AutocompleteProvider* provider) const;
#if BUILDFLAG(BUILD_WITH_TFLITE_LIB)
+ // Given an `ml_score` in the range [0, 1], computes the corresponding
+ // relevance score using the piecewise function described by the given
+ // `break_points`.
+ int ApplyPiecewiseScoringTransform(
+ double ml_score,
+ std::vector<std::pair<double, int>> break_points);
+
// Runs the batch scoring for all the eligible matches in `results_.matches_`.
void RunBatchUrlScoringModel(OldResult& old_result);
void RunBatchUrlScoringModelMappedSearchBlending(OldResult& old_result);
+ void RunBatchUrlScoringModelPiecewiseMappedSearchBlending(
+ OldResult& old_result);
#endif // BUILDFLAG(BUILD_WITH_TFLITE_LIB)
// Constructs a destination URL from supplied search terms args.
diff --git a/components/omnibox/browser/autocomplete_controller_unittest.cc b/components/omnibox/browser/autocomplete_controller_unittest.cc
index 54fd7f1..78c5fda5 100644
--- a/components/omnibox/browser/autocomplete_controller_unittest.cc
+++ b/components/omnibox/browser/autocomplete_controller_unittest.cc
@@ -1064,6 +1064,258 @@
}));
}
+TEST_F(AutocompleteControllerTest, MlRanking_ApplyPiecewiseScoringTransform) {
+ OmniboxFieldTrial::ScopedMLConfigForTesting scoped_ml_config;
+ scoped_ml_config.GetMLConfig().ml_url_scoring = true;
+ scoped_ml_config.GetMLConfig().url_scoring_model = true;
+ scoped_ml_config.GetMLConfig().piecewise_mapped_search_blending = true;
+
+ float ml_score = 0;
+ const std::vector<std::pair<double, int>> break_points = {
+ {0, 500}, {0.25, 1000}, {0.75, 1300}, {1, 1500}};
+
+ ml_score = 0;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 500);
+
+ ml_score = 0.186;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 872);
+
+ ml_score = 0.25;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 1000);
+
+ ml_score = 0.473;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 1133);
+
+ ml_score = 0.75;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 1300);
+
+ ml_score = 0.914;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 1431);
+
+ ml_score = 1;
+ EXPECT_EQ(controller_.ApplyPiecewiseScoringTransform(ml_score, break_points),
+ 1500);
+}
+
+TEST_F(AutocompleteControllerTest, MlRanking_PiecewiseMappedSearchBlending) {
+ OmniboxFieldTrial::ScopedMLConfigForTesting scoped_ml_config;
+ scoped_ml_config.GetMLConfig().ml_url_scoring = true;
+ scoped_ml_config.GetMLConfig().url_scoring_model = true;
+ scoped_ml_config.GetMLConfig().piecewise_mapped_search_blending = true;
+
+ scoped_ml_config.GetMLConfig().piecewise_mapped_search_blending_break_points =
+ "0,500;0.25,1000;0.75,1300;1,1500";
+ scoped_ml_config.GetMLConfig()
+ .piecewise_mapped_search_blending_grouping_threshold = 1100;
+ scoped_ml_config.GetMLConfig()
+ .piecewise_mapped_search_blending_relevance_bias = 0;
+
+ EXPECT_THAT(controller_.SimulateCleanAutocompletePass({}),
+ testing::ElementsAre());
+
+ // If a (remote) document suggestion has a traditional score of zero, then the
+ // final relevance score should remain zero (instead of using the piecewise ML
+ // score mapping function to overwrite the relevance score). This will result
+ // in the document suggestion getting culled from the final list of
+ // suggestions.
+ const auto type = AutocompleteMatchType::DOCUMENT_SUGGESTION;
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1000
+ CreateMlScoredMatch("document 1400 0.25", type, false, 1400, 0.25),
+ // Final score: 0 (!= 1500)
+ CreateMlScoredMatch("document 0 0.95", type, false, 0, 1),
+ // Final score: 1300
+ CreateMlScoredMatch("document 1200 0.75", type, false, 1200, 0.75),
+ }),
+ testing::ElementsAreArray({
+ "document 1200 0.75",
+ "document 1400 0.25",
+ }));
+
+ // Simple case of ranking with piecewise score mapping. The ML
+ // scores used here are the same as those specified in the
+ // `MlRanking_ApplyPiecewiseScoringTransform` test for the sake of simplicity.
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1133
+ CreateHistoryUrlMlScoredMatch("history 1350 .473", true, 1350, .473),
+ // Final score: 1431
+ CreateHistoryUrlMlScoredMatch("history 1200 .914", true, 1200, .914),
+ // Final score: 872
+ CreateHistoryUrlMlScoredMatch("history 1100 .186", false, 1100, .186),
+ // Final score: 1000
+ CreateHistoryUrlMlScoredMatch("history 500 .25", true, 500, .25),
+ }),
+ testing::ElementsAreArray({
+ "history 1200 .914",
+ "history 1350 .473",
+ "history 500 .25",
+ "history 1100 .186",
+ }));
+
+ // Verify that URLs are grouped above searches if their final score is
+ // greater than `grouping_threshold` (i.e. "shortcut boosting").
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1133
+ CreateHistoryUrlMlScoredMatch("history 1350 .473", true, 1350, .473),
+ CreateSearchMatch("search 1400", false, 1400),
+ CreateSearchMatch("search 800", true, 800),
+ CreateSearchMatch("search 600", false, 600),
+ // Final score: 1431
+ CreateHistoryUrlMlScoredMatch("history 1200 .914", true, 1200, .914),
+ // Final score: 872
+ CreateHistoryUrlMlScoredMatch("history 1100 .186", false, 1100, .186),
+ // Final score: 1000
+ CreateHistoryUrlMlScoredMatch("history 500 .25", true, 500, .25),
+ }),
+ testing::ElementsAreArray({
+ "history 1200 .914",
+ "history 1350 .473",
+ "search 1400",
+ "search 800",
+ "search 600",
+ "history 500 .25",
+ "history 1100 .186",
+ }));
+
+ // When multiple URL suggestions have been assigned the same score by the ML
+ // model, those suggestions which were top-ranked according to legacy scoring
+ // should continue to be top-ranked once ML scoring has run.
+ EXPECT_THAT(
+ // Each of the below URL suggestions are assigned an initial relevance
+ // score of 1300. After initial assignment,
+ // score adjustment logic is applied in order to generate the final
+ // relevance scores (which are guaranteed to be distinct).
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1299
+ CreateHistoryUrlMlScoredMatch("history B 1200 .75", true, 1200, .75),
+ // Final score: 1296
+ CreateHistoryUrlMlScoredMatch("history E 200 .75", true, 200, .75),
+ // Final score: 1300
+ CreateHistoryUrlMlScoredMatch("history A 1350 .75", true, 1350, .75),
+ // Final score: 1297
+ CreateHistoryUrlMlScoredMatch("history D 300 .75", true, 300, .75),
+ // Final score: 1298
+ CreateHistoryUrlMlScoredMatch("history C 1100 .75", false, 1100, .75),
+ // Final score: 1295
+ CreateHistoryUrlMlScoredMatch("history F 100 .75", true, 100, .75),
+ }),
+ testing::ElementsAreArray({
+ "history A 1350 .75",
+ "history B 1200 .75",
+ "history C 1100 .75",
+ "history D 300 .75",
+ "history E 200 .75",
+ "history F 100 .75",
+ }));
+
+ // Can change the default suggestion from 1 history to another.
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1000
+ CreateHistoryUrlMlScoredMatch("history 1400 .25", true, 1400, .25),
+ CreateSearchMatch("search", true, 1100),
+ // Final score: 1300
+ CreateHistoryUrlMlScoredMatch("history 1200 .75", true, 1200, .75),
+ }),
+ testing::ElementsAreArray({
+ "history 1200 .75",
+ "search",
+ "history 1400 .25",
+ }));
+
+ // Can change the default from search to history.
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ CreateSearchMatch("search 1200", true, 1200),
+ // Final score: 1000
+ CreateHistoryUrlMlScoredMatch("history 1400 .25", false, 1400, .25),
+ // Final score: 1300
+ CreateHistoryUrlMlScoredMatch("history 1100 .75", true, 1100, .75),
+ }),
+ testing::ElementsAreArray({
+ "history 1100 .75",
+ "search 1200",
+ "history 1400 .25",
+ }));
+
+ // Can change the default from history to search.
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ // Final score: 1000
+ CreateHistoryUrlMlScoredMatch("history 1400 .25", true, 1400, .25),
+ CreateSearchMatch("search 1300", true, 1300),
+ // Final score: 872
+ CreateHistoryUrlMlScoredMatch("history 1200 .186", false, 1200, .186),
+ }),
+ testing::ElementsAreArray({
+ "search 1300",
+ "history 1400 .25",
+ "history 1200 .186",
+ }));
+
+ // When transferring matches, culls the lowest ML ranked matches, rather than
+ // the lowest traditional ranked matches.
+ controller_.internal_result_.Reset();
+ EXPECT_THAT(
+ controller_.SimulateAutocompletePass(
+ true, false,
+ {
+ CreateSearchMatch("search 1270", true, 1270),
+ CreateSearchMatch("search 1260", true, 1260),
+ CreateSearchMatch("search 1250", true, 1250),
+ CreateSearchMatch("search 1240", true, 1240),
+ CreateSearchMatch("search 1230", true, 1230),
+ CreateSearchMatch("search 1220", true, 1220),
+ CreateSearchMatch("search 1210", true, 1210),
+ CreateHistoryUrlMlScoredMatch("history 1100 .1", true, 1100, .1),
+ CreateHistoryUrlMlScoredMatch("history 1000 .2", true, 1000, .2),
+ }),
+ testing::ElementsAreArray({
+ "search 1270",
+ "search 1260",
+ "search 1250",
+ "search 1240",
+ "search 1230",
+ "search 1220",
+ "search 1210",
+ "history 1000 .2",
+ }));
+
+ // When not transferring matches, like above, culls the lowest ML ranked
+ // matches, rather than the lowest traditional ranked matches.
+ EXPECT_THAT(
+ controller_.SimulateCleanAutocompletePass({
+ CreateSearchMatch("search 1270", true, 1270),
+ CreateSearchMatch("search 1260", true, 1260),
+ CreateSearchMatch("search 1250", true, 1250),
+ CreateSearchMatch("search 1240", true, 1240),
+ CreateSearchMatch("search 1230", true, 1230),
+ CreateSearchMatch("search 1220", true, 1220),
+ CreateSearchMatch("search 1210", true, 1210),
+ CreateHistoryUrlMlScoredMatch("history 1100 .1", true, 1100, .1),
+ CreateHistoryUrlMlScoredMatch("history 1000 .2", true, 1000, .2),
+ }),
+ testing::ElementsAreArray({
+ "search 1270",
+ "search 1260",
+ "search 1250",
+ "search 1240",
+ "search 1230",
+ "search 1220",
+ "search 1210",
+ "history 1000 .2",
+ }));
+}
+
TEST_F(AutocompleteControllerTest, MlRanking_MappedSearchBlending) {
OmniboxFieldTrial::ScopedMLConfigForTesting scoped_ml_config;
scoped_ml_config.GetMLConfig().ml_url_scoring = true;
diff --git a/components/omnibox/browser/fake_autocomplete_controller.h b/components/omnibox/browser/fake_autocomplete_controller.h
index 205b8bf1..eef8eaf 100644
--- a/components/omnibox/browser/fake_autocomplete_controller.h
+++ b/components/omnibox/browser/fake_autocomplete_controller.h
@@ -92,6 +92,7 @@
using AutocompleteController::OldResult;
// AutocompleteController (methods):
+ using AutocompleteController::ApplyPiecewiseScoringTransform;
using AutocompleteController::MaybeRemoveCompanyEntityImages;
using AutocompleteController::ShouldRunProvider;
using AutocompleteController::UpdateResult;
diff --git a/components/omnibox/browser/omnibox_field_trial.cc b/components/omnibox/browser/omnibox_field_trial.cc
index fc3a2c38..3cd76d7 100644
--- a/components/omnibox/browser/omnibox_field_trial.cc
+++ b/components/omnibox/browser/omnibox_field_trial.cc
@@ -1023,6 +1023,30 @@
mapped_search_blending_grouping_threshold)
.Get();
+ // `kMlUrlPiecewiseMappedSearchBlending` parameters.
+ piecewise_mapped_search_blending =
+ base::FeatureParam<bool>(&omnibox::kMlUrlPiecewiseMappedSearchBlending,
+ "MlUrlPiecewiseMappedSearchBlending",
+ piecewise_mapped_search_blending)
+ .Get();
+ piecewise_mapped_search_blending_grouping_threshold =
+ base::FeatureParam<int>(
+ &omnibox::kMlUrlPiecewiseMappedSearchBlending,
+ "MlUrlPiecewiseMappedSearchBlending_GroupingThreshold",
+ piecewise_mapped_search_blending_grouping_threshold)
+ .Get();
+ piecewise_mapped_search_blending_break_points =
+ base::FeatureParam<std::string>(
+ &omnibox::kMlUrlPiecewiseMappedSearchBlending,
+ "MlUrlPiecewiseMappedSearchBlending_BreakPoints", "")
+ .Get();
+ piecewise_mapped_search_blending_relevance_bias =
+ base::FeatureParam<int>(
+ &omnibox::kMlUrlPiecewiseMappedSearchBlending,
+ "MlUrlPiecewiseMappedSearchBlending_RelevanceBias",
+ piecewise_mapped_search_blending_relevance_bias)
+ .Get();
+
url_scoring_model = base::FeatureList::IsEnabled(omnibox::kUrlScoringModel);
ml_url_score_caching =
@@ -1087,6 +1111,26 @@
return GetMLConfig().ml_url_score_caching;
}
+std::vector<std::pair<double, int>> GetPiecewiseMappingBreakPoints() {
+ std::vector<std::pair<double, int>> break_points;
+
+ std::string param_value = OmniboxFieldTrial::GetMLConfig()
+ .piecewise_mapped_search_blending_break_points;
+ base::StringPairs pairs;
+ if (base::SplitStringIntoKeyValuePairs(param_value, ',', ';', &pairs)) {
+ for (const auto& p : pairs) {
+ double ml_score;
+ base::StringToDouble(p.first, &ml_score);
+ int relevance;
+ base::StringToInt(p.second, &relevance);
+
+ break_points.push_back(std::make_pair(ml_score, relevance));
+ }
+ }
+
+ return break_points;
+}
+
// <- ML Relevance Scoring
// ---------------------------------------------------------
// Touch Down Trigger For Prefetch ->
diff --git a/components/omnibox/browser/omnibox_field_trial.h b/components/omnibox/browser/omnibox_field_trial.h
index 0f62c93..fc279ffb 100644
--- a/components/omnibox/browser/omnibox_field_trial.h
+++ b/components/omnibox/browser/omnibox_field_trial.h
@@ -565,7 +565,7 @@
// has no effect if `ml_url_scoring_unlimited_num_candidates` is true.
std::string ml_url_scoring_max_matches_by_provider;
- // There are 2 implementations for mapping ML scores [0, 1] to usable
+ // There are 3 implementations for mapping ML scores [0, 1] to usable
// relevances scores.
// 1) The original implementation in `RunBatchUrlScoringModel()`. This
// redistributes the traditional relevance scores and shortcut boosting so
@@ -584,15 +584,43 @@
// Not keeping the search v URL balance fixed for each individual input is
// the long term goal, though we may end up with a more complicated or ML
// approach.
+ // 3) The `piecewise_mapped_search_blending` implementation in
+ // `RunBatchUrlScoringModelPiecewiseMappedSearchBlending()`. It maps ML
+ // scores to relevance scores using a piecewise function (which must be
+ // continuous with respect to ML scores) composed of individual line
+ // segments whose break points are specified by
+ // `piecewise_mapped_search_blending_break_points`. The Search vs Url
+ // behavior of this implementation is similar to that noted above in (2).
// Enables approach (2) above.
// Map ML scores [0, 1] to [`min`, `max`]. Groups URLs above searches if their
- // mapped relevance is greater than `grouping_threshold`
+ // mapped relevance is greater than
+ // `mapped_search_blending_grouping_threshold`.
bool mapped_search_blending{false};
int mapped_search_blending_min{600};
int mapped_search_blending_max{2800};
int mapped_search_blending_grouping_threshold{1400};
+ // Enables approach (3) above.
+ // Map ML scores [0, 1] to relevance scores by using a piecewise score mapping
+ // function. Groups URLs above searches if their mapped relevance is greater
+ // than `piecewise_mapped_search_blending_grouping_threshold`.
+ bool piecewise_mapped_search_blending{false};
+ int piecewise_mapped_search_blending_grouping_threshold{1400};
+ // Specifies a list of N break points (x, y) which collectively define the N-1
+ // line segments that comprise the piecewise score mapping function. The list
+ // of break points must be sorted in ascending order with respect to their
+ // x-coordinates.
+ // As an example, if we use "0,550;0.018,1300;0.14,1398;1,1422" as the value
+ // for this param, then the resulting list of break points would be [(0, 550),
+ // (0.018, 1300), (0.14, 1398), (1, 1422)].
+ std::string piecewise_mapped_search_blending_break_points;
+ // Specifies a bias term that will be added to the relevance score which was
+ // computed by the piecewise score mapping function. By varying this term,
+ // it's possible to make the piecewise mapping function more or less
+ // aggressive at a global scale.
+ int piecewise_mapped_search_blending_relevance_bias{0};
+
// If true, ML scoring service will utilize in-memory ML score cache.
// Equivalent to omnibox::kMlUrlScoreCaching.
bool ml_url_score_caching{false};
@@ -649,6 +677,11 @@
// Whether ML URL score caching is enabled.
bool IsMlUrlScoreCachingEnabled();
+// Converts the `piecewise_break_points` feature param into a vector of (x, y)
+// coordinates specifying the "break points" of the piecewise ML score mapping
+// function.
+std::vector<std::pair<double, int>> GetPiecewiseMappingBreakPoints();
+
// <- ML Relevance Scoring
// ---------------------------------------------------------
// Inspire Me ->
diff --git a/components/omnibox/common/omnibox_features.cc b/components/omnibox/common/omnibox_features.cc
index 8f5fe45..b4cf4482 100644
--- a/components/omnibox/common/omnibox_features.cc
+++ b/components/omnibox/common/omnibox_features.cc
@@ -420,6 +420,13 @@
"LogUrlScoringSignals",
base::FEATURE_DISABLED_BY_DEFAULT);
+// If enabled, (floating-point) ML model scores are mapped to (integral)
+// relevance scores by means of a piecewise function. This allows for the
+// integration of URL model scores with search traditional scores.
+BASE_FEATURE(kMlUrlPiecewiseMappedSearchBlending,
+ "MlUrlPiecewiseMappedSearchBlending",
+ base::FEATURE_DISABLED_BY_DEFAULT);
+
// If enabled, the ML scoring service will make use of an in-memory ML score
// cache in order to speed up the overall scoring process.
BASE_FEATURE(kMlUrlScoreCaching,
diff --git a/components/omnibox/common/omnibox_features.h b/components/omnibox/common/omnibox_features.h
index 0c5df2c..a415e73 100644
--- a/components/omnibox/common/omnibox_features.h
+++ b/components/omnibox/common/omnibox_features.h
@@ -120,6 +120,7 @@
// Omnibox ML scoring.
BASE_DECLARE_FEATURE(kLogUrlScoringSignals);
+BASE_DECLARE_FEATURE(kMlUrlPiecewiseMappedSearchBlending);
BASE_DECLARE_FEATURE(kMlUrlScoreCaching);
BASE_DECLARE_FEATURE(kMlUrlScoring);
BASE_DECLARE_FEATURE(kMlUrlSearchBlending);
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index f9bc6a7..986ee4c10e 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -16227,6 +16227,7 @@
<int value="-1816654207" label="SharedZstd:disabled"/>
<int value="-1816066138" label="CastAllowAllIPs:enabled"/>
<int value="-1814266088" label="ContextualPageActionShareModel:enabled"/>
+ <int value="-1813120536" label="MlUrlPiecewiseMappedSearchBlending:disabled"/>
<int value="-1813088913" label="CCTResizable90MaximumHeight:disabled"/>
<int value="-1813070975" label="PageContentAnnotations:disabled"/>
<int value="-1812838429" label="DragWindowToNewDesk:disabled"/>
@@ -22600,6 +22601,7 @@
<int value="960696967" label="TabResumptionModuleAndroid:enabled"/>
<int value="961019394" label="IOSPromoRefreshedPasswordBubble:disabled"/>
<int value="962191833" label="StopAppIndexingReport:disabled"/>
+ <int value="963054071" label="MlUrlPiecewiseMappedSearchBlending:enabled"/>
<int value="963457392" label="ChromeHomeModernLayout:disabled"/>
<int value="963671232" label="DrawOcclusion:disabled"/>
<int value="964606028"