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