history_contents_provider.cc revision 731df977c0511bca2206b5f333555b1205ff1f43
1// Copyright (c) 2010 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/history_contents_provider.h" 6 7#include "base/callback.h" 8#include "base/metrics/histogram.h" 9#include "base/string_util.h" 10#include "base/utf_string_conversions.h" 11#include "chrome/browser/bookmarks/bookmark_model.h" 12#include "chrome/browser/bookmarks/bookmark_utils.h" 13#include "chrome/browser/history/query_parser.h" 14#include "chrome/browser/profile.h" 15#include "chrome/common/url_constants.h" 16#include "googleurl/src/url_util.h" 17#include "net/base/net_util.h" 18 19using base::TimeTicks; 20 21namespace { 22 23// Number of days to search for full text results. The longer this is, the more 24// time it will take. 25const int kDaysToSearch = 30; 26 27// When processing the results from the history query, this structure points to 28// a single result. It allows the results to be sorted and processed without 29// modifying the larger and slower results structure. 30struct MatchReference { 31 MatchReference(const history::URLResult* result, int relevance) 32 : result(result), 33 relevance(relevance) { 34 } 35 36 const history::URLResult* result; 37 int relevance; // Score of relevance computed by CalculateRelevance. 38}; 39 40// This is a > operator for MatchReference. 41bool CompareMatchRelevance(const MatchReference& a, const MatchReference& b) { 42 if (a.relevance != b.relevance) 43 return a.relevance > b.relevance; 44 45 // Want results in reverse-chronological order all else being equal. 46 return a.result->last_visit() > b.result->last_visit(); 47} 48 49} // namespace 50 51using history::HistoryDatabase; 52 53HistoryContentsProvider::HistoryContentsProvider(ACProviderListener* listener, 54 Profile* profile) 55 : AutocompleteProvider(listener, profile, "HistoryContents"), 56 star_title_count_(0), 57 star_contents_count_(0), 58 title_count_(0), 59 contents_count_(0), 60 input_type_(AutocompleteInput::INVALID), 61 trim_http_(false), 62 have_results_(false) { 63} 64 65void HistoryContentsProvider::Start(const AutocompleteInput& input, 66 bool minimal_changes) { 67 matches_.clear(); 68 69 if (input.text().empty() || (input.type() == AutocompleteInput::INVALID) || 70 !profile_ || 71 // The history service or bookmark bar model must exist. 72 !(profile_->GetHistoryService(Profile::EXPLICIT_ACCESS) || 73 profile_->GetBookmarkModel())) { 74 Stop(); 75 return; 76 } 77 78 // TODO(pkasting): http://b/888148 We disallow URL input and "URL-like" input 79 // (REQUESTED_URL or UNKNOWN with dots) because we get poor results for it, 80 // but we could get better results if we did better tokenizing instead. 81 if ((input.type() == AutocompleteInput::URL) || 82 (((input.type() == AutocompleteInput::REQUESTED_URL) || 83 (input.type() == AutocompleteInput::UNKNOWN)) && 84 (input.text().find('.') != std::wstring::npos))) { 85 Stop(); 86 return; 87 } 88 89 // Change input type so matches will be marked up properly. 90 input_type_ = input.type(); 91 trim_http_ = !HasHTTPScheme(input.text()); 92 93 // Decide what to do about any previous query/results. 94 if (!minimal_changes) { 95 // Any in-progress request is irrelevant, cancel it. 96 Stop(); 97 } else if (have_results_) { 98 // We finished the previous query and still have its results. Mark them up 99 // again for the new input. 100 ConvertResults(); 101 return; 102 } else if (!done_) { 103 // We're still running the previous query on the HistoryService. If we're 104 // allowed to keep running it, do so, and when it finishes, its results will 105 // get marked up for this new input. In synchronous_only mode, cancel the 106 // history query. 107 if (input.synchronous_only()) { 108 done_ = true; 109 request_consumer_.CancelAllRequests(); 110 } 111 ConvertResults(); 112 return; 113 } 114 115 if (results_.size() != 0) { 116 // Clear the results. We swap in an empty one as the easy way to clear it. 117 history::QueryResults empty_results; 118 results_.Swap(&empty_results); 119 } 120 121 // Querying bookmarks is synchronous, so we always do it. 122 QueryBookmarks(input); 123 124 // Convert the bookmark results. 125 ConvertResults(); 126 127 if (!input.synchronous_only()) { 128 HistoryService* history = 129 profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); 130 if (history) { 131 done_ = false; 132 133 history::QueryOptions options; 134 options.SetRecentDayRange(kDaysToSearch); 135 options.max_count = kMaxMatchCount; 136 history->QueryHistory(WideToUTF16(input.text()), options, 137 &request_consumer_, 138 NewCallback(this, &HistoryContentsProvider::QueryComplete)); 139 } 140 } 141} 142 143void HistoryContentsProvider::Stop() { 144 done_ = true; 145 request_consumer_.CancelAllRequests(); 146 147 // Clear the results. We swap in an empty one as the easy way to clear it. 148 history::QueryResults empty_results; 149 results_.Swap(&empty_results); 150 have_results_ = false; 151} 152 153HistoryContentsProvider::~HistoryContentsProvider() { 154} 155 156void HistoryContentsProvider::QueryComplete(HistoryService::Handle handle, 157 history::QueryResults* results) { 158 results_.AppendResultsBySwapping(results, true); 159 have_results_ = true; 160 ConvertResults(); 161 162 done_ = true; 163 if (listener_) 164 listener_->OnProviderUpdate(!matches_.empty()); 165} 166 167void HistoryContentsProvider::ConvertResults() { 168 // Reset the relevance counters so that result relevance won't vary on 169 // subsequent passes of ConvertResults. 170 star_title_count_ = star_contents_count_ = title_count_ = contents_count_ = 0; 171 172 // Make the result references and score the results. 173 std::vector<MatchReference> result_refs; 174 result_refs.reserve(results_.size()); 175 176 // Results are sorted in decreasing order so we run the loop backwards so that 177 // the relevance increment favors the higher ranked results. 178 for (std::vector<history::URLResult*>::const_reverse_iterator i = 179 results_.rbegin(); i != results_.rend(); ++i) { 180 history::URLResult* result = *i; 181 MatchReference ref(result, CalculateRelevance(*result)); 182 result_refs.push_back(ref); 183 } 184 185 // Get the top matches and add them. Always do max number of matches the popup 186 // will show plus one. This ensures that if the other providers provide the 187 // exact same set of results, and the db only has max_matches + 1 results 188 // available for this query, we know the last one. 189 // 190 // This is done to avoid having the history search shortcut show 191 // 'See 1 previously viewed ...'. 192 // 193 // Note that AutocompleteResult::kMaxMatches (maximum size of the popup) 194 // is different than both kMaxMatches (the provider's maximum) and 195 // kMaxMatchCount (the number of items we want from the history). 196 size_t max_for_popup = std::min(AutocompleteResult::kMaxMatches + 1, 197 result_refs.size()); 198 size_t max_for_provider = std::min(kMaxMatches, result_refs.size()); 199 std::partial_sort(result_refs.begin(), result_refs.begin() + max_for_popup, 200 result_refs.end(), &CompareMatchRelevance); 201 matches_.clear(); 202 for (size_t i = 0; i < max_for_popup; i++) { 203 matches_.push_back(ResultToMatch(*result_refs[i].result, 204 result_refs[i].relevance)); 205 } 206 207 // We made more matches than the autocomplete service requested for this 208 // provider (see previous comment). We invert the weights for the items 209 // we want to get removed, but preserve their magnitude which will be used 210 // to fill them in with our other results. 211 for (size_t i = max_for_provider; i < max_for_popup; i++) 212 matches_[i].relevance = -matches_[i].relevance; 213} 214 215static bool MatchInTitle(const history::URLResult& result) { 216 return !result.title_match_positions().empty(); 217} 218 219AutocompleteMatch HistoryContentsProvider::ResultToMatch( 220 const history::URLResult& result, 221 int score) { 222 // TODO(sky): if matched title highlight matching words in title. 223 // Also show star in popup. 224 AutocompleteMatch match(this, score, false, MatchInTitle(result) ? 225 AutocompleteMatch::HISTORY_TITLE : AutocompleteMatch::HISTORY_BODY); 226 match.contents = StringForURLDisplay(result.url(), true, trim_http_); 227 match.fill_into_edit = 228 AutocompleteInput::FormattedStringWithEquivalentMeaning(result.url(), 229 match.contents); 230 match.destination_url = result.url(); 231 match.contents_class.push_back( 232 ACMatchClassification(0, ACMatchClassification::URL)); 233 match.description = UTF16ToWide(result.title()); 234 match.starred = 235 (profile_->GetBookmarkModel() && 236 profile_->GetBookmarkModel()->IsBookmarked(result.url())); 237 238 ClassifyDescription(result, &match); 239 return match; 240} 241 242void HistoryContentsProvider::ClassifyDescription( 243 const history::URLResult& result, 244 AutocompleteMatch* match) const { 245 const Snippet::MatchPositions& title_matches = result.title_match_positions(); 246 247 size_t offset = 0; 248 if (!title_matches.empty()) { 249 // Classify matches in the title. 250 for (Snippet::MatchPositions::const_iterator i = title_matches.begin(); 251 i != title_matches.end(); ++i) { 252 if (i->first != offset) { 253 match->description_class.push_back( 254 ACMatchClassification(offset, ACMatchClassification::NONE)); 255 } 256 match->description_class.push_back( 257 ACMatchClassification(i->first, ACMatchClassification::MATCH)); 258 offset = i->second; 259 } 260 } 261 if (offset != result.title().size()) { 262 match->description_class.push_back( 263 ACMatchClassification(offset, ACMatchClassification::NONE)); 264 } 265} 266 267int HistoryContentsProvider::CalculateRelevance( 268 const history::URLResult& result) { 269 const bool in_title = MatchInTitle(result); 270 if (!profile_->GetBookmarkModel() || 271 !profile_->GetBookmarkModel()->IsBookmarked(result.url())) 272 return in_title ? (700 + title_count_++) : (500 + contents_count_++); 273 return in_title ? 274 (1000 + star_title_count_++) : (550 + star_contents_count_++); 275} 276 277void HistoryContentsProvider::QueryBookmarks(const AutocompleteInput& input) { 278 BookmarkModel* bookmark_model = profile_->GetBookmarkModel(); 279 if (!bookmark_model) 280 return; 281 282 DCHECK(results_.empty()); 283 284 TimeTicks start_time = TimeTicks::Now(); 285 std::vector<bookmark_utils::TitleMatch> matches; 286 bookmark_model->GetBookmarksWithTitlesMatching(WideToUTF16Hack(input.text()), 287 kMaxMatches, &matches); 288 for (size_t i = 0; i < matches.size(); ++i) 289 AddBookmarkTitleMatchToResults(matches[i]); 290 UMA_HISTOGRAM_TIMES("Omnibox.QueryBookmarksTime", 291 TimeTicks::Now() - start_time); 292} 293 294void HistoryContentsProvider::AddBookmarkTitleMatchToResults( 295 const bookmark_utils::TitleMatch& match) { 296 history::URLResult url_result(match.node->GetURL(), match.match_positions); 297 url_result.set_title(match.node->GetTitle()); 298 results_.AppendURLBySwapping(&url_result); 299} 300