Omnibox - Boost Frequency Scores Based on Number of Matching Pages

This changelist adds a knob, controlled by an experiment, that
enables boosting the frequency scores in HistoryQuick provider
based on the number of documents matching the user's input.

The intuition is that if a user's input matches a single or
tiny number of documents from the user's history, then that input
is probably seeking those pages and they should be scored more
aggressively.  Boosting their frequency score (in effect the
document quality / popularity score) makes more sense to me
than boosting the topicality score or altering the final function
that translates the frequency and topicality score into a final
score.

This should be the last changelist I need.  Next up is
to use the knobs to find reasonable parameters and run some
experiments.

BUG=369989

Review-Url: https://codereview.chromium.org/2541143002
Cr-Commit-Position: refs/heads/master@{#438290}
diff --git a/components/omnibox/browser/omnibox_field_trial.cc b/components/omnibox/browser/omnibox_field_trial.cc
index 371c7626..3aeff57 100644
--- a/components/omnibox/browser/omnibox_field_trial.cc
+++ b/components/omnibox/browser/omnibox_field_trial.cc
@@ -431,6 +431,28 @@
   return typed_value;
 }
 
+OmniboxFieldTrial::NumMatchesScores OmniboxFieldTrial::HQPNumMatchesScores() {
+  std::string str = variations::GetVariationParamValue(
+      kBundledExperimentFieldTrialName, kHQPNumMatchesScoresRule);
+  // The parameter is a comma-separated list of (number, value) pairs, e.g.
+  // "1:3,2:2.5,3:2,4:1.5".
+  // This is a best-effort conversion; we trust the hand-crafted parameters
+  // downloaded from the server to be perfect.  There's no need to handle
+  // errors smartly.
+  base::StringPairs kv_pairs;
+  if (!base::SplitStringIntoKeyValuePairs(str, ':', ',', &kv_pairs))
+    return NumMatchesScores{};
+  NumMatchesScores num_matches_scores(kv_pairs.size());
+  for (size_t i = 0; i < kv_pairs.size(); ++i) {
+    base::StringToSizeT(kv_pairs[i].first, &num_matches_scores[i].first);
+    // The input must be sorted by number of matches.
+    DCHECK((i == 0) ||
+           (num_matches_scores[i].first > num_matches_scores[i - 1].first));
+    base::StringToDouble(kv_pairs[i].second, &num_matches_scores[i].second);
+  }
+  return num_matches_scores;
+}
+
 size_t OmniboxFieldTrial::HQPNumTitleWordsToAllow() {
   // The value of the rule is a string that encodes an integer (actually
   // size_t) containing the number of words.
@@ -536,6 +558,8 @@
 const char OmniboxFieldTrial::kHQPFreqencyUsesSumRule[] = "HQPFreqencyUsesSum";
 const char OmniboxFieldTrial::kHQPMaxVisitsToScoreRule[] =
     "HQPMaxVisitsToScoreRule";
+const char OmniboxFieldTrial::kHQPNumMatchesScoresRule[] =
+    "HQPNumMatchesScores";
 const char OmniboxFieldTrial::kHQPNumTitleWordsRule[] = "HQPNumTitleWords";
 const char OmniboxFieldTrial::kHQPAlsoDoHUPLikeScoringRule[] =
     "HQPAlsoDoHUPLikeScoring";
diff --git a/components/omnibox/browser/omnibox_field_trial.h b/components/omnibox/browser/omnibox_field_trial.h
index ff72ad1f..46b8f490 100644
--- a/components/omnibox/browser/omnibox_field_trial.h
+++ b/components/omnibox/browser/omnibox_field_trial.h
@@ -103,6 +103,15 @@
   // given number.  Omitted types are assumed to have multipliers of 1.0.
   typedef std::map<AutocompleteMatchType::Type, float> DemotionMultipliers;
 
+  // A vector that maps from the number of matching pages to the document
+  // specificity score used in HistoryQuick provider / ScoredHistoryMatch
+  // scoring. The vector is sorted by the size_t (the number of matching pages).
+  // If an entry is omitted, the appropriate value is assumed to be the one in
+  // the later bucket.  For example, with a vector containing {{1, 2.0},
+  // {3, 1.5}}, the score for 2 is inferred to be 1.5.  Values beyond the
+  // end of the vector are assumed to have scores of 1.0.
+  typedef std::vector<std::pair<size_t, double>> NumMatchesScores;
+
   // Do not change these values as they need to be in sync with values
   // specified in experiment configs on the variations server.
   enum EmphasizeTitlesCondition {
@@ -302,6 +311,10 @@
   // of non-typed transitions is 1.)  Returns 20 if the experiment isn't active.
   static float HQPTypedValue();
 
+  // Returns NumMatchesScores; see comment by the declaration of it.
+  // Returns an empty NumMatchesScores if the experiment isn't active.
+  static NumMatchesScores HQPNumMatchesScores();
+
   // ---------------------------------------------------------
   // For the HQPNumTitleWords experiment that's part of the
   // bundled omnibox field trial.
@@ -382,6 +395,7 @@
   static const char kHQPFixFewVisitsBugRule[];
   static const char kHQPFreqencyUsesSumRule[];
   static const char kHQPMaxVisitsToScoreRule[];
+  static const char kHQPNumMatchesScoresRule[];
   static const char kHQPNumTitleWordsRule[];
   static const char kHQPAlsoDoHUPLikeScoringRule[];
   static const char kHUPSearchDatabaseRule[];
diff --git a/components/omnibox/browser/scored_history_match.cc b/components/omnibox/browser/scored_history_match.cc
index 8e88bfa..5dddb3f 100644
--- a/components/omnibox/browser/scored_history_match.cc
+++ b/components/omnibox/browser/scored_history_match.cc
@@ -117,6 +117,8 @@
 float ScoredHistoryMatch::topicality_threshold_;
 ScoredHistoryMatch::ScoreMaxRelevances*
     ScoredHistoryMatch::relevance_buckets_override_ = nullptr;
+OmniboxFieldTrial::NumMatchesScores*
+    ScoredHistoryMatch::matches_to_specificity_override_ = nullptr;
 
 ScoredHistoryMatch::ScoredHistoryMatch()
     : ScoredHistoryMatch(history::URLRow(),
@@ -126,8 +128,8 @@
                          WordStarts(),
                          RowWordStarts(),
                          false,
-                         base::Time::Max()) {
-}
+                         1,
+                         base::Time::Max()) {}
 
 ScoredHistoryMatch::ScoredHistoryMatch(
     const history::URLRow& row,
@@ -137,6 +139,7 @@
     const WordStarts& terms_to_word_starts_offsets,
     const RowWordStarts& word_starts,
     bool is_url_bookmarked,
+    size_t num_matching_pages,
     base::Time now)
     : HistoryMatch(row, 0, false, false), raw_score(0) {
   // NOTE: Call Init() before doing any validity checking to ensure that the
@@ -254,8 +257,10 @@
   const float topicality_score = GetTopicalityScore(
       terms_vector.size(), url, terms_to_word_starts_offsets, word_starts);
   const float frequency_score = GetFrequency(now, is_url_bookmarked, visits);
-  raw_score = base::saturated_cast<int>(
-      GetFinalRelevancyScore(topicality_score, frequency_score));
+  const float specificity_score =
+      GetDocumentSpecificityScore(num_matching_pages);
+  raw_score = base::saturated_cast<int>(GetFinalRelevancyScore(
+      topicality_score, frequency_score, specificity_score));
 
   if (also_do_hup_like_scoring_ && likely_can_inline) {
     // HistoryURL-provider-like scoring gives any match that is
@@ -610,9 +615,29 @@
          ScoredHistoryMatch::max_visits_to_score_;
 }
 
+float ScoredHistoryMatch::GetDocumentSpecificityScore(
+    size_t num_matching_pages) const {
+  // A mapping from the number of matching pages to their associated document
+  // specificity scores.  See omnibox_field_trial.h for more details.
+  CR_DEFINE_STATIC_LOCAL(OmniboxFieldTrial::NumMatchesScores,
+                         default_matches_to_specificity,
+                         (OmniboxFieldTrial::HQPNumMatchesScores()));
+  OmniboxFieldTrial::NumMatchesScores* matches_to_specificity =
+      matches_to_specificity_override_ ? matches_to_specificity_override_
+                                       : &default_matches_to_specificity;
+
+  // The floating point value below must be less than the lowest score the
+  // server would send down.
+  OmniboxFieldTrial::NumMatchesScores::const_iterator it = std::upper_bound(
+      matches_to_specificity->begin(), matches_to_specificity->end(),
+      std::pair<size_t, double>{num_matching_pages, -1});
+  return (it != matches_to_specificity->end()) ? it->second : 1.0;
+};
+
 // static
 float ScoredHistoryMatch::GetFinalRelevancyScore(float topicality_score,
-                                                 float frequency_score) {
+                                                 float frequency_score,
+                                                 float specificity_score) {
   // |relevance_buckets| gives a mapping from intemerdiate score to the final
   // relevance score.
   CR_DEFINE_STATIC_LOCAL(ScoreMaxRelevances, default_relevance_buckets,
@@ -625,8 +650,9 @@
 
   if (topicality_score == 0)
     return 0;
-  // Here's how to interpret intermediate_score: Suppose the omnibox
-  // has one input term.  Suppose we have a URL for which the omnibox
+  // Here's how to interpret intermediate_score: Suppose the omnibox has one
+  // input term.  Suppose the input matches many documents.  (This implies
+  // specificity_score == 1.0.)  Suppose we have a URL for which the omnibox
   // input term has a single URL hostname hit at a word boundary.  (This
   // implies topicality_score = 1.0.).  Then the intermediate_score for
   // this URL will depend entirely on the frequency_score with
@@ -650,7 +676,8 @@
   //
   // The score maxes out at 1399 (i.e., cannot beat a good inlineable result
   // from HistoryURL provider).
-  const float intermediate_score = topicality_score * frequency_score;
+  const float intermediate_score =
+      topicality_score * frequency_score * specificity_score;
 
   // Find the threshold where intermediate score is greater than bucket.
   size_t i = 1;
diff --git a/components/omnibox/browser/scored_history_match.h b/components/omnibox/browser/scored_history_match.h
index a58c62a..51d18835 100644
--- a/components/omnibox/browser/scored_history_match.h
+++ b/components/omnibox/browser/scored_history_match.h
@@ -16,6 +16,7 @@
 #include "components/history/core/browser/history_match.h"
 #include "components/history/core/browser/history_types.h"
 #include "components/omnibox/browser/in_memory_url_index_types.h"
+#include "components/omnibox/browser/omnibox_field_trial.h"
 
 class ScoredHistoryMatchTest;
 
@@ -48,9 +49,11 @@
   // These offsets (".net" should have an offset of 1) come from
   // |terms_to_word_starts_offsets|. |is_url_bookmarked| indicates whether the
   // match's URL is referenced by any bookmarks, which can also affect the raw
-  // score.  The raw score allows the matches to be ordered and can be used to
-  // influence the final score calculated by the client of this index.  If the
-  // row does not qualify the raw score will be 0.
+  // score.  |num_matching_pages| indicates how many URLs in the eligible URL
+  // database match the user's input; it can also affect the raw score.  The raw
+  // score allows the matches to be ordered and can be used to influence the
+  // final score calculated by the client of this index.  If the row does not
+  // qualify the raw score will be 0.
   ScoredHistoryMatch(const history::URLRow& row,
                      const VisitInfoVector& visits,
                      const base::string16& lower_string,
@@ -58,6 +61,7 @@
                      const WordStarts& terms_to_word_starts_offsets,
                      const RowWordStarts& word_starts,
                      bool is_url_bookmarked,
+                     size_t num_matching_pages,
                      base::Time now);
 
   ~ScoredHistoryMatch();
@@ -98,6 +102,7 @@
 
  private:
   friend class ScoredHistoryMatchTest;
+  FRIEND_TEST_ALL_PREFIXES(ScoredHistoryMatchTest, GetDocumentSpecificityScore);
   FRIEND_TEST_ALL_PREFIXES(ScoredHistoryMatchTest, GetFinalRelevancyScore);
   FRIEND_TEST_ALL_PREFIXES(ScoredHistoryMatchTest, GetFrequency);
   FRIEND_TEST_ALL_PREFIXES(ScoredHistoryMatchTest, GetHQPBucketsFromString);
@@ -133,10 +138,15 @@
                      const bool bookmarked,
                      const VisitInfoVector& visits) const;
 
-  // Combines the two component scores into a final score that's an appropriate
-  // value to use as a relevancy score.
+  // Returns a document specificity score based on how many pages matched the
+  // user's input.
+  float GetDocumentSpecificityScore(size_t num_matching_pages) const;
+
+  // Combines the three component scores into a final score that's
+  // an appropriate value to use as a relevancy score.
   static float GetFinalRelevancyScore(float topicality_score,
-                                      float frequency_score);
+                                      float frequency_score,
+                                      float specificity_score);
 
   // Helper function that returns the string containing the scoring buckets
   // (either the default ones or ones specified in an experiment).
@@ -191,6 +201,11 @@
   // overrides the static local variable |relevance_buckets| declared in
   // GetFinalRelevancyScore().
   static ScoreMaxRelevances* relevance_buckets_override_;
+
+  // Used for testing.  If this pointer is not null, it overrides the static
+  // local variable |default_matches_to_specificity| declared in
+  // GetDocumentSpecificityScore().
+  static OmniboxFieldTrial::NumMatchesScores* matches_to_specificity_override_;
 };
 typedef std::vector<ScoredHistoryMatch> ScoredHistoryMatches;
 
diff --git a/components/omnibox/browser/scored_history_match_unittest.cc b/components/omnibox/browser/scored_history_match_unittest.cc
index 06400b2..f3fb2fa 100644
--- a/components/omnibox/browser/scored_history_match_unittest.cc
+++ b/components/omnibox/browser/scored_history_match_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/memory/ptr_util.h"
 #include "base/strings/string16.h"
 #include "base/strings/utf_string_conversions.h"
+#include "components/omnibox/browser/omnibox_field_trial.h"
 #include "components/search_engines/search_terms_data.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -122,7 +123,7 @@
   String16SetFromString16(title, &row_word_starts.title_word_starts_);
   ScoredHistoryMatch scored_match(history::URLRow(GURL(url)), VisitInfoVector(),
                                   term, term_vector, term_word_starts,
-                                  row_word_starts, false, base::Time::Max());
+                                  row_word_starts, false, 1, base::Time::Max());
   scored_match.url_matches = MatchTermInString(term, url, 0);
   scored_match.title_matches = MatchTermInString(term, title, 0);
   scored_match.topicality_threshold_ = -1;
@@ -144,7 +145,7 @@
   visits_a[0].second = ui::PAGE_TRANSITION_TYPED;
   ScoredHistoryMatch scored_a(row_a, visits_a, ASCIIToUTF16("abc"),
                               Make1Term("abc"), one_word_no_offset,
-                              word_starts_a, false, now);
+                              word_starts_a, false, 1, now);
 
   // Test scores based on visit_count.
   history::URLRow row_b(MakeURLRow("http://abcdef", "abcd bcd", 10, 30, 1));
@@ -154,7 +155,7 @@
   visits_b[0].second = ui::PAGE_TRANSITION_TYPED;
   ScoredHistoryMatch scored_b(row_b, visits_b, ASCIIToUTF16("abc"),
                               Make1Term("abc"), one_word_no_offset,
-                              word_starts_b, false, now);
+                              word_starts_b, false, 1, now);
   EXPECT_GT(scored_b.raw_score, scored_a.raw_score);
 
   // Test scores based on last_visit.
@@ -165,7 +166,7 @@
   visits_c[0].second = ui::PAGE_TRANSITION_TYPED;
   ScoredHistoryMatch scored_c(row_c, visits_c, ASCIIToUTF16("abc"),
                               Make1Term("abc"), one_word_no_offset,
-                              word_starts_c, false, now);
+                              word_starts_c, false, 1, now);
   EXPECT_GT(scored_c.raw_score, scored_a.raw_score);
 
   // Test scores based on typed_count.
@@ -178,7 +179,7 @@
   visits_d[2].second = ui::PAGE_TRANSITION_TYPED;
   ScoredHistoryMatch scored_d(row_d, visits_d, ASCIIToUTF16("abc"),
                               Make1Term("abc"), one_word_no_offset,
-                              word_starts_d, false, now);
+                              word_starts_d, false, 1, now);
   EXPECT_GT(scored_d.raw_score, scored_a.raw_score);
 
   // Test scores based on a terms appearing multiple times.
@@ -190,14 +191,14 @@
   const VisitInfoVector visits_e = visits_d;
   ScoredHistoryMatch scored_e(row_e, visits_e, ASCIIToUTF16("csi"),
                               Make1Term("csi"), one_word_no_offset,
-                              word_starts_e, false, now);
+                              word_starts_e, false, 1, now);
   EXPECT_LT(scored_e.raw_score, 1400);
 
   // Test that a result with only a mid-term match (i.e., not at a word
   // boundary) scores 0.
   ScoredHistoryMatch scored_f(row_a, visits_a, ASCIIToUTF16("cd"),
                               Make1Term("cd"), one_word_no_offset,
-                              word_starts_a, false, now);
+                              word_starts_a, false, 1, now);
   EXPECT_EQ(scored_f.raw_score, 0);
 }
 
@@ -214,12 +215,12 @@
   WordStarts one_word_no_offset(1, 0u);
   VisitInfoVector visits = CreateVisitInfoVector(8, 3, now);
   ScoredHistoryMatch scored(row, visits, ASCIIToUTF16("abc"), Make1Term("abc"),
-                            one_word_no_offset, word_starts, false, now);
+                            one_word_no_offset, word_starts, false, 1, now);
   // Now check that if URL is bookmarked then its score increases.
   base::AutoReset<float> reset(&ScoredHistoryMatch::bookmark_value_, 5);
   ScoredHistoryMatch scored_with_bookmark(row, visits, ASCIIToUTF16("abc"),
                                           Make1Term("abc"), one_word_no_offset,
-                                          word_starts, true, now);
+                                          word_starts, true, 1, now);
   EXPECT_GT(scored_with_bookmark.raw_score, scored.raw_score);
 }
 
@@ -238,14 +239,14 @@
   VisitInfoVector visits = CreateVisitInfoVector(8, 3, now);
   ScoredHistoryMatch scored(row, visits, ASCIIToUTF16("fed com"),
                             Make2Terms("fed", "com"), two_words_no_offsets,
-                            word_starts, false, now);
+                            word_starts, false, 1, now);
   EXPECT_EQ(0, scored.raw_score);
 
   // Now allow credit for the match in the TLD.
   base::AutoReset<bool> reset(&ScoredHistoryMatch::allow_tld_matches_, true);
   ScoredHistoryMatch scored_with_tld(
       row, visits, ASCIIToUTF16("fed com"), Make2Terms("fed", "com"),
-      two_words_no_offsets, word_starts, false, now);
+      two_words_no_offsets, word_starts, false, 1, now);
   EXPECT_GT(scored_with_tld.raw_score, 0);
 }
 
@@ -264,14 +265,14 @@
   VisitInfoVector visits = CreateVisitInfoVector(8, 3, now);
   ScoredHistoryMatch scored(row, visits, ASCIIToUTF16("fed http"),
                             Make2Terms("fed", "http"), two_words_no_offsets,
-                            word_starts, false, now);
+                            word_starts, false, 1, now);
   EXPECT_EQ(0, scored.raw_score);
 
   // Now allow credit for the match in the scheme.
   base::AutoReset<bool> reset(&ScoredHistoryMatch::allow_scheme_matches_, true);
   ScoredHistoryMatch scored_with_scheme(
       row, visits, ASCIIToUTF16("fed http"), Make2Terms("fed", "http"),
-      two_words_no_offsets, word_starts, false, now);
+      two_words_no_offsets, word_starts, false, 1, now);
   EXPECT_GT(scored_with_scheme.raw_score, 0);
 }
 
@@ -288,16 +289,16 @@
         MakeURLRow("http://www.google.com", "abcdef", 3, 30, 1));
     PopulateWordStarts(row, &word_starts);
     ScoredHistoryMatch scored_a(row, visits, ASCIIToUTF16("g"), Make1Term("g"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_a.match_in_scheme);
     ScoredHistoryMatch scored_b(row, visits, ASCIIToUTF16("w"), Make1Term("w"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_b.match_in_scheme);
     ScoredHistoryMatch scored_c(row, visits, ASCIIToUTF16("h"), Make1Term("h"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_TRUE(scored_c.match_in_scheme);
     ScoredHistoryMatch scored_d(row, visits, ASCIIToUTF16("o"), Make1Term("o"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_d.match_in_scheme);
   }
 
@@ -305,13 +306,13 @@
     history::URLRow row(MakeURLRow("http://teams.foo.com", "abcdef", 3, 30, 1));
     PopulateWordStarts(row, &word_starts);
     ScoredHistoryMatch scored_a(row, visits, ASCIIToUTF16("t"), Make1Term("t"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_a.match_in_scheme);
     ScoredHistoryMatch scored_b(row, visits, ASCIIToUTF16("f"), Make1Term("f"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_b.match_in_scheme);
     ScoredHistoryMatch scored_c(row, visits, ASCIIToUTF16("o"), Make1Term("o"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_c.match_in_scheme);
   }
 
@@ -320,13 +321,13 @@
         MakeURLRow("https://www.testing.com", "abcdef", 3, 30, 1));
     PopulateWordStarts(row, &word_starts);
     ScoredHistoryMatch scored_a(row, visits, ASCIIToUTF16("t"), Make1Term("t"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_a.match_in_scheme);
     ScoredHistoryMatch scored_b(row, visits, ASCIIToUTF16("h"), Make1Term("h"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_TRUE(scored_b.match_in_scheme);
     ScoredHistoryMatch scored_c(row, visits, ASCIIToUTF16("w"), Make1Term("w"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_c.match_in_scheme);
   }
 
@@ -335,14 +336,14 @@
         MakeURLRow("http://www.xn--1lq90ic7f1rc.cn/xnblah", "abcd", 3, 30, 1));
     PopulateWordStarts(row, &word_starts);
     ScoredHistoryMatch scored_a(row, visits, ASCIIToUTF16("x"), Make1Term("x"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_a.match_in_scheme);
     ScoredHistoryMatch scored_b(row, visits, ASCIIToUTF16("xn"),
                                 Make1Term("xn"), one_word_no_offset,
-                                word_starts, false, now);
+                                word_starts, false, 1, now);
     EXPECT_FALSE(scored_b.match_in_scheme);
     ScoredHistoryMatch scored_c(row, visits, ASCIIToUTF16("w"), Make1Term("w"),
-                                one_word_no_offset, word_starts, false, now);
+                                one_word_no_offset, word_starts, false, 1, now);
     EXPECT_FALSE(scored_c.match_in_scheme);
   }
 }
@@ -481,7 +482,7 @@
   base::Time now(base::Time::Max());
   VisitInfoVector visits;
   ScoredHistoryMatch match(row, visits, ASCIIToUTF16("foo"), Make1Term("foo"),
-                           WordStarts{0}, row_word_starts, false, now);
+                           WordStarts{0}, row_word_starts, false, 1, now);
 
   // Record the score for one untyped visit.
   visits = {{now, ui::PAGE_TRANSITION_LINK}};
@@ -581,6 +582,36 @@
   }
 }
 
+TEST_F(ScoredHistoryMatchTest, GetDocumentSpecificityScore) {
+  // Build a fake ScoredHistoryMatch, which we'll then reuse multiple times.
+  history::URLRow row(GURL("http://foo"));
+  RowWordStarts row_word_starts;
+  PopulateWordStarts(row, &row_word_starts);
+  base::Time now(base::Time::Max());
+  VisitInfoVector visits;
+  ScoredHistoryMatch match(row, visits, ASCIIToUTF16("foo"), Make1Term("foo"),
+                           WordStarts{0}, row_word_starts, false, 1, now);
+
+  EXPECT_EQ(1.0, match.GetDocumentSpecificityScore(1));
+  EXPECT_EQ(1.0, match.GetDocumentSpecificityScore(5));
+  EXPECT_EQ(1.0, match.GetDocumentSpecificityScore(50));
+
+  OmniboxFieldTrial::NumMatchesScores matches_to_specificity;
+  base::AutoReset<OmniboxFieldTrial::NumMatchesScores*> tmp(
+      &ScoredHistoryMatch::matches_to_specificity_override_,
+      &matches_to_specificity);
+
+  matches_to_specificity = {{1, 3.0}};
+  EXPECT_EQ(3.0, match.GetDocumentSpecificityScore(1));
+  EXPECT_EQ(1.0, match.GetDocumentSpecificityScore(5));
+
+  matches_to_specificity = {{1, 3.0}, {3, 1.5}};
+  EXPECT_EQ(3.0, match.GetDocumentSpecificityScore(1));
+  EXPECT_EQ(1.5, match.GetDocumentSpecificityScore(2));
+  EXPECT_EQ(1.5, match.GetDocumentSpecificityScore(3));
+  EXPECT_EQ(1.0, match.GetDocumentSpecificityScore(4));
+}
+
 // This function only tests scoring of single terms that match exactly
 // once somewhere in the URL or title.
 TEST_F(ScoredHistoryMatchTest, GetTopicalityScore) {
@@ -656,31 +687,32 @@
   // Check when topicality score is zero.
   float topicality_score = 0.0;
   float frequency_score = 10.0;
-  // intermediate_score = 0.0 * 10.0 = 0.0.
-  EXPECT_EQ(0, ScoredHistoryMatch::GetFinalRelevancyScore(topicality_score,
-                                                          frequency_score));
+  float specificity_score = 1.0;
+  // intermediate_score = 0.0 * 10.0 * 1.0 = 0.0.
+  EXPECT_EQ(0, ScoredHistoryMatch::GetFinalRelevancyScore(
+                   topicality_score, frequency_score, specificity_score));
 
   // Check when intermediate score falls at the border range.
   topicality_score = 0.4f;
   frequency_score = 10.0f;
-  // intermediate_score = 0.5 * 10.0 = 4.0.
-  EXPECT_EQ(500, ScoredHistoryMatch::GetFinalRelevancyScore(topicality_score,
-                                                            frequency_score));
+  // intermediate_score = 0.4 * 10.0 * 1.0 = 4.0.
+  EXPECT_EQ(500, ScoredHistoryMatch::GetFinalRelevancyScore(
+                     topicality_score, frequency_score, specificity_score));
 
   // Checking the score that falls into one of the buckets.
   topicality_score = 0.5f;
   frequency_score = 10.0f;
-  // intermediate_score = 0.5 * 10.0 = 5.0.
+  // intermediate_score = 0.5 * 10.0 * 1.0 = 5.0.
   EXPECT_EQ(600,  // 500 + (((900 - 500)/(8 -4)) * 1) = 600.
-            ScoredHistoryMatch::GetFinalRelevancyScore(topicality_score,
-                                                       frequency_score));
+            ScoredHistoryMatch::GetFinalRelevancyScore(
+                topicality_score, frequency_score, specificity_score));
 
   // Never give the score greater than maximum specified.
   topicality_score = 0.5f;
   frequency_score = 22.0f;
-  // intermediate_score = 0.5 * 22.0 = 11.0
-  EXPECT_EQ(1000, ScoredHistoryMatch::GetFinalRelevancyScore(topicality_score,
-                                                             frequency_score));
+  // intermediate_score = 0.5 * 22.0 * 1.0 = 11.0
+  EXPECT_EQ(1000, ScoredHistoryMatch::GetFinalRelevancyScore(
+                      topicality_score, frequency_score, specificity_score));
 }
 
 // Test the function GetHQPBucketsFromString().
diff --git a/components/omnibox/browser/url_index_private_data.cc b/components/omnibox/browser/url_index_private_data.cc
index d297b64..8088389 100644
--- a/components/omnibox/browser/url_index_private_data.cc
+++ b/components/omnibox/browser/url_index_private_data.cc
@@ -715,13 +715,11 @@
   }
 
   // Score the matches.
+  const size_t num_matches = history_id_set.size();
   const base::Time now = base::Time::Now();
   std::transform(
       history_id_set.begin(), history_id_set.end(),
-      std::back_inserter(*scored_items),
-      [this, &lower_raw_string, &lower_raw_terms,
-       &lower_terms_to_word_starts_offsets, &bookmark_model,
-       &now](const HistoryID history_id) {
+      std::back_inserter(*scored_items), [&](const HistoryID history_id) {
         auto hist_pos = history_info_map_.find(history_id);
         const history::URLRow& hist_item = hist_pos->second.url_row;
         auto starts_pos = word_starts_map_.find(history_id);
@@ -731,7 +729,7 @@
             lower_raw_terms, lower_terms_to_word_starts_offsets,
             starts_pos->second,
             bookmark_model && bookmark_model->IsBookmarked(hist_item.url()),
-            now);
+            num_matches, now);
       });
 
   // Filter all matches that ended up scoring 0.  (These are usually matches