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