OLD | NEW |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "ui/app_list/search/mixer.h" | 5 #include "ui/app_list/search/mixer.h" |
6 | 6 |
7 #include <algorithm> | 7 #include <algorithm> |
8 #include <map> | 8 #include <map> |
9 #include <set> | 9 #include <set> |
10 #include <string> | 10 #include <string> |
11 #include <vector> | 11 #include <vector> |
12 | 12 |
| 13 #include "base/metrics/field_trial.h" |
13 #include "ui/app_list/search_provider.h" | 14 #include "ui/app_list/search_provider.h" |
14 #include "ui/app_list/search_result.h" | 15 #include "ui/app_list/search_result.h" |
15 | 16 |
16 namespace app_list { | 17 namespace app_list { |
17 | 18 |
18 namespace { | 19 namespace { |
19 | 20 |
20 // Maximum number of results to show. | 21 // Maximum number of results to show. Ignored if the AppListMixer field trial is |
| 22 // "Blended". |
21 const size_t kMaxResults = 6; | 23 const size_t kMaxResults = 6; |
22 | 24 |
| 25 // The minimum number of results to show, if the AppListMixer field trial is |
| 26 // "Blended". If this quota is not reached, the per-group limitations are |
| 27 // removed and we try again. (We may still not reach the minumum, but at least |
| 28 // we tried.) Ignored if the field trial is off. |
| 29 const size_t kMinBlendedResults = 6; |
| 30 |
| 31 const char kAppListMixerFieldTrialName[] = "AppListMixer"; |
| 32 const char kAppListMixerFieldTrialEnabled[] = "Blended"; |
| 33 |
23 void UpdateResult(const SearchResult& source, SearchResult* target) { | 34 void UpdateResult(const SearchResult& source, SearchResult* target) { |
24 target->set_display_type(source.display_type()); | 35 target->set_display_type(source.display_type()); |
25 target->set_title(source.title()); | 36 target->set_title(source.title()); |
26 target->set_title_tags(source.title_tags()); | 37 target->set_title_tags(source.title_tags()); |
27 target->set_details(source.details()); | 38 target->set_details(source.details()); |
28 target->set_details_tags(source.details_tags()); | 39 target->set_details_tags(source.details_tags()); |
29 } | 40 } |
30 | 41 |
| 42 // Returns true if the "AppListMixer" trial is set to "Blended". This is an |
| 43 // experiment on the new Mixer logic that allows results from different groups |
| 44 // to be blended together, rather than stratified. |
| 45 bool IsBlendedMixerTrialEnabled() { |
| 46 const std::string group_name = |
| 47 base::FieldTrialList::FindFullName(kAppListMixerFieldTrialName); |
| 48 return group_name == kAppListMixerFieldTrialEnabled; |
| 49 } |
| 50 |
31 } // namespace | 51 } // namespace |
32 | 52 |
33 Mixer::SortData::SortData() : result(NULL), score(0.0) { | 53 Mixer::SortData::SortData() : result(NULL), score(0.0) { |
34 } | 54 } |
35 | 55 |
36 Mixer::SortData::SortData(SearchResult* result, double score) | 56 Mixer::SortData::SortData(SearchResult* result, double score) |
37 : result(result), score(score) { | 57 : result(result), score(score) { |
38 } | 58 } |
39 | 59 |
40 bool Mixer::SortData::operator<(const SortData& other) const { | 60 bool Mixer::SortData::operator<(const SortData& other) const { |
41 // This data precedes (less than) |other| if it has higher score. | 61 // This data precedes (less than) |other| if it has higher score. |
42 return score > other.score; | 62 return score > other.score; |
43 } | 63 } |
44 | 64 |
45 // Used to group relevant providers together for mixing their results. | 65 // Used to group relevant providers together for mixing their results. |
46 class Mixer::Group { | 66 class Mixer::Group { |
47 public: | 67 public: |
48 Group(size_t max_results, double boost) | 68 Group(size_t max_results, double boost, double multiplier) |
49 : max_results_(max_results), boost_(boost) {} | 69 : max_results_(max_results), boost_(boost), multiplier_(multiplier) {} |
50 ~Group() {} | 70 ~Group() {} |
51 | 71 |
52 void AddProvider(SearchProvider* provider) { providers_.push_back(provider); } | 72 void AddProvider(SearchProvider* provider) { providers_.push_back(provider); } |
53 | 73 |
54 void FetchResults(bool is_voice_query, const KnownResults& known_results) { | 74 void FetchResults(bool is_voice_query, const KnownResults& known_results) { |
55 results_.clear(); | 75 results_.clear(); |
56 | 76 |
57 for (const SearchProvider* provider : providers_) { | 77 for (const SearchProvider* provider : providers_) { |
58 for (SearchResult* result : provider->results()) { | 78 for (SearchResult* result : provider->results()) { |
59 DCHECK(!result->id().empty()); | 79 DCHECK(!result->id().empty()); |
60 | 80 |
61 // We cannot rely on providers to give relevance scores in the range | 81 // We cannot rely on providers to give relevance scores in the range |
62 // [0.0, 1.0] (e.g., PeopleProvider directly gives values from the | 82 // [0.0, 1.0] (e.g., PeopleProvider directly gives values from the |
63 // Google+ API). Clamp to that range. | 83 // Google+ API). Clamp to that range. |
64 double relevance = std::min(std::max(result->relevance(), 0.0), 1.0); | 84 double relevance = std::min(std::max(result->relevance(), 0.0), 1.0); |
65 | 85 |
| 86 double multiplier = multiplier_; |
66 double boost = boost_; | 87 double boost = boost_; |
67 KnownResults::const_iterator known_it = | 88 KnownResults::const_iterator known_it = |
68 known_results.find(result->id()); | 89 known_results.find(result->id()); |
69 if (known_it != known_results.end()) { | 90 if (known_it != known_results.end()) { |
70 switch (known_it->second) { | 91 switch (known_it->second) { |
71 case PERFECT_PRIMARY: | 92 case PERFECT_PRIMARY: |
72 boost = 4.0; | 93 boost = 4.0; |
73 break; | 94 break; |
74 case PREFIX_PRIMARY: | 95 case PREFIX_PRIMARY: |
75 boost = 3.75; | 96 boost = 3.75; |
76 break; | 97 break; |
77 case PERFECT_SECONDARY: | 98 case PERFECT_SECONDARY: |
78 boost = 3.25; | 99 boost = 3.25; |
79 break; | 100 break; |
80 case PREFIX_SECONDARY: | 101 case PREFIX_SECONDARY: |
81 boost = 3.0; | 102 boost = 3.0; |
82 break; | 103 break; |
83 case UNKNOWN_RESULT: | 104 case UNKNOWN_RESULT: |
84 NOTREACHED() << "Unknown result in KnownResults?"; | 105 NOTREACHED() << "Unknown result in KnownResults?"; |
85 break; | 106 break; |
86 } | 107 } |
87 } | 108 } |
88 | 109 |
89 // If this is a voice query, voice results receive a massive boost. | 110 // If this is a voice query, voice results receive a massive boost. |
90 if (is_voice_query && result->voice_result()) | 111 if (is_voice_query && result->voice_result()) |
91 boost += 4.0; | 112 boost += 4.0; |
92 | 113 |
93 results_.push_back(SortData(result, relevance + boost)); | 114 results_.push_back(SortData(result, relevance * multiplier + boost)); |
94 } | 115 } |
95 } | 116 } |
96 | 117 |
97 std::sort(results_.begin(), results_.end()); | 118 std::sort(results_.begin(), results_.end()); |
98 } | 119 } |
99 | 120 |
100 const SortedResults& results() const { return results_; } | 121 const SortedResults& results() const { return results_; } |
101 | 122 |
102 const size_t max_results() const { return max_results_; } | 123 const size_t max_results() const { return max_results_; } |
103 | 124 |
104 private: | 125 private: |
105 typedef std::vector<SearchProvider*> Providers; | 126 typedef std::vector<SearchProvider*> Providers; |
106 const size_t max_results_; | 127 const size_t max_results_; |
107 const double boost_; | 128 const double boost_; |
| 129 const double multiplier_; |
108 | 130 |
109 Providers providers_; // Not owned. | 131 Providers providers_; // Not owned. |
110 SortedResults results_; | 132 SortedResults results_; |
111 | 133 |
112 DISALLOW_COPY_AND_ASSIGN(Group); | 134 DISALLOW_COPY_AND_ASSIGN(Group); |
113 }; | 135 }; |
114 | 136 |
115 Mixer::Mixer(AppListModel::SearchResults* ui_results) | 137 Mixer::Mixer(AppListModel::SearchResults* ui_results) |
116 : ui_results_(ui_results) { | 138 : ui_results_(ui_results) { |
117 } | 139 } |
118 Mixer::~Mixer() { | 140 Mixer::~Mixer() { |
119 } | 141 } |
120 | 142 |
121 size_t Mixer::AddGroup(size_t max_results, double boost) { | 143 size_t Mixer::AddGroup(size_t max_results, double boost, double multiplier) { |
122 groups_.push_back(new Group(max_results, boost)); | 144 // Only consider |boost| if the AppListMixer field trial is default. |
| 145 // Only consider |multiplier| if the AppListMixer field trial is "Blended". |
| 146 if (IsBlendedMixerTrialEnabled()) |
| 147 boost = 0.0; |
| 148 else |
| 149 multiplier = 1.0; |
| 150 groups_.push_back(new Group(max_results, boost, multiplier)); |
123 return groups_.size() - 1; | 151 return groups_.size() - 1; |
124 } | 152 } |
125 | 153 |
126 size_t Mixer::AddOmniboxGroup(size_t max_results, double boost) { | 154 size_t Mixer::AddOmniboxGroup(size_t max_results, |
| 155 double boost, |
| 156 double multiplier) { |
127 // There should not already be an omnibox group. | 157 // There should not already be an omnibox group. |
128 DCHECK(!has_omnibox_group_); | 158 DCHECK(!has_omnibox_group_); |
129 size_t id = AddGroup(max_results, boost); | 159 size_t id = AddGroup(max_results, boost, multiplier); |
130 omnibox_group_ = id; | 160 omnibox_group_ = id; |
131 has_omnibox_group_ = true; | 161 has_omnibox_group_ = true; |
132 return id; | 162 return id; |
133 } | 163 } |
134 | 164 |
135 void Mixer::AddProviderToGroup(size_t group_id, SearchProvider* provider) { | 165 void Mixer::AddProviderToGroup(size_t group_id, SearchProvider* provider) { |
136 groups_[group_id]->AddProvider(provider); | 166 groups_[group_id]->AddProvider(provider); |
137 } | 167 } |
138 | 168 |
139 void Mixer::MixAndPublish(bool is_voice_query, | 169 void Mixer::MixAndPublish(bool is_voice_query, |
140 const KnownResults& known_results) { | 170 const KnownResults& known_results) { |
141 FetchResults(is_voice_query, known_results); | 171 FetchResults(is_voice_query, known_results); |
142 | 172 |
143 SortedResults results; | 173 SortedResults results; |
144 results.reserve(kMaxResults); | |
145 | 174 |
146 // Add results from non-omnibox groups first. Limit to the maximum number of | 175 if (IsBlendedMixerTrialEnabled()) { |
147 // results in each group. | 176 results.reserve(kMinBlendedResults); |
148 for (size_t i = 0; i < groups_.size(); ++i) { | 177 |
149 if (!has_omnibox_group_ || i != omnibox_group_) { | 178 // Add results from each group. Limit to the maximum number of results in |
150 const Group& group = *groups_[i]; | 179 // each group. |
| 180 for (const Group* group : groups_) { |
151 size_t num_results = | 181 size_t num_results = |
152 std::min(group.results().size(), group.max_results()); | 182 std::min(group->results().size(), group->max_results()); |
153 results.insert(results.end(), group.results().begin(), | 183 results.insert(results.end(), group->results().begin(), |
154 group.results().begin() + num_results); | 184 group->results().begin() + num_results); |
155 } | 185 } |
| 186 // Remove results with duplicate IDs before sorting. If two providers give a |
| 187 // result with the same ID, the result from the provider with the *lower |
| 188 // group number* will be kept (e.g., an app result takes priority over a web |
| 189 // store result with the same ID). |
| 190 RemoveDuplicates(&results); |
| 191 std::sort(results.begin(), results.end()); |
| 192 |
| 193 if (results.size() < kMinBlendedResults) { |
| 194 size_t original_size = results.size(); |
| 195 // We didn't get enough results. Insert all the results again, and this |
| 196 // time, do not limit the maximum number of results from each group. (This |
| 197 // will result in duplicates, which will be removed by RemoveDuplicates.) |
| 198 for (const Group* group : groups_) { |
| 199 results.insert(results.end(), group->results().begin(), |
| 200 group->results().end()); |
| 201 } |
| 202 RemoveDuplicates(&results); |
| 203 // Sort just the newly added results. This ensures that, for example, if |
| 204 // there are 6 Omnibox results (score = 0.8) and 1 People result (score = |
| 205 // 0.4) that the People result will be 5th, not 7th, because the Omnibox |
| 206 // group has a soft maximum of 4 results. (Otherwise, the People result |
| 207 // would not be seen at all once the result list is truncated.) |
| 208 std::sort(results.begin() + original_size, results.end()); |
| 209 } |
| 210 } else { |
| 211 results.reserve(kMaxResults); |
| 212 |
| 213 // Add results from non-omnibox groups first. Limit to the maximum number of |
| 214 // results in each group. |
| 215 for (size_t i = 0; i < groups_.size(); ++i) { |
| 216 if (!has_omnibox_group_ || i != omnibox_group_) { |
| 217 const Group& group = *groups_[i]; |
| 218 size_t num_results = |
| 219 std::min(group.results().size(), group.max_results()); |
| 220 results.insert(results.end(), group.results().begin(), |
| 221 group.results().begin() + num_results); |
| 222 } |
| 223 } |
| 224 |
| 225 // Collapse duplicate apps from local and web store. |
| 226 RemoveDuplicates(&results); |
| 227 |
| 228 // Fill the remaining slots with omnibox results. Always add at least one |
| 229 // omnibox result (even if there are no more slots; if we over-fill the |
| 230 // vector, the web store and people results will be removed in a later |
| 231 // step). Note: max_results() is ignored for the omnibox group. |
| 232 if (has_omnibox_group_) { |
| 233 CHECK_LT(omnibox_group_, groups_.size()); |
| 234 const Group& omnibox_group = *groups_[omnibox_group_]; |
| 235 const size_t omnibox_results = std::min( |
| 236 omnibox_group.results().size(), |
| 237 results.size() < kMaxResults ? kMaxResults - results.size() : 1); |
| 238 results.insert(results.end(), omnibox_group.results().begin(), |
| 239 omnibox_group.results().begin() + omnibox_results); |
| 240 } |
| 241 |
| 242 std::sort(results.begin(), results.end()); |
| 243 RemoveDuplicates(&results); |
| 244 if (results.size() > kMaxResults) |
| 245 results.resize(kMaxResults); |
156 } | 246 } |
157 | 247 |
158 // Collapse duplicate apps from local and web store. | |
159 RemoveDuplicates(&results); | |
160 | |
161 // Fill the remaining slots with omnibox results. Always add at least one | |
162 // omnibox result (even if there are no more slots; if we over-fill the | |
163 // vector, the web store and people results will be removed in a later step). | |
164 // Note: max_results() is ignored for the omnibox group. | |
165 if (has_omnibox_group_) { | |
166 CHECK_LT(omnibox_group_, groups_.size()); | |
167 const Group& omnibox_group = *groups_[omnibox_group_]; | |
168 const size_t omnibox_results = std::min( | |
169 omnibox_group.results().size(), | |
170 results.size() < kMaxResults ? kMaxResults - results.size() : 1); | |
171 results.insert(results.end(), omnibox_group.results().begin(), | |
172 omnibox_group.results().begin() + omnibox_results); | |
173 } | |
174 | |
175 std::sort(results.begin(), results.end()); | |
176 RemoveDuplicates(&results); | |
177 if (results.size() > kMaxResults) | |
178 results.resize(kMaxResults); | |
179 | |
180 Publish(results, ui_results_); | 248 Publish(results, ui_results_); |
181 } | 249 } |
182 | 250 |
183 void Mixer::Publish(const SortedResults& new_results, | 251 void Mixer::Publish(const SortedResults& new_results, |
184 AppListModel::SearchResults* ui_results) { | 252 AppListModel::SearchResults* ui_results) { |
185 typedef std::map<std::string, SearchResult*> IdToResultMap; | 253 typedef std::map<std::string, SearchResult*> IdToResultMap; |
186 | 254 |
187 // The following algorithm is used: | 255 // The following algorithm is used: |
188 // 1. Transform the |ui_results| list into an unordered map from result ID | 256 // 1. Transform the |ui_results| list into an unordered map from result ID |
189 // to item. | 257 // to item. |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
248 results->swap(final); | 316 results->swap(final); |
249 } | 317 } |
250 | 318 |
251 void Mixer::FetchResults(bool is_voice_query, | 319 void Mixer::FetchResults(bool is_voice_query, |
252 const KnownResults& known_results) { | 320 const KnownResults& known_results) { |
253 for (auto* group : groups_) | 321 for (auto* group : groups_) |
254 group->FetchResults(is_voice_query, known_results); | 322 group->FetchResults(is_voice_query, known_results); |
255 } | 323 } |
256 | 324 |
257 } // namespace app_list | 325 } // namespace app_list |
OLD | NEW |