zero_suggest_provider.cc revision 03b57e008b61dfcb1fbad3aea950ae0e001748b0
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/metrics/user_metrics.h"
12#include "base/prefs/pref_service.h"
13#include "base/strings/string16.h"
14#include "base/strings/string_util.h"
15#include "base/strings/utf_string_conversions.h"
16#include "base/time/time.h"
17#include "chrome/browser/autocomplete/autocomplete_classifier.h"
18#include "chrome/browser/autocomplete/autocomplete_classifier_factory.h"
19#include "chrome/browser/autocomplete/chrome_autocomplete_scheme_classifier.h"
20#include "chrome/browser/autocomplete/history_url_provider.h"
21#include "chrome/browser/autocomplete/search_provider.h"
22#include "chrome/browser/history/history_types.h"
23#include "chrome/browser/history/top_sites.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/omnibox/omnibox_field_trial.h"
31#include "components/pref_registry/pref_registry_syncable.h"
32#include "components/search_engines/template_url_service.h"
33#include "components/variations/variations_http_header_provider.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::Stop(bool clear_cached_results) {
143  if (fetcher_)
144    LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_INVALIDATED);
145  fetcher_.reset();
146  done_ = true;
147
148  if (clear_cached_results) {
149    // We do not call Clear() on |results_| to retain |verbatim_relevance|
150    // value in the |results_| object. |verbatim_relevance| is used at the
151    // beginning of the next StartZeroSuggest() call to determine the current
152    // url match relevance.
153    results_.suggest_results.clear();
154    results_.navigation_results.clear();
155    current_query_.clear();
156  }
157}
158
159void ZeroSuggestProvider::DeleteMatch(const AutocompleteMatch& match) {
160  if (OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial()) {
161    // Remove the deleted match from the cache, so it is not shown to the user
162    // again. Since we cannot remove just one result, blow away the cache.
163    profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults,
164                                    std::string());
165  }
166  BaseSearchProvider::DeleteMatch(match);
167}
168
169void ZeroSuggestProvider::AddProviderInfo(ProvidersInfo* provider_info) const {
170  BaseSearchProvider::AddProviderInfo(provider_info);
171  if (!results_.suggest_results.empty() || !results_.navigation_results.empty())
172    provider_info->back().set_times_returned_results_in_session(1);
173}
174
175void ZeroSuggestProvider::ResetSession() {
176  // The user has started editing in the omnibox, so leave
177  // |field_trial_triggered_in_session_| unchanged and set
178  // |field_trial_triggered_| to false since zero suggest is inactive now.
179  field_trial_triggered_ = false;
180}
181
182ZeroSuggestProvider::ZeroSuggestProvider(
183  AutocompleteProviderListener* listener,
184  TemplateURLService* template_url_service,
185  Profile* profile)
186    : BaseSearchProvider(template_url_service, profile,
187                         AutocompleteProvider::TYPE_ZERO_SUGGEST),
188      listener_(listener),
189      results_from_cache_(false),
190      weak_ptr_factory_(this) {
191}
192
193ZeroSuggestProvider::~ZeroSuggestProvider() {
194}
195
196const TemplateURL* ZeroSuggestProvider::GetTemplateURL(bool is_keyword) const {
197  // Zero suggest provider should not receive keyword results.
198  DCHECK(!is_keyword);
199  return template_url_service_->GetDefaultSearchProvider();
200}
201
202const AutocompleteInput ZeroSuggestProvider::GetInput(bool is_keyword) const {
203  return AutocompleteInput(
204      base::string16(), base::string16::npos, base::string16(),
205      GURL(current_query_), current_page_classification_, true, false, false,
206      true, ChromeAutocompleteSchemeClassifier(profile_));
207}
208
209bool ZeroSuggestProvider::ShouldAppendExtraParams(
210      const SearchSuggestionParser::SuggestResult& result) const {
211  // We always use the default provider for search, so append the params.
212  return true;
213}
214
215void ZeroSuggestProvider::RecordDeletionResult(bool success) {
216  if (success) {
217    base::RecordAction(
218        base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Success"));
219  } else {
220    base::RecordAction(
221        base::UserMetricsAction("Omnibox.ZeroSuggestDelete.Failure"));
222  }
223}
224
225void ZeroSuggestProvider::OnURLFetchComplete(const net::URLFetcher* source) {
226  DCHECK(!done_);
227  DCHECK_EQ(fetcher_.get(), source);
228
229  LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REPLY_RECEIVED);
230
231  bool results_updated = false;
232  if (source->GetStatus().is_success() && source->GetResponseCode() == 200) {
233    std::string json_data = SearchSuggestionParser::ExtractJsonData(source);
234    scoped_ptr<base::Value> data(
235        SearchSuggestionParser::DeserializeJsonData(json_data));
236    if (data) {
237      if (StoreSuggestionResponse(json_data, *data))
238        return;
239      results_updated = ParseSuggestResults(
240          *data, kDefaultZeroSuggestRelevance, false, &results_);
241    }
242  }
243  fetcher_.reset();
244  done_ = true;
245  ConvertResultsToAutocompleteMatches();
246  listener_->OnProviderUpdate(results_updated);
247}
248
249bool ZeroSuggestProvider::StoreSuggestionResponse(
250    const std::string& json_data,
251    const base::Value& parsed_data) {
252  if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial() ||
253      json_data.empty())
254    return false;
255  profile_->GetPrefs()->SetString(prefs::kZeroSuggestCachedResults, json_data);
256
257  // If we received an empty result list, we should update the display, as it
258  // may be showing cached results that should not be shown.
259  const base::ListValue* root_list = NULL;
260  const base::ListValue* results_list = NULL;
261  if (parsed_data.GetAsList(&root_list) &&
262      root_list->GetList(1, &results_list) &&
263      results_list->empty())
264    return false;
265
266  // We are finished with the request and want to bail early.
267  if (results_from_cache_)
268    done_ = true;
269
270  return results_from_cache_;
271}
272
273void ZeroSuggestProvider::AddSuggestResultsToMap(
274    const SearchSuggestionParser::SuggestResults& results,
275    MatchMap* map) {
276  for (size_t i = 0; i < results.size(); ++i)
277    AddMatchToMap(results[i], std::string(), i, false, false, map);
278}
279
280AutocompleteMatch ZeroSuggestProvider::NavigationToMatch(
281    const SearchSuggestionParser::NavigationResult& navigation) {
282  AutocompleteMatch match(this, navigation.relevance(), false,
283                          navigation.type());
284  match.destination_url = navigation.url();
285
286  // Zero suggest results should always omit protocols and never appear bold.
287  const std::string languages(
288      profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
289  match.contents = net::FormatUrl(navigation.url(), languages,
290      net::kFormatUrlOmitAll, net::UnescapeRule::SPACES, NULL, NULL, NULL);
291  match.fill_into_edit +=
292      AutocompleteInput::FormattedStringWithEquivalentMeaning(navigation.url(),
293          match.contents, ChromeAutocompleteSchemeClassifier(profile_));
294
295  AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0,
296      match.contents.length(), ACMatchClassification::URL,
297      &match.contents_class);
298
299  match.description =
300      AutocompleteMatch::SanitizeString(navigation.description());
301  AutocompleteMatch::ClassifyLocationInString(base::string16::npos, 0,
302      match.description.length(), ACMatchClassification::NONE,
303      &match.description_class);
304  return match;
305}
306
307void ZeroSuggestProvider::Run(const GURL& suggest_url) {
308  const int kFetcherID = 1;
309  fetcher_.reset(
310      net::URLFetcher::Create(kFetcherID,
311          suggest_url,
312          net::URLFetcher::GET, this));
313  fetcher_->SetRequestContext(profile_->GetRequestContext());
314  fetcher_->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES);
315  // Add Chrome experiment state to the request headers.
316  net::HttpRequestHeaders headers;
317  variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
318      fetcher_->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers);
319  fetcher_->SetExtraRequestHeaders(headers.ToString());
320  fetcher_->Start();
321
322  if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) {
323    most_visited_urls_.clear();
324    history::TopSites* ts = profile_->GetTopSites();
325    if (ts) {
326      ts->GetMostVisitedURLs(
327          base::Bind(&ZeroSuggestProvider::OnMostVisitedUrlsAvailable,
328                     weak_ptr_factory_.GetWeakPtr()), false);
329    }
330  }
331  LogOmniboxZeroSuggestRequest(ZERO_SUGGEST_REQUEST_SENT);
332}
333
334void ZeroSuggestProvider::OnMostVisitedUrlsAvailable(
335    const history::MostVisitedURLList& urls) {
336  most_visited_urls_ = urls;
337}
338
339void ZeroSuggestProvider::ConvertResultsToAutocompleteMatches() {
340  matches_.clear();
341
342  const TemplateURL* default_provider =
343      template_url_service_->GetDefaultSearchProvider();
344  // Fail if we can't set the clickthrough URL for query suggestions.
345  if (default_provider == NULL || !default_provider->SupportsReplacement(
346          template_url_service_->search_terms_data()))
347    return;
348
349  MatchMap map;
350  AddSuggestResultsToMap(results_.suggest_results, &map);
351
352  const int num_query_results = map.size();
353  const int num_nav_results = results_.navigation_results.size();
354  const int num_results = num_query_results + num_nav_results;
355  UMA_HISTOGRAM_COUNTS("ZeroSuggest.QueryResults", num_query_results);
356  UMA_HISTOGRAM_COUNTS("ZeroSuggest.URLResults", num_nav_results);
357  UMA_HISTOGRAM_COUNTS("ZeroSuggest.AllResults", num_results);
358
359  // Show Most Visited results after ZeroSuggest response is received.
360  if (OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial()) {
361    if (!current_url_match_.destination_url.is_valid())
362      return;
363    matches_.push_back(current_url_match_);
364    int relevance = 600;
365    if (num_results > 0) {
366      UMA_HISTOGRAM_COUNTS(
367          "Omnibox.ZeroSuggest.MostVisitedResultsCounterfactual",
368          most_visited_urls_.size());
369    }
370    const base::string16 current_query_string16(
371        base::ASCIIToUTF16(current_query_));
372    const std::string languages(
373        profile_->GetPrefs()->GetString(prefs::kAcceptLanguages));
374    for (size_t i = 0; i < most_visited_urls_.size(); i++) {
375      const history::MostVisitedURL& url = most_visited_urls_[i];
376      SearchSuggestionParser::NavigationResult nav(
377          ChromeAutocompleteSchemeClassifier(profile_), url.url,
378          AutocompleteMatchType::NAVSUGGEST, url.title, std::string(), false,
379          relevance, true, current_query_string16, languages);
380      matches_.push_back(NavigationToMatch(nav));
381      --relevance;
382    }
383    return;
384  }
385
386  if (num_results == 0)
387    return;
388
389  // TODO(jered): Rip this out once the first match is decoupled from the
390  // current typing in the omnibox.
391  matches_.push_back(current_url_match_);
392
393  for (MatchMap::const_iterator it(map.begin()); it != map.end(); ++it)
394    matches_.push_back(it->second);
395
396  const SearchSuggestionParser::NavigationResults& nav_results(
397      results_.navigation_results);
398  for (SearchSuggestionParser::NavigationResults::const_iterator it(
399           nav_results.begin()); it != nav_results.end(); ++it)
400    matches_.push_back(NavigationToMatch(*it));
401}
402
403AutocompleteMatch ZeroSuggestProvider::MatchForCurrentURL() {
404  AutocompleteMatch match;
405  AutocompleteClassifierFactory::GetForProfile(profile_)->Classify(
406      permanent_text_, false, true, current_page_classification_, &match, NULL);
407  match.is_history_what_you_typed_match = false;
408  match.allowed_to_be_default_match = true;
409
410  // The placeholder suggestion for the current URL has high relevance so
411  // that it is in the first suggestion slot and inline autocompleted. It
412  // gets dropped as soon as the user types something.
413  match.relevance = GetVerbatimRelevance();
414
415  return match;
416}
417
418int ZeroSuggestProvider::GetVerbatimRelevance() const {
419  return results_.verbatim_relevance >= 0 ?
420      results_.verbatim_relevance : kDefaultVerbatimZeroSuggestRelevance;
421}
422
423bool ZeroSuggestProvider::CanShowZeroSuggestWithoutSendingURL(
424    const GURL& suggest_url,
425    const GURL& current_page_url) const {
426  if (!ZeroSuggestEnabled(suggest_url,
427                          template_url_service_->GetDefaultSearchProvider(),
428                          current_page_classification_,
429                          template_url_service_->search_terms_data(), profile_))
430    return false;
431
432  // If we cannot send URLs, then only the MostVisited and Personalized
433  // variations can be shown.
434  if (!OmniboxFieldTrial::InZeroSuggestMostVisitedFieldTrial() &&
435      !OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
436    return false;
437
438  // Only show zero suggest for HTTP[S] pages.
439  // TODO(mariakhomenko): We may be able to expand this set to include pages
440  // with other schemes (e.g. chrome://). That may require improvements to
441  // the formatting of the verbatim result returned by MatchForCurrentURL().
442  if (!current_page_url.is_valid() ||
443      ((current_page_url.scheme() != url::kHttpScheme) &&
444      (current_page_url.scheme() != url::kHttpsScheme)))
445    return false;
446
447  return true;
448}
449
450void ZeroSuggestProvider::MaybeUseCachedSuggestions() {
451  if (!OmniboxFieldTrial::InZeroSuggestPersonalizedFieldTrial())
452    return;
453
454  std::string json_data = profile_->GetPrefs()->GetString(
455      prefs::kZeroSuggestCachedResults);
456  if (!json_data.empty()) {
457    scoped_ptr<base::Value> data(
458        SearchSuggestionParser::DeserializeJsonData(json_data));
459    if (data && ParseSuggestResults(
460            *data, kDefaultZeroSuggestRelevance, false, &results_)) {
461      ConvertResultsToAutocompleteMatches();
462      results_from_cache_ = !matches_.empty();
463    }
464  }
465}
466