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