Home | History | Annotate | Download | only in history
      1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "chrome/browser/history/scored_history_match.h"
      6 
      7 #include <algorithm>
      8 #include <functional>
      9 #include <iterator>
     10 #include <numeric>
     11 #include <set>
     12 
     13 #include <math.h>
     14 
     15 #include "base/metrics/histogram.h"
     16 #include "base/strings/string_util.h"
     17 #include "base/strings/utf_string_conversions.h"
     18 #include "chrome/browser/autocomplete/history_url_provider.h"
     19 #include "chrome/browser/autocomplete/url_prefix.h"
     20 #include "chrome/browser/bookmarks/bookmark_service.h"
     21 #include "chrome/common/chrome_switches.h"
     22 #include "content/public/browser/browser_thread.h"
     23 
     24 namespace history {
     25 
     26 // ScoredHistoryMatch ----------------------------------------------------------
     27 
     28 bool ScoredHistoryMatch::initialized_ = false;
     29 const size_t ScoredHistoryMatch::kMaxVisitsToScore = 10u;
     30 bool ScoredHistoryMatch::also_do_hup_like_scoring = false;
     31 int ScoredHistoryMatch::max_assigned_score_for_non_inlineable_matches = -1;
     32 
     33 ScoredHistoryMatch::ScoredHistoryMatch()
     34     : raw_score(0),
     35       can_inline(false) {
     36   if (!initialized_) {
     37     InitializeAlsoDoHUPLikeScoringFieldAndMaxScoreField();
     38     initialized_ = true;
     39   }
     40 }
     41 
     42 ScoredHistoryMatch::ScoredHistoryMatch(const URLRow& row,
     43                                        const VisitInfoVector& visits,
     44                                        const std::string& languages,
     45                                        const string16& lower_string,
     46                                        const String16Vector& terms,
     47                                        const RowWordStarts& word_starts,
     48                                        const base::Time now,
     49                                        BookmarkService* bookmark_service)
     50     : HistoryMatch(row, 0, false, false),
     51       raw_score(0),
     52       can_inline(false) {
     53   if (!initialized_) {
     54     InitializeAlsoDoHUPLikeScoringFieldAndMaxScoreField();
     55     initialized_ = true;
     56   }
     57 
     58   GURL gurl = row.url();
     59   if (!gurl.is_valid())
     60     return;
     61 
     62   // Figure out where each search term appears in the URL and/or page title
     63   // so that we can score as well as provide autocomplete highlighting.
     64   string16 url = CleanUpUrlForMatching(gurl, languages);
     65   string16 title = CleanUpTitleForMatching(row.title());
     66   int term_num = 0;
     67   for (String16Vector::const_iterator iter = terms.begin(); iter != terms.end();
     68        ++iter, ++term_num) {
     69     string16 term = *iter;
     70     TermMatches url_term_matches = MatchTermInString(term, url, term_num);
     71     TermMatches title_term_matches = MatchTermInString(term, title, term_num);
     72     if (url_term_matches.empty() && title_term_matches.empty())
     73       return;  // A term was not found in either URL or title - reject.
     74     url_matches.insert(url_matches.end(), url_term_matches.begin(),
     75                        url_term_matches.end());
     76     title_matches.insert(title_matches.end(), title_term_matches.begin(),
     77                          title_term_matches.end());
     78   }
     79 
     80   // Sort matches by offset and eliminate any which overlap.
     81   // TODO(mpearson): Investigate whether this has any meaningful
     82   // effect on scoring.  (It's necessary at some point: removing
     83   // overlaps and sorting is needed to decide what to highlight in the
     84   // suggestion string.  But this sort and de-overlap doesn't have to
     85   // be done before scoring.)
     86   url_matches = SortAndDeoverlapMatches(url_matches);
     87   title_matches = SortAndDeoverlapMatches(title_matches);
     88 
     89   // We can inline autocomplete a match if:
     90   //  1) there is only one search term
     91   //  2) AND the match begins immediately after one of the prefixes in
     92   //     URLPrefix such as http://www and https:// (note that one of these
     93   //     is the empty prefix, for cases where the user has typed the scheme)
     94   //  3) AND the search string does not end in whitespace (making it look to
     95   //     the IMUI as though there is a single search term when actually there
     96   //     is a second, empty term).
     97   // |best_inlineable_prefix| stores the inlineable prefix computed in
     98   // clause (2) or NULL if no such prefix exists.  (The URL is not inlineable.)
     99   // Note that using the best prefix here means that when multiple
    100   // prefixes match, we'll choose to inline following the longest one.
    101   // For a URL like "http://www.washingtonmutual.com", this means
    102   // typing "w" will inline "ashington..." instead of "ww.washington...".
    103   const URLPrefix* best_inlineable_prefix =
    104       (!url_matches.empty() && (terms.size() == 1)) ?
    105       URLPrefix::BestURLPrefix(UTF8ToUTF16(gurl.spec()), terms[0]) :
    106       NULL;
    107   can_inline = (best_inlineable_prefix != NULL) &&
    108       !IsWhitespace(*(lower_string.rbegin()));
    109   match_in_scheme = can_inline && best_inlineable_prefix->prefix.empty();
    110   if (can_inline) {
    111     // Initialize innermost_match.
    112     // The idea here is that matches that occur in the scheme or
    113     // "www." are worse than matches which don't.  For the URLs
    114     // "http://www.google.com" and "http://wellsfargo.com", we want
    115     // the omnibox input "w" to cause the latter URL to rank higher
    116     // than the former.  Note that this is not the same as checking
    117     // whether one match's inlinable prefix has more components than
    118     // the other match's, since in this example, both matches would
    119     // have an inlinable prefix of "http://", which is one component.
    120     //
    121     // Instead, we look for the overall best (i.e., most components)
    122     // prefix of the current URL, and then check whether the inlinable
    123     // prefix has that many components.  If it does, this is an
    124     // "innermost" match, and should be boosted.  In the example
    125     // above, the best prefixes for the two URLs have two and one
    126     // components respectively, while the inlinable prefixes each
    127     // have one component; this means the first match is not innermost
    128     // and the second match is innermost, resulting in us boosting the
    129     // second match.
    130     //
    131     // Now, the code that implements this.
    132     // The deepest prefix for this URL regardless of where the match is.
    133     const URLPrefix* best_prefix =
    134         URLPrefix::BestURLPrefix(UTF8ToUTF16(gurl.spec()), string16());
    135     DCHECK(best_prefix != NULL);
    136     const int num_components_in_best_prefix = best_prefix->num_components;
    137     // If the URL is inlineable, we must have a match.  Note the prefix that
    138     // makes it inlineable may be empty.
    139     DCHECK(best_inlineable_prefix != NULL);
    140     const int num_components_in_best_inlineable_prefix =
    141         best_inlineable_prefix->num_components;
    142     innermost_match = (num_components_in_best_inlineable_prefix ==
    143         num_components_in_best_prefix);
    144   }
    145 
    146   const float topicality_score = GetTopicalityScore(
    147       terms.size(), url, url_matches, title_matches, word_starts);
    148   const float frecency_score = GetFrecency(now, visits);
    149   raw_score = GetFinalRelevancyScore(topicality_score, frecency_score);
    150   raw_score =
    151       (raw_score <= kint32max) ? static_cast<int>(raw_score) : kint32max;
    152 
    153   if (also_do_hup_like_scoring && can_inline) {
    154     // HistoryURL-provider-like scoring gives any match that is
    155     // capable of being inlined a certain minimum score.  Some of these
    156     // are given a higher score that lets them be shown in inline.
    157     // This test here derives from the test in
    158     // HistoryURLProvider::PromoteMatchForInlineAutocomplete().
    159     const bool promote_to_inline = (row.typed_count() > 1) ||
    160         (IsHostOnly() && (row.typed_count() == 1));
    161     int hup_like_score = promote_to_inline ?
    162         HistoryURLProvider::kScoreForBestInlineableResult :
    163         HistoryURLProvider::kBaseScoreForNonInlineableResult;
    164 
    165     // Also, if the user types the hostname of a host with a typed
    166     // visit, then everything from that host get given inlineable scores
    167     // (because the URL-that-you-typed will go first and everything
    168     // else will be assigned one minus the previous score, as coded
    169     // at the end of HistoryURLProvider::DoAutocomplete().
    170     if (UTF8ToUTF16(gurl.host()) == terms[0])
    171       hup_like_score = HistoryURLProvider::kScoreForBestInlineableResult;
    172 
    173     // HistoryURLProvider has the function PromoteOrCreateShorterSuggestion()
    174     // that's meant to promote prefixes of the best match (if they've
    175     // been visited enough related to the best match) or
    176     // create/promote host-only suggestions (even if they've never
    177     // been typed).  The code is complicated and we don't try to
    178     // duplicate the logic here.  Instead, we handle a simple case: in
    179     // low-typed-count ranges, give host-only matches (i.e.,
    180     // http://www.foo.com/ vs. http://www.foo.com/bar.html) a boost so
    181     // that the host-only match outscores all the other matches that
    182     // would normally have the same base score.  This behavior is not
    183     // identical to what happens in HistoryURLProvider even in these
    184     // low typed count ranges--sometimes it will create/promote when
    185     // this test does not (indeed, we cannot create matches like HUP
    186     // can) and vice versa--but the underlying philosophy is similar.
    187     if (!promote_to_inline && IsHostOnly())
    188       hup_like_score++;
    189 
    190     // All the other logic to goes into hup-like-scoring happens in
    191     // the tie-breaker case of MatchScoreGreater().
    192 
    193     // Incorporate hup_like_score into raw_score.
    194     raw_score = std::max(raw_score, hup_like_score);
    195   }
    196 
    197   // If this match is not inlineable and there's a cap on the maximum
    198   // score that can be given to non-inlineable matches, apply the cap.
    199   if (!can_inline && (max_assigned_score_for_non_inlineable_matches != -1)) {
    200     raw_score = std::min(max_assigned_score_for_non_inlineable_matches,
    201                          raw_score);
    202   }
    203 }
    204 
    205 ScoredHistoryMatch::~ScoredHistoryMatch() {}
    206 
    207 // Comparison function for sorting ScoredMatches by their scores with
    208 // intelligent tie-breaking.
    209 bool ScoredHistoryMatch::MatchScoreGreater(const ScoredHistoryMatch& m1,
    210                                            const ScoredHistoryMatch& m2) {
    211   if (m1.raw_score != m2.raw_score)
    212     return m1.raw_score > m2.raw_score;
    213 
    214   // This tie-breaking logic is inspired by / largely copied from the
    215   // ordering logic in history_url_provider.cc CompareHistoryMatch().
    216 
    217   // A URL that has been typed at all is better than one that has never been
    218   // typed.  (Note "!"s on each side.)
    219   if (!m1.url_info.typed_count() != !m2.url_info.typed_count())
    220     return m1.url_info.typed_count() > m2.url_info.typed_count();
    221 
    222   // Innermost matches (matches after any scheme or "www.") are better than
    223   // non-innermost matches.
    224   if (m1.innermost_match != m2.innermost_match)
    225     return m1.innermost_match;
    226 
    227   // URLs that have been typed more often are better.
    228   if (m1.url_info.typed_count() != m2.url_info.typed_count())
    229     return m1.url_info.typed_count() > m2.url_info.typed_count();
    230 
    231   // For URLs that have each been typed once, a host (alone) is better
    232   // than a page inside.
    233   if (m1.url_info.typed_count() == 1) {
    234     if (m1.IsHostOnly() != m2.IsHostOnly())
    235       return m1.IsHostOnly();
    236   }
    237 
    238   // URLs that have been visited more often are better.
    239   if (m1.url_info.visit_count() != m2.url_info.visit_count())
    240     return m1.url_info.visit_count() > m2.url_info.visit_count();
    241 
    242   // URLs that have been visited more recently are better.
    243   return m1.url_info.last_visit() > m2.url_info.last_visit();
    244 }
    245 
    246 // static
    247 float ScoredHistoryMatch::GetTopicalityScore(
    248     const int num_terms,
    249     const string16& url,
    250     const TermMatches& url_matches,
    251     const TermMatches& title_matches,
    252     const RowWordStarts& word_starts) {
    253   // Because the below thread is not thread safe, we check that we're
    254   // only calling it from one thread: the UI thread.  Specifically,
    255   // we check "if we've heard of the UI thread then we'd better
    256   // be on it."  The first part is necessary so unit tests pass.  (Many
    257   // unit tests don't set up the threading naming system; hence
    258   // CurrentlyOn(UI thread) will fail.)
    259   DCHECK(!content::BrowserThread::IsThreadInitialized(
    260              content::BrowserThread::UI) ||
    261          content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    262   if (raw_term_score_to_topicality_score == NULL) {
    263     raw_term_score_to_topicality_score = new float[kMaxRawTermScore];
    264     FillInTermScoreToTopicalityScoreArray();
    265   }
    266   // A vector that accumulates per-term scores.  The strongest match--a
    267   // match in the hostname at a word boundary--is worth 10 points.
    268   // Everything else is less.  In general, a match that's not at a word
    269   // boundary is worth about 1/4th or 1/5th of a match at the word boundary
    270   // in the same part of the URL/title.
    271   DCHECK_GT(num_terms, 0);
    272   std::vector<int> term_scores(num_terms, 0);
    273   std::vector<size_t>::const_iterator next_word_starts =
    274       word_starts.url_word_starts_.begin();
    275   std::vector<size_t>::const_iterator end_word_starts =
    276       word_starts.url_word_starts_.end();
    277   const size_t question_mark_pos = url.find('?');
    278   const size_t colon_pos = url.find(':');
    279   // The + 3 skips the // that probably appears in the protocol
    280   // after the colon.  If the protocol doesn't have two slashes after
    281   // the colon, that's okay--all this ends up doing is starting our
    282   // search for the next / a few characters into the hostname.  The
    283   // only times this can cause problems is if we have a protocol without
    284   // a // after the colon and the hostname is only one or two characters.
    285   // This isn't worth worrying about.
    286   const size_t end_of_hostname_pos = (colon_pos != std::string::npos) ?
    287       url.find('/', colon_pos + 3) : url.find('/');
    288   size_t last_part_of_hostname_pos =
    289       (end_of_hostname_pos != std::string::npos) ?
    290       url.rfind('.', end_of_hostname_pos) :
    291       url.rfind('.');
    292   // Loop through all URL matches and score them appropriately.
    293   for (TermMatches::const_iterator iter = url_matches.begin();
    294        iter != url_matches.end(); ++iter) {
    295     // Advance next_word_starts until it's >= the position of the term
    296     // we're considering.
    297     while ((next_word_starts != end_word_starts) &&
    298            (*next_word_starts < iter->offset)) {
    299       ++next_word_starts;
    300     }
    301     const bool at_word_boundary = (next_word_starts != end_word_starts) &&
    302         (*next_word_starts == iter->offset);
    303     if ((question_mark_pos != std::string::npos) &&
    304         (iter->offset > question_mark_pos)) {
    305       // match in CGI ?... fragment
    306       term_scores[iter->term_num] += at_word_boundary ? 5 : 0;
    307     } else if ((end_of_hostname_pos != std::string::npos) &&
    308         (iter->offset > end_of_hostname_pos)) {
    309       // match in path
    310       term_scores[iter->term_num] += at_word_boundary ? 8 : 0;
    311     } else if ((colon_pos == std::string::npos) ||
    312          (iter->offset > colon_pos)) {
    313       // match in hostname
    314       if ((last_part_of_hostname_pos == std::string::npos) ||
    315           (iter->offset < last_part_of_hostname_pos)) {
    316         // Either there are no dots in the hostname or this match isn't
    317         // the last dotted component.
    318         term_scores[iter->term_num] += at_word_boundary ? 10 : 2;
    319       } // else: match in the last part of a dotted hostname (usually
    320         // this is the top-level domain .com, .net, etc.).  Do not
    321         // count this match for scoring.
    322     } // else: match in protocol.  Do not count this match for scoring.
    323   }
    324   // Now do the analogous loop over all matches in the title.
    325   next_word_starts = word_starts.title_word_starts_.begin();
    326   end_word_starts = word_starts.title_word_starts_.end();
    327   int word_num = 0;
    328   for (TermMatches::const_iterator iter = title_matches.begin();
    329        iter != title_matches.end(); ++iter) {
    330     // Advance next_word_starts until it's >= the position of the term
    331     // we're considering.
    332     while ((next_word_starts != end_word_starts) &&
    333            (*next_word_starts < iter->offset)) {
    334       ++next_word_starts;
    335       ++word_num;
    336     }
    337     if (word_num >= 10) break;  // only count the first ten words
    338     const bool at_word_boundary = (next_word_starts != end_word_starts) &&
    339         (*next_word_starts == iter->offset);
    340     term_scores[iter->term_num] += at_word_boundary ? 8 : 0;
    341   }
    342   // TODO(mpearson): Restore logic for penalizing out-of-order matches.
    343   // (Perhaps discount them by 0.8?)
    344   // TODO(mpearson): Consider: if the earliest match occurs late in the string,
    345   // should we discount it?
    346   // TODO(mpearson): Consider: do we want to score based on how much of the
    347   // input string the input covers?  (I'm leaning toward no.)
    348 
    349   // Compute the topicality_score as the sum of transformed term_scores.
    350   float topicality_score = 0;
    351   for (size_t i = 0; i < term_scores.size(); ++i) {
    352     // Drop this URL if it seems like a term didn't appear or, more precisely,
    353     // didn't appear in a part of the URL or title that we trust enough
    354     // to give it credit for.  For instance, terms that appear in the middle
    355     // of a CGI parameter get no credit.  Almost all the matches dropped
    356     // due to this test would look stupid if shown to the user.
    357     if (term_scores[i] == 0)
    358       return 0;
    359     topicality_score += raw_term_score_to_topicality_score[
    360         (term_scores[i] >= kMaxRawTermScore) ? (kMaxRawTermScore - 1) :
    361         term_scores[i]];
    362   }
    363   // TODO(mpearson): If there are multiple terms, consider taking the
    364   // geometric mean of per-term scores rather than the arithmetic mean.
    365 
    366   return topicality_score / num_terms;
    367 }
    368 
    369 // static
    370 float* ScoredHistoryMatch::raw_term_score_to_topicality_score = NULL;
    371 
    372 // static
    373 void ScoredHistoryMatch::FillInTermScoreToTopicalityScoreArray() {
    374   for (int term_score = 0; term_score < kMaxRawTermScore; ++term_score) {
    375     float topicality_score;
    376     if (term_score < 10) {
    377       // If the term scores less than 10 points (no full-credit hit, or
    378       // no combination of hits that score that well), then the topicality
    379       // score is linear in the term score.
    380       topicality_score = 0.1 * term_score;
    381     } else {
    382       // For term scores of at least ten points, pass them through a log
    383       // function so a score of 10 points gets a 1.0 (to meet up exactly
    384       // with the linear component) and increases logarithmically until
    385       // maxing out at 30 points, with computes to a score around 2.1.
    386       topicality_score = (1.0 + 2.25 * log10(0.1 * term_score));
    387     }
    388     raw_term_score_to_topicality_score[term_score] = topicality_score;
    389   }
    390 }
    391 
    392 // static
    393 float* ScoredHistoryMatch::days_ago_to_recency_score = NULL;
    394 
    395 // static
    396 float ScoredHistoryMatch::GetRecencyScore(int last_visit_days_ago) {
    397   // Because the below thread is not thread safe, we check that we're
    398   // only calling it from one thread: the UI thread.  Specifically,
    399   // we check "if we've heard of the UI thread then we'd better
    400   // be on it."  The first part is necessary so unit tests pass.  (Many
    401   // unit tests don't set up the threading naming system; hence
    402   // CurrentlyOn(UI thread) will fail.)
    403   DCHECK(!content::BrowserThread::IsThreadInitialized(
    404              content::BrowserThread::UI) ||
    405          content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    406   if (days_ago_to_recency_score == NULL) {
    407     days_ago_to_recency_score = new float[kDaysToPrecomputeRecencyScoresFor];
    408     FillInDaysAgoToRecencyScoreArray();
    409   }
    410   // Lookup the score in days_ago_to_recency_score, treating
    411   // everything older than what we've precomputed as the oldest thing
    412   // we've precomputed.  The std::max is to protect against corruption
    413   // in the database (in case last_visit_days_ago is negative).
    414   return days_ago_to_recency_score[
    415       std::max(
    416       std::min(last_visit_days_ago, kDaysToPrecomputeRecencyScoresFor - 1),
    417       0)];
    418 }
    419 
    420 void ScoredHistoryMatch::FillInDaysAgoToRecencyScoreArray() {
    421   for (int days_ago = 0; days_ago < kDaysToPrecomputeRecencyScoresFor;
    422        days_ago++) {
    423     int unnormalized_recency_score;
    424     if (days_ago <= 4) {
    425       unnormalized_recency_score = 100;
    426     } else if (days_ago <= 14) {
    427       // Linearly extrapolate between 4 and 14 days so 14 days has a score
    428       // of 70.
    429       unnormalized_recency_score = 70 + (14 - days_ago) * (100 - 70) / (14 - 4);
    430     } else if (days_ago <= 31) {
    431       // Linearly extrapolate between 14 and 31 days so 31 days has a score
    432       // of 50.
    433       unnormalized_recency_score = 50 + (31 - days_ago) * (70 - 50) / (31 - 14);
    434     } else if (days_ago <= 90) {
    435       // Linearly extrapolate between 30 and 90 days so 90 days has a score
    436       // of 30.
    437       unnormalized_recency_score = 30 + (90 - days_ago) * (50 - 30) / (90 - 30);
    438     } else {
    439       // Linearly extrapolate between 90 and 365 days so 365 days has a score
    440       // of 10.
    441       unnormalized_recency_score =
    442           10 + (365 - days_ago) * (20 - 10) / (365 - 90);
    443     }
    444     days_ago_to_recency_score[days_ago] = unnormalized_recency_score / 100.0;
    445     if (days_ago > 0) {
    446       DCHECK_LE(days_ago_to_recency_score[days_ago],
    447                 days_ago_to_recency_score[days_ago - 1]);
    448     }
    449   }
    450 }
    451 
    452 // static
    453 float ScoredHistoryMatch::GetFrecency(const base::Time& now,
    454                                       const VisitInfoVector& visits) {
    455   // Compute the weighted average |value_of_transition| over the last at
    456   // most kMaxVisitsToScore visits, where each visit is weighted using
    457   // GetRecencyScore() based on how many days ago it happened.  Use
    458   // kMaxVisitsToScore as the denominator for the average regardless of
    459   // how many visits there were in order to penalize a match that has
    460   // fewer visits than kMaxVisitsToScore.
    461   const int total_sampled_visits = std::min(visits.size(), kMaxVisitsToScore);
    462   if (total_sampled_visits == 0)
    463     return 0.0f;
    464   float summed_visit_points = 0;
    465   for (int i = 0; i < total_sampled_visits; ++i) {
    466     const int value_of_transition =
    467         (visits[i].second == content::PAGE_TRANSITION_TYPED) ? 20 : 1;
    468     const float bucket_weight =
    469         GetRecencyScore((now - visits[i].first).InDays());
    470     summed_visit_points += (value_of_transition * bucket_weight);
    471   }
    472   return visits.size() * summed_visit_points / total_sampled_visits;
    473 }
    474 
    475 // static
    476 float ScoredHistoryMatch::GetFinalRelevancyScore(float topicality_score,
    477                                                  float frecency_score) {
    478   if (topicality_score == 0)
    479     return 0;
    480   // Here's how to interpret intermediate_score: Suppose the omnibox
    481   // has one input term.  Suppose we have a URL for which the omnibox
    482   // input term has a single URL hostname hit at a word boundary.  (This
    483   // implies topicality_score = 1.0.).  Then the intermediate_score for
    484   // this URL will depend entirely on the frecency_score with
    485   // this interpretation:
    486   // - a single typed visit more than three months ago, no other visits -> 0.2
    487   // - a visit every three days, no typed visits -> 0.706
    488   // - a visit every day, no typed visits -> 0.916
    489   // - a single typed visit yesterday, no other visits -> 2.0
    490   // - a typed visit once a week -> 11.77
    491   // - a typed visit every three days -> 14.12
    492   // - at least ten typed visits today -> 20.0 (maximum score)
    493   const float intermediate_score = topicality_score * frecency_score;
    494   // The below code maps intermediate_score to the range [0, 1399].
    495   // The score maxes out at 1400 (i.e., cannot beat a good inline result).
    496   if (intermediate_score <= 1) {
    497     // Linearly extrapolate between 0 and 1.5 so 0 has a score of 400
    498     // and 1.5 has a score of 600.
    499     const float slope = (600 - 400) / (1.5f - 0.0f);
    500     return 400 + slope * intermediate_score;
    501   }
    502   if (intermediate_score <= 12.0) {
    503     // Linearly extrapolate up to 12 so 12 has a score of 1300.
    504     const float slope = (1300 - 600) / (12.0f - 1.5f);
    505     return 600 + slope * (intermediate_score - 1.5);
    506   }
    507   // Linearly extrapolate so a score of 20 (or more) has a score of 1399.
    508   // (Scores above 20 are possible for URLs that have multiple term hits
    509   // in the URL and/or title and that are visited practically all
    510   // the time using typed visits.  We don't attempt to distinguish
    511   // between these very good results.)
    512   const float slope = (1399 - 1300) / (20.0f - 12.0f);
    513   return std::min(1399.0, 1300 + slope * (intermediate_score - 12.0));
    514 }
    515 
    516 void ScoredHistoryMatch::InitializeAlsoDoHUPLikeScoringFieldAndMaxScoreField() {
    517   also_do_hup_like_scoring = false;
    518   // When doing HUP-like scoring, don't allow a non-inlineable match
    519   // to beat the score of good inlineable matches.  This is a problem
    520   // because if a non-inlineable match ends up with the highest score
    521   // from HistoryQuick provider, all HistoryQuick matches get demoted
    522   // to non-inlineable scores (scores less than 1200).  Without
    523   // HUP-like-scoring, these results would actually come from the HUP
    524   // and not be demoted, thus outscoring the demoted HQP results.
    525   // When the HQP provides these, we need to clamp the non-inlineable
    526   // results to preserve this behavior.
    527   if (also_do_hup_like_scoring) {
    528     max_assigned_score_for_non_inlineable_matches =
    529         HistoryURLProvider::kScoreForBestInlineableResult - 1;
    530   }
    531 }
    532 
    533 }  // namespace history
    534