search_provider.cc revision 3345a6884c488ff3a535c2c9acdd33d74b37e311
1// Copyright (c) 2010 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 "app/l10n_util.h" 11#include "base/callback.h" 12#include "base/i18n/icu_string_conversions.h" 13#include "base/message_loop.h" 14#include "base/string16.h" 15#include "base/utf_string_conversions.h" 16#include "chrome/browser/autocomplete/keyword_provider.h" 17#include "chrome/browser/browser_process.h" 18#include "chrome/browser/google/google_util.h" 19#include "chrome/browser/history/history.h" 20#include "chrome/browser/net/url_fixer_upper.h" 21#include "chrome/browser/prefs/pref_service.h" 22#include "chrome/browser/profile.h" 23#include "chrome/browser/search_engines/template_url_model.h" 24#include "chrome/common/json_value_serializer.h" 25#include "chrome/common/pref_names.h" 26#include "chrome/common/url_constants.h" 27#include "googleurl/src/url_util.h" 28#include "grit/generated_resources.h" 29#include "net/base/escape.h" 30#include "net/http/http_response_headers.h" 31#include "net/url_request/url_request_status.h" 32 33using base::Time; 34using base::TimeDelta; 35 36// static 37const int SearchProvider::kDefaultProviderURLFetcherID = 1; 38// static 39const int SearchProvider::kKeywordProviderURLFetcherID = 2; 40 41// static 42bool SearchProvider::query_suggest_immediately_ = false; 43 44void SearchProvider::Providers::Set(const TemplateURL* default_provider, 45 const TemplateURL* keyword_provider) { 46 // TODO(pkasting): http://b/1162970 We shouldn't need to structure-copy 47 // this. Nor should we need |default_provider_| and |keyword_provider_| 48 // just to know whether the provider changed. 49 default_provider_ = default_provider; 50 if (default_provider) 51 cached_default_provider_ = *default_provider; 52 keyword_provider_ = keyword_provider; 53 if (keyword_provider) 54 cached_keyword_provider_ = *keyword_provider; 55} 56 57SearchProvider::SearchProvider(ACProviderListener* listener, Profile* profile) 58 : AutocompleteProvider(listener, profile, "Search"), 59 have_history_results_(false), 60 history_request_pending_(false), 61 suggest_results_pending_(0), 62 have_suggest_results_(false) { 63} 64 65void SearchProvider::Start(const AutocompleteInput& input, 66 bool minimal_changes) { 67 matches_.clear(); 68 69 // Can't return search/suggest results for bogus input or without a profile. 70 if (!profile_ || (input.type() == AutocompleteInput::INVALID)) { 71 Stop(); 72 return; 73 } 74 75 keyword_input_text_.clear(); 76 const TemplateURL* keyword_provider = 77 KeywordProvider::GetSubstitutingTemplateURLForInput(profile_, input, 78 &keyword_input_text_); 79 if (!TemplateURL::SupportsReplacement(keyword_provider) || 80 keyword_input_text_.empty()) { 81 keyword_provider = NULL; 82 } 83 84 const TemplateURL* default_provider = 85 profile_->GetTemplateURLModel()->GetDefaultSearchProvider(); 86 if (!TemplateURL::SupportsReplacement(default_provider)) 87 default_provider = NULL; 88 89 if (keyword_provider == default_provider) 90 keyword_provider = NULL; // No use in querying the same provider twice. 91 92 if (!default_provider && !keyword_provider) { 93 // No valid providers. 94 Stop(); 95 return; 96 } 97 98 // If we're still running an old query but have since changed the query text 99 // or the providers, abort the query. 100 if (!done_ && (!minimal_changes || 101 !providers_.equals(default_provider, keyword_provider))) { 102 Stop(); 103 } 104 105 providers_.Set(default_provider, keyword_provider); 106 107 if (input.text().empty()) { 108 // User typed "?" alone. Give them a placeholder result indicating what 109 // this syntax does. 110 if (default_provider) { 111 AutocompleteMatch match; 112 match.provider = this; 113 match.contents.assign(l10n_util::GetString(IDS_EMPTY_KEYWORD_VALUE)); 114 match.contents_class.push_back( 115 ACMatchClassification(0, ACMatchClassification::NONE)); 116 match.description.assign(l10n_util::GetStringF( 117 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, 118 default_provider->AdjustedShortNameForLocaleDirection())); 119 match.description_class.push_back( 120 ACMatchClassification(0, ACMatchClassification::DIM)); 121 matches_.push_back(match); 122 } 123 Stop(); 124 return; 125 } 126 127 input_ = input; 128 129 StartOrStopHistoryQuery(minimal_changes); 130 StartOrStopSuggestQuery(minimal_changes); 131 ConvertResultsToAutocompleteMatches(); 132} 133 134void SearchProvider::Run() { 135 // Start a new request with the current input. 136 DCHECK(!done_); 137 suggest_results_pending_ = 0; 138 if (providers_.valid_suggest_for_keyword_provider()) { 139 suggest_results_pending_++; 140 keyword_fetcher_.reset( 141 CreateSuggestFetcher(kKeywordProviderURLFetcherID, 142 providers_.keyword_provider(), 143 keyword_input_text_)); 144 } 145 if (providers_.valid_suggest_for_default_provider()) { 146 suggest_results_pending_++; 147 default_fetcher_.reset( 148 CreateSuggestFetcher(kDefaultProviderURLFetcherID, 149 providers_.default_provider(), input_.text())); 150 } 151 // We should only get here if we have a suggest url for the keyword or default 152 // providers. 153 DCHECK_GT(suggest_results_pending_, 0); 154} 155 156void SearchProvider::Stop() { 157 StopHistory(); 158 StopSuggest(); 159 done_ = true; 160} 161 162void SearchProvider::OnURLFetchComplete(const URLFetcher* source, 163 const GURL& url, 164 const URLRequestStatus& status, 165 int response_code, 166 const ResponseCookies& cookie, 167 const std::string& data) { 168 DCHECK(!done_); 169 suggest_results_pending_--; 170 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. 171 const net::HttpResponseHeaders* const response_headers = 172 source->response_headers(); 173 std::string json_data(data); 174 // JSON is supposed to be UTF-8, but some suggest service providers send JSON 175 // files in non-UTF-8 encodings. The actual encoding is usually specified in 176 // the Content-Type header field. 177 if (response_headers) { 178 std::string charset; 179 if (response_headers->GetCharset(&charset)) { 180 std::wstring wide_data; 181 // TODO(jungshik): Switch to CodePageToUTF8 after it's added. 182 if (base::CodepageToWide(data, charset.c_str(), 183 base::OnStringConversionError::FAIL, 184 &wide_data)) 185 json_data = WideToUTF8(wide_data); 186 } 187 } 188 189 bool is_keyword_results = (source == keyword_fetcher_.get()); 190 SuggestResults* suggest_results = is_keyword_results ? 191 &keyword_suggest_results_ : &default_suggest_results_; 192 193 if (status.is_success() && response_code == 200) { 194 JSONStringValueSerializer deserializer(json_data); 195 deserializer.set_allow_trailing_comma(true); 196 scoped_ptr<Value> root_val(deserializer.Deserialize(NULL, NULL)); 197 const std::wstring& input_text = 198 is_keyword_results ? keyword_input_text_ : input_.text(); 199 have_suggest_results_ = 200 root_val.get() && 201 ParseSuggestResults(root_val.get(), is_keyword_results, input_text, 202 suggest_results); 203 } 204 205 ConvertResultsToAutocompleteMatches(); 206 listener_->OnProviderUpdate(!suggest_results->empty()); 207} 208 209SearchProvider::~SearchProvider() { 210} 211 212void SearchProvider::StartOrStopHistoryQuery(bool minimal_changes) { 213 // For the minimal_changes case, if we finished the previous query and still 214 // have its results, or are allowed to keep running it, just do that, rather 215 // than starting a new query. 216 if (minimal_changes && 217 (have_history_results_ || (!done_ && !input_.synchronous_only()))) 218 return; 219 220 // We can't keep running any previous query, so halt it. 221 StopHistory(); 222 223 // We can't start a new query if we're only allowed synchronous results. 224 if (input_.synchronous_only()) 225 return; 226 227 // Request history for both the keyword and default provider. 228 if (providers_.valid_keyword_provider()) { 229 ScheduleHistoryQuery(providers_.keyword_provider().id(), 230 keyword_input_text_); 231 } 232 if (providers_.valid_default_provider()) { 233 ScheduleHistoryQuery(providers_.default_provider().id(), 234 input_.text()); 235 } 236} 237 238void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) { 239 // Don't send any queries to the server until some time has elapsed after 240 // the last keypress, to avoid flooding the server with requests we are 241 // likely to end up throwing away anyway. 242 static const int kQueryDelayMs = 200; 243 244 if (!IsQuerySuitableForSuggest()) { 245 StopSuggest(); 246 return; 247 } 248 249 // For the minimal_changes case, if we finished the previous query and still 250 // have its results, or are allowed to keep running it, just do that, rather 251 // than starting a new query. 252 if (minimal_changes && 253 (have_suggest_results_ || (!done_ && !input_.synchronous_only()))) 254 return; 255 256 // We can't keep running any previous query, so halt it. 257 StopSuggest(); 258 259 // We can't start a new query if we're only allowed synchronous results. 260 if (input_.synchronous_only()) 261 return; 262 263 // We'll have at least one pending fetch. Set it to 1 now, but the value is 264 // correctly set in Run. As Run isn't invoked immediately we need to set this 265 // now, else we won't think we're waiting on results from the server when we 266 // really are. 267 suggest_results_pending_ = 1; 268 269 // Kick off a timer that will start the URL fetch if it completes before 270 // the user types another character. 271 int delay = query_suggest_immediately_ ? 0 : kQueryDelayMs; 272 timer_.Start(TimeDelta::FromMilliseconds(delay), this, &SearchProvider::Run); 273} 274 275bool SearchProvider::IsQuerySuitableForSuggest() const { 276 // Don't run Suggest when off the record, the engine doesn't support it, or 277 // the user has disabled it. 278 if (profile_->IsOffTheRecord() || 279 (!providers_.valid_suggest_for_keyword_provider() && 280 !providers_.valid_suggest_for_default_provider()) || 281 !profile_->GetPrefs()->GetBoolean(prefs::kSearchSuggestEnabled)) 282 return false; 283 284 // If the input type might be a URL, we take extra care so that private data 285 // isn't sent to the server. 286 287 // FORCED_QUERY means the user is explicitly asking us to search for this, so 288 // we assume it isn't a URL and/or there isn't private data. 289 if (input_.type() == AutocompleteInput::FORCED_QUERY) 290 return true; 291 292 // Next we check the scheme. If this is UNKNOWN/REQUESTED_URL/URL with a 293 // scheme that isn't http/https/ftp, we shouldn't send it. Sending things 294 // like file: and data: is both a waste of time and a disclosure of 295 // potentially private, local data. Other "schemes" may actually be 296 // usernames, and we don't want to send passwords. If the scheme is OK, we 297 // still need to check other cases below. If this is QUERY, then the presence 298 // of these schemes means the user explicitly typed one, and thus this is 299 // probably a URL that's being entered and happens to currently be invalid -- 300 // in which case we again want to run our checks below. Other QUERY cases are 301 // less likely to be URLs and thus we assume we're OK. 302 if ((input_.scheme() != L"http") && (input_.scheme() != L"https") && 303 (input_.scheme() != L"ftp")) 304 return (input_.type() == AutocompleteInput::QUERY); 305 306 // Don't send URLs with usernames, queries or refs. Some of these are 307 // private, and the Suggest server is unlikely to have any useful results 308 // for any of them. Also don't send URLs with ports, as we may initially 309 // think that a username + password is a host + port (and we don't want to 310 // send usernames/passwords), and even if the port really is a port, the 311 // server is once again unlikely to have and useful results. 312 const url_parse::Parsed& parts = input_.parts(); 313 if (parts.username.is_nonempty() || parts.port.is_nonempty() || 314 parts.query.is_nonempty() || parts.ref.is_nonempty()) 315 return false; 316 317 // Don't send anything for https except the hostname. Hostnames are OK 318 // because they are visible when the TCP connection is established, but the 319 // specific path may reveal private information. 320 if ((input_.scheme() == L"https") && parts.path.is_nonempty()) 321 return false; 322 323 return true; 324} 325 326void SearchProvider::StopHistory() { 327 history_request_consumer_.CancelAllRequests(); 328 history_request_pending_ = false; 329 keyword_history_results_.clear(); 330 default_history_results_.clear(); 331 have_history_results_ = false; 332} 333 334void SearchProvider::StopSuggest() { 335 suggest_results_pending_ = 0; 336 timer_.Stop(); 337 // Stop any in-progress URL fetches. 338 keyword_fetcher_.reset(); 339 default_fetcher_.reset(); 340 keyword_suggest_results_.clear(); 341 default_suggest_results_.clear(); 342 keyword_navigation_results_.clear(); 343 default_navigation_results_.clear(); 344 have_suggest_results_ = false; 345} 346 347void SearchProvider::ScheduleHistoryQuery(TemplateURLID search_id, 348 const std::wstring& text) { 349 DCHECK(!text.empty()); 350 HistoryService* const history_service = 351 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); 352 HistoryService::Handle request_handle = 353 history_service->GetMostRecentKeywordSearchTerms( 354 search_id, WideToUTF16(text), static_cast<int>(kMaxMatches), 355 &history_request_consumer_, 356 NewCallback(this, 357 &SearchProvider::OnGotMostRecentKeywordSearchTerms)); 358 history_request_consumer_.SetClientData(history_service, request_handle, 359 search_id); 360 history_request_pending_ = true; 361} 362 363void SearchProvider::OnGotMostRecentKeywordSearchTerms( 364 CancelableRequestProvider::Handle handle, 365 HistoryResults* results) { 366 HistoryService* history_service = 367 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); 368 DCHECK(history_service); 369 if (providers_.valid_keyword_provider() && 370 (providers_.keyword_provider().id() == 371 history_request_consumer_.GetClientData(history_service, handle))) { 372 keyword_history_results_ = *results; 373 } else { 374 default_history_results_ = *results; 375 } 376 377 if (history_request_consumer_.PendingRequestCount() == 1) { 378 // Requests are removed AFTER the callback is invoked. If the count == 1, 379 // it means no more history requests are pending. 380 history_request_pending_ = false; 381 have_history_results_ = true; 382 } 383 384 ConvertResultsToAutocompleteMatches(); 385 listener_->OnProviderUpdate(!results->empty()); 386} 387 388URLFetcher* SearchProvider::CreateSuggestFetcher(int id, 389 const TemplateURL& provider, 390 const std::wstring& text) { 391 const TemplateURLRef* const suggestions_url = provider.suggestions_url(); 392 DCHECK(suggestions_url->SupportsReplacement()); 393 URLFetcher* fetcher = URLFetcher::Create(id, 394 GURL(suggestions_url->ReplaceSearchTerms( 395 provider, text, TemplateURLRef::NO_SUGGESTIONS_AVAILABLE, 396 std::wstring())), 397 URLFetcher::GET, this); 398 fetcher->set_request_context(profile_->GetRequestContext()); 399 fetcher->Start(); 400 return fetcher; 401} 402 403bool SearchProvider::ParseSuggestResults(Value* root_val, 404 bool is_keyword, 405 const std::wstring& input_text, 406 SuggestResults* suggest_results) { 407 if (!root_val->IsType(Value::TYPE_LIST)) 408 return false; 409 ListValue* root_list = static_cast<ListValue*>(root_val); 410 411 Value* query_val; 412 string16 query_str; 413 Value* result_val; 414 if ((root_list->GetSize() < 2) || !root_list->Get(0, &query_val) || 415 !query_val->GetAsString(&query_str) || 416 (query_str != WideToUTF16Hack(input_text)) || 417 !root_list->Get(1, &result_val) || !result_val->IsType(Value::TYPE_LIST)) 418 return false; 419 420 ListValue* description_list = NULL; 421 if (root_list->GetSize() > 2) { 422 // 3rd element: Description list. 423 Value* description_val; 424 if (root_list->Get(2, &description_val) && 425 description_val->IsType(Value::TYPE_LIST)) 426 description_list = static_cast<ListValue*>(description_val); 427 } 428 429 // We don't care about the query URL list (the fourth element in the 430 // response) for now. 431 432 // Parse optional data in the results from the Suggest server if any. 433 ListValue* type_list = NULL; 434 // 5th argument: Optional key-value pairs. 435 // TODO: We may iterate the 5th+ arguments of the root_list if any other 436 // optional data are defined. 437 if (root_list->GetSize() > 4) { 438 Value* optional_val; 439 if (root_list->Get(4, &optional_val) && 440 optional_val->IsType(Value::TYPE_DICTIONARY)) { 441 DictionaryValue* dict_val = static_cast<DictionaryValue*>(optional_val); 442 443 // Parse Google Suggest specific type extension. 444 static const std::string kGoogleSuggestType("google:suggesttype"); 445 if (dict_val->HasKey(kGoogleSuggestType)) 446 dict_val->GetList(kGoogleSuggestType, &type_list); 447 } 448 } 449 450 ListValue* result_list = static_cast<ListValue*>(result_val); 451 for (size_t i = 0; i < result_list->GetSize(); ++i) { 452 Value* suggestion_val; 453 string16 suggestion_str; 454 if (!result_list->Get(i, &suggestion_val) || 455 !suggestion_val->GetAsString(&suggestion_str)) 456 return false; 457 458 // Google search may return empty suggestions for weird input characters, 459 // they make no sense at all and can cause problem in our code. 460 // See http://crbug.com/56214 461 if (!suggestion_str.length()) 462 continue; 463 464 Value* type_val; 465 std::string type_str; 466 if (type_list && type_list->Get(i, &type_val) && 467 type_val->GetAsString(&type_str) && (type_str == "NAVIGATION")) { 468 Value* site_val; 469 string16 site_name; 470 NavigationResults& navigation_results = 471 is_keyword ? keyword_navigation_results_ : 472 default_navigation_results_; 473 if ((navigation_results.size() < kMaxMatches) && 474 description_list && description_list->Get(i, &site_val) && 475 site_val->IsType(Value::TYPE_STRING) && 476 site_val->GetAsString(&site_name)) { 477 // We can't blindly trust the URL coming from the server to be valid. 478 GURL result_url(URLFixerUpper::FixupURL(UTF16ToUTF8(suggestion_str), 479 std::string())); 480 if (result_url.is_valid()) { 481 navigation_results.push_back(NavigationResult(result_url, 482 UTF16ToWideHack(site_name))); 483 } 484 } 485 } else { 486 // TODO(kochi): Currently we treat a calculator result as a query, but it 487 // is better to have better presentation for caluculator results. 488 if (suggest_results->size() < kMaxMatches) 489 suggest_results->push_back(UTF16ToWideHack(suggestion_str)); 490 } 491 } 492 493 return true; 494} 495 496void SearchProvider::ConvertResultsToAutocompleteMatches() { 497 // Convert all the results to matches and add them to a map, so we can keep 498 // the most relevant match for each result. 499 MatchMap map; 500 const Time no_time; 501 int did_not_accept_keyword_suggestion = keyword_suggest_results_.empty() ? 502 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 503 TemplateURLRef::NO_SUGGESTION_CHOSEN; 504 // Keyword what you typed results are handled by the KeywordProvider. 505 506 int did_not_accept_default_suggestion = default_suggest_results_.empty() ? 507 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : 508 TemplateURLRef::NO_SUGGESTION_CHOSEN; 509 if (providers_.valid_default_provider()) { 510 AddMatchToMap(input_.text(), CalculateRelevanceForWhatYouTyped(), 511 AutocompleteMatch::SEARCH_WHAT_YOU_TYPED, 512 did_not_accept_default_suggestion, false, &map); 513 } 514 515 AddHistoryResultsToMap(keyword_history_results_, true, 516 did_not_accept_keyword_suggestion, &map); 517 AddHistoryResultsToMap(default_history_results_, false, 518 did_not_accept_default_suggestion, &map); 519 520 AddSuggestResultsToMap(keyword_suggest_results_, true, 521 did_not_accept_keyword_suggestion, &map); 522 AddSuggestResultsToMap(default_suggest_results_, false, 523 did_not_accept_default_suggestion, &map); 524 525 // Now add the most relevant matches from the map to |matches_|. 526 matches_.clear(); 527 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) 528 matches_.push_back(i->second); 529 530 AddNavigationResultsToMatches(keyword_navigation_results_, true); 531 AddNavigationResultsToMatches(default_navigation_results_, false); 532 533 const size_t max_total_matches = kMaxMatches + 1; // 1 for "what you typed" 534 std::partial_sort(matches_.begin(), 535 matches_.begin() + std::min(max_total_matches, matches_.size()), 536 matches_.end(), &AutocompleteMatch::MoreRelevant); 537 if (matches_.size() > max_total_matches) 538 matches_.erase(matches_.begin() + max_total_matches, matches_.end()); 539 540 UpdateStarredStateOfMatches(); 541 542 // We're done when both asynchronous subcomponents have finished. We can't 543 // use CancelableRequestConsumer.HasPendingRequests() for history requests 544 // here. A pending request is not cleared until after the completion 545 // callback has returned, but we've reached here from inside that callback. 546 // HasPendingRequests() would therefore return true, and if this is the last 547 // thing left to calculate for this query, we'll never mark the query "done". 548 done_ = !history_request_pending_ && !suggest_results_pending_; 549} 550 551void SearchProvider::AddNavigationResultsToMatches( 552 const NavigationResults& navigation_results, 553 bool is_keyword) { 554 if (!navigation_results.empty()) { 555 // TODO(kochi): http://b/1170574 We add only one results for navigational 556 // suggestions. If we can get more useful information about the score, 557 // consider adding more results. 558 const size_t num_results = is_keyword ? 559 keyword_navigation_results_.size() : default_navigation_results_.size(); 560 matches_.push_back(NavigationToMatch(navigation_results.front(), 561 CalculateRelevanceForNavigation(num_results, 0, is_keyword), 562 is_keyword)); 563 } 564} 565 566void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results, 567 bool is_keyword, 568 int did_not_accept_suggestion, 569 MatchMap* map) { 570 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); 571 ++i) { 572 AddMatchToMap(UTF16ToWide(i->term), 573 CalculateRelevanceForHistory(i->time, is_keyword), 574 AutocompleteMatch::SEARCH_HISTORY, did_not_accept_suggestion, 575 is_keyword, map); 576 } 577} 578 579void SearchProvider::AddSuggestResultsToMap( 580 const SuggestResults& suggest_results, 581 bool is_keyword, 582 int did_not_accept_suggestion, 583 MatchMap* map) { 584 for (size_t i = 0; i < suggest_results.size(); ++i) { 585 AddMatchToMap(suggest_results[i], 586 CalculateRelevanceForSuggestion(suggest_results.size(), i, 587 is_keyword), 588 AutocompleteMatch::SEARCH_SUGGEST, 589 static_cast<int>(i), is_keyword, map); 590 } 591} 592 593int SearchProvider::CalculateRelevanceForWhatYouTyped() const { 594 if (providers_.valid_keyword_provider()) 595 return 250; 596 597 switch (input_.type()) { 598 case AutocompleteInput::UNKNOWN: 599 case AutocompleteInput::QUERY: 600 case AutocompleteInput::FORCED_QUERY: 601 return 1300; 602 603 case AutocompleteInput::REQUESTED_URL: 604 return 1150; 605 606 case AutocompleteInput::URL: 607 return 850; 608 609 default: 610 NOTREACHED(); 611 return 0; 612 } 613} 614 615int SearchProvider::CalculateRelevanceForHistory(const Time& time, 616 bool is_keyword) const { 617 // The relevance of past searches falls off over time. This curve is chosen 618 // so that the relevance of a search 15 minutes ago is discounted about 50 619 // points, while the relevance of a search two weeks ago is discounted about 620 // 450 points. 621 const double elapsed_time = std::max((Time::Now() - time).InSecondsF(), 0.); 622 const int score_discount = 623 static_cast<int>(6.5 * std::pow(elapsed_time, 0.3)); 624 625 // Don't let scores go below 0. Negative relevance scores are meaningful in 626 // a different way. 627 int base_score; 628 if (!providers_.is_primary_provider(is_keyword)) 629 base_score = 200; 630 else 631 base_score = (input_.type() == AutocompleteInput::URL) ? 750 : 1050; 632 return std::max(0, base_score - score_discount); 633} 634 635int SearchProvider::CalculateRelevanceForSuggestion(size_t num_results, 636 size_t result_number, 637 bool is_keyword) const { 638 DCHECK(result_number < num_results); 639 int base_score; 640 if (!providers_.is_primary_provider(is_keyword)) 641 base_score = 100; 642 else 643 base_score = (input_.type() == AutocompleteInput::URL) ? 300 : 600; 644 return base_score + 645 static_cast<int>(num_results - 1 - result_number); 646} 647 648int SearchProvider::CalculateRelevanceForNavigation(size_t num_results, 649 size_t result_number, 650 bool is_keyword) const { 651 DCHECK(result_number < num_results); 652 // TODO(kochi): http://b/784900 Use relevance score from the NavSuggest 653 // server if possible. 654 return (providers_.is_primary_provider(is_keyword) ? 800 : 150) + 655 static_cast<int>(num_results - 1 - result_number); 656} 657 658void SearchProvider::AddMatchToMap(const std::wstring& query_string, 659 int relevance, 660 AutocompleteMatch::Type type, 661 int accepted_suggestion, 662 bool is_keyword, 663 MatchMap* map) { 664 const std::wstring& input_text = 665 is_keyword ? keyword_input_text_ : input_.text(); 666 AutocompleteMatch match(this, relevance, false, type); 667 std::vector<size_t> content_param_offsets; 668 const TemplateURL& provider = is_keyword ? providers_.keyword_provider() : 669 providers_.default_provider(); 670 // We do intra-string highlighting for suggestions - the suggested segment 671 // will be highlighted, e.g. for input_text = "you" the suggestion may be 672 // "youtube", so we'll bold the "tube" section: you*tube*. 673 if (input_text != query_string) { 674 match.contents.assign(query_string); 675 size_t input_position = match.contents.find(input_text); 676 if (input_position == std::wstring::npos) { 677 // The input text is not a substring of the query string, e.g. input 678 // text is "slasdot" and the query string is "slashdot", so we bold the 679 // whole thing. 680 match.contents_class.push_back( 681 ACMatchClassification(0, ACMatchClassification::MATCH)); 682 } else { 683 // TODO(beng): ACMatchClassification::MATCH now seems to just mean 684 // "bold" this. Consider modifying the terminology. 685 // We don't iterate over the string here annotating all matches because 686 // it looks odd to have every occurrence of a substring that may be as 687 // short as a single character highlighted in a query suggestion result, 688 // e.g. for input text "s" and query string "southwest airlines", it 689 // looks odd if both the first and last s are highlighted. 690 if (input_position != 0) { 691 match.contents_class.push_back( 692 ACMatchClassification(0, ACMatchClassification::NONE)); 693 } 694 match.contents_class.push_back( 695 ACMatchClassification(input_position, ACMatchClassification::DIM)); 696 size_t next_fragment_position = input_position + input_text.length(); 697 if (next_fragment_position < query_string.length()) { 698 match.contents_class.push_back( 699 ACMatchClassification(next_fragment_position, 700 ACMatchClassification::NONE)); 701 } 702 } 703 } else { 704 // Otherwise, we're dealing with the "default search" result which has no 705 // completion, but has the search provider name as the description. 706 match.contents.assign(query_string); 707 match.contents_class.push_back( 708 ACMatchClassification(0, ACMatchClassification::NONE)); 709 match.description.assign(l10n_util::GetStringF( 710 IDS_AUTOCOMPLETE_SEARCH_DESCRIPTION, 711 provider.AdjustedShortNameForLocaleDirection())); 712 match.description_class.push_back( 713 ACMatchClassification(0, ACMatchClassification::DIM)); 714 } 715 716 // When the user forced a query, we need to make sure all the fill_into_edit 717 // values preserve that property. Otherwise, if the user starts editing a 718 // suggestion, non-Search results will suddenly appear. 719 size_t search_start = 0; 720 if (input_.type() == AutocompleteInput::FORCED_QUERY) { 721 match.fill_into_edit.assign(L"?"); 722 ++search_start; 723 } 724 if (is_keyword) { 725 match.fill_into_edit.append(providers_.keyword_provider().keyword() + L" "); 726 match.template_url = &providers_.keyword_provider(); 727 } 728 match.fill_into_edit.append(query_string); 729 // Not all suggestions start with the original input. 730 if (!input_.prevent_inline_autocomplete() && 731 !match.fill_into_edit.compare(search_start, input_text.length(), 732 input_text)) 733 match.inline_autocomplete_offset = search_start + input_text.length(); 734 735 const TemplateURLRef* const search_url = provider.url(); 736 DCHECK(search_url->SupportsReplacement()); 737 match.destination_url = 738 GURL(search_url->ReplaceSearchTerms(provider, 739 query_string, 740 accepted_suggestion, 741 input_text)); 742 743 // Search results don't look like URLs. 744 match.transition = 745 is_keyword ? PageTransition::KEYWORD : PageTransition::GENERATED; 746 747 // Try to add |match| to |map|. If a match for |query_string| is already in 748 // |map|, replace it if |match| is more relevant. 749 // NOTE: Keep this ToLower() call in sync with url_database.cc. 750 const std::pair<MatchMap::iterator, bool> i = map->insert( 751 std::pair<std::wstring, AutocompleteMatch>( 752 UTF16ToWide(l10n_util::ToLower(WideToUTF16(query_string))), match)); 753 // NOTE: We purposefully do a direct relevance comparison here instead of 754 // using AutocompleteMatch::MoreRelevant(), so that we'll prefer "items added 755 // first" rather than "items alphabetically first" when the scores are equal. 756 // The only case this matters is when a user has results with the same score 757 // that differ only by capitalization; because the history system returns 758 // results sorted by recency, this means we'll pick the most recent such 759 // result even if the precision of our relevance score is too low to 760 // distinguish the two. 761 if (!i.second && (match.relevance > i.first->second.relevance)) 762 i.first->second = match; 763} 764 765AutocompleteMatch SearchProvider::NavigationToMatch( 766 const NavigationResult& navigation, 767 int relevance, 768 bool is_keyword) { 769 const std::wstring& input_text = 770 is_keyword ? keyword_input_text_ : input_.text(); 771 AutocompleteMatch match(this, relevance, false, 772 AutocompleteMatch::NAVSUGGEST); 773 match.destination_url = navigation.url; 774 match.contents = 775 StringForURLDisplay(navigation.url, true, !HasHTTPScheme(input_text)); 776 AutocompleteMatch::ClassifyMatchInString(input_text, match.contents, 777 ACMatchClassification::URL, 778 &match.contents_class); 779 780 match.description = navigation.site_name; 781 AutocompleteMatch::ClassifyMatchInString(input_text, navigation.site_name, 782 ACMatchClassification::NONE, 783 &match.description_class); 784 785 // When the user forced a query, we need to make sure all the fill_into_edit 786 // values preserve that property. Otherwise, if the user starts editing a 787 // suggestion, non-Search results will suddenly appear. 788 if (input_.type() == AutocompleteInput::FORCED_QUERY) 789 match.fill_into_edit.assign(L"?"); 790 match.fill_into_edit.append( 791 AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url, 792 match.contents)); 793 // TODO(pkasting): http://b/1112879 These should perhaps be 794 // inline-autocompletable? 795 796 return match; 797} 798