Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / app_list / search / mixer.cc
blob70afc3ac84fec8fd3465b97d0dd20a3fa3820d66
1 // Copyright 2013 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.
5 #include "ui/app_list/search/mixer.h"
7 #include <algorithm>
8 #include <map>
9 #include <set>
10 #include <string>
11 #include <vector>
13 #include "base/command_line.h"
14 #include "base/metrics/field_trial.h"
15 #include "ui/app_list/app_list_switches.h"
16 #include "ui/app_list/search_provider.h"
17 #include "ui/app_list/search_result.h"
19 namespace app_list {
21 namespace {
23 // Maximum number of results to show. Ignored if the AppListMixer field trial is
24 // "Blended".
25 const size_t kMaxResults = 6;
27 // The minimum number of results to show, if the AppListMixer field trial is
28 // "Blended". If this quota is not reached, the per-group limitations are
29 // removed and we try again. (We may still not reach the minumum, but at least
30 // we tried.) Ignored if the field trial is off.
31 const size_t kMinBlendedResults = 6;
33 const char kAppListMixerFieldTrialName[] = "AppListMixer";
34 const char kAppListMixerFieldTrialEnabled[] = "Blended";
36 void UpdateResult(const SearchResult& source, SearchResult* target) {
37 target->set_display_type(source.display_type());
38 target->set_title(source.title());
39 target->set_title_tags(source.title_tags());
40 target->set_details(source.details());
41 target->set_details_tags(source.details_tags());
44 // Returns true if the "AppListMixer" trial is set to "Blended". This is an
45 // experiment on the new Mixer logic that allows results from different groups
46 // to be blended together, rather than stratified.
47 bool IsBlendedMixerTrialEnabled() {
48 // Note: It's important to query the field trial state first, to ensure that
49 // UMA reports the correct group.
50 const std::string group_name =
51 base::FieldTrialList::FindFullName(kAppListMixerFieldTrialName);
53 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
54 switches::kDisableNewAppListMixer)) {
55 return false;
58 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
59 switches::kEnableNewAppListMixer)) {
60 return true;
63 return group_name == kAppListMixerFieldTrialEnabled;
66 } // namespace
68 Mixer::SortData::SortData() : result(NULL), score(0.0) {
71 Mixer::SortData::SortData(SearchResult* result, double score)
72 : result(result), score(score) {
75 bool Mixer::SortData::operator<(const SortData& other) const {
76 // This data precedes (less than) |other| if it has higher score.
77 return score > other.score;
80 // Used to group relevant providers together for mixing their results.
81 class Mixer::Group {
82 public:
83 Group(size_t max_results, double boost, double multiplier)
84 : max_results_(max_results), boost_(boost), multiplier_(multiplier) {}
85 ~Group() {}
87 void AddProvider(SearchProvider* provider) { providers_.push_back(provider); }
89 void FetchResults(bool is_voice_query, const KnownResults& known_results) {
90 results_.clear();
92 for (const SearchProvider* provider : providers_) {
93 for (SearchResult* result : provider->results()) {
94 DCHECK(!result->id().empty());
96 // We cannot rely on providers to give relevance scores in the range
97 // [0.0, 1.0] (e.g., PeopleProvider directly gives values from the
98 // Google+ API). Clamp to that range.
99 double relevance = std::min(std::max(result->relevance(), 0.0), 1.0);
101 double multiplier = multiplier_;
102 double boost = boost_;
104 // Recommendations should not be affected by query-to-launch correlation
105 // from KnownResults as it causes recommendations to become dominated by
106 // previously clicked results. This happens because the recommendation
107 // query is the empty string and the clicked results get forever
108 // boosted.
109 if (result->display_type() != SearchResult::DISPLAY_RECOMMENDATION) {
110 KnownResults::const_iterator known_it =
111 known_results.find(result->id());
112 if (known_it != known_results.end()) {
113 switch (known_it->second) {
114 case PERFECT_PRIMARY:
115 boost = 4.0;
116 break;
117 case PREFIX_PRIMARY:
118 boost = 3.75;
119 break;
120 case PERFECT_SECONDARY:
121 boost = 3.25;
122 break;
123 case PREFIX_SECONDARY:
124 boost = 3.0;
125 break;
126 case UNKNOWN_RESULT:
127 NOTREACHED() << "Unknown result in KnownResults?";
128 break;
132 // If this is a voice query, voice results receive a massive boost.
133 if (is_voice_query && result->voice_result())
134 boost += 4.0;
137 results_.push_back(SortData(result, relevance * multiplier + boost));
141 std::sort(results_.begin(), results_.end());
144 const SortedResults& results() const { return results_; }
146 size_t max_results() const { return max_results_; }
148 private:
149 typedef std::vector<SearchProvider*> Providers;
150 const size_t max_results_;
151 const double boost_;
152 const double multiplier_;
154 Providers providers_; // Not owned.
155 SortedResults results_;
157 DISALLOW_COPY_AND_ASSIGN(Group);
160 Mixer::Mixer(AppListModel::SearchResults* ui_results)
161 : ui_results_(ui_results) {
163 Mixer::~Mixer() {
166 size_t Mixer::AddGroup(size_t max_results, double boost, double multiplier) {
167 // Only consider |boost| if the AppListMixer field trial is default.
168 // Only consider |multiplier| if the AppListMixer field trial is "Blended".
169 if (IsBlendedMixerTrialEnabled())
170 boost = 0.0;
171 else
172 multiplier = 1.0;
173 groups_.push_back(new Group(max_results, boost, multiplier));
174 return groups_.size() - 1;
177 size_t Mixer::AddOmniboxGroup(size_t max_results,
178 double boost,
179 double multiplier) {
180 // There should not already be an omnibox group.
181 DCHECK(!has_omnibox_group_);
182 size_t id = AddGroup(max_results, boost, multiplier);
183 omnibox_group_ = id;
184 has_omnibox_group_ = true;
185 return id;
188 void Mixer::AddProviderToGroup(size_t group_id, SearchProvider* provider) {
189 groups_[group_id]->AddProvider(provider);
192 void Mixer::MixAndPublish(bool is_voice_query,
193 const KnownResults& known_results) {
194 FetchResults(is_voice_query, known_results);
196 SortedResults results;
198 if (IsBlendedMixerTrialEnabled()) {
199 results.reserve(kMinBlendedResults);
201 // Add results from each group. Limit to the maximum number of results in
202 // each group.
203 for (const Group* group : groups_) {
204 size_t num_results =
205 std::min(group->results().size(), group->max_results());
206 results.insert(results.end(), group->results().begin(),
207 group->results().begin() + num_results);
209 // Remove results with duplicate IDs before sorting. If two providers give a
210 // result with the same ID, the result from the provider with the *lower
211 // group number* will be kept (e.g., an app result takes priority over a web
212 // store result with the same ID).
213 RemoveDuplicates(&results);
214 std::sort(results.begin(), results.end());
216 if (results.size() < kMinBlendedResults) {
217 size_t original_size = results.size();
218 // We didn't get enough results. Insert all the results again, and this
219 // time, do not limit the maximum number of results from each group. (This
220 // will result in duplicates, which will be removed by RemoveDuplicates.)
221 for (const Group* group : groups_) {
222 results.insert(results.end(), group->results().begin(),
223 group->results().end());
225 RemoveDuplicates(&results);
226 // Sort just the newly added results. This ensures that, for example, if
227 // there are 6 Omnibox results (score = 0.8) and 1 People result (score =
228 // 0.4) that the People result will be 5th, not 7th, because the Omnibox
229 // group has a soft maximum of 4 results. (Otherwise, the People result
230 // would not be seen at all once the result list is truncated.)
231 std::sort(results.begin() + original_size, results.end());
233 } else {
234 results.reserve(kMaxResults);
236 // Add results from non-omnibox groups first. Limit to the maximum number of
237 // results in each group.
238 for (size_t i = 0; i < groups_.size(); ++i) {
239 if (!has_omnibox_group_ || i != omnibox_group_) {
240 const Group& group = *groups_[i];
241 size_t num_results =
242 std::min(group.results().size(), group.max_results());
243 results.insert(results.end(), group.results().begin(),
244 group.results().begin() + num_results);
248 // Collapse duplicate apps from local and web store.
249 RemoveDuplicates(&results);
251 // Fill the remaining slots with omnibox results. Always add at least one
252 // omnibox result (even if there are no more slots; if we over-fill the
253 // vector, the web store and people results will be removed in a later
254 // step). Note: max_results() is ignored for the omnibox group.
255 if (has_omnibox_group_) {
256 CHECK_LT(omnibox_group_, groups_.size());
257 const Group& omnibox_group = *groups_[omnibox_group_];
258 const size_t omnibox_results = std::min(
259 omnibox_group.results().size(),
260 results.size() < kMaxResults ? kMaxResults - results.size() : 1);
261 results.insert(results.end(), omnibox_group.results().begin(),
262 omnibox_group.results().begin() + omnibox_results);
265 std::sort(results.begin(), results.end());
266 RemoveDuplicates(&results);
267 if (results.size() > kMaxResults)
268 results.resize(kMaxResults);
271 Publish(results, ui_results_);
274 void Mixer::Publish(const SortedResults& new_results,
275 AppListModel::SearchResults* ui_results) {
276 typedef std::map<std::string, SearchResult*> IdToResultMap;
278 // The following algorithm is used:
279 // 1. Transform the |ui_results| list into an unordered map from result ID
280 // to item.
281 // 2. Use the order of items in |new_results| to build an ordered list. If the
282 // result IDs exist in the map, update and use the item in the map and delete
283 // it from the map afterwards. Otherwise, clone new items from |new_results|.
284 // 3. Delete the objects remaining in the map as they are unused.
286 // A map of the items in |ui_results| that takes ownership of the items.
287 IdToResultMap ui_results_map;
288 for (SearchResult* ui_result : *ui_results)
289 ui_results_map[ui_result->id()] = ui_result;
290 // We have to erase all results at once so that observers are notified with
291 // meaningful indexes.
292 ui_results->RemoveAll();
294 // Add items back to |ui_results| in the order of |new_results|.
295 for (const SortData& sort_data : new_results) {
296 const SearchResult& new_result = *sort_data.result;
297 IdToResultMap::const_iterator ui_result_it =
298 ui_results_map.find(new_result.id());
299 if (ui_result_it != ui_results_map.end()) {
300 // Update and use the old result if it exists.
301 SearchResult* ui_result = ui_result_it->second;
302 UpdateResult(new_result, ui_result);
303 ui_result->set_relevance(sort_data.score);
305 // |ui_results| takes back ownership from |ui_results_map| here.
306 ui_results->Add(ui_result);
308 // Remove the item from the map so that it ends up only with unused
309 // results.
310 ui_results_map.erase(ui_result->id());
311 } else {
312 scoped_ptr<SearchResult> result_copy = new_result.Duplicate();
313 result_copy->set_relevance(sort_data.score);
314 // Copy the result from |new_results| otherwise.
315 ui_results->Add(result_copy.release());
319 // Delete the results remaining in the map as they are not in the new results.
320 for (const auto& ui_result : ui_results_map) {
321 delete ui_result.second;
325 void Mixer::RemoveDuplicates(SortedResults* results) {
326 SortedResults final;
327 final.reserve(results->size());
329 std::set<std::string> id_set;
330 for (const SortData& sort_data : *results) {
331 const std::string& id = sort_data.result->id();
332 if (id_set.find(id) != id_set.end())
333 continue;
335 id_set.insert(id);
336 final.push_back(sort_data);
339 results->swap(final);
342 void Mixer::FetchResults(bool is_voice_query,
343 const KnownResults& known_results) {
344 for (auto* group : groups_)
345 group->FetchResults(is_voice_query, known_results);
348 } // namespace app_list