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