Commit 965f3753 authored by mgiuca's avatar mgiuca Committed by Commit bot

App launcher: Added Finch experiment for the search ranking algorithm.

Adds the "AppListMixer" Finch experiment, affecting the search result
ranking and mixing in the App Launcher. By default, the mixer behaves
exactly as before. With the "Blended" option, results are selected and
ordered more naturally according to their relevance to the query, rather
than in arbitrary groups ("apps, then omnibox, then webstore, then
people").

Specific changes under the "Blended" option:

- Replaced the per-group "boost" with a per-group "multiplier". Now,
  instead of stratifying results from each group, they are mixed
  together according to the relevance score assigned by the provider.
  Some groups have a multiplier to de-prioritize them (people results
  x0.85, webstore results x0.4). This means that relevance scores now
  need to be comparable between groups!

- Removed the 6-result limit from the mixer. It can now return an
  arbitrary number of results, which will later be truncated by the
  view. (This is necessary because the view now supports more than 6
  results, since all app results take up a single row.)

- The per-group limit is now a "soft" limit. If there aren't enough
  total results to fill a minimum quota of 6, we go back and get as many
  results as we can from each group.

- The omnibox group is no longer special. Previously, it had no limit,
  but would only provide more than 1 result if there was room. Now, it
  has a soft limit of 4, and behaves the same as other groups (can
  provide more results if necessary).

Can be enabled with --force-fieldtrials=AppListMixer/Blended.

BUG=487494,460451,422610,338418

Review URL: https://codereview.chromium.org/1113483002

Cr-Commit-Position: refs/heads/master@{#329601}
parent d9f36da7
......@@ -31,7 +31,8 @@ namespace {
// Maximum number of results to show in each mixer group.
const size_t kMaxAppsGroupResults = 4;
const size_t kMaxOmniboxResults = 0; // Ignored.
// Ignored unless AppListMixer field trial is "Blended".
const size_t kMaxOmniboxResults = 4;
const size_t kMaxWebstoreResults = 2;
const size_t kMaxPeopleResults = 2;
const size_t kMaxSuggestionsResults = 6;
......@@ -63,14 +64,19 @@ scoped_ptr<SearchController> CreateSearchController(
HistoryFactory::GetForBrowserContext(profile)));
// Add mixer groups. There are four main groups: apps, people, webstore and
// omnibox. The apps, people and webstore groups each have a fixed maximum
// number of results. The omnibox group fills the remaining slots (with a
// minimum of one result).
size_t apps_group_id = controller->AddGroup(kMaxAppsGroupResults, 3.0);
// omnibox. The behaviour depends on the AppListMixer field trial:
// - If default: The apps, people and webstore groups each have a fixed
// maximum number of results. The omnibox group fills the remaining slots
// (with a minimum of one result).
// - If "Blended": Each group has a "soft" maximum number of results. However,
// if a query turns up very few results, the mixer may take more than this
// maximum from a particular group.
size_t apps_group_id = controller->AddGroup(kMaxAppsGroupResults, 3.0, 1.0);
size_t omnibox_group_id =
controller->AddOmniboxGroup(kMaxOmniboxResults, 2.0);
size_t webstore_group_id = controller->AddGroup(kMaxWebstoreResults, 1.0);
size_t people_group_id = controller->AddGroup(kMaxPeopleResults, 0.0);
controller->AddOmniboxGroup(kMaxOmniboxResults, 2.0, 1.0);
size_t webstore_group_id =
controller->AddGroup(kMaxWebstoreResults, 1.0, 0.4);
size_t people_group_id = controller->AddGroup(kMaxPeopleResults, 0.0, 0.85);
// Add search providers.
controller->AddProvider(
......@@ -89,7 +95,7 @@ scoped_ptr<SearchController> CreateSearchController(
scoped_ptr<SearchProvider>(new PeopleProvider(profile, list_controller)));
if (IsSuggestionsSearchProviderEnabled()) {
size_t suggestions_group_id =
controller->AddGroup(kMaxSuggestionsResults, 3.0);
controller->AddGroup(kMaxSuggestionsResults, 3.0, 1.0);
controller->AddProvider(
suggestions_group_id,
scoped_ptr<SearchProvider>(
......@@ -101,7 +107,7 @@ scoped_ptr<SearchController> CreateSearchController(
#if defined(OS_CHROMEOS)
if (app_list::switches::IsLauncherSearchProviderApiEnabled()) {
size_t search_api_group_id =
controller->AddGroup(kMaxLauncherSearchResults, 0.0);
controller->AddGroup(kMaxLauncherSearchResults, 0.0, 1.0);
controller->AddProvider(
search_api_group_id,
scoped_ptr<SearchProvider>(new LauncherSearchProvider(profile)));
......
......@@ -10,6 +10,7 @@
#include <string>
#include <vector>
#include "base/metrics/field_trial.h"
#include "ui/app_list/search_provider.h"
#include "ui/app_list/search_result.h"
......@@ -17,9 +18,19 @@ namespace app_list {
namespace {
// Maximum number of results to show.
// Maximum number of results to show. Ignored if the AppListMixer field trial is
// "Blended".
const size_t kMaxResults = 6;
// The minimum number of results to show, if the AppListMixer field trial is
// "Blended". If this quota is not reached, the per-group limitations are
// removed and we try again. (We may still not reach the minumum, but at least
// we tried.) Ignored if the field trial is off.
const size_t kMinBlendedResults = 6;
const char kAppListMixerFieldTrialName[] = "AppListMixer";
const char kAppListMixerFieldTrialEnabled[] = "Blended";
void UpdateResult(const SearchResult& source, SearchResult* target) {
target->set_display_type(source.display_type());
target->set_title(source.title());
......@@ -28,6 +39,15 @@ void UpdateResult(const SearchResult& source, SearchResult* target) {
target->set_details_tags(source.details_tags());
}
// Returns true if the "AppListMixer" trial is set to "Blended". This is an
// experiment on the new Mixer logic that allows results from different groups
// to be blended together, rather than stratified.
bool IsBlendedMixerTrialEnabled() {
const std::string group_name =
base::FieldTrialList::FindFullName(kAppListMixerFieldTrialName);
return group_name == kAppListMixerFieldTrialEnabled;
}
} // namespace
Mixer::SortData::SortData() : result(NULL), score(0.0) {
......@@ -45,8 +65,8 @@ bool Mixer::SortData::operator<(const SortData& other) const {
// Used to group relevant providers together for mixing their results.
class Mixer::Group {
public:
Group(size_t max_results, double boost)
: max_results_(max_results), boost_(boost) {}
Group(size_t max_results, double boost, double multiplier)
: max_results_(max_results), boost_(boost), multiplier_(multiplier) {}
~Group() {}
void AddProvider(SearchProvider* provider) { providers_.push_back(provider); }
......@@ -63,6 +83,7 @@ class Mixer::Group {
// Google+ API). Clamp to that range.
double relevance = std::min(std::max(result->relevance(), 0.0), 1.0);
double multiplier = multiplier_;
double boost = boost_;
KnownResults::const_iterator known_it =
known_results.find(result->id());
......@@ -90,7 +111,7 @@ class Mixer::Group {
if (is_voice_query && result->voice_result())
boost += 4.0;
results_.push_back(SortData(result, relevance + boost));
results_.push_back(SortData(result, relevance * multiplier + boost));
}
}
......@@ -105,6 +126,7 @@ class Mixer::Group {
typedef std::vector<SearchProvider*> Providers;
const size_t max_results_;
const double boost_;
const double multiplier_;
Providers providers_; // Not owned.
SortedResults results_;
......@@ -118,15 +140,23 @@ Mixer::Mixer(AppListModel::SearchResults* ui_results)
Mixer::~Mixer() {
}
size_t Mixer::AddGroup(size_t max_results, double boost) {
groups_.push_back(new Group(max_results, boost));
size_t Mixer::AddGroup(size_t max_results, double boost, double multiplier) {
// Only consider |boost| if the AppListMixer field trial is default.
// Only consider |multiplier| if the AppListMixer field trial is "Blended".
if (IsBlendedMixerTrialEnabled())
boost = 0.0;
else
multiplier = 1.0;
groups_.push_back(new Group(max_results, boost, multiplier));
return groups_.size() - 1;
}
size_t Mixer::AddOmniboxGroup(size_t max_results, double boost) {
size_t Mixer::AddOmniboxGroup(size_t max_results,
double boost,
double multiplier) {
// There should not already be an omnibox group.
DCHECK(!has_omnibox_group_);
size_t id = AddGroup(max_results, boost);
size_t id = AddGroup(max_results, boost, multiplier);
omnibox_group_ = id;
has_omnibox_group_ = true;
return id;
......@@ -141,41 +171,79 @@ void Mixer::MixAndPublish(bool is_voice_query,
FetchResults(is_voice_query, known_results);
SortedResults results;
results.reserve(kMaxResults);
// Add results from non-omnibox groups first. Limit to the maximum number of
// results in each group.
for (size_t i = 0; i < groups_.size(); ++i) {
if (!has_omnibox_group_ || i != omnibox_group_) {
const Group& group = *groups_[i];
if (IsBlendedMixerTrialEnabled()) {
results.reserve(kMinBlendedResults);
// Add results from each group. Limit to the maximum number of results in
// each group.
for (const Group* group : groups_) {
size_t num_results =
std::min(group.results().size(), group.max_results());
results.insert(results.end(), group.results().begin(),
group.results().begin() + num_results);
std::min(group->results().size(), group->max_results());
results.insert(results.end(), group->results().begin(),
group->results().begin() + num_results);
}
// Remove results with duplicate IDs before sorting. If two providers give a
// result with the same ID, the result from the provider with the *lower
// group number* will be kept (e.g., an app result takes priority over a web
// store result with the same ID).
RemoveDuplicates(&results);
std::sort(results.begin(), results.end());
if (results.size() < kMinBlendedResults) {
size_t original_size = results.size();
// We didn't get enough results. Insert all the results again, and this
// time, do not limit the maximum number of results from each group. (This
// will result in duplicates, which will be removed by RemoveDuplicates.)
for (const Group* group : groups_) {
results.insert(results.end(), group->results().begin(),
group->results().end());
}
RemoveDuplicates(&results);
// Sort just the newly added results. This ensures that, for example, if
// there are 6 Omnibox results (score = 0.8) and 1 People result (score =
// 0.4) that the People result will be 5th, not 7th, because the Omnibox
// group has a soft maximum of 4 results. (Otherwise, the People result
// would not be seen at all once the result list is truncated.)
std::sort(results.begin() + original_size, results.end());
}
} else {
results.reserve(kMaxResults);
// Add results from non-omnibox groups first. Limit to the maximum number of
// results in each group.
for (size_t i = 0; i < groups_.size(); ++i) {
if (!has_omnibox_group_ || i != omnibox_group_) {
const Group& group = *groups_[i];
size_t num_results =
std::min(group.results().size(), group.max_results());
results.insert(results.end(), group.results().begin(),
group.results().begin() + num_results);
}
}
}
// Collapse duplicate apps from local and web store.
RemoveDuplicates(&results);
// Fill the remaining slots with omnibox results. Always add at least one
// omnibox result (even if there are no more slots; if we over-fill the
// vector, the web store and people results will be removed in a later step).
// Note: max_results() is ignored for the omnibox group.
if (has_omnibox_group_) {
CHECK_LT(omnibox_group_, groups_.size());
const Group& omnibox_group = *groups_[omnibox_group_];
const size_t omnibox_results = std::min(
omnibox_group.results().size(),
results.size() < kMaxResults ? kMaxResults - results.size() : 1);
results.insert(results.end(), omnibox_group.results().begin(),
omnibox_group.results().begin() + omnibox_results);
}
// Collapse duplicate apps from local and web store.
RemoveDuplicates(&results);
// Fill the remaining slots with omnibox results. Always add at least one
// omnibox result (even if there are no more slots; if we over-fill the
// vector, the web store and people results will be removed in a later
// step). Note: max_results() is ignored for the omnibox group.
if (has_omnibox_group_) {
CHECK_LT(omnibox_group_, groups_.size());
const Group& omnibox_group = *groups_[omnibox_group_];
const size_t omnibox_results = std::min(
omnibox_group.results().size(),
results.size() < kMaxResults ? kMaxResults - results.size() : 1);
results.insert(results.end(), omnibox_group.results().begin(),
omnibox_group.results().begin() + omnibox_results);
}
std::sort(results.begin(), results.end());
RemoveDuplicates(&results);
if (results.size() > kMaxResults)
results.resize(kMaxResults);
std::sort(results.begin(), results.end());
RemoveDuplicates(&results);
if (results.size() > kMaxResults)
results.resize(kMaxResults);
}
Publish(results, ui_results_);
}
......
......@@ -35,15 +35,21 @@ class APP_LIST_EXPORT Mixer {
// Adds a new mixer group. A maximum of |max_results| results will be
// displayed from this group (if 0, will allow unlimited results from this
// group). Each result in the group will have its score boosted by |boost|.
// group). Behaviour depends on the AppListMixer field trial:
// - If default: Each result in the group will have its score boosted by
// |boost|. |multiplier| is ignored.
// - If "Blended": |max_results| is a "soft" maximum; if there aren't enough
// results from all groups, more than |max_results| may be chosen from this
// group. Each result in the group will have its score multiplied by
// |multiplier|. |boost| is ignored.
// Returns the group's group_id.
size_t AddGroup(size_t max_results, double boost);
size_t AddGroup(size_t max_results, double boost, double multiplier);
// Adds a new mixer group for the special "omnibox" group. This group will be
// treated specially by the Mixer (it will be truncated such that it fills the
// remaining slots without overflowing, but with at least one result). A
// maximum of one group should be added using this method.
size_t AddOmniboxGroup(size_t max_results, double boost);
size_t AddOmniboxGroup(size_t max_results, double boost, double multiplier);
// Associates a provider with a mixer group.
void AddProviderToGroup(size_t group_id, SearchProvider* provider);
......@@ -87,8 +93,9 @@ class APP_LIST_EXPORT Mixer {
Groups groups_;
// The ID of the omnibox group. The group with this ID will be treated
// specially by the Mixer.
// TODO(mgiuca): Omnibox group should not be treated specially.
// specially by the Mixer. Ignored if the AppListMixer field trial is
// "Blended".
// TODO(mgiuca): Remove this after the field trial is complete.
size_t omnibox_group_ = 0;
// Whether |omnibox_group_| has been set.
bool has_omnibox_group_ = false;
......
......@@ -21,7 +21,8 @@ namespace test {
// Maximum number of results to show in each mixer group.
const size_t kMaxAppsGroupResults = 4;
const size_t kMaxOmniboxResults = 0; // Ignored.
// Ignored unless AppListMixer field trial is "Blended".
const size_t kMaxOmniboxResults = 4;
const size_t kMaxWebstoreResults = 2;
const size_t kMaxPeopleResults = 2;
......@@ -100,6 +101,8 @@ class TestSearchProvider : public SearchProvider {
DISALLOW_COPY_AND_ASSIGN(TestSearchProvider);
};
// TODO(mgiuca): Parameterize this test so it tests both the default and
// "Blended" states for the AppListMixer field trial.
class MixerTest : public testing::Test {
public:
MixerTest() : is_voice_query_(false) {}
......@@ -118,10 +121,11 @@ class MixerTest : public testing::Test {
mixer_.reset(new Mixer(results_.get()));
size_t apps_group_id = mixer_->AddGroup(kMaxAppsGroupResults, 3.0);
size_t omnibox_group_id = mixer_->AddOmniboxGroup(kMaxOmniboxResults, 2.0);
size_t webstore_group_id = mixer_->AddGroup(kMaxWebstoreResults, 1.0);
size_t people_group_id = mixer_->AddGroup(kMaxPeopleResults, 0.0);
size_t apps_group_id = mixer_->AddGroup(kMaxAppsGroupResults, 3.0, 1.0);
size_t omnibox_group_id =
mixer_->AddOmniboxGroup(kMaxOmniboxResults, 2.0, 1.0);
size_t webstore_group_id = mixer_->AddGroup(kMaxWebstoreResults, 1.0, 0.5);
size_t people_group_id = mixer_->AddGroup(kMaxPeopleResults, 0.0, 1.0);
mixer_->AddProviderToGroup(apps_group_id, providers_[0]);
mixer_->AddProviderToGroup(omnibox_group_id, providers_[1]);
......
......@@ -104,12 +104,16 @@ void SearchController::InvokeResultAction(SearchResult* result,
result->InvokeAction(action_index, event_flags);
}
size_t SearchController::AddGroup(size_t max_results, double boost) {
return mixer_->AddGroup(max_results, boost);
size_t SearchController::AddGroup(size_t max_results,
double boost,
double multiplier) {
return mixer_->AddGroup(max_results, boost, multiplier);
}
size_t SearchController::AddOmniboxGroup(size_t max_results, double boost) {
return mixer_->AddOmniboxGroup(max_results, boost);
size_t SearchController::AddOmniboxGroup(size_t max_results,
double boost,
double multiplier) {
return mixer_->AddOmniboxGroup(max_results, boost, multiplier);
}
void SearchController::AddProvider(size_t group_id,
......
......@@ -40,10 +40,10 @@ class APP_LIST_EXPORT SearchController {
int event_flags);
// Adds a new mixer group. See Mixer::AddGroup.
size_t AddGroup(size_t max_results, double boost);
size_t AddGroup(size_t max_results, double boost, double multiplier);
// Adds a new mixer group. See Mixer::AddOmniboxGroup.
size_t AddOmniboxGroup(size_t max_results, double boost);
size_t AddOmniboxGroup(size_t max_results, double boost, double multiplier);
// Takes ownership of |provider| and associates it with given mixer group.
void AddProvider(size_t group_id, scoped_ptr<SearchProvider> provider);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment