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"
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"
23 // Maximum number of results to show. Ignored if the AppListMixer field trial is
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
)) {
58 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
59 switches::kEnableNewAppListMixer
)) {
63 return group_name
== kAppListMixerFieldTrialEnabled
;
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.
83 Group(size_t max_results
, double boost
, double multiplier
)
84 : max_results_(max_results
), boost_(boost
), multiplier_(multiplier
) {}
87 void AddProvider(SearchProvider
* provider
) { providers_
.push_back(provider
); }
89 void FetchResults(bool is_voice_query
, const KnownResults
& known_results
) {
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
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
:
120 case PERFECT_SECONDARY
:
123 case PREFIX_SECONDARY
:
127 NOTREACHED() << "Unknown result in KnownResults?";
132 // If this is a voice query, voice results receive a massive boost.
133 if (is_voice_query
&& result
->voice_result())
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_
; }
149 typedef std::vector
<SearchProvider
*> Providers
;
150 const size_t max_results_
;
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
) {
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())
173 groups_
.push_back(new Group(max_results
, boost
, multiplier
));
174 return groups_
.size() - 1;
177 size_t Mixer::AddOmniboxGroup(size_t max_results
,
180 // There should not already be an omnibox group.
181 DCHECK(!has_omnibox_group_
);
182 size_t id
= AddGroup(max_results
, boost
, multiplier
);
184 has_omnibox_group_
= true;
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
203 for (const Group
* group
: groups_
) {
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());
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
];
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
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
310 ui_results_map
.erase(ui_result
->id());
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
) {
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())
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