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