Home | History | Annotate | Download | only in autocomplete
      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/autocomplete_result.h"
      6 
      7 #include <algorithm>
      8 #include <iterator>
      9 
     10 #include "base/logging.h"
     11 #include "chrome/browser/autocomplete/autocomplete_input.h"
     12 #include "chrome/browser/autocomplete/autocomplete_match.h"
     13 #include "chrome/browser/autocomplete/autocomplete_provider.h"
     14 #include "chrome/browser/omnibox/omnibox_field_trial.h"
     15 #include "chrome/browser/search/search.h"
     16 #include "chrome/common/autocomplete_match_type.h"
     17 
     18 namespace {
     19 
     20 // This class implements a special version of AutocompleteMatch::MoreRelevant
     21 // that allows matches of particular types to be demoted in AutocompleteResult.
     22 class CompareWithDemoteByType {
     23  public:
     24   CompareWithDemoteByType(
     25       AutocompleteInput::PageClassification current_page_classification);
     26 
     27   // Returns the relevance score of |match| demoted appropriately by
     28   // |demotions_by_type_|.
     29   int GetDemotedRelevance(const AutocompleteMatch& match);
     30 
     31   // Comparison function.
     32   bool operator()(const AutocompleteMatch& elem1,
     33                   const AutocompleteMatch& elem2);
     34 
     35  private:
     36   OmniboxFieldTrial::DemotionMultipliers demotions_;
     37 };
     38 
     39 CompareWithDemoteByType::CompareWithDemoteByType(
     40     AutocompleteInput::PageClassification current_page_classification) {
     41   OmniboxFieldTrial::GetDemotionsByType(current_page_classification,
     42                                         &demotions_);
     43 }
     44 
     45 int CompareWithDemoteByType::GetDemotedRelevance(
     46     const AutocompleteMatch& match) {
     47   OmniboxFieldTrial::DemotionMultipliers::const_iterator demotion_it =
     48       demotions_.find(match.type);
     49   return (demotion_it == demotions_.end()) ?
     50       match.relevance : (match.relevance * demotion_it->second);
     51 }
     52 
     53 bool CompareWithDemoteByType::operator()(const AutocompleteMatch& elem1,
     54                                          const AutocompleteMatch& elem2) {
     55   // Compute demoted relevance scores for each match.
     56   const int demoted_relevance1 = GetDemotedRelevance(elem1);
     57   const int demoted_relevance2 = GetDemotedRelevance(elem2);
     58   // For equal-relevance matches, we sort alphabetically, so that providers
     59   // who return multiple elements at the same priority get a "stable" sort
     60   // across multiple updates.
     61   return (demoted_relevance1 == demoted_relevance2) ?
     62       (elem1.contents < elem2.contents) :
     63       (demoted_relevance1 > demoted_relevance2);
     64 }
     65 
     66 };  // namespace
     67 
     68 // static
     69 const size_t AutocompleteResult::kMaxMatches = 6;
     70 const int AutocompleteResult::kLowestDefaultScore = 1200;
     71 
     72 void AutocompleteResult::Selection::Clear() {
     73   destination_url = GURL();
     74   provider_affinity = NULL;
     75   is_history_what_you_typed_match = false;
     76 }
     77 
     78 AutocompleteResult::AutocompleteResult() {
     79   // Reserve space for the max number of matches we'll show.
     80   matches_.reserve(kMaxMatches);
     81 
     82   // It's probably safe to do this in the initializer list, but there's little
     83   // penalty to doing it here and it ensures our object is fully constructed
     84   // before calling member functions.
     85   default_match_ = end();
     86 }
     87 
     88 AutocompleteResult::~AutocompleteResult() {}
     89 
     90 void AutocompleteResult::CopyOldMatches(const AutocompleteInput& input,
     91                                         const AutocompleteResult& old_matches,
     92                                         Profile* profile) {
     93   if (old_matches.empty())
     94     return;
     95 
     96   if (empty()) {
     97     // If we've got no matches we can copy everything from the last result.
     98     CopyFrom(old_matches);
     99     for (ACMatches::iterator i(begin()); i != end(); ++i)
    100       i->from_previous = true;
    101     return;
    102   }
    103 
    104   // In hopes of providing a stable popup we try to keep the number of matches
    105   // per provider consistent. Other schemes (such as blindly copying the most
    106   // relevant matches) typically result in many successive 'What You Typed'
    107   // results filling all the matches, which looks awful.
    108   //
    109   // Instead of starting with the current matches and then adding old matches
    110   // until we hit our overall limit, we copy enough old matches so that each
    111   // provider has at least as many as before, and then use SortAndCull() to
    112   // clamp globally. This way, old high-relevance matches will starve new
    113   // low-relevance matches, under the assumption that the new matches will
    114   // ultimately be similar.  If the assumption holds, this prevents seeing the
    115   // new low-relevance match appear and then quickly get pushed off the bottom;
    116   // if it doesn't, then once the providers are done and we expire the old
    117   // matches, the new ones will all become visible, so we won't have lost
    118   // anything permanently.
    119   ProviderToMatches matches_per_provider, old_matches_per_provider;
    120   BuildProviderToMatches(&matches_per_provider);
    121   old_matches.BuildProviderToMatches(&old_matches_per_provider);
    122   for (ProviderToMatches::const_iterator i(old_matches_per_provider.begin());
    123        i != old_matches_per_provider.end(); ++i) {
    124     MergeMatchesByProvider(input.current_page_classification(),
    125                            i->second, matches_per_provider[i->first]);
    126   }
    127 
    128   SortAndCull(input, profile);
    129 }
    130 
    131 void AutocompleteResult::AppendMatches(const ACMatches& matches) {
    132 #ifndef NDEBUG
    133   for (ACMatches::const_iterator i(matches.begin()); i != matches.end(); ++i) {
    134     DCHECK_EQ(AutocompleteMatch::SanitizeString(i->contents), i->contents);
    135     DCHECK_EQ(AutocompleteMatch::SanitizeString(i->description),
    136               i->description);
    137   }
    138 #endif
    139   std::copy(matches.begin(), matches.end(), std::back_inserter(matches_));
    140   default_match_ = end();
    141   alternate_nav_url_ = GURL();
    142 }
    143 
    144 void AutocompleteResult::SortAndCull(const AutocompleteInput& input,
    145                                      Profile* profile) {
    146   for (ACMatches::iterator i(matches_.begin()); i != matches_.end(); ++i)
    147     i->ComputeStrippedDestinationURL(profile);
    148 
    149   // Remove duplicates.
    150   std::sort(matches_.begin(), matches_.end(),
    151             &AutocompleteMatch::DestinationSortFunc);
    152   matches_.erase(std::unique(matches_.begin(), matches_.end(),
    153                              &AutocompleteMatch::DestinationsEqual),
    154                  matches_.end());
    155 
    156   // Sort and trim to the most relevant kMaxMatches matches.
    157   size_t max_num_matches = std::min(kMaxMatches, matches_.size());
    158   CompareWithDemoteByType comparing_object(input.current_page_classification());
    159   std::sort(matches_.begin(), matches_.end(), comparing_object);
    160   if (!matches_.empty() && !matches_.begin()->allowed_to_be_default_match &&
    161       OmniboxFieldTrial::ReorderForLegalDefaultMatch(
    162           input.current_page_classification())) {
    163     // Top match is not allowed to be the default match.  Find the most
    164     // relevant legal match and shift it to the front.
    165     for (AutocompleteResult::iterator it = matches_.begin() + 1;
    166          it != matches_.end(); ++it) {
    167       if (it->allowed_to_be_default_match) {
    168         std::rotate(matches_.begin(), it, it + 1);
    169         break;
    170       }
    171     }
    172   }
    173   // In the process of trimming, drop all matches with a demoted relevance
    174   // score of 0.
    175   size_t num_matches;
    176   for (num_matches = 0u; (num_matches < max_num_matches) &&
    177        (comparing_object.GetDemotedRelevance(*match_at(num_matches)) > 0);
    178        ++num_matches) {}
    179   matches_.resize(num_matches);
    180 
    181   default_match_ = matches_.begin();
    182   DCHECK((default_match_ == matches_.end()) ||
    183       default_match_->allowed_to_be_default_match);
    184 
    185   // Set the alternate nav URL.
    186   alternate_nav_url_ = (default_match_ == matches_.end()) ?
    187       GURL() : ComputeAlternateNavUrl(input, *default_match_);
    188 }
    189 
    190 bool AutocompleteResult::HasCopiedMatches() const {
    191   for (ACMatches::const_iterator i(begin()); i != end(); ++i) {
    192     if (i->from_previous)
    193       return true;
    194   }
    195   return false;
    196 }
    197 
    198 size_t AutocompleteResult::size() const {
    199   return matches_.size();
    200 }
    201 
    202 bool AutocompleteResult::empty() const {
    203   return matches_.empty();
    204 }
    205 
    206 AutocompleteResult::const_iterator AutocompleteResult::begin() const {
    207   return matches_.begin();
    208 }
    209 
    210 AutocompleteResult::iterator AutocompleteResult::begin() {
    211   return matches_.begin();
    212 }
    213 
    214 AutocompleteResult::const_iterator AutocompleteResult::end() const {
    215   return matches_.end();
    216 }
    217 
    218 AutocompleteResult::iterator AutocompleteResult::end() {
    219   return matches_.end();
    220 }
    221 
    222 // Returns the match at the given index.
    223 const AutocompleteMatch& AutocompleteResult::match_at(size_t index) const {
    224   DCHECK_LT(index, matches_.size());
    225   return matches_[index];
    226 }
    227 
    228 AutocompleteMatch* AutocompleteResult::match_at(size_t index) {
    229   DCHECK_LT(index, matches_.size());
    230   return &matches_[index];
    231 }
    232 
    233 bool AutocompleteResult::ShouldHideTopMatch() const {
    234   // Gate on our field trial flag.
    235   if (!chrome::ShouldHideTopVerbatimMatch())
    236     return false;
    237 
    238   // If we don't have a verbatim first match, show everything.
    239   if (empty() || !match_at(0).IsVerbatimType())
    240     return false;
    241 
    242   // If the verbatim first match is followed by another verbatim match, don't
    243   // hide anything, lest we cause user confusion.
    244   if ((size() > 1) && match_at(1).IsVerbatimType())
    245     return false;
    246 
    247   // Otherwise, it's safe to hide the verbatim first match.
    248   return true;
    249 }
    250 
    251 void AutocompleteResult::Reset() {
    252   matches_.clear();
    253   default_match_ = end();
    254 }
    255 
    256 void AutocompleteResult::Swap(AutocompleteResult* other) {
    257   const size_t default_match_offset = default_match_ - begin();
    258   const size_t other_default_match_offset =
    259       other->default_match_ - other->begin();
    260   matches_.swap(other->matches_);
    261   default_match_ = begin() + other_default_match_offset;
    262   other->default_match_ = other->begin() + default_match_offset;
    263   alternate_nav_url_.Swap(&(other->alternate_nav_url_));
    264 }
    265 
    266 #ifndef NDEBUG
    267 void AutocompleteResult::Validate() const {
    268   for (const_iterator i(begin()); i != end(); ++i)
    269     i->Validate();
    270 }
    271 #endif
    272 
    273 // static
    274 GURL AutocompleteResult::ComputeAlternateNavUrl(
    275     const AutocompleteInput& input,
    276     const AutocompleteMatch& match) {
    277   return ((input.type() == AutocompleteInput::UNKNOWN) &&
    278           (AutocompleteMatch::IsSearchType(match.type)) &&
    279           (match.transition != content::PAGE_TRANSITION_KEYWORD) &&
    280           (input.canonicalized_url() != match.destination_url)) ?
    281       input.canonicalized_url() : GURL();
    282 }
    283 
    284 void AutocompleteResult::CopyFrom(const AutocompleteResult& rhs) {
    285   if (this == &rhs)
    286     return;
    287 
    288   matches_ = rhs.matches_;
    289   // Careful!  You can't just copy iterators from another container, you have to
    290   // reconstruct them.
    291   default_match_ = (rhs.default_match_ == rhs.end()) ?
    292       end() : (begin() + (rhs.default_match_ - rhs.begin()));
    293 
    294   alternate_nav_url_ = rhs.alternate_nav_url_;
    295 }
    296 
    297 void AutocompleteResult::AddMatch(
    298     AutocompleteInput::PageClassification page_classification,
    299     const AutocompleteMatch& match) {
    300   DCHECK(default_match_ != end());
    301   DCHECK_EQ(AutocompleteMatch::SanitizeString(match.contents), match.contents);
    302   DCHECK_EQ(AutocompleteMatch::SanitizeString(match.description),
    303             match.description);
    304   CompareWithDemoteByType comparing_object(page_classification);
    305   ACMatches::iterator insertion_point =
    306       std::upper_bound(begin(), end(), match, comparing_object);
    307   matches_difference_type default_offset = default_match_ - begin();
    308   if ((insertion_point - begin()) <= default_offset)
    309     ++default_offset;
    310   matches_.insert(insertion_point, match);
    311   default_match_ = begin() + default_offset;
    312 }
    313 
    314 void AutocompleteResult::BuildProviderToMatches(
    315     ProviderToMatches* provider_to_matches) const {
    316   for (ACMatches::const_iterator i(begin()); i != end(); ++i)
    317     (*provider_to_matches)[i->provider].push_back(*i);
    318 }
    319 
    320 // static
    321 bool AutocompleteResult::HasMatchByDestination(const AutocompleteMatch& match,
    322                                                const ACMatches& matches) {
    323   for (ACMatches::const_iterator i(matches.begin()); i != matches.end(); ++i) {
    324     if (i->destination_url == match.destination_url)
    325       return true;
    326   }
    327   return false;
    328 }
    329 
    330 void AutocompleteResult::MergeMatchesByProvider(
    331     AutocompleteInput::PageClassification page_classification,
    332     const ACMatches& old_matches,
    333     const ACMatches& new_matches) {
    334   if (new_matches.size() >= old_matches.size())
    335     return;
    336 
    337   size_t delta = old_matches.size() - new_matches.size();
    338   const int max_relevance = (new_matches.empty() ?
    339       matches_.front().relevance : new_matches[0].relevance) - 1;
    340   // Because the goal is a visibly-stable popup, rather than one that preserves
    341   // the highest-relevance matches, we copy in the lowest-relevance matches
    342   // first. This means that within each provider's "group" of matches, any
    343   // synchronous matches (which tend to have the highest scores) will
    344   // "overwrite" the initial matches from that provider's previous results,
    345   // minimally disturbing the rest of the matches.
    346   for (ACMatches::const_reverse_iterator i(old_matches.rbegin());
    347        i != old_matches.rend() && delta > 0; ++i) {
    348     if (!HasMatchByDestination(*i, new_matches)) {
    349       AutocompleteMatch match = *i;
    350       match.relevance = std::min(max_relevance, match.relevance);
    351       match.from_previous = true;
    352       AddMatch(page_classification, match);
    353       delta--;
    354     }
    355   }
    356 }
    357