Omnibox SearchProvider Experiment Client Implementation.
See http://goto/omnibox-v20-experiment and http://b/6426178

*Accept suggestion and verbatim relevance scores extensions.
*Keep inline autocompletable matches between URL requests.
-Only accept suggested relevances if Instant is disabled.
-Reject scores that make the top match non-inlinable.
-Make BestURLPrefix perform case-insensitive comparison.

TODO: Limit # of suggestions parsed or clamp their scores?

BUG=125871
TEST=SearchProviderTest.SuggestRelevanceExperiment & manual.

Review URL: https://chromiumcodereview.appspot.com/10274023

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@140512 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/autocomplete/search_provider.cc b/chrome/browser/autocomplete/search_provider.cc
index 28a5d2b..d9d5529 100644
--- a/chrome/browser/autocomplete/search_provider.cc
+++ b/chrome/browser/autocomplete/search_provider.cc
@@ -63,7 +63,7 @@
   return false;
 }
 
-};
+}  // namespace
 
 
 // SearchProvider::Providers --------------------------------------------------
@@ -96,6 +96,8 @@
     : AutocompleteProvider(listener, profile, "Search"),
       providers_(TemplateURLServiceFactory::GetForProfile(profile)),
       suggest_results_pending_(0),
+      has_suggested_relevance_(false),
+      verbatim_relevance_(-1),
       have_suggest_results_(false),
       instant_finalized_(false) {
   // We use GetSuggestNumberOfGroups() as the group ID to mean "not in field
@@ -308,7 +310,6 @@
 
 void SearchProvider::Stop() {
   StopSuggest();
-  ClearResults();
   done_ = true;
   default_provider_suggest_text_.clear();
 }
@@ -441,7 +442,9 @@
 
   // We can't keep running any previous query, so halt it.
   StopSuggest();
-  ClearResults();
+
+  // Remove existing results that cannot inline autocomplete the new input.
+  RemoveStaleResults();
 
   // We can't start a new query if we're only allowed synchronous results.
   if (input_.matches_requested() != AutocompleteInput::ALL_MATCHES)
@@ -528,9 +531,60 @@
   default_suggest_results_.clear();
   keyword_navigation_results_.clear();
   default_navigation_results_.clear();
+  has_suggested_relevance_ = false;
+  verbatim_relevance_ = -1;
   have_suggest_results_ = false;
 }
 
+void SearchProvider::RemoveStaleResults() {
+  RemoveStaleSuggestResults(&keyword_suggest_results_, true);
+  RemoveStaleSuggestResults(&default_suggest_results_, false);
+  RemoveStaleNavigationResults(&keyword_navigation_results_, true);
+  RemoveStaleNavigationResults(&default_navigation_results_, false);
+}
+
+void SearchProvider::RemoveStaleSuggestResults(SuggestResults* list,
+                                               bool is_keyword) {
+  const string16& input = is_keyword ? keyword_input_text_ : input_.text();
+  for (SuggestResults::iterator i = list->begin(); i < list->end();)
+    i = StartsWith(i->suggestion(), input, false) ? (i + 1) : list->erase(i);
+}
+
+void SearchProvider::RemoveStaleNavigationResults(NavigationResults* list,
+                                                  bool is_keyword) {
+  const string16& input = is_keyword ? keyword_input_text_ : input_.text();
+  for (NavigationResults::iterator i = list->begin(); i < list->end();) {
+    const string16 fill(AutocompleteInput::FormattedStringWithEquivalentMeaning(
+        i->url(), StringForURLDisplay(i->url(), true, false)));
+    i = URLPrefix::BestURLPrefix(fill, input) ? (i + 1) : list->erase(i);
+  }
+}
+
+void SearchProvider::ApplyCalculatedRelevance() {
+  ApplyCalculatedSuggestRelevance(&keyword_suggest_results_, true);
+  ApplyCalculatedSuggestRelevance(&default_suggest_results_, false);
+  ApplyCalculatedNavigationRelevance(&keyword_navigation_results_, true);
+  ApplyCalculatedNavigationRelevance(&default_navigation_results_, false);
+  has_suggested_relevance_ = false;
+  verbatim_relevance_ = -1;
+}
+
+void SearchProvider::ApplyCalculatedSuggestRelevance(SuggestResults* list,
+                                                     bool is_keyword) {
+  for (size_t i = 0; i < list->size(); ++i) {
+    (*list)[i].set_relevance(CalculateRelevanceForSuggestion(is_keyword) +
+                             (list->size() - i - 1));
+  }
+}
+
+void SearchProvider::ApplyCalculatedNavigationRelevance(NavigationResults* list,
+                                                        bool is_keyword) {
+  for (size_t i = 0; i < list->size(); ++i) {
+    (*list)[i].set_relevance(CalculateRelevanceForNavigation(is_keyword) +
+                             (list->size() - i - 1));
+  }
+}
+
 net::URLFetcher* SearchProvider::CreateSuggestFetcher(
     int id,
     const TemplateURLRef& suggestions_url,
@@ -564,12 +618,28 @@
 
   // 4th element: Disregard the query URL list for now.
 
+  // Reset suggested relevance information from the default provider.
+  if (!is_keyword) {
+    has_suggested_relevance_ = false;
+    verbatim_relevance_ = -1;
+  }
+
   // 5th element: Optional key-value pairs from the Suggest server.
   ListValue* types = NULL;
-  DictionaryValue* dict_val = NULL;
-  if (root_list->GetDictionary(4, &dict_val)) {
-    // Parse Google Suggest specific type extension.
-    dict_val->GetList("google:suggesttype", &types);
+  ListValue* relevances = NULL;
+  DictionaryValue* extras = NULL;
+  if (root_list->GetDictionary(4, &extras)) {
+    extras->GetList("google:suggesttype", &types);
+
+    // Only accept relevance suggestions if Instant is disabled.
+    if (!is_keyword && !InstantController::IsEnabled(profile_)) {
+      // Discard this list if its size does not match that of the suggestions.
+      if (extras->GetList("google:suggestrelevance", &relevances) &&
+          relevances->GetSize() != results->GetSize())
+        relevances = NULL;
+
+      extras->GetInteger("google:verbatimrelevance", &verbatim_relevance_);
+    }
   }
 
   SuggestResults* suggest_results =
@@ -577,46 +647,46 @@
   NavigationResults* navigation_results =
       is_keyword ? &keyword_navigation_results_ : &default_navigation_results_;
 
-  string16 result;
-  bool results_updated = false;
+  // Clear the previous results now that new results are available.
+  suggest_results->clear();
+  navigation_results->clear();
+
+  string16 result, title;
+  std::string type;
+  int relevance = -1;
   for (size_t index = 0; results->GetString(index, &result); ++index) {
     // Google search may return empty suggestions for weird input characters,
     // they make no sense at all and can cause problems in our code.
     if (result.empty())
       continue;
 
-    std::string type;
+    // Apply valid suggested relevance scores; discard invalid lists.
+    if (relevances != NULL && !relevances->GetInteger(index, &relevance))
+      relevances = NULL;
     if (types && types->GetString(index, &type) && (type == "NAVIGATION")) {
-      if (navigation_results->size() < kMaxMatches) {
-        // Do not blindly trust the URL coming from the server to be valid.
-        GURL url(URLFixerUpper::FixupURL(UTF16ToUTF8(result), std::string()));
-        if (url.is_valid()) {
-          string16 description;
-          if (descriptions != NULL)
-            descriptions->GetString(index, &description);
-          // Decrement the relevance for successive results to preserve order.
-          int relevance = CalculateRelevanceForNavigation(is_keyword) +
-                          (kMaxMatches - navigation_results->size() - 1);
-          navigation_results->push_back(
-              NavigationResult(url, description, relevance));
-          results_updated = true;
-        }
+      // Do not blindly trust the URL coming from the server to be valid.
+      GURL url(URLFixerUpper::FixupURL(UTF16ToUTF8(result), std::string()));
+      if (url.is_valid()) {
+        if (descriptions != NULL)
+          descriptions->GetString(index, &title);
+        navigation_results->push_back(NavigationResult(url, title, relevance));
       }
     } else {
-      // TODO(kochi): Currently we treat a calculator result as a query, but it
-      // is better to have better presentation for caluculator results.
-      if (suggest_results->size() < kMaxMatches) {
-        // Decrement the relevance for successive results to preserve order.
-        int relevance = CalculateRelevanceForSuggestion(is_keyword) +
-                        (kMaxMatches - suggest_results->size() - 1);
-        suggest_results->push_back(SuggestResult(result, relevance));
-        results_updated = true;
-      }
+      // TODO(kochi): Improve calculator result presentation.
+      suggest_results->push_back(SuggestResult(result, relevance));
     }
   }
 
+  // Apply calculated relevance scores if a valid list was not provided.
+  if (relevances == NULL) {
+    ApplyCalculatedSuggestRelevance(suggest_results, is_keyword);
+    ApplyCalculatedNavigationRelevance(navigation_results, is_keyword);
+  } else if (!is_keyword) {
+    has_suggested_relevance_ = true;
+  }
+
   have_suggest_results_ = true;
-  return results_updated;
+  return true;
 }
 
 void SearchProvider::ConvertResultsToAutocompleteMatches() {
@@ -633,9 +703,11 @@
   int did_not_accept_default_suggestion = default_suggest_results_.empty() ?
       TemplateURLRef::NO_SUGGESTIONS_AVAILABLE :
       TemplateURLRef::NO_SUGGESTION_CHOSEN;
-  AddMatchToMap(input_.text(), input_.text(), verbatim_relevance,
-                AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
-                did_not_accept_default_suggestion, false, &map);
+  if (verbatim_relevance > 0) {
+    AddMatchToMap(input_.text(), input_.text(), verbatim_relevance,
+                  AutocompleteMatch::SEARCH_WHAT_YOU_TYPED,
+                  did_not_accept_default_suggestion, false, &map);
+  }
   const size_t what_you_typed_size = map.size();
   if (!default_provider_suggest_text_.empty()) {
     AddMatchToMap(input_.text() + default_provider_suggest_text_,
@@ -668,6 +740,17 @@
   if (matches_.size() > max_total_matches)
     matches_.erase(matches_.begin() + max_total_matches, matches_.end());
 
+  // The top result must be inlinable; apply calculated scores if it is not.
+  if (!matches_.empty() &&
+      (has_suggested_relevance_ || verbatim_relevance_ >= 0) &&
+      (matches_.front().type == AutocompleteMatch::SEARCH_SUGGEST ||
+       matches_.front().type == AutocompleteMatch::NAVSUGGEST) &&
+      matches_.front().inline_autocomplete_offset == string16::npos &&
+      matches_.front().fill_into_edit != input_.text()) {
+    ApplyCalculatedRelevance();
+    ConvertResultsToAutocompleteMatches();
+  }
+
   UpdateStarredStateOfMatches();
   UpdateDone();
 }
@@ -796,6 +879,9 @@
 }
 
 int SearchProvider::CalculateRelevanceForWhatYouTyped() const {
+  if (verbatim_relevance_ >= 0 && !input_.prevent_inline_autocomplete())
+    return verbatim_relevance_;
+
   if (!providers_.keyword_provider().empty())
     return 250;