1 // Copyright 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/autocomplete/search_provider.h" 6 7 #include <algorithm> 8 #include <cmath> 9 10 #include "base/callback.h" 11 #include "base/i18n/break_iterator.h" 12 #include "base/i18n/case_conversion.h" 13 #include "base/i18n/icu_string_conversions.h" 14 #include "base/json/json_string_value_serializer.h" 15 #include "base/message_loop/message_loop.h" 16 #include "base/metrics/histogram.h" 17 #include "base/prefs/pref_service.h" 18 #include "base/strings/string16.h" 19 #include "base/strings/string_util.h" 20 #include "base/strings/utf_string_conversions.h" 21 #include "chrome/browser/autocomplete/autocomplete_classifier.h" 22 #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" 23 #include "chrome/browser/autocomplete/autocomplete_match.h" 24 #include "chrome/browser/autocomplete/autocomplete_provider_listener.h" 25 #include "chrome/browser/autocomplete/autocomplete_result.h" 26 #include "chrome/browser/autocomplete/keyword_provider.h" 27 #include "chrome/browser/autocomplete/url_prefix.h" 28 #include "chrome/browser/history/history_service.h" 29 #include "chrome/browser/history/history_service_factory.h" 30 #include "chrome/browser/history/in_memory_database.h" 31 #include "chrome/browser/metrics/variations/variations_http_header_provider.h" 32 #include "chrome/browser/omnibox/omnibox_field_trial.h" 33 #include "chrome/browser/profiles/profile.h" 34 #include "chrome/browser/search/search.h" 35 #include "chrome/browser/search_engines/template_url_prepopulate_data.h" 36 #include "chrome/browser/search_engines/template_url_service.h" 37 #include "chrome/browser/search_engines/template_url_service_factory.h" 38 #include "chrome/browser/ui/browser.h" 39 #include "chrome/browser/ui/browser_finder.h" 40 #include "chrome/browser/ui/browser_instant_controller.h" 41 #include "chrome/browser/ui/search/instant_controller.h" 42 #include "chrome/common/net/url_fixer_upper.h" 43 #include "chrome/common/pref_names.h" 44 #include "chrome/common/url_constants.h" 45 #include "grit/generated_resources.h" 46 #include "net/base/escape.h" 47 #include "net/base/load_flags.h" 48 #include "net/base/net_util.h" 49 #include "net/http/http_request_headers.h" 50 #include "net/http/http_response_headers.h" 51 #include "net/url_request/url_fetcher.h" 52 #include "net/url_request/url_request_status.h" 53 #include "ui/base/l10n/l10n_util.h" 54 #include "url/url_util.h" 55 56 57 // Helpers -------------------------------------------------------------------- 58 59 namespace { 60 61 // We keep track in a histogram how many suggest requests we send, how 62 // many suggest requests we invalidate (e.g., due to a user typing 63 // another character), and how many replies we receive. 64 // *** ADD NEW ENUMS AFTER ALL PREVIOUSLY DEFINED ONES! *** 65 // (excluding the end-of-list enum value) 66 // We do not want values of existing enums to change or else it screws 67 // up the statistics. 68 enum SuggestRequestsHistogramValue { 69 REQUEST_SENT = 1, 70 REQUEST_INVALIDATED, 71 REPLY_RECEIVED, 72 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE 73 }; 74 75 // The verbatim score for an input which is not an URL. 76 const int kNonURLVerbatimRelevance = 1300; 77 78 // Increments the appropriate value in the histogram by one. 79 void LogOmniboxSuggestRequest( 80 SuggestRequestsHistogramValue request_value) { 81 UMA_HISTOGRAM_ENUMERATION("Omnibox.SuggestRequests", request_value, 82 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE); 83 } 84 85 bool HasMultipleWords(const string16& text) { 86 base::i18n::BreakIterator i(text, base::i18n::BreakIterator::BREAK_WORD); 87 bool found_word = false; 88 if (i.Init()) { 89 while (i.Advance()) { 90 if (i.IsWord()) { 91 if (found_word) 92 return true; 93 found_word = true; 94 } 95 } 96 } 97 return false; 98 } 99 100 } // namespace 101 102 103 // SearchProvider::Providers -------------------------------------------------- 104 105 SearchProvider::Providers::Providers(TemplateURLService* template_url_service) 106 : template_url_service_(template_url_service) { 107 } 108 109 const TemplateURL* SearchProvider::Providers::GetDefaultProviderURL() const { 110 return default_provider_.empty() ? NULL : 111 template_url_service_->GetTemplateURLForKeyword(default_provider_); 112 } 113 114 const TemplateURL* SearchProvider::Providers::GetKeywordProviderURL() const { 115 return keyword_provider_.empty() ? NULL : 116 template_url_service_->GetTemplateURLForKeyword(keyword_provider_); 117 } 118 119 120 // SearchProvider::Result ----------------------------------------------------- 121 122 SearchProvider::Result::Result(bool from_keyword_provider, 123 int relevance, 124 bool relevance_from_server) 125 : from_keyword_provider_(from_keyword_provider), 126 relevance_(relevance), 127 relevance_from_server_(relevance_from_server) { 128 } 129 130 SearchProvider::Result::~Result() { 131 } 132 133 134 // SearchProvider::SuggestResult ---------------------------------------------- 135 136 SearchProvider::SuggestResult::SuggestResult(const string16& suggestion, 137 bool from_keyword_provider, 138 int relevance, 139 bool relevance_from_server) 140 : Result(from_keyword_provider, relevance, relevance_from_server), 141 suggestion_(suggestion) { 142 } 143 144 SearchProvider::SuggestResult::~SuggestResult() { 145 } 146 147 bool SearchProvider::SuggestResult::IsInlineable(const string16& input) const { 148 return StartsWith(suggestion_, input, false); 149 } 150 151 int SearchProvider::SuggestResult::CalculateRelevance( 152 const AutocompleteInput& input, 153 bool keyword_provider_requested) const { 154 if (!from_keyword_provider_ && keyword_provider_requested) 155 return 100; 156 return ((input.type() == AutocompleteInput::URL) ? 300 : 600); 157 } 158 159 160 // SearchProvider::NavigationResult ------------------------------------------- 161 162 SearchProvider::NavigationResult::NavigationResult( 163 const AutocompleteProvider& provider, 164 const GURL& url, 165 const string16& description, 166 bool from_keyword_provider, 167 int relevance, 168 bool relevance_from_server) 169 : Result(from_keyword_provider, relevance, relevance_from_server), 170 url_(url), 171 formatted_url_(AutocompleteInput::FormattedStringWithEquivalentMeaning( 172 url, provider.StringForURLDisplay(url, true, false))), 173 description_(description) { 174 DCHECK(url_.is_valid()); 175 } 176 177 SearchProvider::NavigationResult::~NavigationResult() { 178 } 179 180 bool SearchProvider::NavigationResult::IsInlineable( 181 const string16& input) const { 182 return URLPrefix::BestURLPrefix(formatted_url_, input) != NULL; 183 } 184 185 int SearchProvider::NavigationResult::CalculateRelevance( 186 const AutocompleteInput& input, 187 bool keyword_provider_requested) const { 188 return (from_keyword_provider_ || !keyword_provider_requested) ? 800 : 150; 189 } 190 191 192 // SearchProvider::CompareScoredResults --------------------------------------- 193 194 class SearchProvider::CompareScoredResults { 195 public: 196 bool operator()(const Result& a, const Result& b) { 197 // Sort in descending relevance order. 198 return a.relevance() > b.relevance(); 199 } 200 }; 201 202 203 // SearchProvider::Results ---------------------------------------------------- 204 205 SearchProvider::Results::Results() : verbatim_relevance(-1) { 206 } 207 208 SearchProvider::Results::~Results() { 209 } 210 211 void SearchProvider::Results::Clear() { 212 suggest_results.clear(); 213 navigation_results.clear(); 214 verbatim_relevance = -1; 215 } 216 217 bool SearchProvider::Results::HasServerProvidedScores() const { 218 if (verbatim_relevance >= 0) 219 return true; 220 221 // Right now either all results of one type will be server-scored or they will 222 // all be locally scored, but in case we change this later, we'll just check 223 // them all. 224 for (SuggestResults::const_iterator i(suggest_results.begin()); 225 i != suggest_results.end(); ++i) { 226 if (i->relevance_from_server()) 227 return true; 228 } 229 for (NavigationResults::const_iterator i(navigation_results.begin()); 230 i != navigation_results.end(); ++i) { 231 if (i->relevance_from_server()) 232 return true; 233 } 234 235 return false; 236 } 237 238 239 // SearchProvider ------------------------------------------------------------- 240 241 // static 242 const int SearchProvider::kDefaultProviderURLFetcherID = 1; 243 const int SearchProvider::kKeywordProviderURLFetcherID = 2; 244 int SearchProvider::kMinimumTimeBetweenSuggestQueriesMs = 100; 245 const char SearchProvider::kRelevanceFromServerKey[] = "relevance_from_server"; 246 const char SearchProvider::kTrue[] = "true"; 247 const char SearchProvider::kFalse[] = "false"; 248 249 SearchProvider::SearchProvider(AutocompleteProviderListener* listener, 250 Profile* profile) 251 : AutocompleteProvider(listener, profile, 252 AutocompleteProvider::TYPE_SEARCH), 253 providers_(TemplateURLServiceFactory::GetForProfile(profile)), 254 suggest_results_pending_(0), 255 field_trial_triggered_(false), 256 field_trial_triggered_in_session_(false) { 257 } 258 259 // static 260 AutocompleteMatch SearchProvider::CreateSearchSuggestion( 261 AutocompleteProvider* autocomplete_provider, 262 int relevance, 263 AutocompleteMatch::Type type, 264 const TemplateURL* template_url, 265 const string16& query_string, 266 const string16& input_text, 267 const AutocompleteInput& input, 268 bool is_keyword, 269 int accepted_suggestion, 270 int omnibox_start_margin, 271 bool append_extra_query_params) { 272 AutocompleteMatch match(autocomplete_provider, relevance, false, type); 273 274 if (!template_url) 275 return match; 276 match.keyword = template_url->keyword(); 277 278 match.contents.assign(query_string); 279 // We do intra-string highlighting for suggestions - the suggested segment 280 // will be highlighted, e.g. for input_text = "you" the suggestion may be 281 // "youtube", so we'll bold the "tube" section: you*tube*. 282 if (input_text != query_string) { 283 size_t input_position = match.contents.find(input_text); 284 if (input_position == string16::npos) { 285 // The input text is not a substring of the query string, e.g. input 286 // text is "slasdot" and the query string is "slashdot", so we bold the 287 // whole thing. 288 match.contents_class.push_back( 289 ACMatchClassification(0, ACMatchClassification::MATCH)); 290 } else { 291 // TODO(beng): ACMatchClassification::MATCH now seems to just mean 292 // "bold" this. Consider modifying the terminology. 293 // We don't iterate over the string here annotating all matches because 294 // it looks odd to have every occurrence of a substring that may be as 295 // short as a single character highlighted in a query suggestion result, 296 // e.g. for input text "s" and query string "southwest airlines", it 297 // looks odd if both the first and last s are highlighted. 298 if (input_position != 0) { 299 match.contents_class.push_back( 300 ACMatchClassification(0, ACMatchClassification::MATCH)); 301 } 302 match.contents_class.push_back( 303 ACMatchClassification(input_position, ACMatchClassification::NONE)); 304 size_t next_fragment_position = input_position + input_text.length(); 305 if (next_fragment_position < query_string.length()) { 306 match.contents_class.push_back( 307 ACMatchClassification(next_fragment_position, 308 ACMatchClassification::MATCH)); 309 } 310 } 311 } else { 312 // Otherwise, |match| is a verbatim (what-you-typed) match, either for the 313 // default provider or a keyword search provider. 314 match.contents_class.push_back( 315 ACMatchClassification(0, ACMatchClassification::NONE)); 316 match.allowed_to_be_default_match = true; 317 } 318 319 // When the user forced a query, we need to make sure all the fill_into_edit 320 // values preserve that property. Otherwise, if the user starts editing a 321 // suggestion, non-Search results will suddenly appear. 322 if (input.type() == AutocompleteInput::FORCED_QUERY) 323 match.fill_into_edit.assign(ASCIIToUTF16("?")); 324 if (is_keyword) 325 match.fill_into_edit.append(match.keyword + char16(' ')); 326 if (!input.prevent_inline_autocomplete() && 327 StartsWith(query_string, input_text, false)) { 328 match.inline_autocompletion = query_string.substr(input_text.length()); 329 match.allowed_to_be_default_match = true; 330 } 331 match.fill_into_edit.append(query_string); 332 333 const TemplateURLRef& search_url = template_url->url_ref(); 334 DCHECK(search_url.SupportsReplacement()); 335 match.search_terms_args.reset( 336 new TemplateURLRef::SearchTermsArgs(query_string)); 337 match.search_terms_args->original_query = input_text; 338 match.search_terms_args->accepted_suggestion = accepted_suggestion; 339 match.search_terms_args->omnibox_start_margin = omnibox_start_margin; 340 match.search_terms_args->append_extra_query_params = 341 append_extra_query_params; 342 // This is the destination URL sans assisted query stats. This must be set 343 // so the AutocompleteController can properly de-dupe; the controller will 344 // eventually overwrite it before it reaches the user. 345 match.destination_url = 346 GURL(search_url.ReplaceSearchTerms(*match.search_terms_args.get())); 347 348 // Search results don't look like URLs. 349 match.transition = is_keyword ? 350 content::PAGE_TRANSITION_KEYWORD : content::PAGE_TRANSITION_GENERATED; 351 352 return match; 353 } 354 355 void SearchProvider::AddProviderInfo(ProvidersInfo* provider_info) const { 356 provider_info->push_back(metrics::OmniboxEventProto_ProviderInfo()); 357 metrics::OmniboxEventProto_ProviderInfo& new_entry = provider_info->back(); 358 new_entry.set_provider(AsOmniboxEventProviderType()); 359 new_entry.set_provider_done(done_); 360 std::vector<uint32> field_trial_hashes; 361 OmniboxFieldTrial::GetActiveSuggestFieldTrialHashes(&field_trial_hashes); 362 for (size_t i = 0; i < field_trial_hashes.size(); ++i) { 363 if (field_trial_triggered_) 364 new_entry.mutable_field_trial_triggered()->Add(field_trial_hashes[i]); 365 if (field_trial_triggered_in_session_) { 366 new_entry.mutable_field_trial_triggered_in_session()->Add( 367 field_trial_hashes[i]); 368 } 369 } 370 } 371 372 void SearchProvider::ResetSession() { 373 field_trial_triggered_in_session_ = false; 374 } 375 376 SearchProvider::~SearchProvider() { 377 } 378 379 // static 380 void SearchProvider::RemoveStaleResults(const string16& input, 381 int verbatim_relevance, 382 SuggestResults* suggest_results, 383 NavigationResults* navigation_results) { 384 DCHECK_GE(verbatim_relevance, 0); 385 // Keep pointers to the head of (the highest scoring elements of) 386 // |suggest_results| and |navigation_results|. Iterate down the lists 387 // removing non-inlineable results in order of decreasing relevance 388 // scores. Stop when the highest scoring element among those remaining 389 // is inlineable or the element is less than |verbatim_relevance|. 390 // This allows non-inlineable lower-scoring results to remain 391 // because (i) they are guaranteed to not be inlined and (ii) 392 // letting them remain reduces visual jank. For instance, as the 393 // user types the mis-spelled query "fpobar" (for foobar), the 394 // suggestion "foobar" will be suggested on every keystroke. If the 395 // SearchProvider always removes all non-inlineable results, the user will 396 // see visual jitter/jank as the result disappears and re-appears moments 397 // later as the suggest server returns results. 398 SuggestResults::iterator sug_it = suggest_results->begin(); 399 NavigationResults::iterator nav_it = navigation_results->begin(); 400 while ((sug_it != suggest_results->end()) || 401 (nav_it != navigation_results->end())) { 402 const int sug_rel = 403 (sug_it != suggest_results->end()) ? sug_it->relevance() : -1; 404 const int nav_rel = 405 (nav_it != navigation_results->end()) ? nav_it->relevance() : -1; 406 if (std::max(sug_rel, nav_rel) < verbatim_relevance) 407 break; 408 if (sug_rel > nav_rel) { 409 // The current top result is a search suggestion. 410 if (sug_it->IsInlineable(input)) 411 break; 412 sug_it = suggest_results->erase(sug_it); 413 } else if (sug_rel == nav_rel) { 414 // Have both results and they're tied. 415 const bool sug_inlineable = sug_it->IsInlineable(input); 416 const bool nav_inlineable = nav_it->IsInlineable(input); 417 if (!sug_inlineable) 418 sug_it = suggest_results->erase(sug_it); 419 if (!nav_inlineable) 420 nav_it = navigation_results->erase(nav_it); 421 if (sug_inlineable || nav_inlineable) 422 break; 423 } else { 424 // The current top result is a navigational suggestion. 425 if (nav_it->IsInlineable(input)) 426 break; 427 nav_it = navigation_results->erase(nav_it); 428 } 429 } 430 } 431 432 // static 433 int SearchProvider::CalculateRelevanceForKeywordVerbatim( 434 AutocompleteInput::Type type, 435 bool prefer_keyword) { 436 // This function is responsible for scoring verbatim query matches 437 // for non-extension keywords. KeywordProvider::CalculateRelevance() 438 // scores verbatim query matches for extension keywords, as well as 439 // for keyword matches (i.e., suggestions of a keyword itself, not a 440 // suggestion of a query on a keyword search engine). These two 441 // functions are currently in sync, but there's no reason we 442 // couldn't decide in the future to score verbatim matches 443 // differently for extension and non-extension keywords. If you 444 // make such a change, however, you should update this comment to 445 // describe it, so it's clear why the functions diverge. 446 if (prefer_keyword) 447 return 1500; 448 return (type == AutocompleteInput::QUERY) ? 1450 : 1100; 449 } 450 451 void SearchProvider::Start(const AutocompleteInput& input, 452 bool minimal_changes) { 453 // Do our best to load the model as early as possible. This will reduce 454 // odds of having the model not ready when really needed (a non-empty input). 455 TemplateURLService* model = providers_.template_url_service(); 456 DCHECK(model); 457 model->Load(); 458 459 matches_.clear(); 460 field_trial_triggered_ = false; 461 462 // Can't return search/suggest results for bogus input or without a profile. 463 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) { 464 Stop(false); 465 return; 466 } 467 468 keyword_input_ = input; 469 const TemplateURL* keyword_provider = 470 KeywordProvider::GetSubstitutingTemplateURLForInput(model, 471 &keyword_input_); 472 if (keyword_provider == NULL) 473 keyword_input_.Clear(); 474 else if (keyword_input_.text().empty()) 475 keyword_provider = NULL; 476 477 const TemplateURL* default_provider = model->GetDefaultSearchProvider(); 478 if (default_provider && !default_provider->SupportsReplacement()) 479 default_provider = NULL; 480 481 if (keyword_provider == default_provider) 482 default_provider = NULL; // No use in querying the same provider twice. 483 484 if (!default_provider && !keyword_provider) { 485 // No valid providers. 486 Stop(false); 487 return; 488 } 489 490 // If we're still running an old query but have since changed the query text 491 // or the providers, abort the query. 492 string16 default_provider_keyword(default_provider ? 493 default_provider->keyword() : string16()); 494 string16 keyword_provider_keyword(keyword_provider ? 495 keyword_provider->keyword() : string16()); 496 if (!minimal_changes || 497 !providers_.equal(default_provider_keyword, keyword_provider_keyword)) { 498 // Cancel any in-flight suggest requests. 499 if (!done_) 500 Stop(false); 501 } 502 503 providers_.set(default_provider_keyword, keyword_provider_keyword); 504 505 if (input.text().empty()) { 506 // User typed "?" alone. Give them a placeholder result indicating what 507 // this syntax does. 508 if (default_provider) { 509 AutocompleteMatch match; 510 match.provider = this; 511 match.contents.assign(l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE)); 512 match.contents_class.push_back( 513 ACMatchClassification(0, ACMatchClassification::NONE)); 514 match.keyword = providers_.default_provider(); 515 match.allowed_to_be_default_match = true; 516 matches_.push_back(match); 517 } 518 Stop(false); 519 return; 520 } 521 522 input_ = input; 523 524 DoHistoryQuery(minimal_changes); 525 StartOrStopSuggestQuery(minimal_changes); 526 UpdateMatches(); 527 } 528 529 void SearchProvider::Stop(bool clear_cached_results) { 530 StopSuggest(); 531 done_ = true; 532 533 if (clear_cached_results) 534 ClearAllResults(); 535 } 536 537 void SearchProvider::OnURLFetchComplete(const net::URLFetcher* source) { 538 DCHECK(!done_); 539 suggest_results_pending_--; 540 LogOmniboxSuggestRequest(REPLY_RECEIVED); 541 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. 542 const net::HttpResponseHeaders* const response_headers = 543 source->GetResponseHeaders(); 544 std::string json_data; 545 source->GetResponseAsString(&json_data); 546 // JSON is supposed to be UTF-8, but some suggest service providers send JSON 547 // files in non-UTF-8 encodings. The actual encoding is usually specified in 548 // the Content-Type header field. 549 if (response_headers) { 550 std::string charset; 551 if (response_headers->GetCharset(&charset)) { 552 string16 data_16; 553 // TODO(jungshik): Switch to CodePageToUTF8 after it's added. 554 if (base::CodepageToUTF16(json_data, charset.c_str(), 555 base::OnStringConversionError::FAIL, 556 &data_16)) 557 json_data = UTF16ToUTF8(data_16); 558 } 559 } 560 561 const bool is_keyword = (source == keyword_fetcher_.get()); 562 // Ensure the request succeeded and that the provider used is still available. 563 // A verbatim match cannot be generated without this provider, causing errors. 564 const bool request_succeeded = 565 source->GetStatus().is_success() && (source->GetResponseCode() == 200) && 566 (is_keyword ? 567 providers_.GetKeywordProviderURL() : 568 providers_.GetDefaultProviderURL()); 569 570 // Record response time for suggest requests sent to Google. We care 571 // only about the common case: the Google default provider used in 572 // non-keyword mode. 573 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); 574 if (!is_keyword && default_url && 575 (TemplateURLPrepopulateData::GetEngineType(*default_url) == 576 SEARCH_ENGINE_GOOGLE)) { 577 const base::TimeDelta elapsed_time = 578 base::TimeTicks::Now() - time_suggest_request_sent_; 579 if (request_succeeded) { 580 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Success.GoogleResponseTime", 581 elapsed_time); 582 } else { 583 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Failure.GoogleResponseTime", 584 elapsed_time); 585 } 586 } 587 588 bool results_updated = false; 589 if (request_succeeded) { 590 JSONStringValueSerializer deserializer(json_data); 591 deserializer.set_allow_trailing_comma(true); 592 scoped_ptr<Value> data(deserializer.Deserialize(NULL, NULL)); 593 results_updated = data.get() && ParseSuggestResults(data.get(), is_keyword); 594 } 595 596 UpdateMatches(); 597 if (done_ || results_updated) 598 listener_->OnProviderUpdate(results_updated); 599 } 600 601 void SearchProvider::Run() { 602 // Start a new request with the current input. 603 suggest_results_pending_ = 0; 604 time_suggest_request_sent_ = base::TimeTicks::Now(); 605 606 default_fetcher_.reset(CreateSuggestFetcher(kDefaultProviderURLFetcherID, 607 providers_.GetDefaultProviderURL(), input_)); 608 keyword_fetcher_.reset(CreateSuggestFetcher(kKeywordProviderURLFetcherID, 609 providers_.GetKeywordProviderURL(), keyword_input_)); 610 611 // Both the above can fail if the providers have been modified or deleted 612 // since the query began. 613 if (suggest_results_pending_ == 0) { 614 UpdateDone(); 615 // We only need to update the listener if we're actually done. 616 if (done_) 617 listener_->OnProviderUpdate(false); 618 } 619 } 620 621 void SearchProvider::DoHistoryQuery(bool minimal_changes) { 622 // The history query results are synchronous, so if minimal_changes is true, 623 // we still have the last results and don't need to do anything. 624 if (minimal_changes) 625 return; 626 627 keyword_history_results_.clear(); 628 default_history_results_.clear(); 629 630 if (OmniboxFieldTrial::SearchHistoryDisable( 631 input_.current_page_classification())) 632 return; 633 634 HistoryService* const history_service = 635 HistoryServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); 636 history::URLDatabase* url_db = history_service ? 637 history_service->InMemoryDatabase() : NULL; 638 if (!url_db) 639 return; 640 641 // Request history for both the keyword and default provider. We grab many 642 // more matches than we'll ultimately clamp to so that if there are several 643 // recent multi-word matches who scores are lowered (see 644 // AddHistoryResultsToMap()), they won't crowd out older, higher-scoring 645 // matches. Note that this doesn't fix the problem entirely, but merely 646 // limits it to cases with a very large number of such multi-word matches; for 647 // now, this seems OK compared with the complexity of a real fix, which would 648 // require multiple searches and tracking of "single- vs. multi-word" in the 649 // database. 650 int num_matches = kMaxMatches * 5; 651 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); 652 if (default_url) { 653 url_db->GetMostRecentKeywordSearchTerms(default_url->id(), input_.text(), 654 num_matches, &default_history_results_); 655 } 656 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); 657 if (keyword_url) { 658 url_db->GetMostRecentKeywordSearchTerms(keyword_url->id(), 659 keyword_input_.text(), num_matches, &keyword_history_results_); 660 } 661 } 662 663 void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) { 664 if (!IsQuerySuitableForSuggest()) { 665 StopSuggest(); 666 ClearAllResults(); 667 return; 668 } 669 670 // For the minimal_changes case, if we finished the previous query and still 671 // have its results, or are allowed to keep running it, just do that, rather 672 // than starting a new query. 673 if (minimal_changes && 674 (!default_results_.suggest_results.empty() || 675 !default_results_.navigation_results.empty() || 676 !keyword_results_.suggest_results.empty() || 677 !keyword_results_.navigation_results.empty() || 678 (!done_ && 679 input_.matches_requested() == AutocompleteInput::ALL_MATCHES))) 680 return; 681 682 // We can't keep running any previous query, so halt it. 683 StopSuggest(); 684 685 // Remove existing results that cannot inline autocomplete the new input. 686 RemoveAllStaleResults(); 687 688 // We can't start a new query if we're only allowed synchronous results. 689 if (input_.matches_requested() != AutocompleteInput::ALL_MATCHES) 690 return; 691 692 // To avoid flooding the suggest server, don't send a query until at 693 // least 100 ms since the last query. 694 base::TimeTicks next_suggest_time(time_suggest_request_sent_ + 695 base::TimeDelta::FromMilliseconds(kMinimumTimeBetweenSuggestQueriesMs)); 696 base::TimeTicks now(base::TimeTicks::Now()); 697 if (now >= next_suggest_time) { 698 Run(); 699 return; 700 } 701 timer_.Start(FROM_HERE, next_suggest_time - now, this, &SearchProvider::Run); 702 } 703 704 bool SearchProvider::IsQuerySuitableForSuggest() const { 705 // Don't run Suggest in incognito mode, if the engine doesn't support it, or 706 // if the user has disabled it. 707 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); 708 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); 709 if (profile_->IsOffTheRecord() || 710 ((!default_url || default_url->suggestions_url().empty()) && 711 (!keyword_url || keyword_url->suggestions_url().empty())) || 712 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled)) 713 return false; 714 715 // If the input type might be a URL, we take extra care so that private data 716 // isn't sent to the server. 717 718 // FORCED_QUERY means the user is explicitly asking us to search for this, so 719 // we assume it isn't a URL and/or there isn't private data. 720 if (input_.type() == AutocompleteInput::FORCED_QUERY) 721 return true; 722 723 // Next we check the scheme. If this is UNKNOWN/URL with a scheme that isn't 724 // http/https/ftp, we shouldn't send it. Sending things like file: and data: 725 // is both a waste of time and a disclosure of potentially private, local 726 // data. Other "schemes" may actually be usernames, and we don't want to send 727 // passwords. If the scheme is OK, we still need to check other cases below. 728 // If this is QUERY, then the presence of these schemes means the user 729 // explicitly typed one, and thus this is probably a URL that's being entered 730 // and happens to currently be invalid -- in which case we again want to run 731 // our checks below. Other QUERY cases are less likely to be URLs and thus we 732 // assume we're OK. 733 if (!LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpScheme) && 734 !LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpsScheme) && 735 !LowerCaseEqualsASCII(input_.scheme(), chrome::kFtpScheme)) 736 return (input_.type() == AutocompleteInput::QUERY); 737 738 // Don't send URLs with usernames, queries or refs. Some of these are 739 // private, and the Suggest server is unlikely to have any useful results 740 // for any of them. Also don't send URLs with ports, as we may initially 741 // think that a username + password is a host + port (and we don't want to 742 // send usernames/passwords), and even if the port really is a port, the 743 // server is once again unlikely to have and useful results. 744 // Note that we only block based on refs if the input is URL-typed, as search 745 // queries can legitimately have #s in them which the URL parser 746 // overaggressively categorizes as a url with a ref. 747 const url_parse::Parsed& parts = input_.parts(); 748 if (parts.username.is_nonempty() || parts.port.is_nonempty() || 749 parts.query.is_nonempty() || 750 (parts.ref.is_nonempty() && (input_.type() == AutocompleteInput::URL))) 751 return false; 752 753 // Don't send anything for https except the hostname. Hostnames are OK 754 // because they are visible when the TCP connection is established, but the 755 // specific path may reveal private information. 756 if (LowerCaseEqualsASCII(input_.scheme(), chrome::kHttpsScheme) && 757 parts.path.is_nonempty()) 758 return false; 759 760 return true; 761 } 762 763 void SearchProvider::StopSuggest() { 764 // Increment the appropriate field in the histogram by the number of 765 // pending requests that were invalidated. 766 for (int i = 0; i < suggest_results_pending_; i++) 767 LogOmniboxSuggestRequest(REQUEST_INVALIDATED); 768 suggest_results_pending_ = 0; 769 timer_.Stop(); 770 // Stop any in-progress URL fetches. 771 keyword_fetcher_.reset(); 772 default_fetcher_.reset(); 773 } 774 775 void SearchProvider::ClearAllResults() { 776 keyword_results_.Clear(); 777 default_results_.Clear(); 778 } 779 780 void SearchProvider::RemoveAllStaleResults() { 781 // In theory it would be better to run an algorithm like that in 782 // RemoveStaleResults(...) below that uses all four results lists 783 // and both verbatim scores at once. However, that will be much 784 // more complicated for little obvious gain. For code simplicity 785 // and ease in reasoning about the invariants involved, this code 786 // removes stales results from the keyword provider and default 787 // provider independently. 788 RemoveStaleResults(input_.text(), GetVerbatimRelevance(NULL), 789 &default_results_.suggest_results, 790 &default_results_.navigation_results); 791 if (!keyword_input_.text().empty()) { 792 RemoveStaleResults(keyword_input_.text(), GetKeywordVerbatimRelevance(NULL), 793 &keyword_results_.suggest_results, 794 &keyword_results_.navigation_results); 795 } else { 796 // User is either in keyword mode with a blank input or out of 797 // keyword mode entirely. 798 keyword_results_.Clear(); 799 } 800 } 801 802 void SearchProvider::ApplyCalculatedRelevance() { 803 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); 804 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); 805 ApplyCalculatedNavigationRelevance(&keyword_results_.navigation_results); 806 ApplyCalculatedNavigationRelevance(&default_results_.navigation_results); 807 default_results_.verbatim_relevance = -1; 808 keyword_results_.verbatim_relevance = -1; 809 } 810 811 void SearchProvider::ApplyCalculatedSuggestRelevance(SuggestResults* list) { 812 for (size_t i = 0; i < list->size(); ++i) { 813 SuggestResult& result = (*list)[i]; 814 result.set_relevance( 815 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + 816 (list->size() - i - 1)); 817 result.set_relevance_from_server(false); 818 } 819 } 820 821 void SearchProvider::ApplyCalculatedNavigationRelevance( 822 NavigationResults* list) { 823 for (size_t i = 0; i < list->size(); ++i) { 824 NavigationResult& result = (*list)[i]; 825 result.set_relevance( 826 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + 827 (list->size() - i - 1)); 828 result.set_relevance_from_server(false); 829 } 830 } 831 832 net::URLFetcher* SearchProvider::CreateSuggestFetcher( 833 int id, 834 const TemplateURL* template_url, 835 const AutocompleteInput& input) { 836 if (!template_url || template_url->suggestions_url().empty()) 837 return NULL; 838 839 // Bail if the suggestion URL is invalid with the given replacements. 840 TemplateURLRef::SearchTermsArgs search_term_args(input.text()); 841 search_term_args.cursor_position = input.cursor_position(); 842 search_term_args.page_classification = input.current_page_classification(); 843 GURL suggest_url(template_url->suggestions_url_ref().ReplaceSearchTerms( 844 search_term_args)); 845 if (!suggest_url.is_valid()) 846 return NULL; 847 848 suggest_results_pending_++; 849 LogOmniboxSuggestRequest(REQUEST_SENT); 850 851 net::URLFetcher* fetcher = 852 net::URLFetcher::Create(id, suggest_url, net::URLFetcher::GET, this); 853 fetcher->SetRequestContext(profile_->GetRequestContext()); 854 fetcher->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); 855 // Add Chrome experiment state to the request headers. 856 net::HttpRequestHeaders headers; 857 chrome_variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( 858 fetcher->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers); 859 fetcher->SetExtraRequestHeaders(headers.ToString()); 860 fetcher->Start(); 861 return fetcher; 862 } 863 864 bool SearchProvider::ParseSuggestResults(Value* root_val, bool is_keyword) { 865 string16 query; 866 ListValue* root_list = NULL; 867 ListValue* results_list = NULL; 868 const string16& input_text = 869 is_keyword ? keyword_input_.text() : input_.text(); 870 if (!root_val->GetAsList(&root_list) || !root_list->GetString(0, &query) || 871 (query != input_text) || !root_list->GetList(1, &results_list)) 872 return false; 873 874 // 3rd element: Description list. 875 ListValue* descriptions = NULL; 876 root_list->GetList(2, &descriptions); 877 878 // 4th element: Disregard the query URL list for now. 879 880 // Reset suggested relevance information from the default provider. 881 Results* results = is_keyword ? &keyword_results_ : &default_results_; 882 results->verbatim_relevance = -1; 883 884 // 5th element: Optional key-value pairs from the Suggest server. 885 ListValue* types = NULL; 886 ListValue* relevances = NULL; 887 DictionaryValue* extras = NULL; 888 if (root_list->GetDictionary(4, &extras)) { 889 extras->GetList("google:suggesttype", &types); 890 891 // Discard this list if its size does not match that of the suggestions. 892 if (extras->GetList("google:suggestrelevance", &relevances) && 893 relevances->GetSize() != results_list->GetSize()) 894 relevances = NULL; 895 extras->GetInteger("google:verbatimrelevance", 896 &results->verbatim_relevance); 897 898 // Check if the active suggest field trial (if any) has triggered either 899 // for the default provider or keyword provider. 900 bool triggered = false; 901 extras->GetBoolean("google:fieldtrialtriggered", &triggered); 902 field_trial_triggered_ |= triggered; 903 field_trial_triggered_in_session_ |= triggered; 904 } 905 906 // Clear the previous results now that new results are available. 907 results->suggest_results.clear(); 908 results->navigation_results.clear(); 909 910 string16 result, title; 911 std::string type; 912 int relevance = -1; 913 for (size_t index = 0; results_list->GetString(index, &result); ++index) { 914 // Google search may return empty suggestions for weird input characters, 915 // they make no sense at all and can cause problems in our code. 916 if (result.empty()) 917 continue; 918 919 // Apply valid suggested relevance scores; discard invalid lists. 920 if (relevances != NULL && !relevances->GetInteger(index, &relevance)) 921 relevances = NULL; 922 if (types && types->GetString(index, &type) && (type == "NAVIGATION")) { 923 // Do not blindly trust the URL coming from the server to be valid. 924 GURL url(URLFixerUpper::FixupURL(UTF16ToUTF8(result), std::string())); 925 if (url.is_valid()) { 926 if (descriptions != NULL) 927 descriptions->GetString(index, &title); 928 results->navigation_results.push_back(NavigationResult( 929 *this, url, title, is_keyword, relevance, true)); 930 } 931 } else { 932 // TODO(kochi): Improve calculator result presentation. 933 results->suggest_results.push_back( 934 SuggestResult(result, is_keyword, relevance, true)); 935 } 936 } 937 938 // Apply calculated relevance scores if a valid list was not provided. 939 if (relevances == NULL) { 940 ApplyCalculatedSuggestRelevance(&results->suggest_results); 941 ApplyCalculatedNavigationRelevance(&results->navigation_results); 942 } 943 // Keep the result lists sorted. 944 const CompareScoredResults comparator = CompareScoredResults(); 945 std::stable_sort(results->suggest_results.begin(), 946 results->suggest_results.end(), 947 comparator); 948 std::stable_sort(results->navigation_results.begin(), 949 results->navigation_results.end(), 950 comparator); 951 return true; 952 } 953 954 void SearchProvider::ConvertResultsToAutocompleteMatches() { 955 // Convert all the results to matches and add them to a map, so we can keep 956 // the most relevant match for each result. 957 MatchMap map; 958 const base::Time no_time; 959 int did_not_accept_keyword_suggestion = 960 keyword_results_.suggest_results.empty() ? 961 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 962 TemplateURLRef::NO_SUGGESTION_CHOSEN; 963 964 bool relevance_from_server; 965 int verbatim_relevance = GetVerbatimRelevance(&relevance_from_server); 966 int did_not_accept_default_suggestion = 967 default_results_.suggest_results.empty() ? 968 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 969 TemplateURLRef::NO_SUGGESTION_CHOSEN; 970 if (verbatim_relevance > 0) { 971 AddMatchToMap(input_.text(), input_.text(), verbatim_relevance, 972 relevance_from_server, 973 AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, 974 did_not_accept_default_suggestion, false, &map); 975 } 976 if (!keyword_input_.text().empty()) { 977 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); 978 // We only create the verbatim search query match for a keyword 979 // if it's not an extension keyword. Extension keywords are handled 980 // in KeywordProvider::Start(). (Extensions are complicated...) 981 // Note: in this provider, SEARCH_OTHER_ENGINE must correspond 982 // to the keyword verbatim search query. Do not create other matches 983 // of type SEARCH_OTHER_ENGINE. 984 if (keyword_url && !keyword_url->IsExtensionKeyword()) { 985 bool keyword_relevance_from_server; 986 const int keyword_verbatim_relevance = 987 GetKeywordVerbatimRelevance(&keyword_relevance_from_server); 988 if (keyword_verbatim_relevance > 0) { 989 AddMatchToMap(keyword_input_.text(), keyword_input_.text(), 990 keyword_verbatim_relevance, keyword_relevance_from_server, 991 AutocompleteMatchType::SEARCH_OTHER_ENGINE, 992 did_not_accept_keyword_suggestion, true, &map); 993 } 994 } 995 } 996 AddHistoryResultsToMap(keyword_history_results_, true, 997 did_not_accept_keyword_suggestion, &map); 998 AddHistoryResultsToMap(default_history_results_, false, 999 did_not_accept_default_suggestion, &map); 1000 1001 AddSuggestResultsToMap(keyword_results_.suggest_results, &map); 1002 AddSuggestResultsToMap(default_results_.suggest_results, &map); 1003 1004 ACMatches matches; 1005 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) 1006 matches.push_back(i->second); 1007 1008 AddNavigationResultsToMatches(keyword_results_.navigation_results, &matches); 1009 AddNavigationResultsToMatches(default_results_.navigation_results, &matches); 1010 1011 // Now add the most relevant matches to |matches_|. We take up to kMaxMatches 1012 // suggest/navsuggest matches, regardless of origin. If Instant Extended is 1013 // enabled and we have server-provided (and thus hopefully more accurate) 1014 // scores for some suggestions, we allow more of those, until we reach 1015 // AutocompleteResult::kMaxMatches total matches (that is, enough to fill the 1016 // whole popup). 1017 // 1018 // We will always return any verbatim matches, no matter how we obtained their 1019 // scores, unless we have already accepted AutocompleteResult::kMaxMatches 1020 // higher-scoring matches under the conditions above. 1021 std::sort(matches.begin(), matches.end(), &AutocompleteMatch::MoreRelevant); 1022 matches_.clear(); 1023 1024 size_t num_suggestions = 0; 1025 for (ACMatches::const_iterator i(matches.begin()); 1026 (i != matches.end()) && 1027 (matches_.size() < AutocompleteResult::kMaxMatches); 1028 ++i) { 1029 // SEARCH_OTHER_ENGINE is only used in the SearchProvider for the keyword 1030 // verbatim result, so this condition basically means "if this match is a 1031 // suggestion of some sort". 1032 if ((i->type != AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED) && 1033 (i->type != AutocompleteMatchType::SEARCH_OTHER_ENGINE)) { 1034 // If we've already hit the limit on non-server-scored suggestions, and 1035 // this isn't a server-scored suggestion we can add, skip it. 1036 if ((num_suggestions >= kMaxMatches) && 1037 (!chrome::IsInstantExtendedAPIEnabled() || 1038 (i->GetAdditionalInfo(kRelevanceFromServerKey) != kTrue))) { 1039 continue; 1040 } 1041 1042 ++num_suggestions; 1043 } 1044 1045 matches_.push_back(*i); 1046 } 1047 } 1048 1049 bool SearchProvider::IsTopMatchNavigationInKeywordMode() const { 1050 return (!providers_.keyword_provider().empty() && 1051 (matches_.front().type == AutocompleteMatchType::NAVSUGGEST)); 1052 } 1053 1054 bool SearchProvider::IsTopMatchScoreTooLow() const { 1055 // Here we use CalculateRelevanceForVerbatimIgnoringKeywordModeState() 1056 // rather than CalculateRelevanceForVerbatim() because the latter returns 1057 // a very low score (250) if keyword mode is active. This is because 1058 // when keyword mode is active the user probably wants the keyword matches, 1059 // not matches from the default provider. Hence, we use the version of 1060 // the function that ignores whether keyword mode is active. This allows 1061 // SearchProvider to maintain its contract with the AutocompleteController 1062 // that it will always provide an inlineable match with a reasonable 1063 // score. 1064 return matches_.front().relevance < 1065 CalculateRelevanceForVerbatimIgnoringKeywordModeState(); 1066 } 1067 1068 bool SearchProvider::IsTopMatchSearchWithURLInput() const { 1069 return input_.type() == AutocompleteInput::URL && 1070 matches_.front().relevance > CalculateRelevanceForVerbatim() && 1071 matches_.front().type != AutocompleteMatchType::NAVSUGGEST; 1072 } 1073 1074 bool SearchProvider::HasValidDefaultMatch( 1075 bool autocomplete_result_will_reorder_for_default_match) const { 1076 // One of the SearchProvider matches may need to be the overall default. If 1077 // AutocompleteResult is allowed to reorder matches, this means we simply 1078 // need at least one match in the list to be |allowed_to_be_default_match|. 1079 // If no reordering is possible, however, then our first match needs to have 1080 // this flag. 1081 for (ACMatches::const_iterator it = matches_.begin(); it != matches_.end(); 1082 ++it) { 1083 if (it->allowed_to_be_default_match) 1084 return true; 1085 if (!autocomplete_result_will_reorder_for_default_match) 1086 return false; 1087 } 1088 return false; 1089 } 1090 1091 void SearchProvider::UpdateMatches() { 1092 ConvertResultsToAutocompleteMatches(); 1093 1094 // Check constraints that may be violated by suggested relevances. 1095 if (!matches_.empty() && 1096 (default_results_.HasServerProvidedScores() || 1097 keyword_results_.HasServerProvidedScores())) { 1098 // These blocks attempt to repair undesirable behavior by suggested 1099 // relevances with minimal impact, preserving other suggested relevances. 1100 if (IsTopMatchNavigationInKeywordMode()) { 1101 // Correct the suggested relevance scores if the top match is a 1102 // navigation in keyword mode, since inlining a navigation match 1103 // would break the user out of keyword mode. By the way, if the top 1104 // match is a non-keyword match (query or navsuggestion) in keyword 1105 // mode, the user would also break out of keyword mode. However, 1106 // that situation is impossible given the current scoring paradigm 1107 // and the fact that only one search engine (Google) provides suggested 1108 // relevance scores at this time. 1109 DemoteKeywordNavigationMatchesPastTopQuery(); 1110 ConvertResultsToAutocompleteMatches(); 1111 DCHECK(!IsTopMatchNavigationInKeywordMode()); 1112 } 1113 // True if the omnibox will reorder matches as necessary to make the top 1114 // one something that is allowed to be the default match. 1115 const bool omnibox_will_reorder_for_legal_default_match = 1116 OmniboxFieldTrial::ReorderForLegalDefaultMatch( 1117 input_.current_page_classification()); 1118 if (!omnibox_will_reorder_for_legal_default_match && 1119 IsTopMatchScoreTooLow()) { 1120 // Disregard the suggested verbatim relevance if the top score is below 1121 // the usual verbatim value. For example, a BarProvider may rely on 1122 // SearchProvider's verbatim or inlineable matches for input "foo" (all 1123 // allowed to be default match) to always outrank its own lowly-ranked 1124 // "bar" matches that shouldn't be the default match. This only needs 1125 // to be enforced when the omnibox will not reorder results to make a 1126 // legal default match first. 1127 default_results_.verbatim_relevance = -1; 1128 keyword_results_.verbatim_relevance = -1; 1129 ConvertResultsToAutocompleteMatches(); 1130 } 1131 if (IsTopMatchSearchWithURLInput()) { 1132 // Disregard the suggested search and verbatim relevances if the input 1133 // type is URL and the top match is a highly-ranked search suggestion. 1134 // For example, prevent a search for "foo.com" from outranking another 1135 // provider's navigation for "foo.com" or "foo.com/url_from_history". 1136 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); 1137 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); 1138 default_results_.verbatim_relevance = -1; 1139 keyword_results_.verbatim_relevance = -1; 1140 ConvertResultsToAutocompleteMatches(); 1141 } 1142 if (!HasValidDefaultMatch(omnibox_will_reorder_for_legal_default_match)) { 1143 // If the omnibox is not going to reorder results to put a legal default 1144 // match at the top, then this provider needs to guarantee that its top 1145 // scoring result is a legal default match (i.e., it's either a verbatim 1146 // match or inlinable). For example, input "foo" should not invoke a 1147 // search for "bar", which would happen if the "bar" search match 1148 // outranked all other matches. On the other hand, if the omnibox will 1149 // reorder matches as necessary to put a legal default match at the top, 1150 // all we need to guarantee is that SearchProvider returns a legal 1151 // default match. (The omnibox always needs at least one legal default 1152 // match, and it relies on SearchProvider to always return one.) 1153 ApplyCalculatedRelevance(); 1154 ConvertResultsToAutocompleteMatches(); 1155 } 1156 DCHECK(!IsTopMatchNavigationInKeywordMode()); 1157 DCHECK(omnibox_will_reorder_for_legal_default_match || 1158 !IsTopMatchScoreTooLow()); 1159 DCHECK(!IsTopMatchSearchWithURLInput()); 1160 DCHECK(HasValidDefaultMatch(omnibox_will_reorder_for_legal_default_match)); 1161 } 1162 1163 UpdateStarredStateOfMatches(); 1164 UpdateDone(); 1165 } 1166 1167 void SearchProvider::AddNavigationResultsToMatches( 1168 const NavigationResults& navigation_results, 1169 ACMatches* matches) { 1170 for (NavigationResults::const_iterator it = navigation_results.begin(); 1171 it != navigation_results.end(); ++it) { 1172 matches->push_back(NavigationToMatch(*it)); 1173 // In the absence of suggested relevance scores, use only the single 1174 // highest-scoring result. (The results are already sorted by relevance.) 1175 if (!it->relevance_from_server()) 1176 return; 1177 } 1178 } 1179 1180 void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results, 1181 bool is_keyword, 1182 int did_not_accept_suggestion, 1183 MatchMap* map) { 1184 if (results.empty()) 1185 return; 1186 1187 bool prevent_inline_autocomplete = input_.prevent_inline_autocomplete() || 1188 (input_.type() == AutocompleteInput::URL); 1189 const string16& input_text = 1190 is_keyword ? keyword_input_.text() : input_.text(); 1191 bool input_multiple_words = HasMultipleWords(input_text); 1192 1193 SuggestResults scored_results; 1194 if (!prevent_inline_autocomplete && input_multiple_words) { 1195 // ScoreHistoryResults() allows autocompletion of multi-word, 1-visit 1196 // queries if the input also has multiple words. But if we were already 1197 // autocompleting a multi-word, multi-visit query, and the current input is 1198 // still a prefix of it, then changing the autocompletion suddenly feels 1199 // wrong. To detect this case, first score as if only one word has been 1200 // typed, then check for a best result that is an autocompleted, multi-word 1201 // query. If we find one, then just keep that score set. 1202 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, 1203 false, input_text, is_keyword); 1204 if ((scored_results.front().relevance() < 1205 AutocompleteResult::kLowestDefaultScore) || 1206 !HasMultipleWords(scored_results.front().suggestion())) 1207 scored_results.clear(); // Didn't detect the case above, score normally. 1208 } 1209 if (scored_results.empty()) 1210 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, 1211 input_multiple_words, input_text, 1212 is_keyword); 1213 for (SuggestResults::const_iterator i(scored_results.begin()); 1214 i != scored_results.end(); ++i) { 1215 AddMatchToMap(i->suggestion(), input_text, i->relevance(), false, 1216 AutocompleteMatchType::SEARCH_HISTORY, 1217 did_not_accept_suggestion, 1218 is_keyword, map); 1219 } 1220 } 1221 1222 SearchProvider::SuggestResults SearchProvider::ScoreHistoryResults( 1223 const HistoryResults& results, 1224 bool base_prevent_inline_autocomplete, 1225 bool input_multiple_words, 1226 const string16& input_text, 1227 bool is_keyword) { 1228 AutocompleteClassifier* classifier = 1229 AutocompleteClassifierFactory::GetForProfile(profile_); 1230 SuggestResults scored_results; 1231 const bool prevent_search_history_inlining = 1232 OmniboxFieldTrial::SearchHistoryPreventInlining( 1233 input_.current_page_classification()); 1234 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); 1235 ++i) { 1236 // Don't autocomplete multi-word queries that have only been seen once 1237 // unless the user has typed more than one word. 1238 bool prevent_inline_autocomplete = base_prevent_inline_autocomplete || 1239 (!input_multiple_words && (i->visits < 2) && HasMultipleWords(i->term)); 1240 1241 // Don't autocomplete search terms that would normally be treated as URLs 1242 // when typed. For example, if the user searched for "google.com" and types 1243 // "goog", don't autocomplete to the search term "google.com". Otherwise, 1244 // the input will look like a URL but act like a search, which is confusing. 1245 // NOTE: We don't check this in the following cases: 1246 // * When inline autocomplete is disabled, we won't be inline 1247 // autocompleting this term, so we don't need to worry about confusion as 1248 // much. This also prevents calling Classify() again from inside the 1249 // classifier (which will corrupt state and likely crash), since the 1250 // classifier always disables inline autocomplete. 1251 // * When the user has typed the whole term, the "what you typed" history 1252 // match will outrank us for URL-like inputs anyway, so we need not do 1253 // anything special. 1254 if (!prevent_inline_autocomplete && classifier && (i->term != input_text)) { 1255 AutocompleteMatch match; 1256 classifier->Classify(i->term, false, false, &match, NULL); 1257 prevent_inline_autocomplete = 1258 !AutocompleteMatch::IsSearchType(match.type); 1259 } 1260 1261 int relevance = CalculateRelevanceForHistory( 1262 i->time, is_keyword, !prevent_inline_autocomplete, 1263 prevent_search_history_inlining); 1264 scored_results.push_back( 1265 SuggestResult(i->term, is_keyword, relevance, false)); 1266 } 1267 1268 // History returns results sorted for us. However, we may have docked some 1269 // results' scores, so things are no longer in order. Do a stable sort to get 1270 // things back in order without otherwise disturbing results with equal 1271 // scores, then force the scores to be unique, so that the order in which 1272 // they're shown is deterministic. 1273 std::stable_sort(scored_results.begin(), scored_results.end(), 1274 CompareScoredResults()); 1275 int last_relevance = 0; 1276 for (SuggestResults::iterator i(scored_results.begin()); 1277 i != scored_results.end(); ++i) { 1278 if ((i != scored_results.begin()) && (i->relevance() >= last_relevance)) 1279 i->set_relevance(last_relevance - 1); 1280 last_relevance = i->relevance(); 1281 } 1282 1283 return scored_results; 1284 } 1285 1286 void SearchProvider::AddSuggestResultsToMap(const SuggestResults& results, 1287 MatchMap* map) { 1288 for (size_t i = 0; i < results.size(); ++i) { 1289 const bool is_keyword = results[i].from_keyword_provider(); 1290 const string16& input = is_keyword ? keyword_input_.text() : input_.text(); 1291 AddMatchToMap(results[i].suggestion(), input, results[i].relevance(), 1292 results[i].relevance_from_server(), 1293 AutocompleteMatchType::SEARCH_SUGGEST, i, is_keyword, map); 1294 } 1295 } 1296 1297 int SearchProvider::GetVerbatimRelevance(bool* relevance_from_server) const { 1298 // Use the suggested verbatim relevance score if it is non-negative (valid), 1299 // if inline autocomplete isn't prevented (always show verbatim on backspace), 1300 // and if it won't suppress verbatim, leaving no default provider matches. 1301 // Otherwise, if the default provider returned no matches and was still able 1302 // to suppress verbatim, the user would have no search/nav matches and may be 1303 // left unable to search using their default provider from the omnibox. 1304 // Check for results on each verbatim calculation, as results from older 1305 // queries (on previous input) may be trimmed for failing to inline new input. 1306 bool use_server_relevance = 1307 (default_results_.verbatim_relevance >= 0) && 1308 !input_.prevent_inline_autocomplete() && 1309 ((default_results_.verbatim_relevance > 0) || 1310 !default_results_.suggest_results.empty() || 1311 !default_results_.navigation_results.empty()); 1312 if (relevance_from_server) 1313 *relevance_from_server = use_server_relevance; 1314 return use_server_relevance ? 1315 default_results_.verbatim_relevance : CalculateRelevanceForVerbatim(); 1316 } 1317 1318 int SearchProvider::CalculateRelevanceForVerbatim() const { 1319 if (!providers_.keyword_provider().empty()) 1320 return 250; 1321 return CalculateRelevanceForVerbatimIgnoringKeywordModeState(); 1322 } 1323 1324 int SearchProvider:: 1325 CalculateRelevanceForVerbatimIgnoringKeywordModeState() const { 1326 switch (input_.type()) { 1327 case AutocompleteInput::UNKNOWN: 1328 case AutocompleteInput::QUERY: 1329 case AutocompleteInput::FORCED_QUERY: 1330 return kNonURLVerbatimRelevance; 1331 1332 case AutocompleteInput::URL: 1333 return 850; 1334 1335 default: 1336 NOTREACHED(); 1337 return 0; 1338 } 1339 } 1340 1341 int SearchProvider::GetKeywordVerbatimRelevance( 1342 bool* relevance_from_server) const { 1343 // Use the suggested verbatim relevance score if it is non-negative (valid), 1344 // if inline autocomplete isn't prevented (always show verbatim on backspace), 1345 // and if it won't suppress verbatim, leaving no keyword provider matches. 1346 // Otherwise, if the keyword provider returned no matches and was still able 1347 // to suppress verbatim, the user would have no search/nav matches and may be 1348 // left unable to search using their keyword provider from the omnibox. 1349 // Check for results on each verbatim calculation, as results from older 1350 // queries (on previous input) may be trimmed for failing to inline new input. 1351 bool use_server_relevance = 1352 (keyword_results_.verbatim_relevance >= 0) && 1353 !input_.prevent_inline_autocomplete() && 1354 ((keyword_results_.verbatim_relevance > 0) || 1355 !keyword_results_.suggest_results.empty() || 1356 !keyword_results_.navigation_results.empty()); 1357 if (relevance_from_server) 1358 *relevance_from_server = use_server_relevance; 1359 return use_server_relevance ? 1360 keyword_results_.verbatim_relevance : 1361 CalculateRelevanceForKeywordVerbatim(keyword_input_.type(), 1362 keyword_input_.prefer_keyword()); 1363 } 1364 1365 int SearchProvider::CalculateRelevanceForHistory( 1366 const base::Time& time, 1367 bool is_keyword, 1368 bool use_aggressive_method, 1369 bool prevent_search_history_inlining) const { 1370 // The relevance of past searches falls off over time. There are two distinct 1371 // equations used. If the first equation is used (searches to the primary 1372 // provider that we want to score aggressively), the score is in the range 1373 // 1300-1599 (unless |prevent_search_history_inlining|, in which case 1374 // it's in the range 1200-1299). If the second equation is used the 1375 // relevance of a search 15 minutes ago is discounted 50 points, while the 1376 // relevance of a search two weeks ago is discounted 450 points. 1377 double elapsed_time = std::max((base::Time::Now() - time).InSecondsF(), 0.0); 1378 bool is_primary_provider = is_keyword || !providers_.has_keyword_provider(); 1379 if (is_primary_provider && use_aggressive_method) { 1380 // Searches with the past two days get a different curve. 1381 const double autocomplete_time = 2 * 24 * 60 * 60; 1382 if (elapsed_time < autocomplete_time) { 1383 int max_score = is_keyword ? 1599 : 1399; 1384 if (prevent_search_history_inlining) 1385 max_score = 1299; 1386 return max_score - static_cast<int>(99 * 1387 std::pow(elapsed_time / autocomplete_time, 2.5)); 1388 } 1389 elapsed_time -= autocomplete_time; 1390 } 1391 1392 const int score_discount = 1393 static_cast<int>(6.5 * std::pow(elapsed_time, 0.3)); 1394 1395 // Don't let scores go below 0. Negative relevance scores are meaningful in 1396 // a different way. 1397 int base_score; 1398 if (is_primary_provider) 1399 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050; 1400 else 1401 base_score = 200; 1402 return std::max(0, base_score - score_discount); 1403 } 1404 1405 void SearchProvider::AddMatchToMap(const string16& query_string, 1406 const string16& input_text, 1407 int relevance, 1408 bool relevance_from_server, 1409 AutocompleteMatch::Type type, 1410 int accepted_suggestion, 1411 bool is_keyword, 1412 MatchMap* map) { 1413 // On non-mobile, ask the instant controller for the appropriate start margin. 1414 // On mobile the start margin is unused, so leave the value as default there. 1415 int omnibox_start_margin = chrome::kDisableStartMargin; 1416 #if !defined(OS_ANDROID) && !defined(IOS) 1417 if (chrome::IsInstantExtendedAPIEnabled()) { 1418 Browser* browser = 1419 chrome::FindBrowserWithProfile(profile_, chrome::GetActiveDesktop()); 1420 if (browser && browser->instant_controller() && 1421 browser->instant_controller()->instant()) { 1422 omnibox_start_margin = 1423 browser->instant_controller()->instant()->omnibox_bounds().x(); 1424 } 1425 } 1426 #endif // !defined(OS_ANDROID) && !defined(IOS) 1427 1428 const TemplateURL* template_url = is_keyword ? 1429 providers_.GetKeywordProviderURL() : providers_.GetDefaultProviderURL(); 1430 AutocompleteMatch match = CreateSearchSuggestion(this, relevance, type, 1431 template_url, query_string, input_text, input_, is_keyword, 1432 accepted_suggestion, omnibox_start_margin, 1433 !is_keyword || providers_.default_provider().empty()); 1434 if (!match.destination_url.is_valid()) 1435 return; 1436 match.RecordAdditionalInfo(kRelevanceFromServerKey, 1437 relevance_from_server ? kTrue : kFalse); 1438 1439 // Try to add |match| to |map|. If a match for |query_string| is already in 1440 // |map|, replace it if |match| is more relevant. 1441 // NOTE: Keep this ToLower() call in sync with url_database.cc. 1442 const std::pair<MatchMap::iterator, bool> i( 1443 map->insert(std::make_pair(base::i18n::ToLower(query_string), match))); 1444 // NOTE: We purposefully do a direct relevance comparison here instead of 1445 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added 1446 // first" rather than "items alphabetically first" when the scores are equal. 1447 // The only case this matters is when a user has results with the same score 1448 // that differ only by capitalization; because the history system returns 1449 // results sorted by recency, this means we'll pick the most recent such 1450 // result even if the precision of our relevance score is too low to 1451 // distinguish the two. 1452 if (!i.second && (match.relevance > i.first->second.relevance)) 1453 i.first->second = match; 1454 } 1455 1456 AutocompleteMatch SearchProvider::NavigationToMatch( 1457 const NavigationResult& navigation) { 1458 const string16& input = navigation.from_keyword_provider() ? 1459 keyword_input_.text() : input_.text(); 1460 AutocompleteMatch match(this, navigation.relevance(), false, 1461 AutocompleteMatchType::NAVSUGGEST); 1462 match.destination_url = navigation.url(); 1463 1464 // First look for the user's input inside the fill_into_edit as it would be 1465 // without trimming the scheme, so we can find matches at the beginning of the 1466 // scheme. 1467 const string16& untrimmed_fill_into_edit = navigation.formatted_url(); 1468 const URLPrefix* prefix = 1469 URLPrefix::BestURLPrefix(untrimmed_fill_into_edit, input); 1470 size_t match_start = (prefix == NULL) ? 1471 untrimmed_fill_into_edit.find(input) : prefix->prefix.length(); 1472 size_t inline_autocomplete_offset = (prefix == NULL) ? 1473 string16::npos : (match_start + input.length()); 1474 bool trim_http = !HasHTTPScheme(input) && (!prefix || (match_start != 0)); 1475 1476 // Preserve the forced query '?' prefix in |match.fill_into_edit|. 1477 // Otherwise, user edits to a suggestion would show non-Search results. 1478 if (input_.type() == AutocompleteInput::FORCED_QUERY) { 1479 match.fill_into_edit = ASCIIToUTF16("?"); 1480 if (inline_autocomplete_offset != string16::npos) 1481 ++inline_autocomplete_offset; 1482 } 1483 1484 const std::string languages( 1485 profile_->GetPrefs()->GetString(prefs::kAcceptLanguages)); 1486 const net::FormatUrlTypes format_types = 1487 net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP); 1488 match.fill_into_edit += 1489 AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url(), 1490 net::FormatUrl(navigation.url(), languages, format_types, 1491 net::UnescapeRule::SPACES, NULL, NULL, 1492 &inline_autocomplete_offset)); 1493 if (!input_.prevent_inline_autocomplete() && 1494 (inline_autocomplete_offset != string16::npos)) { 1495 DCHECK(inline_autocomplete_offset <= match.fill_into_edit.length()); 1496 match.allowed_to_be_default_match = true; 1497 match.inline_autocompletion = 1498 match.fill_into_edit.substr(inline_autocomplete_offset); 1499 } 1500 1501 match.contents = net::FormatUrl(navigation.url(), languages, 1502 format_types, net::UnescapeRule::SPACES, NULL, NULL, &match_start); 1503 // If the first match in the untrimmed string was inside a scheme that we 1504 // trimmed, look for a subsequent match. 1505 if (match_start == string16::npos) 1506 match_start = match.contents.find(input); 1507 // Safe if |match_start| is npos; also safe if the input is longer than the 1508 // remaining contents after |match_start|. 1509 AutocompleteMatch::ClassifyLocationInString(match_start, input.length(), 1510 match.contents.length(), ACMatchClassification::URL, 1511 &match.contents_class); 1512 1513 match.description = navigation.description(); 1514 AutocompleteMatch::ClassifyMatchInString(input, match.description, 1515 ACMatchClassification::NONE, &match.description_class); 1516 1517 match.RecordAdditionalInfo( 1518 kRelevanceFromServerKey, 1519 navigation.relevance_from_server() ? kTrue : kFalse); 1520 1521 return match; 1522 } 1523 1524 void SearchProvider::DemoteKeywordNavigationMatchesPastTopQuery() { 1525 // First, determine the maximum score of any keyword query match (verbatim or 1526 // query suggestion). 1527 bool relevance_from_server; 1528 int max_query_relevance = GetKeywordVerbatimRelevance(&relevance_from_server); 1529 if (!keyword_results_.suggest_results.empty()) { 1530 const SuggestResult& top_keyword = keyword_results_.suggest_results.front(); 1531 const int suggest_relevance = top_keyword.relevance(); 1532 if (suggest_relevance > max_query_relevance) { 1533 max_query_relevance = suggest_relevance; 1534 relevance_from_server = top_keyword.relevance_from_server(); 1535 } else if (suggest_relevance == max_query_relevance) { 1536 relevance_from_server |= top_keyword.relevance_from_server(); 1537 } 1538 } 1539 // If no query is supposed to appear, then navigational matches cannot 1540 // be demoted past it. Get rid of suggested relevance scores for 1541 // navsuggestions and introduce the verbatim results again. The keyword 1542 // verbatim match will outscore the navsuggest matches. 1543 if (max_query_relevance == 0) { 1544 ApplyCalculatedNavigationRelevance(&keyword_results_.navigation_results); 1545 ApplyCalculatedNavigationRelevance(&default_results_.navigation_results); 1546 keyword_results_.verbatim_relevance = -1; 1547 default_results_.verbatim_relevance = -1; 1548 return; 1549 } 1550 // Now we know we can enforce the minimum score constraint even after 1551 // the navigation matches are demoted. Proceed to demote the navigation 1552 // matches to enforce the query-must-come-first constraint. 1553 // Cap the relevance score of all results. 1554 for (NavigationResults::iterator it = 1555 keyword_results_.navigation_results.begin(); 1556 it != keyword_results_.navigation_results.end(); ++it) { 1557 if (it->relevance() < max_query_relevance) 1558 return; 1559 max_query_relevance = std::max(max_query_relevance - 1, 0); 1560 it->set_relevance(max_query_relevance); 1561 it->set_relevance_from_server(relevance_from_server); 1562 } 1563 } 1564 1565 void SearchProvider::UpdateDone() { 1566 // We're done when the timer isn't running, there are no suggest queries 1567 // pending, and we're not waiting on Instant. 1568 done_ = !timer_.IsRunning() && (suggest_results_pending_ == 0); 1569 } 1570