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