OLD | NEW |
| (Empty) |
1 // Copyright 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/search_provider.h" | |
6 | |
7 #include <algorithm> | |
8 #include <cmath> | |
9 | |
10 #include "base/base64.h" | |
11 #include "base/callback.h" | |
12 #include "base/i18n/break_iterator.h" | |
13 #include "base/i18n/case_conversion.h" | |
14 #include "base/json/json_string_value_serializer.h" | |
15 #include "base/metrics/histogram.h" | |
16 #include "base/metrics/user_metrics.h" | |
17 #include "base/rand_util.h" | |
18 #include "base/strings/string_util.h" | |
19 #include "base/strings/utf_string_conversions.h" | |
20 #include "components/history/core/browser/in_memory_database.h" | |
21 #include "components/history/core/browser/keyword_search_term.h" | |
22 #include "components/metrics/proto/omnibox_input_type.pb.h" | |
23 #include "components/omnibox/autocomplete_provider_delegate.h" | |
24 #include "components/omnibox/autocomplete_provider_listener.h" | |
25 #include "components/omnibox/autocomplete_result.h" | |
26 #include "components/omnibox/keyword_provider.h" | |
27 #include "components/omnibox/omnibox_field_trial.h" | |
28 #include "components/omnibox/url_prefix.h" | |
29 #include "components/search/search.h" | |
30 #include "components/search_engines/template_url_prepopulate_data.h" | |
31 #include "components/search_engines/template_url_service.h" | |
32 #include "components/variations/variations_http_header_provider.h" | |
33 #include "grit/components_strings.h" | |
34 #include "net/base/escape.h" | |
35 #include "net/base/load_flags.h" | |
36 #include "net/base/net_util.h" | |
37 #include "net/http/http_request_headers.h" | |
38 #include "net/url_request/url_fetcher.h" | |
39 #include "net/url_request/url_request_status.h" | |
40 #include "ui/base/l10n/l10n_util.h" | |
41 #include "url/url_constants.h" | |
42 #include "url/url_util.h" | |
43 | |
44 // Helpers -------------------------------------------------------------------- | |
45 | |
46 namespace { | |
47 | |
48 // We keep track in a histogram how many suggest requests we send, how | |
49 // many suggest requests we invalidate (e.g., due to a user typing | |
50 // another character), and how many replies we receive. | |
51 // *** ADD NEW ENUMS AFTER ALL PREVIOUSLY DEFINED ONES! *** | |
52 // (excluding the end-of-list enum value) | |
53 // We do not want values of existing enums to change or else it screws | |
54 // up the statistics. | |
55 enum SuggestRequestsHistogramValue { | |
56 REQUEST_SENT = 1, | |
57 REQUEST_INVALIDATED, | |
58 REPLY_RECEIVED, | |
59 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE | |
60 }; | |
61 | |
62 // The verbatim score for an input which is not an URL. | |
63 const int kNonURLVerbatimRelevance = 1300; | |
64 | |
65 // Increments the appropriate value in the histogram by one. | |
66 void LogOmniboxSuggestRequest( | |
67 SuggestRequestsHistogramValue request_value) { | |
68 UMA_HISTOGRAM_ENUMERATION("Omnibox.SuggestRequests", request_value, | |
69 MAX_SUGGEST_REQUEST_HISTOGRAM_VALUE); | |
70 } | |
71 | |
72 bool HasMultipleWords(const base::string16& text) { | |
73 base::i18n::BreakIterator i(text, base::i18n::BreakIterator::BREAK_WORD); | |
74 bool found_word = false; | |
75 if (i.Init()) { | |
76 while (i.Advance()) { | |
77 if (i.IsWord()) { | |
78 if (found_word) | |
79 return true; | |
80 found_word = true; | |
81 } | |
82 } | |
83 } | |
84 return false; | |
85 } | |
86 | |
87 } // namespace | |
88 | |
89 // SearchProvider::Providers -------------------------------------------------- | |
90 | |
91 SearchProvider::Providers::Providers(TemplateURLService* template_url_service) | |
92 : template_url_service_(template_url_service) {} | |
93 | |
94 const TemplateURL* SearchProvider::Providers::GetDefaultProviderURL() const { | |
95 return default_provider_.empty() ? NULL : | |
96 template_url_service_->GetTemplateURLForKeyword(default_provider_); | |
97 } | |
98 | |
99 const TemplateURL* SearchProvider::Providers::GetKeywordProviderURL() const { | |
100 return keyword_provider_.empty() ? NULL : | |
101 template_url_service_->GetTemplateURLForKeyword(keyword_provider_); | |
102 } | |
103 | |
104 | |
105 // SearchProvider::CompareScoredResults --------------------------------------- | |
106 | |
107 class SearchProvider::CompareScoredResults { | |
108 public: | |
109 bool operator()(const SearchSuggestionParser::Result& a, | |
110 const SearchSuggestionParser::Result& b) { | |
111 // Sort in descending relevance order. | |
112 return a.relevance() > b.relevance(); | |
113 } | |
114 }; | |
115 | |
116 | |
117 // SearchProvider ------------------------------------------------------------- | |
118 | |
119 // static | |
120 int SearchProvider::kMinimumTimeBetweenSuggestQueriesMs = 100; | |
121 | |
122 SearchProvider::SearchProvider( | |
123 AutocompleteProviderListener* listener, | |
124 TemplateURLService* template_url_service, | |
125 scoped_ptr<AutocompleteProviderDelegate> delegate) | |
126 : BaseSearchProvider(template_url_service, delegate.Pass(), | |
127 AutocompleteProvider::TYPE_SEARCH), | |
128 listener_(listener), | |
129 suggest_results_pending_(0), | |
130 providers_(template_url_service), | |
131 answers_cache_(1) { | |
132 } | |
133 | |
134 // static | |
135 std::string SearchProvider::GetSuggestMetadata(const AutocompleteMatch& match) { | |
136 return match.GetAdditionalInfo(kSuggestMetadataKey); | |
137 } | |
138 | |
139 void SearchProvider::ResetSession() { | |
140 field_trial_triggered_in_session_ = false; | |
141 } | |
142 | |
143 SearchProvider::~SearchProvider() { | |
144 } | |
145 | |
146 // static | |
147 int SearchProvider::CalculateRelevanceForKeywordVerbatim( | |
148 metrics::OmniboxInputType::Type type, | |
149 bool prefer_keyword) { | |
150 // This function is responsible for scoring verbatim query matches | |
151 // for non-extension keywords. KeywordProvider::CalculateRelevance() | |
152 // scores verbatim query matches for extension keywords, as well as | |
153 // for keyword matches (i.e., suggestions of a keyword itself, not a | |
154 // suggestion of a query on a keyword search engine). These two | |
155 // functions are currently in sync, but there's no reason we | |
156 // couldn't decide in the future to score verbatim matches | |
157 // differently for extension and non-extension keywords. If you | |
158 // make such a change, however, you should update this comment to | |
159 // describe it, so it's clear why the functions diverge. | |
160 if (prefer_keyword) | |
161 return 1500; | |
162 return (type == metrics::OmniboxInputType::QUERY) ? 1450 : 1100; | |
163 } | |
164 | |
165 void SearchProvider::Start(const AutocompleteInput& input, | |
166 bool minimal_changes) { | |
167 // Do our best to load the model as early as possible. This will reduce | |
168 // odds of having the model not ready when really needed (a non-empty input). | |
169 TemplateURLService* model = providers_.template_url_service(); | |
170 DCHECK(model); | |
171 model->Load(); | |
172 | |
173 matches_.clear(); | |
174 field_trial_triggered_ = false; | |
175 | |
176 // Can't return search/suggest results for bogus input. | |
177 if (input.type() == metrics::OmniboxInputType::INVALID) { | |
178 Stop(true); | |
179 return; | |
180 } | |
181 | |
182 keyword_input_ = input; | |
183 const TemplateURL* keyword_provider = | |
184 KeywordProvider::GetSubstitutingTemplateURLForInput(model, | |
185 &keyword_input_); | |
186 if (keyword_provider == NULL) | |
187 keyword_input_.Clear(); | |
188 else if (keyword_input_.text().empty()) | |
189 keyword_provider = NULL; | |
190 | |
191 const TemplateURL* default_provider = model->GetDefaultSearchProvider(); | |
192 if (default_provider && | |
193 !default_provider->SupportsReplacement(model->search_terms_data())) | |
194 default_provider = NULL; | |
195 | |
196 if (keyword_provider == default_provider) | |
197 default_provider = NULL; // No use in querying the same provider twice. | |
198 | |
199 if (!default_provider && !keyword_provider) { | |
200 // No valid providers. | |
201 Stop(true); | |
202 return; | |
203 } | |
204 | |
205 // If we're still running an old query but have since changed the query text | |
206 // or the providers, abort the query. | |
207 base::string16 default_provider_keyword(default_provider ? | |
208 default_provider->keyword() : base::string16()); | |
209 base::string16 keyword_provider_keyword(keyword_provider ? | |
210 keyword_provider->keyword() : base::string16()); | |
211 if (!minimal_changes || | |
212 !providers_.equal(default_provider_keyword, keyword_provider_keyword)) { | |
213 // Cancel any in-flight suggest requests. | |
214 if (!done_) | |
215 Stop(false); | |
216 } | |
217 | |
218 providers_.set(default_provider_keyword, keyword_provider_keyword); | |
219 | |
220 if (input.text().empty()) { | |
221 // User typed "?" alone. Give them a placeholder result indicating what | |
222 // this syntax does. | |
223 if (default_provider) { | |
224 AutocompleteMatch match; | |
225 match.provider = this; | |
226 match.contents.assign(l10n_util::GetStringUTF16(IDS_EMPTY_KEYWORD_VALUE)); | |
227 match.contents_class.push_back( | |
228 ACMatchClassification(0, ACMatchClassification::NONE)); | |
229 match.keyword = providers_.default_provider(); | |
230 match.allowed_to_be_default_match = true; | |
231 matches_.push_back(match); | |
232 } | |
233 Stop(true); | |
234 return; | |
235 } | |
236 | |
237 input_ = input; | |
238 | |
239 DoHistoryQuery(minimal_changes); | |
240 DoAnswersQuery(input); | |
241 StartOrStopSuggestQuery(minimal_changes); | |
242 UpdateMatches(); | |
243 } | |
244 | |
245 void SearchProvider::Stop(bool clear_cached_results) { | |
246 StopSuggest(); | |
247 done_ = true; | |
248 | |
249 if (clear_cached_results) | |
250 ClearAllResults(); | |
251 } | |
252 | |
253 const TemplateURL* SearchProvider::GetTemplateURL(bool is_keyword) const { | |
254 return is_keyword ? providers_.GetKeywordProviderURL() | |
255 : providers_.GetDefaultProviderURL(); | |
256 } | |
257 | |
258 const AutocompleteInput SearchProvider::GetInput(bool is_keyword) const { | |
259 return is_keyword ? keyword_input_ : input_; | |
260 } | |
261 | |
262 bool SearchProvider::ShouldAppendExtraParams( | |
263 const SearchSuggestionParser::SuggestResult& result) const { | |
264 return !result.from_keyword_provider() || | |
265 providers_.default_provider().empty(); | |
266 } | |
267 | |
268 void SearchProvider::RecordDeletionResult(bool success) { | |
269 if (success) { | |
270 base::RecordAction( | |
271 base::UserMetricsAction("Omnibox.ServerSuggestDelete.Success")); | |
272 } else { | |
273 base::RecordAction( | |
274 base::UserMetricsAction("Omnibox.ServerSuggestDelete.Failure")); | |
275 } | |
276 } | |
277 | |
278 void SearchProvider::OnURLFetchComplete(const net::URLFetcher* source) { | |
279 DCHECK(!done_); | |
280 --suggest_results_pending_; | |
281 DCHECK_GE(suggest_results_pending_, 0); // Should never go negative. | |
282 | |
283 const bool is_keyword = source == keyword_fetcher_.get(); | |
284 | |
285 // Ensure the request succeeded and that the provider used is still available. | |
286 // A verbatim match cannot be generated without this provider, causing errors. | |
287 const bool request_succeeded = | |
288 source->GetStatus().is_success() && (source->GetResponseCode() == 200) && | |
289 GetTemplateURL(is_keyword); | |
290 | |
291 LogFetchComplete(request_succeeded, is_keyword); | |
292 | |
293 bool results_updated = false; | |
294 if (request_succeeded) { | |
295 scoped_ptr<base::Value> data(SearchSuggestionParser::DeserializeJsonData( | |
296 SearchSuggestionParser::ExtractJsonData(source))); | |
297 if (data) { | |
298 SearchSuggestionParser::Results* results = | |
299 is_keyword ? &keyword_results_ : &default_results_; | |
300 results_updated = ParseSuggestResults(*data, -1, is_keyword, results); | |
301 if (results_updated) | |
302 SortResults(is_keyword, results); | |
303 } | |
304 } | |
305 UpdateMatches(); | |
306 if (done_ || results_updated) | |
307 listener_->OnProviderUpdate(results_updated); | |
308 } | |
309 | |
310 void SearchProvider::StopSuggest() { | |
311 // Increment the appropriate field in the histogram by the number of | |
312 // pending requests that were invalidated. | |
313 for (int i = 0; i < suggest_results_pending_; ++i) | |
314 LogOmniboxSuggestRequest(REQUEST_INVALIDATED); | |
315 suggest_results_pending_ = 0; | |
316 timer_.Stop(); | |
317 // Stop any in-progress URL fetches. | |
318 keyword_fetcher_.reset(); | |
319 default_fetcher_.reset(); | |
320 } | |
321 | |
322 void SearchProvider::ClearAllResults() { | |
323 keyword_results_.Clear(); | |
324 default_results_.Clear(); | |
325 } | |
326 | |
327 void SearchProvider::UpdateMatchContentsClass( | |
328 const base::string16& input_text, | |
329 SearchSuggestionParser::Results* results) { | |
330 for (SearchSuggestionParser::SuggestResults::iterator sug_it = | |
331 results->suggest_results.begin(); | |
332 sug_it != results->suggest_results.end(); ++sug_it) { | |
333 sug_it->ClassifyMatchContents(false, input_text); | |
334 } | |
335 const std::string languages(delegate_->AcceptLanguages()); | |
336 for (SearchSuggestionParser::NavigationResults::iterator nav_it = | |
337 results->navigation_results.begin(); | |
338 nav_it != results->navigation_results.end(); ++nav_it) { | |
339 nav_it->CalculateAndClassifyMatchContents(false, input_text, languages); | |
340 } | |
341 } | |
342 | |
343 void SearchProvider::SortResults(bool is_keyword, | |
344 SearchSuggestionParser::Results* results) { | |
345 // Ignore suggested scores for non-keyword matches in keyword mode; if the | |
346 // server is allowed to score these, it could interfere with the user's | |
347 // ability to get good keyword results. | |
348 const bool abandon_suggested_scores = | |
349 !is_keyword && !providers_.keyword_provider().empty(); | |
350 // Apply calculated relevance scores to suggestions if valid relevances were | |
351 // not provided or we're abandoning suggested scores entirely. | |
352 if (!results->relevances_from_server || abandon_suggested_scores) { | |
353 ApplyCalculatedSuggestRelevance(&results->suggest_results); | |
354 ApplyCalculatedNavigationRelevance(&results->navigation_results); | |
355 // If abandoning scores entirely, also abandon the verbatim score. | |
356 if (abandon_suggested_scores) | |
357 results->verbatim_relevance = -1; | |
358 } | |
359 | |
360 // Keep the result lists sorted. | |
361 const CompareScoredResults comparator = CompareScoredResults(); | |
362 std::stable_sort(results->suggest_results.begin(), | |
363 results->suggest_results.end(), | |
364 comparator); | |
365 std::stable_sort(results->navigation_results.begin(), | |
366 results->navigation_results.end(), | |
367 comparator); | |
368 } | |
369 | |
370 void SearchProvider::LogFetchComplete(bool success, bool is_keyword) { | |
371 LogOmniboxSuggestRequest(REPLY_RECEIVED); | |
372 // Record response time for suggest requests sent to Google. We care | |
373 // only about the common case: the Google default provider used in | |
374 // non-keyword mode. | |
375 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
376 if (!is_keyword && default_url && | |
377 (TemplateURLPrepopulateData::GetEngineType( | |
378 *default_url, | |
379 providers_.template_url_service()->search_terms_data()) == | |
380 SEARCH_ENGINE_GOOGLE)) { | |
381 const base::TimeDelta elapsed_time = | |
382 base::TimeTicks::Now() - time_suggest_request_sent_; | |
383 if (success) { | |
384 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Success.GoogleResponseTime", | |
385 elapsed_time); | |
386 } else { | |
387 UMA_HISTOGRAM_TIMES("Omnibox.SuggestRequest.Failure.GoogleResponseTime", | |
388 elapsed_time); | |
389 } | |
390 } | |
391 } | |
392 | |
393 void SearchProvider::UpdateMatches() { | |
394 ConvertResultsToAutocompleteMatches(); | |
395 | |
396 // Check constraints that may be violated by suggested relevances. | |
397 if (!matches_.empty() && | |
398 (default_results_.HasServerProvidedScores() || | |
399 keyword_results_.HasServerProvidedScores())) { | |
400 // These blocks attempt to repair undesirable behavior by suggested | |
401 // relevances with minimal impact, preserving other suggested relevances. | |
402 | |
403 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
404 const bool is_extension_keyword = (keyword_url != NULL) && | |
405 (keyword_url->GetType() == TemplateURL::OMNIBOX_API_EXTENSION); | |
406 if ((keyword_url != NULL) && !is_extension_keyword && | |
407 (FindTopMatch() == matches_.end())) { | |
408 // In non-extension keyword mode, disregard the keyword verbatim suggested | |
409 // relevance if necessary, so at least one match is allowed to be default. | |
410 // (In extension keyword mode this is not necessary because the extension | |
411 // will return a default match.) | |
412 keyword_results_.verbatim_relevance = -1; | |
413 ConvertResultsToAutocompleteMatches(); | |
414 } | |
415 if (IsTopMatchSearchWithURLInput()) { | |
416 // Disregard the suggested search and verbatim relevances if the input | |
417 // type is URL and the top match is a highly-ranked search suggestion. | |
418 // For example, prevent a search for "foo.com" from outranking another | |
419 // provider's navigation for "foo.com" or "foo.com/url_from_history". | |
420 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); | |
421 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); | |
422 default_results_.verbatim_relevance = -1; | |
423 keyword_results_.verbatim_relevance = -1; | |
424 ConvertResultsToAutocompleteMatches(); | |
425 } | |
426 if (!is_extension_keyword && (FindTopMatch() == matches_.end())) { | |
427 // Guarantee that SearchProvider returns a legal default match (except | |
428 // when in extension-based keyword mode). The omnibox always needs at | |
429 // least one legal default match, and it relies on SearchProvider in | |
430 // combination with KeywordProvider (for extension-based keywords) to | |
431 // always return one. | |
432 ApplyCalculatedRelevance(); | |
433 ConvertResultsToAutocompleteMatches(); | |
434 } | |
435 DCHECK(!IsTopMatchSearchWithURLInput()); | |
436 DCHECK(is_extension_keyword || (FindTopMatch() != matches_.end())); | |
437 } | |
438 UMA_HISTOGRAM_CUSTOM_COUNTS( | |
439 "Omnibox.SearchProviderMatches", matches_.size(), 1, 6, 7); | |
440 UpdateDone(); | |
441 } | |
442 | |
443 void SearchProvider::Run() { | |
444 // Start a new request with the current input. | |
445 suggest_results_pending_ = 0; | |
446 time_suggest_request_sent_ = base::TimeTicks::Now(); | |
447 | |
448 default_fetcher_.reset(CreateSuggestFetcher(kDefaultProviderURLFetcherID, | |
449 providers_.GetDefaultProviderURL(), input_)); | |
450 keyword_fetcher_.reset(CreateSuggestFetcher(kKeywordProviderURLFetcherID, | |
451 providers_.GetKeywordProviderURL(), keyword_input_)); | |
452 | |
453 // Both the above can fail if the providers have been modified or deleted | |
454 // since the query began. | |
455 if (suggest_results_pending_ == 0) { | |
456 UpdateDone(); | |
457 // We only need to update the listener if we're actually done. | |
458 if (done_) | |
459 listener_->OnProviderUpdate(false); | |
460 } | |
461 } | |
462 | |
463 void SearchProvider::DoHistoryQuery(bool minimal_changes) { | |
464 // The history query results are synchronous, so if minimal_changes is true, | |
465 // we still have the last results and don't need to do anything. | |
466 if (minimal_changes) | |
467 return; | |
468 | |
469 keyword_history_results_.clear(); | |
470 default_history_results_.clear(); | |
471 | |
472 if (OmniboxFieldTrial::SearchHistoryDisable( | |
473 input_.current_page_classification())) | |
474 return; | |
475 | |
476 history::URLDatabase* url_db = delegate_->InMemoryDatabase(); | |
477 if (!url_db) | |
478 return; | |
479 | |
480 // Request history for both the keyword and default provider. We grab many | |
481 // more matches than we'll ultimately clamp to so that if there are several | |
482 // recent multi-word matches who scores are lowered (see | |
483 // AddHistoryResultsToMap()), they won't crowd out older, higher-scoring | |
484 // matches. Note that this doesn't fix the problem entirely, but merely | |
485 // limits it to cases with a very large number of such multi-word matches; for | |
486 // now, this seems OK compared with the complexity of a real fix, which would | |
487 // require multiple searches and tracking of "single- vs. multi-word" in the | |
488 // database. | |
489 int num_matches = kMaxMatches * 5; | |
490 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
491 if (default_url) { | |
492 const base::TimeTicks start_time = base::TimeTicks::Now(); | |
493 url_db->GetMostRecentKeywordSearchTerms(default_url->id(), input_.text(), | |
494 num_matches, &default_history_results_); | |
495 UMA_HISTOGRAM_TIMES( | |
496 "Omnibox.SearchProvider.GetMostRecentKeywordTermsDefaultProviderTime", | |
497 base::TimeTicks::Now() - start_time); | |
498 } | |
499 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
500 if (keyword_url) { | |
501 url_db->GetMostRecentKeywordSearchTerms(keyword_url->id(), | |
502 keyword_input_.text(), num_matches, &keyword_history_results_); | |
503 } | |
504 } | |
505 | |
506 void SearchProvider::StartOrStopSuggestQuery(bool minimal_changes) { | |
507 if (!IsQuerySuitableForSuggest()) { | |
508 StopSuggest(); | |
509 ClearAllResults(); | |
510 return; | |
511 } | |
512 | |
513 // For the minimal_changes case, if we finished the previous query and still | |
514 // have its results, or are allowed to keep running it, just do that, rather | |
515 // than starting a new query. | |
516 if (minimal_changes && | |
517 (!default_results_.suggest_results.empty() || | |
518 !default_results_.navigation_results.empty() || | |
519 !keyword_results_.suggest_results.empty() || | |
520 !keyword_results_.navigation_results.empty() || | |
521 (!done_ && input_.want_asynchronous_matches()))) | |
522 return; | |
523 | |
524 // We can't keep running any previous query, so halt it. | |
525 StopSuggest(); | |
526 | |
527 // Remove existing results that cannot inline autocomplete the new input. | |
528 RemoveAllStaleResults(); | |
529 | |
530 // Update the content classifications of remaining results so they look good | |
531 // against the current input. | |
532 UpdateMatchContentsClass(input_.text(), &default_results_); | |
533 if (!keyword_input_.text().empty()) | |
534 UpdateMatchContentsClass(keyword_input_.text(), &keyword_results_); | |
535 | |
536 // We can't start a new query if we're only allowed synchronous results. | |
537 if (!input_.want_asynchronous_matches()) | |
538 return; | |
539 | |
540 // To avoid flooding the suggest server, don't send a query until at | |
541 // least 100 ms since the last query. | |
542 base::TimeTicks next_suggest_time(time_suggest_request_sent_ + | |
543 base::TimeDelta::FromMilliseconds(kMinimumTimeBetweenSuggestQueriesMs)); | |
544 base::TimeTicks now(base::TimeTicks::Now()); | |
545 if (now >= next_suggest_time) { | |
546 Run(); | |
547 return; | |
548 } | |
549 timer_.Start(FROM_HERE, next_suggest_time - now, this, &SearchProvider::Run); | |
550 } | |
551 | |
552 bool SearchProvider::IsQuerySuitableForSuggest() const { | |
553 // Don't run Suggest in incognito mode, if the engine doesn't support it, or | |
554 // if the user has disabled it. | |
555 const TemplateURL* default_url = providers_.GetDefaultProviderURL(); | |
556 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
557 if (delegate_->IsOffTheRecord() || | |
558 ((!default_url || default_url->suggestions_url().empty()) && | |
559 (!keyword_url || keyword_url->suggestions_url().empty())) || | |
560 !delegate_->SearchSuggestEnabled()) | |
561 return false; | |
562 | |
563 // If the input type might be a URL, we take extra care so that private data | |
564 // isn't sent to the server. | |
565 | |
566 // FORCED_QUERY means the user is explicitly asking us to search for this, so | |
567 // we assume it isn't a URL and/or there isn't private data. | |
568 if (input_.type() == metrics::OmniboxInputType::FORCED_QUERY) | |
569 return true; | |
570 | |
571 // Next we check the scheme. If this is UNKNOWN/URL with a scheme that isn't | |
572 // http/https/ftp, we shouldn't send it. Sending things like file: and data: | |
573 // is both a waste of time and a disclosure of potentially private, local | |
574 // data. Other "schemes" may actually be usernames, and we don't want to send | |
575 // passwords. If the scheme is OK, we still need to check other cases below. | |
576 // If this is QUERY, then the presence of these schemes means the user | |
577 // explicitly typed one, and thus this is probably a URL that's being entered | |
578 // and happens to currently be invalid -- in which case we again want to run | |
579 // our checks below. Other QUERY cases are less likely to be URLs and thus we | |
580 // assume we're OK. | |
581 if (!LowerCaseEqualsASCII(input_.scheme(), url::kHttpScheme) && | |
582 !LowerCaseEqualsASCII(input_.scheme(), url::kHttpsScheme) && | |
583 !LowerCaseEqualsASCII(input_.scheme(), url::kFtpScheme)) | |
584 return (input_.type() == metrics::OmniboxInputType::QUERY); | |
585 | |
586 // Don't send URLs with usernames, queries or refs. Some of these are | |
587 // private, and the Suggest server is unlikely to have any useful results | |
588 // for any of them. Also don't send URLs with ports, as we may initially | |
589 // think that a username + password is a host + port (and we don't want to | |
590 // send usernames/passwords), and even if the port really is a port, the | |
591 // server is once again unlikely to have and useful results. | |
592 // Note that we only block based on refs if the input is URL-typed, as search | |
593 // queries can legitimately have #s in them which the URL parser | |
594 // overaggressively categorizes as a url with a ref. | |
595 const url::Parsed& parts = input_.parts(); | |
596 if (parts.username.is_nonempty() || parts.port.is_nonempty() || | |
597 parts.query.is_nonempty() || | |
598 (parts.ref.is_nonempty() && | |
599 (input_.type() == metrics::OmniboxInputType::URL))) | |
600 return false; | |
601 | |
602 // Don't send anything for https except the hostname. Hostnames are OK | |
603 // because they are visible when the TCP connection is established, but the | |
604 // specific path may reveal private information. | |
605 if (LowerCaseEqualsASCII(input_.scheme(), url::kHttpsScheme) && | |
606 parts.path.is_nonempty()) | |
607 return false; | |
608 | |
609 return true; | |
610 } | |
611 | |
612 void SearchProvider::RemoveAllStaleResults() { | |
613 if (keyword_input_.text().empty()) { | |
614 // User is either in keyword mode with a blank input or out of | |
615 // keyword mode entirely. | |
616 keyword_results_.Clear(); | |
617 } | |
618 } | |
619 | |
620 void SearchProvider::ApplyCalculatedRelevance() { | |
621 ApplyCalculatedSuggestRelevance(&keyword_results_.suggest_results); | |
622 ApplyCalculatedSuggestRelevance(&default_results_.suggest_results); | |
623 ApplyCalculatedNavigationRelevance(&keyword_results_.navigation_results); | |
624 ApplyCalculatedNavigationRelevance(&default_results_.navigation_results); | |
625 default_results_.verbatim_relevance = -1; | |
626 keyword_results_.verbatim_relevance = -1; | |
627 } | |
628 | |
629 void SearchProvider::ApplyCalculatedSuggestRelevance( | |
630 SearchSuggestionParser::SuggestResults* list) { | |
631 for (size_t i = 0; i < list->size(); ++i) { | |
632 SearchSuggestionParser::SuggestResult& result = (*list)[i]; | |
633 result.set_relevance( | |
634 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + | |
635 (list->size() - i - 1)); | |
636 result.set_relevance_from_server(false); | |
637 } | |
638 } | |
639 | |
640 void SearchProvider::ApplyCalculatedNavigationRelevance( | |
641 SearchSuggestionParser::NavigationResults* list) { | |
642 for (size_t i = 0; i < list->size(); ++i) { | |
643 SearchSuggestionParser::NavigationResult& result = (*list)[i]; | |
644 result.set_relevance( | |
645 result.CalculateRelevance(input_, providers_.has_keyword_provider()) + | |
646 (list->size() - i - 1)); | |
647 result.set_relevance_from_server(false); | |
648 } | |
649 } | |
650 | |
651 net::URLFetcher* SearchProvider::CreateSuggestFetcher( | |
652 int id, | |
653 const TemplateURL* template_url, | |
654 const AutocompleteInput& input) { | |
655 if (!template_url || template_url->suggestions_url().empty()) | |
656 return NULL; | |
657 | |
658 // Bail if the suggestion URL is invalid with the given replacements. | |
659 TemplateURLRef::SearchTermsArgs search_term_args(input.text()); | |
660 search_term_args.input_type = input.type(); | |
661 search_term_args.cursor_position = input.cursor_position(); | |
662 search_term_args.page_classification = input.current_page_classification(); | |
663 if (OmniboxFieldTrial::EnableAnswersInSuggest()) { | |
664 search_term_args.session_token = GetSessionToken(); | |
665 if (!prefetch_data_.full_query_text.empty()) { | |
666 search_term_args.prefetch_query = | |
667 base::UTF16ToUTF8(prefetch_data_.full_query_text); | |
668 search_term_args.prefetch_query_type = | |
669 base::UTF16ToUTF8(prefetch_data_.query_type); | |
670 } | |
671 } | |
672 GURL suggest_url(template_url->suggestions_url_ref().ReplaceSearchTerms( | |
673 search_term_args, | |
674 providers_.template_url_service()->search_terms_data())); | |
675 if (!suggest_url.is_valid()) | |
676 return NULL; | |
677 // Send the current page URL if user setting and URL requirements are met and | |
678 // the user is in the field trial. | |
679 if (CanSendURL(current_page_url_, suggest_url, template_url, | |
680 input.current_page_classification(), | |
681 template_url_service_->search_terms_data(), delegate_.get()) && | |
682 OmniboxFieldTrial::InZeroSuggestAfterTypingFieldTrial()) { | |
683 search_term_args.current_page_url = current_page_url_.spec(); | |
684 // Create the suggest URL again with the current page URL. | |
685 suggest_url = GURL(template_url->suggestions_url_ref().ReplaceSearchTerms( | |
686 search_term_args, | |
687 providers_.template_url_service()->search_terms_data())); | |
688 } | |
689 | |
690 suggest_results_pending_++; | |
691 LogOmniboxSuggestRequest(REQUEST_SENT); | |
692 | |
693 net::URLFetcher* fetcher = | |
694 net::URLFetcher::Create(id, suggest_url, net::URLFetcher::GET, this); | |
695 fetcher->SetRequestContext(delegate_->RequestContext()); | |
696 fetcher->SetLoadFlags(net::LOAD_DO_NOT_SAVE_COOKIES); | |
697 // Add Chrome experiment state to the request headers. | |
698 net::HttpRequestHeaders headers; | |
699 variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders( | |
700 fetcher->GetOriginalURL(), delegate_->IsOffTheRecord(), false, &headers); | |
701 fetcher->SetExtraRequestHeaders(headers.ToString()); | |
702 fetcher->Start(); | |
703 return fetcher; | |
704 } | |
705 | |
706 void SearchProvider::ConvertResultsToAutocompleteMatches() { | |
707 // Convert all the results to matches and add them to a map, so we can keep | |
708 // the most relevant match for each result. | |
709 base::TimeTicks start_time(base::TimeTicks::Now()); | |
710 MatchMap map; | |
711 const base::Time no_time; | |
712 int did_not_accept_keyword_suggestion = | |
713 keyword_results_.suggest_results.empty() ? | |
714 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : | |
715 TemplateURLRef::NO_SUGGESTION_CHOSEN; | |
716 | |
717 bool relevance_from_server; | |
718 int verbatim_relevance = GetVerbatimRelevance(&relevance_from_server); | |
719 int did_not_accept_default_suggestion = | |
720 default_results_.suggest_results.empty() ? | |
721 TemplateURLRef::NO_SUGGESTIONS_AVAILABLE : | |
722 TemplateURLRef::NO_SUGGESTION_CHOSEN; | |
723 const TemplateURL* keyword_url = providers_.GetKeywordProviderURL(); | |
724 if (verbatim_relevance > 0) { | |
725 const base::string16& trimmed_verbatim = | |
726 base::CollapseWhitespace(input_.text(), false); | |
727 | |
728 // Verbatim results don't get suggestions and hence, answers. | |
729 // Scan previous matches if the last answer-bearing suggestion matches | |
730 // verbatim, and if so, copy over answer contents. | |
731 base::string16 answer_contents; | |
732 base::string16 answer_type; | |
733 for (ACMatches::iterator it = matches_.begin(); it != matches_.end(); | |
734 ++it) { | |
735 if (!it->answer_contents.empty() && | |
736 it->fill_into_edit == trimmed_verbatim) { | |
737 answer_contents = it->answer_contents; | |
738 answer_type = it->answer_type; | |
739 break; | |
740 } | |
741 } | |
742 | |
743 SearchSuggestionParser::SuggestResult verbatim( | |
744 trimmed_verbatim, AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED, | |
745 trimmed_verbatim, base::string16(), base::string16(), answer_contents, | |
746 answer_type, std::string(), std::string(), false, verbatim_relevance, | |
747 relevance_from_server, false, trimmed_verbatim); | |
748 AddMatchToMap(verbatim, std::string(), did_not_accept_default_suggestion, | |
749 false, keyword_url != NULL, &map); | |
750 } | |
751 if (!keyword_input_.text().empty()) { | |
752 // We only create the verbatim search query match for a keyword | |
753 // if it's not an extension keyword. Extension keywords are handled | |
754 // in KeywordProvider::Start(). (Extensions are complicated...) | |
755 // Note: in this provider, SEARCH_OTHER_ENGINE must correspond | |
756 // to the keyword verbatim search query. Do not create other matches | |
757 // of type SEARCH_OTHER_ENGINE. | |
758 if (keyword_url && | |
759 (keyword_url->GetType() != TemplateURL::OMNIBOX_API_EXTENSION)) { | |
760 bool keyword_relevance_from_server; | |
761 const int keyword_verbatim_relevance = | |
762 GetKeywordVerbatimRelevance(&keyword_relevance_from_server); | |
763 if (keyword_verbatim_relevance > 0) { | |
764 const base::string16& trimmed_verbatim = | |
765 base::CollapseWhitespace(keyword_input_.text(), false); | |
766 SearchSuggestionParser::SuggestResult verbatim( | |
767 trimmed_verbatim, AutocompleteMatchType::SEARCH_OTHER_ENGINE, | |
768 trimmed_verbatim, base::string16(), base::string16(), | |
769 base::string16(), base::string16(), std::string(), std::string(), | |
770 true, keyword_verbatim_relevance, keyword_relevance_from_server, | |
771 false, trimmed_verbatim); | |
772 AddMatchToMap(verbatim, std::string(), | |
773 did_not_accept_keyword_suggestion, false, true, &map); | |
774 } | |
775 } | |
776 } | |
777 AddHistoryResultsToMap(keyword_history_results_, true, | |
778 did_not_accept_keyword_suggestion, &map); | |
779 AddHistoryResultsToMap(default_history_results_, false, | |
780 did_not_accept_default_suggestion, &map); | |
781 | |
782 AddSuggestResultsToMap(keyword_results_.suggest_results, | |
783 keyword_results_.metadata, &map); | |
784 AddSuggestResultsToMap(default_results_.suggest_results, | |
785 default_results_.metadata, &map); | |
786 | |
787 ACMatches matches; | |
788 for (MatchMap::const_iterator i(map.begin()); i != map.end(); ++i) | |
789 matches.push_back(i->second); | |
790 | |
791 AddNavigationResultsToMatches(keyword_results_.navigation_results, &matches); | |
792 AddNavigationResultsToMatches(default_results_.navigation_results, &matches); | |
793 | |
794 // Now add the most relevant matches to |matches_|. We take up to kMaxMatches | |
795 // suggest/navsuggest matches, regardless of origin. If Instant Extended is | |
796 // enabled and we have server-provided (and thus hopefully more accurate) | |
797 // scores for some suggestions, we allow more of those, until we reach | |
798 // AutocompleteResult::kMaxMatches total matches (that is, enough to fill the | |
799 // whole popup). | |
800 // | |
801 // We will always return any verbatim matches, no matter how we obtained their | |
802 // scores, unless we have already accepted AutocompleteResult::kMaxMatches | |
803 // higher-scoring matches under the conditions above. | |
804 std::sort(matches.begin(), matches.end(), &AutocompleteMatch::MoreRelevant); | |
805 matches_.clear(); | |
806 | |
807 size_t num_suggestions = 0; | |
808 for (ACMatches::const_iterator i(matches.begin()); | |
809 (i != matches.end()) && | |
810 (matches_.size() < AutocompleteResult::kMaxMatches); | |
811 ++i) { | |
812 // SEARCH_OTHER_ENGINE is only used in the SearchProvider for the keyword | |
813 // verbatim result, so this condition basically means "if this match is a | |
814 // suggestion of some sort". | |
815 if ((i->type != AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED) && | |
816 (i->type != AutocompleteMatchType::SEARCH_OTHER_ENGINE)) { | |
817 // If we've already hit the limit on non-server-scored suggestions, and | |
818 // this isn't a server-scored suggestion we can add, skip it. | |
819 if ((num_suggestions >= kMaxMatches) && | |
820 (!chrome::IsInstantExtendedAPIEnabled() || | |
821 (i->GetAdditionalInfo(kRelevanceFromServerKey) != kTrue))) { | |
822 continue; | |
823 } | |
824 | |
825 ++num_suggestions; | |
826 } | |
827 | |
828 matches_.push_back(*i); | |
829 } | |
830 UMA_HISTOGRAM_TIMES("Omnibox.SearchProvider.ConvertResultsTime", | |
831 base::TimeTicks::Now() - start_time); | |
832 } | |
833 | |
834 ACMatches::const_iterator SearchProvider::FindTopMatch() const { | |
835 ACMatches::const_iterator it = matches_.begin(); | |
836 while ((it != matches_.end()) && !it->allowed_to_be_default_match) | |
837 ++it; | |
838 return it; | |
839 } | |
840 | |
841 bool SearchProvider::IsTopMatchSearchWithURLInput() const { | |
842 ACMatches::const_iterator first_match = FindTopMatch(); | |
843 return (input_.type() == metrics::OmniboxInputType::URL) && | |
844 (first_match != matches_.end()) && | |
845 (first_match->relevance > CalculateRelevanceForVerbatim()) && | |
846 (first_match->type != AutocompleteMatchType::NAVSUGGEST) && | |
847 (first_match->type != AutocompleteMatchType::NAVSUGGEST_PERSONALIZED); | |
848 } | |
849 | |
850 void SearchProvider::AddNavigationResultsToMatches( | |
851 const SearchSuggestionParser::NavigationResults& navigation_results, | |
852 ACMatches* matches) { | |
853 for (SearchSuggestionParser::NavigationResults::const_iterator it = | |
854 navigation_results.begin(); it != navigation_results.end(); ++it) { | |
855 matches->push_back(NavigationToMatch(*it)); | |
856 // In the absence of suggested relevance scores, use only the single | |
857 // highest-scoring result. (The results are already sorted by relevance.) | |
858 if (!it->relevance_from_server()) | |
859 return; | |
860 } | |
861 } | |
862 | |
863 void SearchProvider::AddHistoryResultsToMap(const HistoryResults& results, | |
864 bool is_keyword, | |
865 int did_not_accept_suggestion, | |
866 MatchMap* map) { | |
867 if (results.empty()) | |
868 return; | |
869 | |
870 base::TimeTicks start_time(base::TimeTicks::Now()); | |
871 bool prevent_inline_autocomplete = input_.prevent_inline_autocomplete() || | |
872 (input_.type() == metrics::OmniboxInputType::URL); | |
873 const base::string16& input_text = | |
874 is_keyword ? keyword_input_.text() : input_.text(); | |
875 bool input_multiple_words = HasMultipleWords(input_text); | |
876 | |
877 SearchSuggestionParser::SuggestResults scored_results; | |
878 if (!prevent_inline_autocomplete && input_multiple_words) { | |
879 // ScoreHistoryResults() allows autocompletion of multi-word, 1-visit | |
880 // queries if the input also has multiple words. But if we were already | |
881 // scoring a multi-word, multi-visit query aggressively, and the current | |
882 // input is still a prefix of it, then changing the suggestion suddenly | |
883 // feels wrong. To detect this case, first score as if only one word has | |
884 // been typed, then check if the best result came from aggressive search | |
885 // history scoring. If it did, then just keep that score set. This | |
886 // 1200 the lowest possible score in CalculateRelevanceForHistory()'s | |
887 // aggressive-scoring curve. | |
888 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, | |
889 false, input_text, is_keyword); | |
890 if ((scored_results.front().relevance() < 1200) || | |
891 !HasMultipleWords(scored_results.front().suggestion())) | |
892 scored_results.clear(); // Didn't detect the case above, score normally. | |
893 } | |
894 if (scored_results.empty()) | |
895 scored_results = ScoreHistoryResults(results, prevent_inline_autocomplete, | |
896 input_multiple_words, input_text, | |
897 is_keyword); | |
898 for (SearchSuggestionParser::SuggestResults::const_iterator i( | |
899 scored_results.begin()); i != scored_results.end(); ++i) { | |
900 AddMatchToMap(*i, std::string(), did_not_accept_suggestion, true, | |
901 providers_.GetKeywordProviderURL() != NULL, map); | |
902 } | |
903 UMA_HISTOGRAM_TIMES("Omnibox.SearchProvider.AddHistoryResultsTime", | |
904 base::TimeTicks::Now() - start_time); | |
905 } | |
906 | |
907 SearchSuggestionParser::SuggestResults SearchProvider::ScoreHistoryResults( | |
908 const HistoryResults& results, | |
909 bool base_prevent_inline_autocomplete, | |
910 bool input_multiple_words, | |
911 const base::string16& input_text, | |
912 bool is_keyword) { | |
913 SearchSuggestionParser::SuggestResults scored_results; | |
914 // True if the user has asked this exact query previously. | |
915 bool found_what_you_typed_match = false; | |
916 const bool prevent_search_history_inlining = | |
917 OmniboxFieldTrial::SearchHistoryPreventInlining( | |
918 input_.current_page_classification()); | |
919 const base::string16& trimmed_input = | |
920 base::CollapseWhitespace(input_text, false); | |
921 for (HistoryResults::const_iterator i(results.begin()); i != results.end(); | |
922 ++i) { | |
923 const base::string16& trimmed_suggestion = | |
924 base::CollapseWhitespace(i->term, false); | |
925 | |
926 // Don't autocomplete multi-word queries that have only been seen once | |
927 // unless the user has typed more than one word. | |
928 bool prevent_inline_autocomplete = base_prevent_inline_autocomplete || | |
929 (!input_multiple_words && (i->visits < 2) && | |
930 HasMultipleWords(trimmed_suggestion)); | |
931 | |
932 int relevance = CalculateRelevanceForHistory( | |
933 i->time, is_keyword, !prevent_inline_autocomplete, | |
934 prevent_search_history_inlining); | |
935 // Add the match to |scored_results| by putting the what-you-typed match | |
936 // on the front and appending all other matches. We want the what-you- | |
937 // typed match to always be first. | |
938 SearchSuggestionParser::SuggestResults::iterator insertion_position = | |
939 scored_results.end(); | |
940 if (trimmed_suggestion == trimmed_input) { | |
941 found_what_you_typed_match = true; | |
942 insertion_position = scored_results.begin(); | |
943 } | |
944 scored_results.insert( | |
945 insertion_position, | |
946 SearchSuggestionParser::SuggestResult( | |
947 trimmed_suggestion, AutocompleteMatchType::SEARCH_HISTORY, | |
948 trimmed_suggestion, base::string16(), base::string16(), | |
949 base::string16(), base::string16(), std::string(), std::string(), | |
950 is_keyword, relevance, false, false, trimmed_input)); | |
951 } | |
952 | |
953 // History returns results sorted for us. However, we may have docked some | |
954 // results' scores, so things are no longer in order. While keeping the | |
955 // what-you-typed match at the front (if it exists), do a stable sort to get | |
956 // things back in order without otherwise disturbing results with equal | |
957 // scores, then force the scores to be unique, so that the order in which | |
958 // they're shown is deterministic. | |
959 std::stable_sort(scored_results.begin() + | |
960 (found_what_you_typed_match ? 1 : 0), | |
961 scored_results.end(), | |
962 CompareScoredResults()); | |
963 | |
964 // Don't autocomplete to search terms that would normally be treated as URLs | |
965 // when typed. For example, if the user searched for "google.com" and types | |
966 // "goog", don't autocomplete to the search term "google.com". Otherwise, | |
967 // the input will look like a URL but act like a search, which is confusing. | |
968 // The 1200 relevance score threshold in the test below is the lowest | |
969 // possible score in CalculateRelevanceForHistory()'s aggressive-scoring | |
970 // curve. This is an appropriate threshold to use to decide if we're overly | |
971 // aggressively inlining because, if we decide the answer is yes, the | |
972 // way we resolve it it to not use the aggressive-scoring curve. | |
973 // NOTE: We don't check for autocompleting to URLs in the following cases: | |
974 // * When inline autocomplete is disabled, we won't be inline autocompleting | |
975 // this term, so we don't need to worry about confusion as much. This | |
976 // also prevents calling Classify() again from inside the classifier | |
977 // (which will corrupt state and likely crash), since the classifier | |
978 // always disables inline autocomplete. | |
979 // * When the user has typed the whole string before as a query, then it's | |
980 // likely the user has no expectation that term should be interpreted as | |
981 // as a URL, so we need not do anything special to preserve user | |
982 // expectation. | |
983 int last_relevance = 0; | |
984 if (!base_prevent_inline_autocomplete && !found_what_you_typed_match && | |
985 scored_results.front().relevance() >= 1200) { | |
986 AutocompleteMatch match; | |
987 delegate_->Classify(scored_results.front().suggestion(), false, false, | |
988 input_.current_page_classification(), &match, NULL); | |
989 // Demote this match that would normally be interpreted as a URL to have | |
990 // the highest score a previously-issued search query could have when | |
991 // scoring with the non-aggressive method. A consequence of demoting | |
992 // by revising |last_relevance| is that this match and all following | |
993 // matches get demoted; the relative order of matches is preserved. | |
994 // One could imagine demoting only those matches that might cause | |
995 // confusion (which, by the way, might change the relative order of | |
996 // matches. We have decided to go with the simple demote-all approach | |
997 // because selective demotion requires multiple Classify() calls and | |
998 // such calls can be expensive (as expensive as running the whole | |
999 // autocomplete system). | |
1000 if (!AutocompleteMatch::IsSearchType(match.type)) { | |
1001 last_relevance = CalculateRelevanceForHistory( | |
1002 base::Time::Now(), is_keyword, false, | |
1003 prevent_search_history_inlining); | |
1004 } | |
1005 } | |
1006 | |
1007 for (SearchSuggestionParser::SuggestResults::iterator i( | |
1008 scored_results.begin()); i != scored_results.end(); ++i) { | |
1009 if ((last_relevance != 0) && (i->relevance() >= last_relevance)) | |
1010 i->set_relevance(last_relevance - 1); | |
1011 last_relevance = i->relevance(); | |
1012 } | |
1013 | |
1014 return scored_results; | |
1015 } | |
1016 | |
1017 void SearchProvider::AddSuggestResultsToMap( | |
1018 const SearchSuggestionParser::SuggestResults& results, | |
1019 const std::string& metadata, | |
1020 MatchMap* map) { | |
1021 for (size_t i = 0; i < results.size(); ++i) { | |
1022 AddMatchToMap(results[i], metadata, i, false, | |
1023 providers_.GetKeywordProviderURL() != NULL, map); | |
1024 } | |
1025 } | |
1026 | |
1027 int SearchProvider::GetVerbatimRelevance(bool* relevance_from_server) const { | |
1028 // Use the suggested verbatim relevance score if it is non-negative (valid), | |
1029 // if inline autocomplete isn't prevented (always show verbatim on backspace), | |
1030 // and if it won't suppress verbatim, leaving no default provider matches. | |
1031 // Otherwise, if the default provider returned no matches and was still able | |
1032 // to suppress verbatim, the user would have no search/nav matches and may be | |
1033 // left unable to search using their default provider from the omnibox. | |
1034 // Check for results on each verbatim calculation, as results from older | |
1035 // queries (on previous input) may be trimmed for failing to inline new input. | |
1036 bool use_server_relevance = | |
1037 (default_results_.verbatim_relevance >= 0) && | |
1038 !input_.prevent_inline_autocomplete() && | |
1039 ((default_results_.verbatim_relevance > 0) || | |
1040 !default_results_.suggest_results.empty() || | |
1041 !default_results_.navigation_results.empty()); | |
1042 if (relevance_from_server) | |
1043 *relevance_from_server = use_server_relevance; | |
1044 return use_server_relevance ? | |
1045 default_results_.verbatim_relevance : CalculateRelevanceForVerbatim(); | |
1046 } | |
1047 | |
1048 int SearchProvider::CalculateRelevanceForVerbatim() const { | |
1049 if (!providers_.keyword_provider().empty()) | |
1050 return 250; | |
1051 return CalculateRelevanceForVerbatimIgnoringKeywordModeState(); | |
1052 } | |
1053 | |
1054 int SearchProvider:: | |
1055 CalculateRelevanceForVerbatimIgnoringKeywordModeState() const { | |
1056 switch (input_.type()) { | |
1057 case metrics::OmniboxInputType::UNKNOWN: | |
1058 case metrics::OmniboxInputType::QUERY: | |
1059 case metrics::OmniboxInputType::FORCED_QUERY: | |
1060 return kNonURLVerbatimRelevance; | |
1061 | |
1062 case metrics::OmniboxInputType::URL: | |
1063 return 850; | |
1064 | |
1065 default: | |
1066 NOTREACHED(); | |
1067 return 0; | |
1068 } | |
1069 } | |
1070 | |
1071 int SearchProvider::GetKeywordVerbatimRelevance( | |
1072 bool* relevance_from_server) const { | |
1073 // Use the suggested verbatim relevance score if it is non-negative (valid), | |
1074 // if inline autocomplete isn't prevented (always show verbatim on backspace), | |
1075 // and if it won't suppress verbatim, leaving no keyword provider matches. | |
1076 // Otherwise, if the keyword provider returned no matches and was still able | |
1077 // to suppress verbatim, the user would have no search/nav matches and may be | |
1078 // left unable to search using their keyword provider from the omnibox. | |
1079 // Check for results on each verbatim calculation, as results from older | |
1080 // queries (on previous input) may be trimmed for failing to inline new input. | |
1081 bool use_server_relevance = | |
1082 (keyword_results_.verbatim_relevance >= 0) && | |
1083 !input_.prevent_inline_autocomplete() && | |
1084 ((keyword_results_.verbatim_relevance > 0) || | |
1085 !keyword_results_.suggest_results.empty() || | |
1086 !keyword_results_.navigation_results.empty()); | |
1087 if (relevance_from_server) | |
1088 *relevance_from_server = use_server_relevance; | |
1089 return use_server_relevance ? | |
1090 keyword_results_.verbatim_relevance : | |
1091 CalculateRelevanceForKeywordVerbatim(keyword_input_.type(), | |
1092 keyword_input_.prefer_keyword()); | |
1093 } | |
1094 | |
1095 int SearchProvider::CalculateRelevanceForHistory( | |
1096 const base::Time& time, | |
1097 bool is_keyword, | |
1098 bool use_aggressive_method, | |
1099 bool prevent_search_history_inlining) const { | |
1100 // The relevance of past searches falls off over time. There are two distinct | |
1101 // equations used. If the first equation is used (searches to the primary | |
1102 // provider that we want to score aggressively), the score is in the range | |
1103 // 1300-1599 (unless |prevent_search_history_inlining|, in which case | |
1104 // it's in the range 1200-1299). If the second equation is used the | |
1105 // relevance of a search 15 minutes ago is discounted 50 points, while the | |
1106 // relevance of a search two weeks ago is discounted 450 points. | |
1107 double elapsed_time = std::max((base::Time::Now() - time).InSecondsF(), 0.0); | |
1108 bool is_primary_provider = is_keyword || !providers_.has_keyword_provider(); | |
1109 if (is_primary_provider && use_aggressive_method) { | |
1110 // Searches with the past two days get a different curve. | |
1111 const double autocomplete_time = 2 * 24 * 60 * 60; | |
1112 if (elapsed_time < autocomplete_time) { | |
1113 int max_score = is_keyword ? 1599 : 1399; | |
1114 if (prevent_search_history_inlining) | |
1115 max_score = 1299; | |
1116 return max_score - static_cast<int>(99 * | |
1117 std::pow(elapsed_time / autocomplete_time, 2.5)); | |
1118 } | |
1119 elapsed_time -= autocomplete_time; | |
1120 } | |
1121 | |
1122 const int score_discount = | |
1123 static_cast<int>(6.5 * std::pow(elapsed_time, 0.3)); | |
1124 | |
1125 // Don't let scores go below 0. Negative relevance scores are meaningful in | |
1126 // a different way. | |
1127 int base_score; | |
1128 if (is_primary_provider) | |
1129 base_score = (input_.type() == metrics::OmniboxInputType::URL) ? 750 : 1050; | |
1130 else | |
1131 base_score = 200; | |
1132 return std::max(0, base_score - score_discount); | |
1133 } | |
1134 | |
1135 AutocompleteMatch SearchProvider::NavigationToMatch( | |
1136 const SearchSuggestionParser::NavigationResult& navigation) { | |
1137 base::string16 input; | |
1138 const bool trimmed_whitespace = base::TrimWhitespace( | |
1139 navigation.from_keyword_provider() ? | |
1140 keyword_input_.text() : input_.text(), | |
1141 base::TRIM_TRAILING, &input) != base::TRIM_NONE; | |
1142 AutocompleteMatch match(this, navigation.relevance(), false, | |
1143 navigation.type()); | |
1144 match.destination_url = navigation.url(); | |
1145 BaseSearchProvider::SetDeletionURL(navigation.deletion_url(), &match); | |
1146 // First look for the user's input inside the formatted url as it would be | |
1147 // without trimming the scheme, so we can find matches at the beginning of the | |
1148 // scheme. | |
1149 const URLPrefix* prefix = | |
1150 URLPrefix::BestURLPrefix(navigation.formatted_url(), input); | |
1151 size_t match_start = (prefix == NULL) ? | |
1152 navigation.formatted_url().find(input) : prefix->prefix.length(); | |
1153 bool trim_http = !AutocompleteInput::HasHTTPScheme(input) && | |
1154 (!prefix || (match_start != 0)); | |
1155 const net::FormatUrlTypes format_types = | |
1156 net::kFormatUrlOmitAll & ~(trim_http ? 0 : net::kFormatUrlOmitHTTP); | |
1157 | |
1158 const std::string languages(delegate_->AcceptLanguages()); | |
1159 size_t inline_autocomplete_offset = (prefix == NULL) ? | |
1160 base::string16::npos : (match_start + input.length()); | |
1161 match.fill_into_edit += | |
1162 AutocompleteInput::FormattedStringWithEquivalentMeaning( | |
1163 navigation.url(), | |
1164 net::FormatUrl(navigation.url(), languages, format_types, | |
1165 net::UnescapeRule::SPACES, NULL, NULL, | |
1166 &inline_autocomplete_offset), | |
1167 delegate_->SchemeClassifier()); | |
1168 // Preserve the forced query '?' prefix in |match.fill_into_edit|. | |
1169 // Otherwise, user edits to a suggestion would show non-Search results. | |
1170 if (input_.type() == metrics::OmniboxInputType::FORCED_QUERY) { | |
1171 match.fill_into_edit.insert(0, base::ASCIIToUTF16("?")); | |
1172 if (inline_autocomplete_offset != base::string16::npos) | |
1173 ++inline_autocomplete_offset; | |
1174 } | |
1175 if (inline_autocomplete_offset != base::string16::npos) { | |
1176 DCHECK(inline_autocomplete_offset <= match.fill_into_edit.length()); | |
1177 match.inline_autocompletion = | |
1178 match.fill_into_edit.substr(inline_autocomplete_offset); | |
1179 } | |
1180 // An inlineable navsuggestion can only be the default match when there | |
1181 // is no keyword provider active, lest it appear first and break the user | |
1182 // out of keyword mode. It can also only be default if either the inline | |
1183 // autocompletion is empty or we're not preventing inline autocompletion. | |
1184 // Finally, if we have an inlineable navsuggestion with an inline completion | |
1185 // that we're not preventing, make sure we didn't trim any whitespace. | |
1186 // We don't want to claim http://foo.com/bar is inlineable against the | |
1187 // input "foo.com/b ". | |
1188 match.allowed_to_be_default_match = (prefix != NULL) && | |
1189 (providers_.GetKeywordProviderURL() == NULL) && | |
1190 (match.inline_autocompletion.empty() || | |
1191 (!input_.prevent_inline_autocomplete() && !trimmed_whitespace)); | |
1192 match.EnsureUWYTIsAllowedToBeDefault( | |
1193 input_.canonicalized_url(), providers_.template_url_service()); | |
1194 | |
1195 match.contents = navigation.match_contents(); | |
1196 match.contents_class = navigation.match_contents_class(); | |
1197 match.description = navigation.description(); | |
1198 AutocompleteMatch::ClassifyMatchInString(input, match.description, | |
1199 ACMatchClassification::NONE, &match.description_class); | |
1200 | |
1201 match.RecordAdditionalInfo( | |
1202 kRelevanceFromServerKey, | |
1203 navigation.relevance_from_server() ? kTrue : kFalse); | |
1204 match.RecordAdditionalInfo(kShouldPrefetchKey, kFalse); | |
1205 | |
1206 return match; | |
1207 } | |
1208 | |
1209 void SearchProvider::UpdateDone() { | |
1210 // We're done when the timer isn't running, there are no suggest queries | |
1211 // pending, and we're not waiting on Instant. | |
1212 done_ = !timer_.IsRunning() && (suggest_results_pending_ == 0); | |
1213 } | |
1214 | |
1215 std::string SearchProvider::GetSessionToken() { | |
1216 base::TimeTicks current_time(base::TimeTicks::Now()); | |
1217 // Renew token if it expired. | |
1218 if (current_time > token_expiration_time_) { | |
1219 const size_t kTokenBytes = 12; | |
1220 std::string raw_data; | |
1221 base::RandBytes(WriteInto(&raw_data, kTokenBytes + 1), kTokenBytes); | |
1222 base::Base64Encode(raw_data, ¤t_token_); | |
1223 | |
1224 // Make the base64 encoded value URL and filename safe(see RFC 3548). | |
1225 std::replace(current_token_.begin(), current_token_.end(), '+', '-'); | |
1226 std::replace(current_token_.begin(), current_token_.end(), '/', '_'); | |
1227 } | |
1228 | |
1229 // Extend expiration time another 60 seconds. | |
1230 token_expiration_time_ = current_time + base::TimeDelta::FromSeconds(60); | |
1231 | |
1232 return current_token_; | |
1233 } | |
1234 | |
1235 void SearchProvider::RegisterDisplayedAnswers( | |
1236 const AutocompleteResult& result) { | |
1237 if (result.empty()) | |
1238 return; | |
1239 | |
1240 // The answer must be in the first or second slot to be considered. It should | |
1241 // only be in the second slot if AutocompleteController ranked a local search | |
1242 // history or a verbatim item higher than the answer. | |
1243 AutocompleteResult::const_iterator match = result.begin(); | |
1244 if (match->answer_contents.empty() && result.size() > 1) | |
1245 ++match; | |
1246 if (match->answer_contents.empty() || match->answer_type.empty() || | |
1247 match->fill_into_edit.empty()) | |
1248 return; | |
1249 | |
1250 // Valid answer encountered, cache it for further queries. | |
1251 answers_cache_.UpdateRecentAnswers(match->fill_into_edit, match->answer_type); | |
1252 } | |
1253 | |
1254 void SearchProvider::DoAnswersQuery(const AutocompleteInput& input) { | |
1255 prefetch_data_ = answers_cache_.GetTopAnswerEntry(input.text()); | |
1256 } | |
OLD | NEW |