zero_suggest_provider.cc revision 5f1c94371a64b3196d4be9466099bb892df9b88e
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/autocomplete/zero_suggest_provider.h"
6
7#include "base/callback.h"
8#include "base/i18n/case_conversion.h"
9#include "base/json/json_string_value_serializer.h"
10#include "base/metrics/histogram.h"
11#include "base/prefs/pref_service.h"
12#include "base/strings/string16.h"
13#include "base/strings/string_util.h"
14#include "base/strings/utf_string_conversions.h"
15#include "base/time/time.h"
16#include "chrome/browser/autocomplete/autocomplete_classifier.h"
17#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
18#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
19#include "chrome/browser/autocomplete/history_url_provider.h"
20#include "chrome/browser/autocomplete/search_provider.h"
21#include "chrome/browser/history/history_types.h"
22#include "chrome/browser/history/top_sites.h"
23#include "chrome/browser/omnibox/omnibox_field_trial.h"
24#include "chrome/browser/profiles/profile.h"
25#include "chrome/common/pref_names.h"
26#include "components/metrics/proto/omnibox_input_type.pb.h"
27#include "components/omnibox/autocomplete_input.h"
28#include "components/omnibox/autocomplete_match.h"
29#include "components/omnibox/autocomplete_provider_listener.h"
30#include "components/pref_registry/pref_registry_syncable.h"
31#include "components/search_engines/template_url_service.h"
32#include "components/variations/variations_http_header_provider.h"
33#include "content/public/browser/user_metrics.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 "url/gurl.h"
41
42namespace {
43
44// TODO(hfung): The histogram code was copied and modified from
45// search_provider.cc.  Refactor and consolidate the code.
46// We keep track in a histogram how many suggest requests we send, how
47// many suggest requests we invalidate (e.g., due to a user typing
48// another character), and how many replies we receive.
49// *** ADD NEW ENUMS AFTER ALL PREVIOUSLY DEFINED ONES! ***
50//     (excluding the end-of-list enum value)
51// We do not want values of existing enums to change or else it screws
52// up the statistics.
53enum ZeroSuggestRequestsHistogramValue {
54  ZERO_SUGGEST_REQUEST_SENT = 1,
55  ZERO_SUGGEST_REQUEST_INVALIDATED,
56  ZERO_SUGGEST_REPLY_RECEIVED,
57  ZERO_SUGGEST_MAX_REQUEST_HISTOGRAM_VALUE
58};
59
60void LogOmniboxZeroSuggestRequest(
61    ZeroSuggestRequestsHistogramValue request_value) {
62  UMA_HISTOGRAM_ENUMERATION("Omnibox.ZeroSuggestRequests", request_value,
63                            ZERO_SUGGEST_MAX_REQUEST_HISTOGRAM_VALUE);
64}
65
66// The maximum relevance of the top match from this provider.
67const int kDefaultVerbatimZeroSuggestRelevance = 1300;
68
69// Relevance value to use if it was not set explicitly by the server.
70const int kDefaultZeroSuggestRelevance = 100;
71
72}  // namespace
73
74// static
75ZeroSuggestProvider* ZeroSuggestProvider::Create(
76    AutocompleteProviderListener* listener,
77    TemplateURLService* template_url_service,
78    Profile* profile) {
79  return new ZeroSuggestProvider(listener, template_url_service, profile);
80}
81
82// static
83void ZeroSuggestProvider::RegisterProfilePrefs(
84    user_prefs::PrefRegistrySyncable* registry) {
85  registry->RegisterStringPref(
86      prefs::kZeroSuggestCachedResults,
87      std::string(),
88      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
89}
90
91void ZeroSuggestProvider::Start(const AutocompleteInput& input,
92                                bool minimal_changes) {
93  matches_.clear();
94  if (input.type() == metrics::OmniboxInputType::INVALID)
95    return;
96
97  Stop(true);
98  field_trial_triggered_ = false;
99  field_trial_triggered_in_session_ = false;
100  results_from_cache_ = false;
101  permanent_text_ = input.text();
102  current_query_ = input.current_url().spec();
103  current_page_classification_ = input.current_page_classification();
104  current_url_match_ = MatchForCurrentURL();
105
106  const TemplateURL* default_provider =
107     template_url_service_->GetDefaultSearchProvider();
108  if (default_provider == NULL)
109    return;
110
111  base::string16 prefix;
112  TemplateURLRef::SearchTermsArgs search_term_args(prefix);
113  GURL suggest_url(default_provider->suggestions_url_ref().ReplaceSearchTerms(
114      search_term_args, template_url_service_->search_terms_data()));
115  if (!suggest_url.is_valid())
116    return;
117
118  // No need to send the current page URL in personalized suggest field trial.
119  if (CanSendURL(input.current_url(), suggest_url, default_provider,
120                 current_page_classification_,
121                 template_url_service_->search_terms_data(), profile_) &&
122      !OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) {
123    // Update suggest_url to include the current_page_url.
124    search_term_args.current_page_url = current_query_;
125    suggest_url = GURL(default_provider->suggestions_url_ref().
126                       ReplaceSearchTerms(
127                           search_term_args,
128                           template_url_service_->search_terms_data()));
129  } else if (!CanShowZeroSuggestWithoutSendingURL(suggest_url,
130                                                  input.current_url())) {
131    return;
132  }
133
134  done_ = false;
135  // TODO(jered): Consider adding locally-sourced zero-suggestions here too.
136  // These may be useful on the NTP or more relevant to the user than server
137  // suggestions, if based on local browsing history.
138  MaybeUseCachedSuggestions();
139  Run(suggest_url);
140}
141
142void ZeroSuggestProvider::DeleteMatch(const AutocompleteMatch& match) {
143  if (OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) {
144    // Remove the deleted match from the cache, so it is not shown to the user
145    // again. Since we cannot remove just one result, blow away the cache.
146    profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults,
147                                    std::string());
148  }
149  BaseSearchProvider::DeleteMatch(match);
150}
151
152void ZeroSuggestProvider::ResetSession() {
153  // The user has started editing in the omnibox, so leave
154  // |field_trial_triggered_in_session_| unchanged and set
155  // |field_trial_triggered_| to false since zero suggest is inactive now.
156  field_trial_triggered_ = false;
157}
158
159void ZeroSuggestProvider::ModifyProviderInfo(
160    metrics::OmniboxEventProto_ProviderInfo* provider_info) const {
161  if (!results_.suggest_results.empty() || !results_.navigation_results.empty())
162    provider_info->set_times_returned_results_in_session(1);
163}
164
165ZeroSuggestProvider::ZeroSuggestProvider(
166  AutocompleteProviderListener* listener,
167  TemplateURLService* template_url_service,
168  Profile* profile)
169    : BaseSearchProvider(listener, template_url_service, profile,
170                         AutocompleteProvider::TYPE_ZERO_SUGGEST),
171      results_from_cache_(false),
172      weak_ptr_factory_(this) {
173}
174
175ZeroSuggestProvider::~ZeroSuggestProvider() {
176}
177
178bool ZeroSuggestProvider::StoreSuggestionResponse(
179    const std::string& json_data,
180    const base::Value& parsed_data) {
181  if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() ||
182      json_data.empty())
183    return false;
184  profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults, json_data);
185
186  // If we received an empty result list, we should update the display, as it
187  // may be showing cached results that should not be shown.
188  const base::ListValue* root_list = NULL;
189  const base::ListValue* results_list = NULL;
190  if (parsed_data.GetAsList(&root_list) &&
191      root_list->GetList(1, &results_list) &&
192      results_list->empty())
193    return false;
194
195  // We are finished with the request and want to bail early.
196  if (results_from_cache_)
197    done_ = true;
198
199  return results_from_cache_;
200}
201
202const TemplateURL* ZeroSuggestProvider::GetTemplateURL(bool is_keyword) const {
203  // Zero suggest provider should not receive keyword results.
204  DCHECK(!is_keyword);
205  return template_url_service_->GetDefaultSearchProvider();
206}
207
208const AutocompleteInput ZeroSuggestProvider::GetInput(bool is_keyword) const {
209  return AutocompleteInput(
210      base::string16(), base::string16::npos, base::string16(),
211      GURL(current_query_), current_page_classification_, true, false, false,
212      true, ChromeAutocompleteSchemeClassifier(profile_));
213}
214
215SearchSuggestionParser::Results* ZeroSuggestProvider::GetResultsToFill(
216    bool is_keyword) {
217  DCHECK(!is_keyword);
218  return &results_;
219}
220
221bool ZeroSuggestProvider::ShouldAppendExtraParams(
222      const SearchSuggestionParser::SuggestResult& result) const {
223  // We always use the default provider for search, so append the params.
224  return true;
225}
226
227void ZeroSuggestProvider::StopSuggest() {
228  if (suggest_results_pending_ > 0)
229    LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_INVALIDATED);
230  suggest_results_pending_ = 0;
231  fetcher_.reset();
232}
233
234void ZeroSuggestProvider::ClearAllResults() {
235  // We do not call Clear() on |results_| to retain |verbatim_relevance|
236  // value in the |results_| object. |verbatim_relevance| is used at the
237  // beginning of the next StartZeroSuggest() call to determine the current url
238  // match relevance.
239  results_.suggest_results.clear();
240  results_.navigation_results.clear();
241  current_query_.clear();
242}
243
244int ZeroSuggestProvider::GetDefaultResultRelevance() const {
245  return kDefaultZeroSuggestRelevance;
246}
247
248void ZeroSuggestProvider::RecordDeletionResult(bool success) {
249  if (success) {
250    content::RecordAction(
251        base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Success"));
252  } else {
253    content::RecordAction(
254        base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Failure"));
255  }
256}
257
258void ZeroSuggestProvider::LogFetchComplete(bool success, bool is_keyword) {
259  LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REPLY_RECEIVED);
260}
261
262bool ZeroSuggestProvider::IsKeywordFetcher(
263    const net::URLFetcher* fetcher) const {
264  // ZeroSuggestProvider does not have a keyword provider.
265  DCHECK_EQ(fetcher, fetcher_.get());
266  return false;
267}
268
269void ZeroSuggestProvider::UpdateMatches() {
270  done_ = true;
271  ConvertResultsToAutocompleteMatches();
272}
273
274void ZeroSuggestProvider::AddSuggestResultsToMap(
275    const SearchSuggestionParser::SuggestResults& results,
276    MatchMap* map) {
277  for (size_t i = 0; i < results.size(); ++i)
278    AddMatchToMap(results[i], std::string(), i, false, map);
279}
280
281AutocompleteMatch ZeroSuggestProvider::NavigationToMatch(
282    const SearchSuggestionParser::NavigationResult& navigation) {
283  AutocompleteMatch match(this, navigation.relevance(), false,
284                          navigation.type());
285  match.destination_url = navigation.url();
286
287  // Zero suggest results should always omit protocols and never appear bold.
288  const std::string languages(
289      profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
290  match.contents = net::FormatUrl(navigation.url(), languages,
291      net::kFormatUrlOmitAll, net::UnescapeRule::SPACES, NULL, NULL, NULL);
292  match.fill_into_edit +=
293      AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url(),
294          match.contents, ChromeAutocompleteSchemeClassifier(profile_));
295
296  AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0,
297      match.contents.length(), ACMatchClassification::URL,
298      &match.contents_class);
299
300  match.description =
301      AutocompleteMatch::SanitizeString(navigation.description());
302  AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0,
303      match.description.length(), ACMatchClassification::NONE,
304      &match.description_class);
305  return match;
306}
307
308void ZeroSuggestProvider::Run(const GURL& suggest_url) {
309  suggest_results_pending_ = 0;
310  const int kFetcherID = 1;
311  fetcher_.reset(
312      net::URLFetcher::Create(kFetcherID,
313          suggest_url,
314          net::URLFetcher::GET, this));
315  fetcher_->SetRequestContext(profile_->GetRequestContext());
316  fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES);
317  // Add Chrome experiment state to the request headers.
318  net::HttpRequestHeaders headers;
319  variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
320      fetcher_->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers);
321  fetcher_->SetExtraRequestHeaders(headers.ToString());
322  fetcher_->Start();
323
324  if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) {
325    most_visited_urls_.clear();
326    history::TopSites* ts = profile_->GetTopSites();
327    if (ts) {
328      ts->GetMostVisitedURLs(
329          base::Bind(&ZeroSuggestProvider::OnMostVisitedUrlsAvailable,
330                     weak_ptr_factory_.GetWeakPtr()), false);
331    }
332  }
333  suggest_results_pending_ = 1;
334  LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_SENT);
335}
336
337void ZeroSuggestProvider::OnMostVisitedUrlsAvailable(
338    const history::MostVisitedURLList& urls) {
339  most_visited_urls_ = urls;
340}
341
342void ZeroSuggestProvider::ConvertResultsToAutocompleteMatches() {
343  matches_.clear();
344
345  const TemplateURL* default_provider =
346      template_url_service_->GetDefaultSearchProvider();
347  // Fail if we can't set the clickthrough URL for query suggestions.
348  if (default_provider == NULL || !default_provider->SupportsReplacement(
349          template_url_service_->search_terms_data()))
350    return;
351
352  MatchMap map;
353  AddSuggestResultsToMap(results_.suggest_results, &map);
354
355  const int num_query_results = map.size();
356  const int num_nav_results = results_.navigation_results.size();
357  const int num_results = num_query_results + num_nav_results;
358  UMA_HISTOGRAM_COUNTS("ZeroSuggest.QueryResults", num_query_results);
359  UMA_HISTOGRAM_COUNTS("ZeroSuggest.URLResults", num_nav_results);
360  UMA_HISTOGRAM_COUNTS("ZeroSuggest.AllResults", num_results);
361
362  // Show Most Visited results after ZeroSuggest response is received.
363  if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) {
364    if (!current_url_match_.destination_url.is_valid())
365      return;
366    matches_.push_back(current_url_match_);
367    int relevance = 600;
368    if (num_results > 0) {
369      UMA_HISTOGRAM_COUNTS(
370          "Omnibox.ZeroSuggest.MostVisitedResultsCounterfactual",
371          most_visited_urls_.size());
372    }
373    const base::string16 current_query_string16(
374        base::ASCIIToUTF16(current_query_));
375    const std::string languages(
376        profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
377    for (size_t i = 0; i < most_visited_urls_.size(); i++) {
378      const history::MostVisitedURL& url = most_visited_urls_[i];
379      SearchSuggestionParser::NavigationResult nav(
380          ChromeAutocompleteSchemeClassifier(profile_), url.url,
381          AutocompleteMatchType::NAVSUGGEST, url.title, std::string(), false,
382          relevance, true, current_query_string16, languages);
383      matches_.push_back(NavigationToMatch(nav));
384      --relevance;
385    }
386    return;
387  }
388
389  if (num_results == 0)
390    return;
391
392  // TODO(jered): Rip this out once the first match is decoupled from the
393  // current typing in the omnibox.
394  matches_.push_back(current_url_match_);
395
396  for (MatchMap::const_iterator it(map.begin()); it != map.end(); ++it)
397    matches_.push_back(it->second);
398
399  const SearchSuggestionParser::NavigationResults& nav_results(
400      results_.navigation_results);
401  for (SearchSuggestionParser::NavigationResults::const_iterator it(
402           nav_results.begin()); it != nav_results.end(); ++it)
403    matches_.push_back(NavigationToMatch(*it));
404}
405
406AutocompleteMatch ZeroSuggestProvider::MatchForCurrentURL() {
407  AutocompleteMatch match;
408  AutocompleteClassifierFactory::GetForProfile(profile_)->Classify(
409      permanent_text_, false, true, current_page_classification_, &match, NULL);
410  match.is_history_what_you_typed_match = false;
411  match.allowed_to_be_default_match = true;
412
413  // The placeholder suggestion for the current URL has high relevance so
414  // that it is in the first suggestion slot and inline autocompleted. It
415  // gets dropped as soon as the user types something.
416  match.relevance = GetVerbatimRelevance();
417
418  return match;
419}
420
421int ZeroSuggestProvider::GetVerbatimRelevance() const {
422  return results_.verbatim_relevance >= 0 ?
423      results_.verbatim_relevance : kDefaultVerbatimZeroSuggestRelevance;
424}
425
426bool ZeroSuggestProvider::CanShowZeroSuggestWithoutSendingURL(
427    const GURL& suggest_url,
428    const GURL& current_page_url) const {
429  if (!ZeroSuggestEnabled(suggest_url,
430                          template_url_service_->GetDefaultSearchProvider(),
431                          current_page_classification_,
432                          template_url_service_->search_terms_data(), profile_))
433    return false;
434
435  // If we cannot send URLs, then only the MostVisited and Personalized
436  // variations can be shown.
437  if (!OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() &&
438      !OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
439    return false;
440
441  // Only show zero suggest for HTTP[S] pages.
442  // TODO(mariakhomenko): We may be able to expand this set to include pages
443  // with other schemes (e.g. chrome://). That may require improvements to
444  // the formatting of the verbatim result returned by MatchForCurrentURL().
445  if (!current_page_url.is_valid() ||
446      ((current_page_url.scheme() != url::kHttpScheme) &&
447      (current_page_url.scheme() != url::kHttpsScheme)))
448    return false;
449
450  return true;
451}
452
453void ZeroSuggestProvider::MaybeUseCachedSuggestions() {
454  if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
455    return;
456
457  std::string json_data = profile_->GetPrefs()->GetString(
458      prefs::kZeroSuggestCachedResults);
459  if (!json_data.empty()) {
460    scoped_ptr<base::Value> data(
461        SearchSuggestionParser::DeserializeJsonData(json_data));
462    if (data && ParseSuggestResults(*data.get(), false, &results_)) {
463      ConvertResultsToAutocompleteMatches();
464      results_from_cache_ = !matches_.empty();
465    }
466  }
467}
468