Commit e816da2f authored by tby's avatar tby Committed by Commit Bot

[Suggested files] Update ranking for QPS experiment.

The next step for suggested files is to run an experiment to establish
QPS. We want to change the ranking to make the results clearer.

This CL does two things:

1. Splits 'files' in the chips into drive files and local files, to be
   ranked separately.

2. Changes the ranking to a simpler algorithm (chip_ranker.cc lines
   144-end). This ranking always puts apps first, then drive files,
   then local files. The ranking determines how many of each type we
   show, but takes into a account a minimum number of results from each
   type.

The current minimums mean we only ever decide on one chip by ranking,
but we'll probably relax the minimums soon. Once we've decided on the
minimums, I'll add back the tests removed here.

Bug: 1034842
Change-Id: Id58614265e7a79d2761b08f3bffd0529b18c385d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2245939Reviewed-by: default avatarThanh Nguyen <thanhdng@chromium.org>
Commit-Queue: Tony Yeoman <tby@chromium.org>
Cr-Commit-Position: refs/heads/master@{#779142}
parent b422cee0
......@@ -22,8 +22,15 @@ namespace app_list {
namespace {
constexpr int kNumChips = 5;
constexpr int kMinApps = 2;
constexpr int kMinDriveFiles = 1;
constexpr int kMinLocalFiles = 1;
// Strings used for the ranked types in the RecurrenceRanker.
constexpr char kApp[] = "app";
constexpr char kFile[] = "file";
constexpr char kDriveFile[] = "drive";
constexpr char kLocalFile[] = "local";
// A small number that we expect to be smaller than the difference between the
// scores of any two results. This means it can be used to insert a result A
......@@ -43,20 +50,20 @@ float GetScore(const std::map<std::string, float>& scores,
// We expect to always find a score for |key| in |scores|, because the ranker
// is initialized with some default scores. However a state without scores is
// possible, eg. if the recurrence ranker file is corrupted. In this case,
// default a score to 1.
// default the score to 0.
if (it == scores.end()) {
return 1.0f;
return 0.0f;
}
return it->second;
}
void InitializeRanker(RecurrenceRanker* ranker) {
// This initialization puts two files and three apps in the chips.
ranker->Record(kFile);
ranker->Record(kFile);
ranker->Record(kApp);
ranker->Record(kApp);
// This initialization starts with two apps, two drive files, and one local
// file. Apps are left in a close second place, so the first click of an app
// will replace one drive file with one app.
ranker->Record(kApp);
ranker->Record(kDriveFile);
ranker->Record(kDriveFile);
}
} // namespace
......@@ -64,13 +71,13 @@ void InitializeRanker(RecurrenceRanker* ranker) {
ChipRanker::ChipRanker(Profile* profile) : profile_(profile) {
DCHECK(profile);
// Set up ranker model.
// Set up ranker model. This is tuned close to MRU.
RecurrenceRankerConfigProto config;
config.set_min_seconds_between_saves(240u);
config.set_condition_limit(1u);
config.set_condition_decay(0.5f);
config.set_target_limit(5u);
config.set_target_decay(0.95f);
config.set_target_decay(0.9f);
config.mutable_predictor()->mutable_default_predictor();
type_ranker_ = std::make_unique<RecurrenceRanker>(
......@@ -82,35 +89,52 @@ ChipRanker::~ChipRanker() = default;
void ChipRanker::Train(const AppLaunchData& app_launch_data) {
const auto type = app_launch_data.ranking_item_type;
if (type == RankingItemType::kApp) {
type_ranker_->Record(kApp);
} else if (type == RankingItemType::kChip ||
type == RankingItemType::kZeroStateFile ||
type == RankingItemType::kDriveQuickAccess) {
type_ranker_->Record(kFile);
switch (type) {
case RankingItemType::kApp:
type_ranker_->Record(kApp);
break;
case RankingItemType::kDriveQuickAccessChip:
case RankingItemType::kDriveQuickAccess:
type_ranker_->Record(kDriveFile);
break;
case RankingItemType::kZeroStateFileChip:
case RankingItemType::kZeroStateFile:
type_ranker_->Record(kLocalFile);
break;
default:
break;
}
}
void ChipRanker::Rank(Mixer::SortedResults* results) {
// Construct two lists of pointers, containing file chip and app results
// respectively, sorted in decreasing order of score.
// Construct lists of pointers for each ranked result type, sorted in
// decreasing score order.
std::vector<Mixer::SortData*> app_results;
std::vector<Mixer::SortData*> file_results;
std::vector<Mixer::SortData*> drive_results;
std::vector<Mixer::SortData*> local_results;
for (auto& result : *results) {
if (RankingItemTypeFromSearchResult(*result.result) ==
RankingItemType::kApp) {
app_results.emplace_back(&result);
} else if (RankingItemTypeFromSearchResult(*result.result) ==
RankingItemType::kChip) {
file_results.emplace_back(&result);
switch (RankingItemTypeFromSearchResult(*result.result)) {
case RankingItemType::kApp:
app_results.emplace_back(&result);
break;
case RankingItemType::kZeroStateFileChip:
local_results.emplace_back(&result);
break;
case RankingItemType::kDriveQuickAccessChip:
drive_results.emplace_back(&result);
break;
default:
break;
}
}
SortHighToLow(&app_results);
SortHighToLow(&file_results);
SortHighToLow(&drive_results);
SortHighToLow(&local_results);
// The chip ranker only has work to do if both apps and files are present.
if (app_results.empty() || file_results.empty())
if (drive_results.empty() && local_results.empty()) {
return;
}
// If this is the first initialization of the ranker, warm it up with some
// default scores for apps and files.
......@@ -118,27 +142,62 @@ void ChipRanker::Rank(Mixer::SortedResults* results) {
InitializeRanker(type_ranker_.get());
}
// Get the two type scores from the ranker.
// Allocate as many of the per-type minimum number of chips as possible.
const int drive_size = static_cast<int>(drive_results.size());
const int local_size = static_cast<int>(local_results.size());
const int apps_size = static_cast<int>(app_results.size());
int num_drive = std::min(kMinDriveFiles, drive_size);
int num_local = std::min(kMinLocalFiles, local_size);
int num_apps = std::min(kMinApps, apps_size);
const int free_chips = kNumChips - num_drive - num_local - num_apps;
// Get the per-type scores from the ranker.
const auto ranks = type_ranker_->Rank();
float app_score = GetScore(ranks, kApp);
float file_score = GetScore(ranks, kFile);
const float score_delta = (file_score + app_score) / kNumChips;
// Tweak file result scores to fit in with app scores. See header comment for
// ChipRanker::Rank for more details.
const int num_apps = static_cast<int>(app_results.size());
const int num_files = static_cast<int>(file_results.size());
int current_app = 0;
int current_file = 0;
for (int i = 0; i < kNumChips; ++i) {
if (app_score > file_score) {
float drive_score = GetScore(ranks, kDriveFile);
float local_score = GetScore(ranks, kLocalFile);
const float score_delta =
(app_score + drive_score + local_score) / free_chips;
// Allocate the remaining 'free' chips. When there aren't results enough of
// one type to fill the number of chips deserved by that type's score, fall
// back to filling with another type in the order: drive -> local -> app.
for (int i = 0; i < free_chips; ++i) {
if (num_drive < drive_size && drive_score > app_score &&
drive_score > local_score) {
drive_score -= score_delta;
++num_drive;
} else if (num_local < local_size && local_score > app_score) {
local_score -= score_delta;
++num_local;
} else if (num_apps < apps_size) {
app_score -= score_delta;
++current_app;
} else if (current_file < num_files && current_app < num_apps) {
file_results[current_file]->score =
app_results[current_app]->score + kScoreEpsilon;
++current_file;
file_score -= score_delta;
++num_apps;
}
}
// Set result scores to make the final list of results. Use the score of the
// lowest-scoring shown app as the baseline for file results.
double current_score = num_apps > 0 ? app_results[num_apps - 1]->score : 1.0;
// Score the Drive results just below that lowest app. Set unshown results to
// 0.0 to ensure they don't interfere.
for (int i = 0; i < drive_size; ++i) {
if (i < num_drive) {
current_score -= kScoreEpsilon;
drive_results[i]->score = current_score;
} else {
drive_results[i]->score = 0.0;
}
}
// Score the local file results just below that lowest Drive result.
for (int i = 0; i < local_size; ++i) {
if (i < num_local) {
current_score -= kScoreEpsilon;
local_results[i]->score = current_score;
} else {
local_results[i]->score = 0.0;
}
}
}
......
......@@ -28,9 +28,6 @@ namespace {
using ResultType = ash::AppListSearchResultType;
// Consistent with kScoreEpsilon in ChipRanker.
// constexpr float kScoreEpsilon = 1e-5f;
class TestSearchResult : public ChromeSearchResult {
public:
TestSearchResult(const std::string& id, ResultType type)
......@@ -164,23 +161,6 @@ TEST_F(ChipRankerTest, AppsOnly) {
HasScore(8.7))));
}
// Check that ranking only files has no effect.
TEST_F(ChipRankerTest, FilesOnly) {
Mixer::SortedResults results = MakeSearchResults(
{"file1", "file2", "file3"},
{ResultType::kFileChip, ResultType::kDriveQuickAccessChip,
ResultType::kFileChip},
{0.9, 0.6, 0.4});
TrainRanker({"app", "file"});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("file2"),
HasId("file3"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(0.9), HasScore(0.6),
HasScore(0.4))));
}
// Check that ranking a non-chip result does not affect its score.
TEST_F(ChipRankerTest, UnchangedItem) {
Mixer::SortedResults results =
......@@ -204,45 +184,18 @@ TEST_F(ChipRankerTest, UnchangedItem) {
// depending on whether apps initially have identical scores.
TEST_F(ChipRankerTest, DefaultInitialization) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "app3", "file1", "file2"},
{"app1", "app2", "app3", "drive1", "drive2", "local1", "local2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kInstalledApp, ResultType::kFileChip, ResultType::kFileChip},
{8.9, 8.7, 8.5, 0.8, 0.7});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("file1"), HasId("app3"),
HasId("file2"))));
}
// When files have been trained much more than apps, two files should appear
// before two apps.
TEST_F(ChipRankerTest, FilesAboveApps) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kFileChip, ResultType::kFileChip},
{8.9, 8.7, 0.8, 0.7});
TrainRanker({"app", "file", "file", "file", "file"});
ResultType::kInstalledApp, ResultType::kDriveQuickAccessChip,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip,
ResultType::kFileChip},
{8.9, 8.7, 8.5, 0.9, 0.7, 0.8, 0.6});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("file2"),
HasId("app1"), HasId("app2"))));
}
// When apps have been trained much more than files, two apps should appear
// before two files.
TEST_F(ChipRankerTest, AppsAboveFiles) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kFileChip, ResultType::kFileChip},
{8.9, 8.7, 0.8, 0.7});
TrainRanker({"file", "app", "app", "app", "app"});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("file1"), HasId("file2"))));
HasId("drive1"), HasId("drive2"),
HasId("local1"), HasId("app3"),
HasId("local2"))));
}
} // namespace app_list
......@@ -24,13 +24,14 @@ ZeroStateResultType ZeroStateTypeFromRankingType(
case RankingItemType::kFile:
case RankingItemType::kApp:
case RankingItemType::kArcAppShortcut:
case RankingItemType::kChip:
return ZeroStateResultType::kUnanticipated;
case RankingItemType::kOmniboxGeneric:
return ZeroStateResultType::kOmniboxSearch;
case RankingItemType::kZeroStateFile:
case RankingItemType::kZeroStateFileChip:
return ZeroStateResultType::kZeroStateFile;
case RankingItemType::kDriveQuickAccess:
case RankingItemType::kDriveQuickAccessChip:
return ZeroStateResultType::kDriveQuickAccess;
}
}
......
......@@ -42,8 +42,9 @@ RankingItemType RankingItemTypeFromSearchResult(
case ash::AppListSearchResultType::kDriveQuickAccess:
return RankingItemType::kDriveQuickAccess;
case ash::AppListSearchResultType::kFileChip:
return RankingItemType::kZeroStateFileChip;
case ash::AppListSearchResultType::kDriveQuickAccessChip:
return RankingItemType::kChip;
return RankingItemType::kDriveQuickAccessChip;
}
}
......
......@@ -24,9 +24,12 @@ enum class RankingItemType {
kArcAppShortcut = 5,
kZeroStateFile = 6,
kDriveQuickAccess = 7,
kChip = 8,
// Deprecated:
// kChip = 8,
kZeroStateFileChip = 9,
kDriveQuickAccessChip = 10,
// Add new types above this line.
kMaxValue = kChip,
kMaxValue = kDriveQuickAccessChip,
};
// Convert a |ChromeSearchResult| into its |RankingItemType|.
......
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