| OLD | NEW |
| (Empty) |
| 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/base64.h" | |
| 11 #include "base/callback.h" | |
| 12 #include "base/i18n/break_iterator.h" | |
| 13 #include "base/i18n/case_conversion.h" | |
| 14 #include "base/json/json_string_value_serializer.h" | |
| 15 #include "base/metrics/histogram.h" | |
| 16 #include "base/metrics/user_metrics.h" | |
| 17 #include "base/rand_util.h" | |
| 18 #include "base/strings/string_util.h" | |
| 19 #include "base/strings/utf_string_conversions.h" | |
| 20 #include "components/history/core/browser/in_memory_database.h" | |
| 21 #include "components/history/core/browser/keyword_search_term.h" | |
| 22 #include "components/metrics/proto/omnibox_input_type.pb.h" | |
| 23 #include "components/omnibox/autocomplete_provider_delegate.h" | |
| 24 #include "components/omnibox/autocomplete_provider_listener.h" | |
| 25 #include "components/omnibox/autocomplete_result.h" | |
| 26 #include "components/omnibox/keyword_provider.h" | |
| 27 #include "components/omnibox/omnibox_field_trial.h" | |
| 28 #include "components/omnibox/url_prefix.h" | |
| 29 #include "components/search/search.h" | |
| 30 #include "components/search_engines/template_url_prepopulate_data.h" | |
| 31 #include "components/search_engines/template_url_service.h" | |
| 32 #include "components/variations/variations_http_header_provider.h" | |
| 33 #include "grit/components_strings.h" | |
| 34 #include "net/base/escape.h" | |
| 35 #include "net/base/load_flags.h" | |
| 36 #include "net/base/net_util.h" | |
| 37 #include "net/http/http_request_headers.h" | |
| 38 #include "net/url_request/url_fetcher.h" | |
| 39 #include "net/url_request/url_request_status.h" | |
| 40 #include "ui/base/l10n/l10n_util.h" | |
| 41 #include "url/url_constants.h" | |
| 42 #include "url/url_util.h" | |
| 43 | |
| 44 // Helpers -------------------------------------------------------------------- | |
| 45 | |
| 46 namespace { | |
| 47 | |
| 48 // We keep track in a histogram how many suggest requests we send, how | |
| 49 // many suggest requests we invalidate (e.g., due to a user typing | |
| 50 // another character), and how many replies we receive. | |
| 51 // *** ADD NEW ENUMS AFTER ALL PREVIOUSLY DEFINED ONES! *** | |
| 52 // (excluding the end-of-list enum value) | |
| 53 // We do not want values of existing enums to change or else it screws | |
| 54 // up the statistics. | |
| 55 enum SuggestRequestsHistogramValue { | |
| 56 REQUEST_SENT = 1, | |
| 57 REQUEST_INVALIDATED, | |
| 58 REPLY_RECEIVED, | |
| 59 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE | |
| 60 }; | |
| 61 | |
| 62 // The verbatim score for an input which is not an URL. | |
| 63 const int kNonURLVerbatimRelevance = 1300; | |
| 64 | |
| 65 // Increments the appropriate value in the histogram by one. | |
| 66 void LogOmniboxSuggestRequest( | |
| 67 SuggestRequestsHistogramValue request_value) { | |
| 68 UMA_HISTOGRAM_ENUMERATION("Omnibox.SuggestRequests", request_value, | |
| 69 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE); | |
| 70 } | |
| 71 | |
| 72 bool HasMultipleWords(const base::string16& text) { | |
| 73 base::i18n::BreakIterator i(text, base::i18n::BreakIterator::BREAK_WORD); | |
| 74 bool found_word = false; | |
| 75 if (i.Init()) { | |
| 76 while (i.Advance()) { | |
| 77 if (i.IsWord()) { | |
| 78 if (found_word) | |
| 79 return true; | |
| 80 found_word = true; | |
| 81 } | |
| 82 } | |
| 83 } | |
| 84 return false; | |
| 85 } | |
| 86 | |
| 87 } // namespace | |
| 88 | |
| 89 // SearchProvider::Providers -------------------------------------------------- | |
| 90 | |
| 91 SearchProvider::Providers::Providers(TemplateURLService* template_url_service) | |
| 92 : template_url_service_(template_url_service) {} | |
| 93 | |
| 94 const TemplateURL* SearchProvider::Providers::GetDefaultProviderURL() const { | |
| 95 return default_provider_.empty() ? NULL : | |
| 96 template_url_service_->GetTemplateURLForKeyword(default_provider_); | |
| 97 } | |
| 98 | |
| 99 const TemplateURL* SearchProvider::Providers::GetKeywordProviderURL() const { | |
| 100 return keyword_provider_.empty() ? NULL : | |
| 101 template_url_service_->GetTemplateURLForKeyword(keyword_provider_); | |
| 102 } | |
| 103 | |
| 104 | |
| 105 // SearchProvider::CompareScoredResults --------------------------------------- | |
| 106 | |
| 107 class SearchProvider::CompareScoredResults { | |
| 108 public: | |
| 109 bool operator()(const SearchSuggestionParser::Result& a, | |
| 110 const SearchSuggestionParser::Result& b) { | |
| 111 // Sort in descending relevance order. | |
| 112 return a.relevance() > b.relevance(); | |
| 113 } | |
| 114 }; | |
| 115 | |
| 116 | |
| 117 // SearchProvider ------------------------------------------------------------- | |
| 118 | |
| 119 // static | |
| 120 int SearchProvider::kMinimumTimeBetweenSuggestQueriesMs = 100; | |
| 121 | |
| 122 SearchProvider::SearchProvider( | |
| 123 AutocompleteProviderListener* listener, | |
| 124 TemplateURLService* template_url_service, | |
| 125 scoped_ptr<AutocompleteProviderDelegate> delegate) | |
| 126 : BaseSearchProvider(template_url_service, delegate.Pass(), | |
| 127 AutocompleteProvider::TYPE_SEARCH), | |
| 128 listener_(listener), | |
| 129 suggest_results_pending_(0), | |
| 130 providers_(template_url_service), | |
| 131 answers_cache_(1) { | |
| 132 } | |
| 133 | |
| 134 // static | |
| 135 std::string SearchProvider::GetSuggestMetadata(const AutocompleteMatch& match) { | |
| 136 return match.GetAdditionalInfo(kSuggestMetadataKey); | |
| 137 } | |
| 138 | |
| 139 void SearchProvider::ResetSession() { | |
| 140 field_trial_triggered_in_session_ = false; | |
| 141 } | |
| 142 | |
| 143 SearchProvider::~SearchProvider() { | |
| 144 } | |
| 145 | |
| 146 // static | |
| 147 int SearchProvider::CalculateRelevanceForKeywordVerbatim( | |
| 148 metrics::OmniboxInputType::Type type, | |
| 149 bool prefer_keyword) { | |
| 150 // This function is responsible for scoring verbatim query matches | |
| 151 // for non-extension keywords. KeywordProvider::CalculateRelevance() | |
| 152 // scores verbatim query matches for extension keywords, as well as | |
| 153 // for keyword matches (i.e., suggestions of a keyword itself, not a | |
| 154 // suggestion of a query on a keyword search engine). These two | |
| 155 // functions are currently in sync, but there's no reason we | |
| 156 // couldn't decide in the future to score verbatim matches | |
| 157 // differently for extension and non-extension keywords. If you | |
| 158 // make such a change, however, you should update this comment to | |
| 159 // describe it, so it's clear why the functions diverge. | |
| 160 if (prefer_keyword) | |
| 161 return 1500; | |
| 162 return (type == metrics::OmniboxInputType::QUERY) ? 1450 : 1100; | |
| 163 } | |
| 164 | |
| 165 void SearchProvider::Start(const AutocompleteInput& input, | |
| 166 bool minimal_changes) { | |
| 167 // Do our best to load the model as early as possible. This will reduce | |
| 168 // odds of having the model not ready when really needed (a non-empty input). | |
| 169 TemplateURLService* model = providers_.template_url_service(); | |
| 170 DCHECK(model); | |
| 171 model->Load(); | |
| 172 | |
| 173 matches_.clear(); | |
| 174 field_trial_triggered_ = false; | |
| 175 | |
| 176 // Can't return search/suggest results for bogus input. | |
| 177 if (input.type() == metrics::OmniboxInputType::INVALID) { | |
| 178 Stop(true); | |
| 179 return; | |
| 180 } | |
| 181 | |
| 182 keyword_input_ = input; | |
| 183 const TemplateURL* keyword_provider = | |
| 184 KeywordProvider::GetSubstitutingTemplateURLForInput(model, | |
| 185 &keyword_input_); | |
| 186 if (keyword_provider == NULL) | |
| 187 keyword_input_.Clear(); | |
| 188 else if (keyword_input_.text().empty()) | |
| 189 keyword_provider = NULL; | |
| 190 | |
| 191 const TemplateURL* default_provider = model->GetDefaultSearchProvider(); | |
| 192 if (default_provider && | |
| 193 !default_provider->SupportsReplacement(model->search_terms_data())) | |
| 194 default_provider = NULL; | |
| 195 | |
| 196 if (keyword_provider == default_provider) | |
| 197 default_provider = NULL; // No use in querying the same provider twice. | |
| 198 | |
| 199 if (!default_provider && !keyword_provider) { | |
| 200 // No valid providers. | |
| 201 Stop(true); | |
| 202 return; | |
| 203 } | |
| 204 | |
| 205 // If we're still running an old query but have since changed the query text | |
| 206 // or the providers, abort the query. | |
| 207 base::string16 default_provider_keyword(default_provider ? | |
| 208 default_provider->keyword() : base::string16()); | |
| 209 base::string16 keyword_provider_keyword(keyword_provider ? | |
| 210 keyword_provider->keyword() : base::string16()); | |
| 211 if (!minimal_changes || | |
| 212 !providers_.equal(default_provider_keyword, keyword_provider_keyword)) { | |
| 213 // Cancel any in-flight suggest requests. | |
| 214 if (!done_) | |
| 215 Stop(false); | |
| 216 } | |
| 217 | |
| 218 providers_.set(default_provider_keyword, keyword_provider_keyword); | |
| 219 | |
| 220 if (input.text().empty()) { | |
| 221 // User typed "?" alone. Give them a placeholder result indicating what | |
| 222 // this syntax does. | |
| 223 if (default_provider) { | |
| 224 AutocompleteMatch match; | |
| 225 match.provider = this; | |
| 226 match.contents.assign(l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE)); | |
| 227 match.contents_class.push_back( | |
| 228 ACMatchClassification(0, ACMatchClassification::NONE)); | |
| 229 match.keyword = providers_.default_provider(); | |
| 230 match.allowed_to_be_default_match = true; | |
| 231 matches_.push_back(match); | |
| 232 } | |
| 233 Stop(true); | |
| 234 return; | |
| 235 } | |
| 236 | |
| 237 input_ = input; | |
| 238 | |
| 239 DoHistoryQuery(minimal_changes); | |
| 240 DoAnswersQuery(input); | |
| 241 StartOrStopSuggestQuery(minimal_changes); | |
| 242 UpdateMatches(); | |
| 243 } | |
| 244 | |
| 245 void SearchProvider::Stop(bool clear_cached_results) { | |
| 246 StopSuggest(); | |
| 247 done_ = true; | |
| 248 | |
| 249 if (clear_cached_results) | |
| 250 ClearAllResults(); | |
| 251 } | |
| 252 | |
| 253 const TemplateURL* SearchProvider::GetTemplateURL(bool is_keyword) const { | |
| 254 return is_keyword ? providers_.GetKeywordProviderURL() | |
| 255 : providers_.GetDefaultProviderURL(); | |
| 256 } | |
| 257 | |
| 258 const AutocompleteInput SearchProvider::GetInput(bool is_keyword) const { | |
| 259 return is_keyword ? keyword_input_ : input_; | |
| 260 } | |
| 261 | |
| 262 bool SearchProvider::ShouldAppendExtraParams( | |
| 263 const SearchSuggestionParser::SuggestResult& result) const { | |
| 264 return !result.from_keyword_provider() || | |
| 265 providers_.default_provider().empty(); | |
| 266 } | |
| 267 | |
| 268 void SearchProvider::RecordDeletionResult(bool success) { | |
| 269 if (success) { | |
| 270 base::RecordAction( | |
| 271 base::UserMetricsAction("Omnibox.ServerSuggestDelete.Success")); | |
| 272 } else { | |
| 273 base::RecordAction( | |
| 274 base::UserMetricsAction("Omnibox.ServerSuggestDelete.Failure")); | |
| 275 } | |
| 276 } | |
| 277 | |
| 278 void SearchProvider::OnURLFetchComplete(const net::URLFetcher* source) { | |
| 279 DCHECK(!done_); | |
| 280 --suggest_results_pending_; | |
| 281 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. | |
| 282 | |
| 283 const bool is_keyword = source == keyword_fetcher_.get(); | |
| 284 | |
| 285 // Ensure the request succeeded and that the provider used is still available. | |
| 286 // A verbatim match cannot be generated without this provider, causing errors. | |
| 287 const bool request_succeeded = | |
| 288 source->GetStatus().is_success() && (source->GetResponseCode() == 200) && | |
| 289 GetTemplateURL(is_keyword); | |
| 290 | |
| 291 LogFetchComplete(request_succeeded, is_keyword); | |
| 292 | |
| 293 bool results_updated = false; | |
| 294 if (request_succeeded) { | |
| 295 scoped_ptr<base::Value> data(SearchSuggestionParser::DeserializeJsonData( | |
| 296 SearchSuggestionParser::ExtractJsonData(source))); | |
| 297 if (data) { | |
| 298 SearchSuggestionParser::Results* results = | |
| 299 is_keyword ? &keyword_results_ : &default_results_; | |
| 300 results_updated = ParseSuggestResults(*data, -1, is_keyword, results); | |
| 301 if (results_updated) | |
| 302 SortResults(is_keyword, results); | |
| 303 } | |
| 304 } | |
| 305 UpdateMatches(); | |
| 306 if (done_ || results_updated) | |
| 307 listener_->OnProviderUpdate(results_updated); | |
| 308 } | |
| 309 | |
| 310 void SearchProvider::StopSuggest() { | |
| 311 // Increment the appropriate field in the histogram by the number of | |
| 312 // pending requests that were invalidated. | |
| 313 for (int i = 0; i < suggest_results_pending_; ++i) | |
| 314 LogOmniboxSuggestRequest(REQUEST_INVALIDATED); | |
| 315 suggest_results_pending_ = 0; | |
| 316 timer_.Stop(); | |
| 317 // Stop any in-progress URL fetches. | |
| 318 keyword_fetcher_.reset(); | |
| 319 default_fetcher_.reset(); | |
| 320 } | |
| 321 | |
| 322 void SearchProvider::ClearAllResults() { | |
| 323 keyword_results_.Clear(); | |
| 324 default_results_.Clear(); | |
| 325 } | |
| 326 | |
| 327 void SearchProvider::UpdateMatchContentsClass( | |
| 328 const base::string16& input_text, | |
| 329 SearchSuggestionParser::Results* results) { | |
| 330 for (SearchSuggestionParser::SuggestResults::iterator sug_it = | |
| 331 results->suggest_results.begin(); | |
| 332 sug_it != results->suggest_results.end(); ++sug_it) { | |
| 333 sug_it->ClassifyMatchContents(false, input_text); | |
| 334 } | |
| 335 const std::string languages(delegate_->AcceptLanguages()); | |
| 336 for (SearchSuggestionParser::NavigationResults::iterator nav_it = | |
| 337 results->navigation_results.begin(); | |
| 338 nav_it != results->navigation_results.end(); ++nav_it) { | |
| 339 nav_it->CalculateAndClassifyMatchContents(false, input_text, languages); | |
| 340 } | |
| 341 } | |
| 342 | |
| 343 void SearchProvider::SortResults(bool is_keyword, | |
| 344 SearchSuggestionParser::Results* results) { | |
| 345 // Ignore suggested scores for non-keyword matches in keyword mode; if the | |
| 346 // server is allowed to score these, it could interfere with the user's | |
| 347 // ability to get good keyword results. | |
| 348 const bool abandon_suggested_scores = | |
| 349 !is_keyword && !providers_.keyword_provider().empty(); | |
| 350 // Apply calculated relevance scores to suggestions if valid relevances were | |
| 351 // not provided or we're abandoning suggested scores entirely. | |
| 352 if (!results->relevances_from_server || abandon_suggested_scores) { | |
| 353 ApplyCalculatedSuggestRelevance(&results->suggest_results); | |
| 354 ApplyCalculatedNavigationRelevance(&results->navigation_results); | |
| 355 // If abandoning scores entirely, also abandon the verbatim score. | |
| 356 if (abandon_suggested_scores) | |
| 357 results->verbatim_relevance = -1; | |
| 358 } | |
| 359 | |
| 360 // Keep the result lists sorted. | |
| 361 const CompareScoredResults comparator = CompareScoredResults(); | |
| 362 std::stable_sort(results->suggest_results.begin(), | |
| 363 results->suggest_results.end(), | |
| 364 comparator); | |
| 365 std::stable_sort(results->navigation_results.begin(), | |
| 366 results->navigation_results.end(), | |
| 367 comparator); | |
| 368 } | |
| 369 | |
| 370 void SearchProvider::LogFetchComplete(bool success, bool is_keyword) { | |
| 371 LogOmniboxSuggestRequest(REPLY_RECEIVED); | |
| 372 // Record response time for suggest requests sent to Google. We care | |
| 373 // only about the common case: the Google default provider used in | |
| 374 // non-keyword mode. | |
| 375 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
| 376 if (!is_keyword && default_url && | |
| 377 (TemplateURLPrepopulateData::GetEngineType( | |
| 378 *default_url, | |
| 379 providers_.template_url_service()->search_terms_data()) == | |
| 380 SEARCH_ENGINE_GOOGLE)) { | |
| 381 const base::TimeDelta elapsed_time = | |
| 382 base::TimeTicks::Now() - time_suggest_request_sent_; | |
| 383 if (success) { | |
| 384 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Success.GoogleResponseTime", | |
| 385 elapsed_time); | |
| 386 } else { | |
| 387 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Failure.GoogleResponseTime", | |
| 388 elapsed_time); | |
| 389 } | |
| 390 } | |
| 391 } | |
| 392 | |
| 393 void SearchProvider::UpdateMatches() { | |
| 394 ConvertResultsToAutocompleteMatches(); | |
| 395 | |
| 396 // Check constraints that may be violated by suggested relevances. | |
| 397 if (!matches_.empty() && | |
| 398 (default_results_.HasServerProvidedScores() || | |
| 399 keyword_results_.HasServerProvidedScores())) { | |
| 400 // These blocks attempt to repair undesirable behavior by suggested | |
| 401 // relevances with minimal impact, preserving other suggested relevances. | |
| 402 | |
| 403 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
| 404 const bool is_extension_keyword = (keyword_url != NULL) && | |
| 405 (keyword_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION); | |
| 406 if ((keyword_url != NULL) && !is_extension_keyword && | |
| 407 (FindTopMatch() == matches_.end())) { | |
| 408 // In non-extension keyword mode, disregard the keyword verbatim suggested | |
| 409 // relevance if necessary, so at least one match is allowed to be default. | |
| 410 // (In extension keyword mode this is not necessary because the extension | |
| 411 // will return a default match.) | |
| 412 keyword_results_.verbatim_relevance = -1; | |
| 413 ConvertResultsToAutocompleteMatches(); | |
| 414 } | |
| 415 if (IsTopMatchSearchWithURLInput()) { | |
| 416 // Disregard the suggested search and verbatim relevances if the input | |
| 417 // type is URL and the top match is a highly-ranked search suggestion. | |
| 418 // For example, prevent a search for "foo.com" from outranking another | |
| 419 // provider's navigation for "foo.com" or "foo.com/url_from_history". | |
| 420 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); | |
| 421 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); | |
| 422 default_results_.verbatim_relevance = -1; | |
| 423 keyword_results_.verbatim_relevance = -1; | |
| 424 ConvertResultsToAutocompleteMatches(); | |
| 425 } | |
| 426 if (!is_extension_keyword && (FindTopMatch() == matches_.end())) { | |
| 427 // Guarantee that SearchProvider returns a legal default match (except | |
| 428 // when in extension-based keyword mode). The omnibox always needs at | |
| 429 // least one legal default match, and it relies on SearchProvider in | |
| 430 // combination with KeywordProvider (for extension-based keywords) to | |
| 431 // always return one. | |
| 432 ApplyCalculatedRelevance(); | |
| 433 ConvertResultsToAutocompleteMatches(); | |
| 434 } | |
| 435 DCHECK(!IsTopMatchSearchWithURLInput()); | |
| 436 DCHECK(is_extension_keyword || (FindTopMatch() != matches_.end())); | |
| 437 } | |
| 438 UMA_HISTOGRAM_CUSTOM_COUNTS( | |
| 439 "Omnibox.SearchProviderMatches", matches_.size(), 1, 6, 7); | |
| 440 UpdateDone(); | |
| 441 } | |
| 442 | |
| 443 void SearchProvider::Run() { | |
| 444 // Start a new request with the current input. | |
| 445 suggest_results_pending_ = 0; | |
| 446 time_suggest_request_sent_ = base::TimeTicks::Now(); | |
| 447 | |
| 448 default_fetcher_.reset(CreateSuggestFetcher(kDefaultProviderURLFetcherID, | |
| 449 providers_.GetDefaultProviderURL(), input_)); | |
| 450 keyword_fetcher_.reset(CreateSuggestFetcher(kKeywordProviderURLFetcherID, | |
| 451 providers_.GetKeywordProviderURL(), keyword_input_)); | |
| 452 | |
| 453 // Both the above can fail if the providers have been modified or deleted | |
| 454 // since the query began. | |
| 455 if (suggest_results_pending_ == 0) { | |
| 456 UpdateDone(); | |
| 457 // We only need to update the listener if we're actually done. | |
| 458 if (done_) | |
| 459 listener_->OnProviderUpdate(false); | |
| 460 } | |
| 461 } | |
| 462 | |
| 463 void SearchProvider::DoHistoryQuery(bool minimal_changes) { | |
| 464 // The history query results are synchronous, so if minimal_changes is true, | |
| 465 // we still have the last results and don't need to do anything. | |
| 466 if (minimal_changes) | |
| 467 return; | |
| 468 | |
| 469 keyword_history_results_.clear(); | |
| 470 default_history_results_.clear(); | |
| 471 | |
| 472 if (OmniboxFieldTrial::SearchHistoryDisable( | |
| 473 input_.current_page_classification())) | |
| 474 return; | |
| 475 | |
| 476 history::URLDatabase* url_db = delegate_->InMemoryDatabase(); | |
| 477 if (!url_db) | |
| 478 return; | |
| 479 | |
| 480 // Request history for both the keyword and default provider. We grab many | |
| 481 // more matches than we'll ultimately clamp to so that if there are several | |
| 482 // recent multi-word matches who scores are lowered (see | |
| 483 // AddHistoryResultsToMap()), they won't crowd out older, higher-scoring | |
| 484 // matches. Note that this doesn't fix the problem entirely, but merely | |
| 485 // limits it to cases with a very large number of such multi-word matches; for | |
| 486 // now, this seems OK compared with the complexity of a real fix, which would | |
| 487 // require multiple searches and tracking of "single- vs. multi-word" in the | |
| 488 // database. | |
| 489 int num_matches = kMaxMatches * 5; | |
| 490 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
| 491 if (default_url) { | |
| 492 const base::TimeTicks start_time = base::TimeTicks::Now(); | |
| 493 url_db->GetMostRecentKeywordSearchTerms(default_url->id(), input_.text(), | |
| 494 num_matches, &default_history_results_); | |
| 495 UMA_HISTOGRAM_TIMES( | |
| 496 "Omnibox.SearchProvider.GetMostRecentKeywordTermsDefaultProviderTime", | |
| 497 base::TimeTicks::Now() - start_time); | |
| 498 } | |
| 499 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
| 500 if (keyword_url) { | |
| 501 url_db->GetMostRecentKeywordSearchTerms(keyword_url->id(), | |
| 502 keyword_input_.text(), num_matches, &keyword_history_results_); | |
| 503 } | |
| 504 } | |
| 505 | |
| 506 void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) { | |
| 507 if (!IsQuerySuitableForSuggest()) { | |
| 508 StopSuggest(); | |
| 509 ClearAllResults(); | |
| 510 return; | |
| 511 } | |
| 512 | |
| 513 // For the minimal_changes case, if we finished the previous query and still | |
| 514 // have its results, or are allowed to keep running it, just do that, rather | |
| 515 // than starting a new query. | |
| 516 if (minimal_changes && | |
| 517 (!default_results_.suggest_results.empty() || | |
| 518 !default_results_.navigation_results.empty() || | |
| 519 !keyword_results_.suggest_results.empty() || | |
| 520 !keyword_results_.navigation_results.empty() || | |
| 521 (!done_ && input_.want_asynchronous_matches()))) | |
| 522 return; | |
| 523 | |
| 524 // We can't keep running any previous query, so halt it. | |
| 525 StopSuggest(); | |
| 526 | |
| 527 // Remove existing results that cannot inline autocomplete the new input. | |
| 528 RemoveAllStaleResults(); | |
| 529 | |
| 530 // Update the content classifications of remaining results so they look good | |
| 531 // against the current input. | |
| 532 UpdateMatchContentsClass(input_.text(), &default_results_); | |
| 533 if (!keyword_input_.text().empty()) | |
| 534 UpdateMatchContentsClass(keyword_input_.text(), &keyword_results_); | |
| 535 | |
| 536 // We can't start a new query if we're only allowed synchronous results. | |
| 537 if (!input_.want_asynchronous_matches()) | |
| 538 return; | |
| 539 | |
| 540 // To avoid flooding the suggest server, don't send a query until at | |
| 541 // least 100 ms since the last query. | |
| 542 base::TimeTicks next_suggest_time(time_suggest_request_sent_ + | |
| 543 base::TimeDelta::FromMilliseconds(kMinimumTimeBetweenSuggestQueriesMs)); | |
| 544 base::TimeTicks now(base::TimeTicks::Now()); | |
| 545 if (now >= next_suggest_time) { | |
| 546 Run(); | |
| 547 return; | |
| 548 } | |
| 549 timer_.Start(FROM_HERE, next_suggest_time - now, this, &SearchProvider::Run); | |
| 550 } | |
| 551 | |
| 552 bool SearchProvider::IsQuerySuitableForSuggest() const { | |
| 553 // Don't run Suggest in incognito mode, if the engine doesn't support it, or | |
| 554 // if the user has disabled it. | |
| 555 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
| 556 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
| 557 if (delegate_->IsOffTheRecord() || | |
| 558 ((!default_url || default_url->suggestions_url().empty()) && | |
| 559 (!keyword_url || keyword_url->suggestions_url().empty())) || | |
| 560 !delegate_->SearchSuggestEnabled()) | |
| 561 return false; | |
| 562 | |
| 563 // If the input type might be a URL, we take extra care so that private data | |
| 564 // isn't sent to the server. | |
| 565 | |
| 566 // FORCED_QUERY means the user is explicitly asking us to search for this, so | |
| 567 // we assume it isn't a URL and/or there isn't private data. | |
| 568 if (input_.type() == metrics::OmniboxInputType::FORCED_QUERY) | |
| 569 return true; | |
| 570 | |
| 571 // Next we check the scheme. If this is UNKNOWN/URL with a scheme that isn't | |
| 572 // http/https/ftp, we shouldn't send it. Sending things like file: and data: | |
| 573 // is both a waste of time and a disclosure of potentially private, local | |
| 574 // data. Other "schemes" may actually be usernames, and we don't want to send | |
| 575 // passwords. If the scheme is OK, we still need to check other cases below. | |
| 576 // If this is QUERY, then the presence of these schemes means the user | |
| 577 // explicitly typed one, and thus this is probably a URL that's being entered | |
| 578 // and happens to currently be invalid -- in which case we again want to run | |
| 579 // our checks below. Other QUERY cases are less likely to be URLs and thus we | |
| 580 // assume we're OK. | |
| 581 if (!LowerCaseEqualsASCII(input_.scheme(), url::kHttpScheme) && | |
| 582 !LowerCaseEqualsASCII(input_.scheme(), url::kHttpsScheme) && | |
| 583 !LowerCaseEqualsASCII(input_.scheme(), url::kFtpScheme)) | |
| 584 return (input_.type() == metrics::OmniboxInputType::QUERY); | |
| 585 | |
| 586 // Don't send URLs with usernames, queries or refs. Some of these are | |
| 587 // private, and the Suggest server is unlikely to have any useful results | |
| 588 // for any of them. Also don't send URLs with ports, as we may initially | |
| 589 // think that a username + password is a host + port (and we don't want to | |
| 590 // send usernames/passwords), and even if the port really is a port, the | |
| 591 // server is once again unlikely to have and useful results. | |
| 592 // Note that we only block based on refs if the input is URL-typed, as search | |
| 593 // queries can legitimately have #s in them which the URL parser | |
| 594 // overaggressively categorizes as a url with a ref. | |
| 595 const url::Parsed& parts = input_.parts(); | |
| 596 if (parts.username.is_nonempty() || parts.port.is_nonempty() || | |
| 597 parts.query.is_nonempty() || | |
| 598 (parts.ref.is_nonempty() && | |
| 599 (input_.type() == metrics::OmniboxInputType::URL))) | |
| 600 return false; | |
| 601 | |
| 602 // Don't send anything for https except the hostname. Hostnames are OK | |
| 603 // because they are visible when the TCP connection is established, but the | |
| 604 // specific path may reveal private information. | |
| 605 if (LowerCaseEqualsASCII(input_.scheme(), url::kHttpsScheme) && | |
| 606 parts.path.is_nonempty()) | |
| 607 return false; | |
| 608 | |
| 609 return true; | |
| 610 } | |
| 611 | |
| 612 void SearchProvider::RemoveAllStaleResults() { | |
| 613 if (keyword_input_.text().empty()) { | |
| 614 // User is either in keyword mode with a blank input or out of | |
| 615 // keyword mode entirely. | |
| 616 keyword_results_.Clear(); | |
| 617 } | |
| 618 } | |
| 619 | |
| 620 void SearchProvider::ApplyCalculatedRelevance() { | |
| 621 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); | |
| 622 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); | |
| 623 ApplyCalculatedNavigationRelevance(&keyword_results_.navigation_results); | |
| 624 ApplyCalculatedNavigationRelevance(&default_results_.navigation_results); | |
| 625 default_results_.verbatim_relevance = -1; | |
| 626 keyword_results_.verbatim_relevance = -1; | |
| 627 } | |
| 628 | |
| 629 void SearchProvider::ApplyCalculatedSuggestRelevance( | |
| 630 SearchSuggestionParser::SuggestResults* list) { | |
| 631 for (size_t i = 0; i < list->size(); ++i) { | |
| 632 SearchSuggestionParser::SuggestResult& result = (*list)[i]; | |
| 633 result.set_relevance( | |
| 634 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + | |
| 635 (list->size() - i - 1)); | |
| 636 result.set_relevance_from_server(false); | |
| 637 } | |
| 638 } | |
| 639 | |
| 640 void SearchProvider::ApplyCalculatedNavigationRelevance( | |
| 641 SearchSuggestionParser::NavigationResults* list) { | |
| 642 for (size_t i = 0; i < list->size(); ++i) { | |
| 643 SearchSuggestionParser::NavigationResult& result = (*list)[i]; | |
| 644 result.set_relevance( | |
| 645 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + | |
| 646 (list->size() - i - 1)); | |
| 647 result.set_relevance_from_server(false); | |
| 648 } | |
| 649 } | |
| 650 | |
| 651 net::URLFetcher* SearchProvider::CreateSuggestFetcher( | |
| 652 int id, | |
| 653 const TemplateURL* template_url, | |
| 654 const AutocompleteInput& input) { | |
| 655 if (!template_url || template_url->suggestions_url().empty()) | |
| 656 return NULL; | |
| 657 | |
| 658 // Bail if the suggestion URL is invalid with the given replacements. | |
| 659 TemplateURLRef::SearchTermsArgs search_term_args(input.text()); | |
| 660 search_term_args.input_type = input.type(); | |
| 661 search_term_args.cursor_position = input.cursor_position(); | |
| 662 search_term_args.page_classification = input.current_page_classification(); | |
| 663 if (OmniboxFieldTrial::EnableAnswersInSuggest()) { | |
| 664 search_term_args.session_token = GetSessionToken(); | |
| 665 if (!prefetch_data_.full_query_text.empty()) { | |
| 666 search_term_args.prefetch_query = | |
| 667 base::UTF16ToUTF8(prefetch_data_.full_query_text); | |
| 668 search_term_args.prefetch_query_type = | |
| 669 base::UTF16ToUTF8(prefetch_data_.query_type); | |
| 670 } | |
| 671 } | |
| 672 GURL suggest_url(template_url->suggestions_url_ref().ReplaceSearchTerms( | |
| 673 search_term_args, | |
| 674 providers_.template_url_service()->search_terms_data())); | |
| 675 if (!suggest_url.is_valid()) | |
| 676 return NULL; | |
| 677 // Send the current page URL if user setting and URL requirements are met and | |
| 678 // the user is in the field trial. | |
| 679 if (CanSendURL(current_page_url_, suggest_url, template_url, | |
| 680 input.current_page_classification(), | |
| 681 template_url_service_->search_terms_data(), delegate_.get()) && | |
| 682 OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial()) { | |
| 683 search_term_args.current_page_url = current_page_url_.spec(); | |
| 684 // Create the suggest URL again with the current page URL. | |
| 685 suggest_url = GURL(template_url->suggestions_url_ref().ReplaceSearchTerms( | |
| 686 search_term_args, | |
| 687 providers_.template_url_service()->search_terms_data())); | |
| 688 } | |
| 689 | |
| 690 suggest_results_pending_++; | |
| 691 LogOmniboxSuggestRequest(REQUEST_SENT); | |
| 692 | |
| 693 net::URLFetcher* fetcher = | |
| 694 net::URLFetcher::Create(id, suggest_url, net::URLFetcher::GET, this); | |
| 695 fetcher->SetRequestContext(delegate_->RequestContext()); | |
| 696 fetcher->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); | |
| 697 // Add Chrome experiment state to the request headers. | |
| 698 net::HttpRequestHeaders headers; | |
| 699 variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( | |
| 700 fetcher->GetOriginalURL(), delegate_->IsOffTheRecord(), false, &headers); | |
| 701 fetcher->SetExtraRequestHeaders(headers.ToString()); | |
| 702 fetcher->Start(); | |
| 703 return fetcher; | |
| 704 } | |
| 705 | |
| 706 void SearchProvider::ConvertResultsToAutocompleteMatches() { | |
| 707 // Convert all the results to matches and add them to a map, so we can keep | |
| 708 // the most relevant match for each result. | |
| 709 base::TimeTicks start_time(base::TimeTicks::Now()); | |
| 710 MatchMap map; | |
| 711 const base::Time no_time; | |
| 712 int did_not_accept_keyword_suggestion = | |
| 713 keyword_results_.suggest_results.empty() ? | |
| 714 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : | |
| 715 TemplateURLRef::NO_SUGGESTION_CHOSEN; | |
| 716 | |
| 717 bool relevance_from_server; | |
| 718 int verbatim_relevance = GetVerbatimRelevance(&relevance_from_server); | |
| 719 int did_not_accept_default_suggestion = | |
| 720 default_results_.suggest_results.empty() ? | |
| 721 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : | |
| 722 TemplateURLRef::NO_SUGGESTION_CHOSEN; | |
| 723 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
| 724 if (verbatim_relevance > 0) { | |
| 725 const base::string16& trimmed_verbatim = | |
| 726 base::CollapseWhitespace(input_.text(), false); | |
| 727 | |
| 728 // Verbatim results don't get suggestions and hence, answers. | |
| 729 // Scan previous matches if the last answer-bearing suggestion matches | |
| 730 // verbatim, and if so, copy over answer contents. | |
| 731 base::string16 answer_contents; | |
| 732 base::string16 answer_type; | |
| 733 for (ACMatches::iterator it = matches_.begin(); it != matches_.end(); | |
| 734 ++it) { | |
| 735 if (!it->answer_contents.empty() && | |
| 736 it->fill_into_edit == trimmed_verbatim) { | |
| 737 answer_contents = it->answer_contents; | |
| 738 answer_type = it->answer_type; | |
| 739 break; | |
| 740 } | |
| 741 } | |
| 742 | |
| 743 SearchSuggestionParser::SuggestResult verbatim( | |
| 744 trimmed_verbatim, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, | |
| 745 trimmed_verbatim, base::string16(), base::string16(), answer_contents, | |
| 746 answer_type, std::string(), std::string(), false, verbatim_relevance, | |
| 747 relevance_from_server, false, trimmed_verbatim); | |
| 748 AddMatchToMap(verbatim, std::string(), did_not_accept_default_suggestion, | |
| 749 false, keyword_url != NULL, &map); | |
| 750 } | |
| 751 if (!keyword_input_.text().empty()) { | |
| 752 // We only create the verbatim search query match for a keyword | |
| 753 // if it's not an extension keyword. Extension keywords are handled | |
| 754 // in KeywordProvider::Start(). (Extensions are complicated...) | |
| 755 // Note: in this provider, SEARCH_OTHER_ENGINE must correspond | |
| 756 // to the keyword verbatim search query. Do not create other matches | |
| 757 // of type SEARCH_OTHER_ENGINE. | |
| 758 if (keyword_url && | |
| 759 (keyword_url->GetType() != TemplateURL::OMNIBOX_API_EXTENSION)) { | |
| 760 bool keyword_relevance_from_server; | |
| 761 const int keyword_verbatim_relevance = | |
| 762 GetKeywordVerbatimRelevance(&keyword_relevance_from_server); | |
| 763 if (keyword_verbatim_relevance > 0) { | |
| 764 const base::string16& trimmed_verbatim = | |
| 765 base::CollapseWhitespace(keyword_input_.text(), false); | |
| 766 SearchSuggestionParser::SuggestResult verbatim( | |
| 767 trimmed_verbatim, AutocompleteMatchType::SEARCH_OTHER_ENGINE, | |
| 768 trimmed_verbatim, base::string16(), base::string16(), | |
| 769 base::string16(), base::string16(), std::string(), std::string(), | |
| 770 true, keyword_verbatim_relevance, keyword_relevance_from_server, | |
| 771 false, trimmed_verbatim); | |
| 772 AddMatchToMap(verbatim, std::string(), | |
| 773 did_not_accept_keyword_suggestion, false, true, &map); | |
| 774 } | |
| 775 } | |
| 776 } | |
| 777 AddHistoryResultsToMap(keyword_history_results_, true, | |
| 778 did_not_accept_keyword_suggestion, &map); | |
| 779 AddHistoryResultsToMap(default_history_results_, false, | |
| 780 did_not_accept_default_suggestion, &map); | |
| 781 | |
| 782 AddSuggestResultsToMap(keyword_results_.suggest_results, | |
| 783 keyword_results_.metadata, &map); | |
| 784 AddSuggestResultsToMap(default_results_.suggest_results, | |
| 785 default_results_.metadata, &map); | |
| 786 | |
| 787 ACMatches matches; | |
| 788 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) | |
| 789 matches.push_back(i->second); | |
| 790 | |
| 791 AddNavigationResultsToMatches(keyword_results_.navigation_results, &matches); | |
| 792 AddNavigationResultsToMatches(default_results_.navigation_results, &matches); | |
| 793 | |
| 794 // Now add the most relevant matches to |matches_|. We take up to kMaxMatches | |
| 795 // suggest/navsuggest matches, regardless of origin. If Instant Extended is | |
| 796 // enabled and we have server-provided (and thus hopefully more accurate) | |
| 797 // scores for some suggestions, we allow more of those, until we reach | |
| 798 // AutocompleteResult::kMaxMatches total matches (that is, enough to fill the | |
| 799 // whole popup). | |
| 800 // | |
| 801 // We will always return any verbatim matches, no matter how we obtained their | |
| 802 // scores, unless we have already accepted AutocompleteResult::kMaxMatches | |
| 803 // higher-scoring matches under the conditions above. | |
| 804 std::sort(matches.begin(), matches.end(), &AutocompleteMatch::MoreRelevant); | |
| 805 matches_.clear(); | |
| 806 | |
| 807 size_t num_suggestions = 0; | |
| 808 for (ACMatches::const_iterator i(matches.begin()); | |
| 809 (i != matches.end()) && | |
| 810 (matches_.size() < AutocompleteResult::kMaxMatches); | |
| 811 ++i) { | |
| 812 // SEARCH_OTHER_ENGINE is only used in the SearchProvider for the keyword | |
| 813 // verbatim result, so this condition basically means "if this match is a | |
| 814 // suggestion of some sort". | |
| 815 if ((i->type != AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED) && | |
| 816 (i->type != AutocompleteMatchType::SEARCH_OTHER_ENGINE)) { | |
| 817 // If we've already hit the limit on non-server-scored suggestions, and | |
| 818 // this isn't a server-scored suggestion we can add, skip it. | |
| 819 if ((num_suggestions >= kMaxMatches) && | |
| 820 (!chrome::IsInstantExtendedAPIEnabled() || | |
| 821 (i->GetAdditionalInfo(kRelevanceFromServerKey) != kTrue))) { | |
| 822 continue; | |
| 823 } | |
| 824 | |
| 825 ++num_suggestions; | |
| 826 } | |
| 827 | |
| 828 matches_.push_back(*i); | |
| 829 } | |
| 830 UMA_HISTOGRAM_TIMES("Omnibox.SearchProvider.ConvertResultsTime", | |
| 831 base::TimeTicks::Now() - start_time); | |
| 832 } | |
| 833 | |
| 834 ACMatches::const_iterator SearchProvider::FindTopMatch() const { | |
| 835 ACMatches::const_iterator it = matches_.begin(); | |
| 836 while ((it != matches_.end()) && !it->allowed_to_be_default_match) | |
| 837 ++it; | |
| 838 return it; | |
| 839 } | |
| 840 | |
| 841 bool SearchProvider::IsTopMatchSearchWithURLInput() const { | |
| 842 ACMatches::const_iterator first_match = FindTopMatch(); | |
| 843 return (input_.type() == metrics::OmniboxInputType::URL) && | |
| 844 (first_match != matches_.end()) && | |
| 845 (first_match->relevance > CalculateRelevanceForVerbatim()) && | |
| 846 (first_match->type != AutocompleteMatchType::NAVSUGGEST) && | |
| 847 (first_match->type != AutocompleteMatchType::NAVSUGGEST_PERSONALIZED); | |
| 848 } | |
| 849 | |
| 850 void SearchProvider::AddNavigationResultsToMatches( | |
| 851 const SearchSuggestionParser::NavigationResults& navigation_results, | |
| 852 ACMatches* matches) { | |
| 853 for (SearchSuggestionParser::NavigationResults::const_iterator it = | |
| 854 navigation_results.begin(); it != navigation_results.end(); ++it) { | |
| 855 matches->push_back(NavigationToMatch(*it)); | |
| 856 // In the absence of suggested relevance scores, use only the single | |
| 857 // highest-scoring result. (The results are already sorted by relevance.) | |
| 858 if (!it->relevance_from_server()) | |
| 859 return; | |
| 860 } | |
| 861 } | |
| 862 | |
| 863 void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results, | |
| 864 bool is_keyword, | |
| 865 int did_not_accept_suggestion, | |
| 866 MatchMap* map) { | |
| 867 if (results.empty()) | |
| 868 return; | |
| 869 | |
| 870 base::TimeTicks start_time(base::TimeTicks::Now()); | |
| 871 bool prevent_inline_autocomplete = input_.prevent_inline_autocomplete() || | |
| 872 (input_.type() == metrics::OmniboxInputType::URL); | |
| 873 const base::string16& input_text = | |
| 874 is_keyword ? keyword_input_.text() : input_.text(); | |
| 875 bool input_multiple_words = HasMultipleWords(input_text); | |
| 876 | |
| 877 SearchSuggestionParser::SuggestResults scored_results; | |
| 878 if (!prevent_inline_autocomplete && input_multiple_words) { | |
| 879 // ScoreHistoryResults() allows autocompletion of multi-word, 1-visit | |
| 880 // queries if the input also has multiple words. But if we were already | |
| 881 // scoring a multi-word, multi-visit query aggressively, and the current | |
| 882 // input is still a prefix of it, then changing the suggestion suddenly | |
| 883 // feels wrong. To detect this case, first score as if only one word has | |
| 884 // been typed, then check if the best result came from aggressive search | |
| 885 // history scoring. If it did, then just keep that score set. This | |
| 886 // 1200 the lowest possible score in CalculateRelevanceForHistory()'s | |
| 887 // aggressive-scoring curve. | |
| 888 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, | |
| 889 false, input_text, is_keyword); | |
| 890 if ((scored_results.front().relevance() < 1200) || | |
| 891 !HasMultipleWords(scored_results.front().suggestion())) | |
| 892 scored_results.clear(); // Didn't detect the case above, score normally. | |
| 893 } | |
| 894 if (scored_results.empty()) | |
| 895 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, | |
| 896 input_multiple_words, input_text, | |
| 897 is_keyword); | |
| 898 for (SearchSuggestionParser::SuggestResults::const_iterator i( | |
| 899 scored_results.begin()); i != scored_results.end(); ++i) { | |
| 900 AddMatchToMap(*i, std::string(), did_not_accept_suggestion, true, | |
| 901 providers_.GetKeywordProviderURL() != NULL, map); | |
| 902 } | |
| 903 UMA_HISTOGRAM_TIMES("Omnibox.SearchProvider.AddHistoryResultsTime", | |
| 904 base::TimeTicks::Now() - start_time); | |
| 905 } | |
| 906 | |
| 907 SearchSuggestionParser::SuggestResults SearchProvider::ScoreHistoryResults( | |
| 908 const HistoryResults& results, | |
| 909 bool base_prevent_inline_autocomplete, | |
| 910 bool input_multiple_words, | |
| 911 const base::string16& input_text, | |
| 912 bool is_keyword) { | |
| 913 SearchSuggestionParser::SuggestResults scored_results; | |
| 914 // True if the user has asked this exact query previously. | |
| 915 bool found_what_you_typed_match = false; | |
| 916 const bool prevent_search_history_inlining = | |
| 917 OmniboxFieldTrial::SearchHistoryPreventInlining( | |
| 918 input_.current_page_classification()); | |
| 919 const base::string16& trimmed_input = | |
| 920 base::CollapseWhitespace(input_text, false); | |
| 921 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); | |
| 922 ++i) { | |
| 923 const base::string16& trimmed_suggestion = | |
| 924 base::CollapseWhitespace(i->term, false); | |
| 925 | |
| 926 // Don't autocomplete multi-word queries that have only been seen once | |
| 927 // unless the user has typed more than one word. | |
| 928 bool prevent_inline_autocomplete = base_prevent_inline_autocomplete || | |
| 929 (!input_multiple_words && (i->visits < 2) && | |
| 930 HasMultipleWords(trimmed_suggestion)); | |
| 931 | |
| 932 int relevance = CalculateRelevanceForHistory( | |
| 933 i->time, is_keyword, !prevent_inline_autocomplete, | |
| 934 prevent_search_history_inlining); | |
| 935 // Add the match to |scored_results| by putting the what-you-typed match | |
| 936 // on the front and appending all other matches. We want the what-you- | |
| 937 // typed match to always be first. | |
| 938 SearchSuggestionParser::SuggestResults::iterator insertion_position = | |
| 939 scored_results.end(); | |
| 940 if (trimmed_suggestion == trimmed_input) { | |
| 941 found_what_you_typed_match = true; | |
| 942 insertion_position = scored_results.begin(); | |
| 943 } | |
| 944 scored_results.insert( | |
| 945 insertion_position, | |
| 946 SearchSuggestionParser::SuggestResult( | |
| 947 trimmed_suggestion, AutocompleteMatchType::SEARCH_HISTORY, | |
| 948 trimmed_suggestion, base::string16(), base::string16(), | |
| 949 base::string16(), base::string16(), std::string(), std::string(), | |
| 950 is_keyword, relevance, false, false, trimmed_input)); | |
| 951 } | |
| 952 | |
| 953 // History returns results sorted for us. However, we may have docked some | |
| 954 // results' scores, so things are no longer in order. While keeping the | |
| 955 // what-you-typed match at the front (if it exists), do a stable sort to get | |
| 956 // things back in order without otherwise disturbing results with equal | |
| 957 // scores, then force the scores to be unique, so that the order in which | |
| 958 // they're shown is deterministic. | |
| 959 std::stable_sort(scored_results.begin() + | |
| 960 (found_what_you_typed_match ? 1 : 0), | |
| 961 scored_results.end(), | |
| 962 CompareScoredResults()); | |
| 963 | |
| 964 // Don't autocomplete to search terms that would normally be treated as URLs | |
| 965 // when typed. For example, if the user searched for "google.com" and types | |
| 966 // "goog", don't autocomplete to the search term "google.com". Otherwise, | |
| 967 // the input will look like a URL but act like a search, which is confusing. | |
| 968 // The 1200 relevance score threshold in the test below is the lowest | |
| 969 // possible score in CalculateRelevanceForHistory()'s aggressive-scoring | |
| 970 // curve. This is an appropriate threshold to use to decide if we're overly | |
| 971 // aggressively inlining because, if we decide the answer is yes, the | |
| 972 // way we resolve it it to not use the aggressive-scoring curve. | |
| 973 // NOTE: We don't check for autocompleting to URLs in the following cases: | |
| 974 // * When inline autocomplete is disabled, we won't be inline autocompleting | |
| 975 // this term, so we don't need to worry about confusion as much. This | |
| 976 // also prevents calling Classify() again from inside the classifier | |
| 977 // (which will corrupt state and likely crash), since the classifier | |
| 978 // always disables inline autocomplete. | |
| 979 // * When the user has typed the whole string before as a query, then it's | |
| 980 // likely the user has no expectation that term should be interpreted as | |
| 981 // as a URL, so we need not do anything special to preserve user | |
| 982 // expectation. | |
| 983 int last_relevance = 0; | |
| 984 if (!base_prevent_inline_autocomplete && !found_what_you_typed_match && | |
| 985 scored_results.front().relevance() >= 1200) { | |
| 986 AutocompleteMatch match; | |
| 987 delegate_->Classify(scored_results.front().suggestion(), false, false, | |
| 988 input_.current_page_classification(), &match, NULL); | |
| 989 // Demote this match that would normally be interpreted as a URL to have | |
| 990 // the highest score a previously-issued search query could have when | |
| 991 // scoring with the non-aggressive method. A consequence of demoting | |
| 992 // by revising |last_relevance| is that this match and all following | |
| 993 // matches get demoted; the relative order of matches is preserved. | |
| 994 // One could imagine demoting only those matches that might cause | |
| 995 // confusion (which, by the way, might change the relative order of | |
| 996 // matches. We have decided to go with the simple demote-all approach | |
| 997 // because selective demotion requires multiple Classify() calls and | |
| 998 // such calls can be expensive (as expensive as running the whole | |
| 999 // autocomplete system). | |
| 1000 if (!AutocompleteMatch::IsSearchType(match.type)) { | |
| 1001 last_relevance = CalculateRelevanceForHistory( | |
| 1002 base::Time::Now(), is_keyword, false, | |
| 1003 prevent_search_history_inlining); | |
| 1004 } | |
| 1005 } | |
| 1006 | |
| 1007 for (SearchSuggestionParser::SuggestResults::iterator i( | |
| 1008 scored_results.begin()); i != scored_results.end(); ++i) { | |
| 1009 if ((last_relevance != 0) && (i->relevance() >= last_relevance)) | |
| 1010 i->set_relevance(last_relevance - 1); | |
| 1011 last_relevance = i->relevance(); | |
| 1012 } | |
| 1013 | |
| 1014 return scored_results; | |
| 1015 } | |
| 1016 | |
| 1017 void SearchProvider::AddSuggestResultsToMap( | |
| 1018 const SearchSuggestionParser::SuggestResults& results, | |
| 1019 const std::string& metadata, | |
| 1020 MatchMap* map) { | |
| 1021 for (size_t i = 0; i < results.size(); ++i) { | |
| 1022 AddMatchToMap(results[i], metadata, i, false, | |
| 1023 providers_.GetKeywordProviderURL() != NULL, map); | |
| 1024 } | |
| 1025 } | |
| 1026 | |
| 1027 int SearchProvider::GetVerbatimRelevance(bool* relevance_from_server) const { | |
| 1028 // Use the suggested verbatim relevance score if it is non-negative (valid), | |
| 1029 // if inline autocomplete isn't prevented (always show verbatim on backspace), | |
| 1030 // and if it won't suppress verbatim, leaving no default provider matches. | |
| 1031 // Otherwise, if the default provider returned no matches and was still able | |
| 1032 // to suppress verbatim, the user would have no search/nav matches and may be | |
| 1033 // left unable to search using their default provider from the omnibox. | |
| 1034 // Check for results on each verbatim calculation, as results from older | |
| 1035 // queries (on previous input) may be trimmed for failing to inline new input. | |
| 1036 bool use_server_relevance = | |
| 1037 (default_results_.verbatim_relevance >= 0) && | |
| 1038 !input_.prevent_inline_autocomplete() && | |
| 1039 ((default_results_.verbatim_relevance > 0) || | |
| 1040 !default_results_.suggest_results.empty() || | |
| 1041 !default_results_.navigation_results.empty()); | |
| 1042 if (relevance_from_server) | |
| 1043 *relevance_from_server = use_server_relevance; | |
| 1044 return use_server_relevance ? | |
| 1045 default_results_.verbatim_relevance : CalculateRelevanceForVerbatim(); | |
| 1046 } | |
| 1047 | |
| 1048 int SearchProvider::CalculateRelevanceForVerbatim() const { | |
| 1049 if (!providers_.keyword_provider().empty()) | |
| 1050 return 250; | |
| 1051 return CalculateRelevanceForVerbatimIgnoringKeywordModeState(); | |
| 1052 } | |
| 1053 | |
| 1054 int SearchProvider:: | |
| 1055 CalculateRelevanceForVerbatimIgnoringKeywordModeState() const { | |
| 1056 switch (input_.type()) { | |
| 1057 case metrics::OmniboxInputType::UNKNOWN: | |
| 1058 case metrics::OmniboxInputType::QUERY: | |
| 1059 case metrics::OmniboxInputType::FORCED_QUERY: | |
| 1060 return kNonURLVerbatimRelevance; | |
| 1061 | |
| 1062 case metrics::OmniboxInputType::URL: | |
| 1063 return 850; | |
| 1064 | |
| 1065 default: | |
| 1066 NOTREACHED(); | |
| 1067 return 0; | |
| 1068 } | |
| 1069 } | |
| 1070 | |
| 1071 int SearchProvider::GetKeywordVerbatimRelevance( | |
| 1072 bool* relevance_from_server) const { | |
| 1073 // Use the suggested verbatim relevance score if it is non-negative (valid), | |
| 1074 // if inline autocomplete isn't prevented (always show verbatim on backspace), | |
| 1075 // and if it won't suppress verbatim, leaving no keyword provider matches. | |
| 1076 // Otherwise, if the keyword provider returned no matches and was still able | |
| 1077 // to suppress verbatim, the user would have no search/nav matches and may be | |
| 1078 // left unable to search using their keyword provider from the omnibox. | |
| 1079 // Check for results on each verbatim calculation, as results from older | |
| 1080 // queries (on previous input) may be trimmed for failing to inline new input. | |
| 1081 bool use_server_relevance = | |
| 1082 (keyword_results_.verbatim_relevance >= 0) && | |
| 1083 !input_.prevent_inline_autocomplete() && | |
| 1084 ((keyword_results_.verbatim_relevance > 0) || | |
| 1085 !keyword_results_.suggest_results.empty() || | |
| 1086 !keyword_results_.navigation_results.empty()); | |
| 1087 if (relevance_from_server) | |
| 1088 *relevance_from_server = use_server_relevance; | |
| 1089 return use_server_relevance ? | |
| 1090 keyword_results_.verbatim_relevance : | |
| 1091 CalculateRelevanceForKeywordVerbatim(keyword_input_.type(), | |
| 1092 keyword_input_.prefer_keyword()); | |
| 1093 } | |
| 1094 | |
| 1095 int SearchProvider::CalculateRelevanceForHistory( | |
| 1096 const base::Time& time, | |
| 1097 bool is_keyword, | |
| 1098 bool use_aggressive_method, | |
| 1099 bool prevent_search_history_inlining) const { | |
| 1100 // The relevance of past searches falls off over time. There are two distinct | |
| 1101 // equations used. If the first equation is used (searches to the primary | |
| 1102 // provider that we want to score aggressively), the score is in the range | |
| 1103 // 1300-1599 (unless |prevent_search_history_inlining|, in which case | |
| 1104 // it's in the range 1200-1299). If the second equation is used the | |
| 1105 // relevance of a search 15 minutes ago is discounted 50 points, while the | |
| 1106 // relevance of a search two weeks ago is discounted 450 points. | |
| 1107 double elapsed_time = std::max((base::Time::Now() - time).InSecondsF(), 0.0); | |
| 1108 bool is_primary_provider = is_keyword || !providers_.has_keyword_provider(); | |
| 1109 if (is_primary_provider && use_aggressive_method) { | |
| 1110 // Searches with the past two days get a different curve. | |
| 1111 const double autocomplete_time = 2 * 24 * 60 * 60; | |
| 1112 if (elapsed_time < autocomplete_time) { | |
| 1113 int max_score = is_keyword ? 1599 : 1399; | |
| 1114 if (prevent_search_history_inlining) | |
| 1115 max_score = 1299; | |
| 1116 return max_score - static_cast<int>(99 * | |
| 1117 std::pow(elapsed_time / autocomplete_time, 2.5)); | |
| 1118 } | |
| 1119 elapsed_time -= autocomplete_time; | |
| 1120 } | |
| 1121 | |
| 1122 const int score_discount = | |
| 1123 static_cast<int>(6.5 * std::pow(elapsed_time, 0.3)); | |
| 1124 | |
| 1125 // Don't let scores go below 0. Negative relevance scores are meaningful in | |
| 1126 // a different way. | |
| 1127 int base_score; | |
| 1128 if (is_primary_provider) | |
| 1129 base_score = (input_.type() == metrics::OmniboxInputType::URL) ? 750 : 1050; | |
| 1130 else | |
| 1131 base_score = 200; | |
| 1132 return std::max(0, base_score - score_discount); | |
| 1133 } | |
| 1134 | |
| 1135 AutocompleteMatch SearchProvider::NavigationToMatch( | |
| 1136 const SearchSuggestionParser::NavigationResult& navigation) { | |
| 1137 base::string16 input; | |
| 1138 const bool trimmed_whitespace = base::TrimWhitespace( | |
| 1139 navigation.from_keyword_provider() ? | |
| 1140 keyword_input_.text() : input_.text(), | |
| 1141 base::TRIM_TRAILING, &input) != base::TRIM_NONE; | |
| 1142 AutocompleteMatch match(this, navigation.relevance(), false, | |
| 1143 navigation.type()); | |
| 1144 match.destination_url = navigation.url(); | |
| 1145 BaseSearchProvider::SetDeletionURL(navigation.deletion_url(), &match); | |
| 1146 // First look for the user's input inside the formatted url as it would be | |
| 1147 // without trimming the scheme, so we can find matches at the beginning of the | |
| 1148 // scheme. | |
| 1149 const URLPrefix* prefix = | |
| 1150 URLPrefix::BestURLPrefix(navigation.formatted_url(), input); | |
| 1151 size_t match_start = (prefix == NULL) ? | |
| 1152 navigation.formatted_url().find(input) : prefix->prefix.length(); | |
| 1153 bool trim_http = !AutocompleteInput::HasHTTPScheme(input) && | |
| 1154 (!prefix || (match_start != 0)); | |
| 1155 const net::FormatUrlTypes format_types = | |
| 1156 net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP); | |
| 1157 | |
| 1158 const std::string languages(delegate_->AcceptLanguages()); | |
| 1159 size_t inline_autocomplete_offset = (prefix == NULL) ? | |
| 1160 base::string16::npos : (match_start + input.length()); | |
| 1161 match.fill_into_edit += | |
| 1162 AutocompleteInput::FormattedStringWithEquivalentMeaning( | |
| 1163 navigation.url(), | |
| 1164 net::FormatUrl(navigation.url(), languages, format_types, | |
| 1165 net::UnescapeRule::SPACES, NULL, NULL, | |
| 1166 &inline_autocomplete_offset), | |
| 1167 delegate_->SchemeClassifier()); | |
| 1168 // Preserve the forced query '?' prefix in |match.fill_into_edit|. | |
| 1169 // Otherwise, user edits to a suggestion would show non-Search results. | |
| 1170 if (input_.type() == metrics::OmniboxInputType::FORCED_QUERY) { | |
| 1171 match.fill_into_edit.insert(0, base::ASCIIToUTF16("?")); | |
| 1172 if (inline_autocomplete_offset != base::string16::npos) | |
| 1173 ++inline_autocomplete_offset; | |
| 1174 } | |
| 1175 if (inline_autocomplete_offset != base::string16::npos) { | |
| 1176 DCHECK(inline_autocomplete_offset <= match.fill_into_edit.length()); | |
| 1177 match.inline_autocompletion = | |
| 1178 match.fill_into_edit.substr(inline_autocomplete_offset); | |
| 1179 } | |
| 1180 // An inlineable navsuggestion can only be the default match when there | |
| 1181 // is no keyword provider active, lest it appear first and break the user | |
| 1182 // out of keyword mode. It can also only be default if either the inline | |
| 1183 // autocompletion is empty or we're not preventing inline autocompletion. | |
| 1184 // Finally, if we have an inlineable navsuggestion with an inline completion | |
| 1185 // that we're not preventing, make sure we didn't trim any whitespace. | |
| 1186 // We don't want to claim http://foo.com/bar is inlineable against the | |
| 1187 // input "foo.com/b ". | |
| 1188 match.allowed_to_be_default_match = (prefix != NULL) && | |
| 1189 (providers_.GetKeywordProviderURL() == NULL) && | |
| 1190 (match.inline_autocompletion.empty() || | |
| 1191 (!input_.prevent_inline_autocomplete() && !trimmed_whitespace)); | |
| 1192 match.EnsureUWYTIsAllowedToBeDefault( | |
| 1193 input_.canonicalized_url(), providers_.template_url_service()); | |
| 1194 | |
| 1195 match.contents = navigation.match_contents(); | |
| 1196 match.contents_class = navigation.match_contents_class(); | |
| 1197 match.description = navigation.description(); | |
| 1198 AutocompleteMatch::ClassifyMatchInString(input, match.description, | |
| 1199 ACMatchClassification::NONE, &match.description_class); | |
| 1200 | |
| 1201 match.RecordAdditionalInfo( | |
| 1202 kRelevanceFromServerKey, | |
| 1203 navigation.relevance_from_server() ? kTrue : kFalse); | |
| 1204 match.RecordAdditionalInfo(kShouldPrefetchKey, kFalse); | |
| 1205 | |
| 1206 return match; | |
| 1207 } | |
| 1208 | |
| 1209 void SearchProvider::UpdateDone() { | |
| 1210 // We're done when the timer isn't running, there are no suggest queries | |
| 1211 // pending, and we're not waiting on Instant. | |
| 1212 done_ = !timer_.IsRunning() && (suggest_results_pending_ == 0); | |
| 1213 } | |
| 1214 | |
| 1215 std::string SearchProvider::GetSessionToken() { | |
| 1216 base::TimeTicks current_time(base::TimeTicks::Now()); | |
| 1217 // Renew token if it expired. | |
| 1218 if (current_time > token_expiration_time_) { | |
| 1219 const size_t kTokenBytes = 12; | |
| 1220 std::string raw_data; | |
| 1221 base::RandBytes(WriteInto(&raw_data, kTokenBytes + 1), kTokenBytes); | |
| 1222 base::Base64Encode(raw_data, ¤t_token_); | |
| 1223 | |
| 1224 // Make the base64 encoded value URL and filename safe(see RFC 3548). | |
| 1225 std::replace(current_token_.begin(), current_token_.end(), '+', '-'); | |
| 1226 std::replace(current_token_.begin(), current_token_.end(), '/', '_'); | |
| 1227 } | |
| 1228 | |
| 1229 // Extend expiration time another 60 seconds. | |
| 1230 token_expiration_time_ = current_time + base::TimeDelta::FromSeconds(60); | |
| 1231 | |
| 1232 return current_token_; | |
| 1233 } | |
| 1234 | |
| 1235 void SearchProvider::RegisterDisplayedAnswers( | |
| 1236 const AutocompleteResult& result) { | |
| 1237 if (result.empty()) | |
| 1238 return; | |
| 1239 | |
| 1240 // The answer must be in the first or second slot to be considered. It should | |
| 1241 // only be in the second slot if AutocompleteController ranked a local search | |
| 1242 // history or a verbatim item higher than the answer. | |
| 1243 AutocompleteResult::const_iterator match = result.begin(); | |
| 1244 if (match->answer_contents.empty() && result.size() > 1) | |
| 1245 ++match; | |
| 1246 if (match->answer_contents.empty() || match->answer_type.empty() || | |
| 1247 match->fill_into_edit.empty()) | |
| 1248 return; | |
| 1249 | |
| 1250 // Valid answer encountered, cache it for further queries. | |
| 1251 answers_cache_.UpdateRecentAnswers(match->fill_into_edit, match->answer_type); | |
| 1252 } | |
| 1253 | |
| 1254 void SearchProvider::DoAnswersQuery(const AutocompleteInput& input) { | |
| 1255 prefetch_data_ = answers_cache_.GetTopAnswerEntry(input.text()); | |
| 1256 } | |
| OLD | NEW |