Commit a7752d87 authored by Eleni Dimitriadis's avatar Eleni Dimitriadis Committed by Commit Bot

[Suggested Files] Ranking model

Added a new ChipRanker type, which will house a new Dolphin model.
This model will rank apps alongside both local and drive files, which
will allow us to compare the different types of items.
These ranks will be used to adjust the scores of the files to be
in line with the app scores, while still preserving the original
ordering of both groups.
This ensures the files will display appropriately in the suggestion
chips, but that their ranking elsewhere will be unaffected.

Bug: 1034842
Change-Id: I618e2694b74c6c5917eef66a900ef822f5422b92
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1989742Reviewed-by: default avatarTony Yeoman <tby@chromium.org>
Reviewed-by: default avatarJia Meng <jiameng@chromium.org>
Commit-Queue: Eleni Dimitriadis <edimitriadis@google.com>
Cr-Commit-Position: refs/heads/master@{#743401}
parent e4669d48
...@@ -3805,6 +3805,8 @@ jumbo_static_library("ui") { ...@@ -3805,6 +3805,8 @@ jumbo_static_library("ui") {
"app_list/search/search_result_ranker/app_list_launch_recorder_util.h", "app_list/search/search_result_ranker/app_list_launch_recorder_util.h",
"app_list/search/search_result_ranker/app_search_result_ranker.cc", "app_list/search/search_result_ranker/app_search_result_ranker.cc",
"app_list/search/search_result_ranker/app_search_result_ranker.h", "app_list/search/search_result_ranker/app_search_result_ranker.h",
"app_list/search/search_result_ranker/chip_ranker.cc",
"app_list/search/search_result_ranker/chip_ranker.h",
"app_list/search/search_result_ranker/frecency_store.cc", "app_list/search/search_result_ranker/frecency_store.cc",
"app_list/search/search_result_ranker/frecency_store.h", "app_list/search/search_result_ranker/frecency_store.h",
"app_list/search/search_result_ranker/histogram_util.cc", "app_list/search/search_result_ranker/histogram_util.cc",
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
#include "chrome/browser/ui/app_list/search/drive_quick_access_provider.h" #include "chrome/browser/ui/app_list/search/drive_quick_access_provider.h"
#include <algorithm>
#include <memory> #include <memory>
#include <utility> #include <utility>
...@@ -202,6 +203,17 @@ void DriveQuickAccessProvider::OnGetQuickAccessItems( ...@@ -202,6 +203,17 @@ void DriveQuickAccessProvider::OnGetQuickAccessItems(
void DriveQuickAccessProvider::SetResultsCache( void DriveQuickAccessProvider::SetResultsCache(
const std::vector<drive::QuickAccessItem>& drive_results) { const std::vector<drive::QuickAccessItem>& drive_results) {
// Rescale items between 0 and 1
double hi = drive_results[0].confidence;
double lo = drive_results[0].confidence;
for (auto item : drive_results) {
hi = std::max(item.confidence, hi);
lo = std::min(item.confidence, lo);
}
for (auto item : drive_results) {
item.confidence = (item.confidence - lo) / (hi - lo);
}
results_cache_ = std::move(drive_results); results_cache_ = std::move(drive_results);
} }
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
#include "chrome/browser/ui/app_list/app_list_model_updater.h" #include "chrome/browser/ui/app_list/app_list_model_updater.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h" #include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "chrome/browser/ui/app_list/search/search_provider.h" #include "chrome/browser/ui/app_list/search/search_provider.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/chip_ranker.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h" #include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h" #include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h"
...@@ -127,6 +128,11 @@ void Mixer::MixAndPublish(size_t num_max_results, const base::string16& query) { ...@@ -127,6 +128,11 @@ void Mixer::MixAndPublish(size_t num_max_results, const base::string16& query) {
if (query.empty() && non_app_ranker_) if (query.empty() && non_app_ranker_)
non_app_ranker_->OverrideZeroStateResults(&results); non_app_ranker_->OverrideZeroStateResults(&results);
// Chip results: rescore the chip results in line with app results.
if (query.empty() && chip_ranker_) {
chip_ranker_->Rank(&results);
}
std::sort(results.begin(), results.end()); std::sort(results.begin(), results.end());
const size_t original_size = results.size(); const size_t original_size = results.size();
...@@ -186,9 +192,15 @@ SearchResultRanker* Mixer::GetNonAppSearchResultRanker() { ...@@ -186,9 +192,15 @@ SearchResultRanker* Mixer::GetNonAppSearchResultRanker() {
return non_app_ranker_.get(); return non_app_ranker_.get();
} }
void Mixer::SetChipRanker(std::unique_ptr<ChipRanker> ranker) {
chip_ranker_ = std::move(ranker);
}
void Mixer::Train(const AppLaunchData& app_launch_data) { void Mixer::Train(const AppLaunchData& app_launch_data) {
if (non_app_ranker_) if (non_app_ranker_)
non_app_ranker_->Train(app_launch_data); non_app_ranker_->Train(app_launch_data);
if (chip_ranker_)
chip_ranker_->Train(app_launch_data);
} }
} // namespace app_list } // namespace app_list
...@@ -25,6 +25,7 @@ namespace test { ...@@ -25,6 +25,7 @@ namespace test {
FORWARD_DECLARE_TEST(MixerTest, Publish); FORWARD_DECLARE_TEST(MixerTest, Publish);
} }
class ChipRanker;
class SearchProvider; class SearchProvider;
class SearchResultRanker; class SearchResultRanker;
enum class RankingItemType; enum class RankingItemType;
...@@ -60,6 +61,9 @@ class Mixer { ...@@ -60,6 +61,9 @@ class Mixer {
// non-app ranking. // non-app ranking.
SearchResultRanker* GetNonAppSearchResultRanker(); SearchResultRanker* GetNonAppSearchResultRanker();
// Sets a ChipRanker to re-rank chip results before they are published.
void SetChipRanker(std::unique_ptr<ChipRanker> ranker);
// Handle a training signal. // Handle a training signal.
void Train(const AppLaunchData& app_launch_data); void Train(const AppLaunchData& app_launch_data);
...@@ -95,6 +99,7 @@ class Mixer { ...@@ -95,6 +99,7 @@ class Mixer {
// Adaptive models used for re-ranking search results. // Adaptive models used for re-ranking search results.
std::unique_ptr<SearchResultRanker> non_app_ranker_; std::unique_ptr<SearchResultRanker> non_app_ranker_;
std::unique_ptr<ChipRanker> chip_ranker_;
DISALLOW_COPY_AND_ASSIGN(Mixer); DISALLOW_COPY_AND_ASSIGN(Mixer);
}; };
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
#include "chrome/browser/ui/app_list/search/chrome_search_result.h" #include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "chrome/browser/ui/app_list/search/cros_action_history/cros_action_recorder.h" #include "chrome/browser/ui/app_list/search/cros_action_history/cros_action_recorder.h"
#include "chrome/browser/ui/app_list/search/search_provider.h" #include "chrome/browser/ui/app_list/search/search_provider.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/chip_ranker.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/histogram_util.h" #include "chrome/browser/ui/app_list/search/search_result_ranker/histogram_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h" #include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h" #include "chrome/browser/ui/app_list/search/search_result_ranker/search_result_ranker.h"
...@@ -83,6 +84,10 @@ void SearchController::InitializeRankers() { ...@@ -83,6 +84,10 @@ void SearchController::InitializeRankers() {
profile_, ServiceAccessType::EXPLICIT_ACCESS)); profile_, ServiceAccessType::EXPLICIT_ACCESS));
ranker->InitializeRankers(this); ranker->InitializeRankers(this);
mixer_->SetNonAppSearchResultRanker(std::move(ranker)); mixer_->SetNonAppSearchResultRanker(std::move(ranker));
if (app_list_features::IsSuggestedFilesEnabled()) {
mixer_->SetChipRanker(std::make_unique<ChipRanker>(profile_));
}
} }
void SearchController::Start(const base::string16& query) { void SearchController::Start(const base::string16& query) {
......
...@@ -44,8 +44,10 @@ namespace { ...@@ -44,8 +44,10 @@ namespace {
// number of results to be displayed in UI. // number of results to be displayed in UI.
constexpr size_t kMaxAppsGroupResults = 7; constexpr size_t kMaxAppsGroupResults = 7;
constexpr size_t kMaxLauncherSearchResults = 2; constexpr size_t kMaxLauncherSearchResults = 2;
constexpr size_t kMaxZeroStateFileResults = 6; // We need twice as many ZeroState and Drive file results as we need
constexpr size_t kMaxDriveQuickAccessResults = 6; // duplicates of these results for the suggestion chips.
constexpr size_t kMaxZeroStateFileResults = 20;
constexpr size_t kMaxDriveQuickAccessResults = 10;
constexpr size_t kMaxAppReinstallSearchResults = 1; constexpr size_t kMaxAppReinstallSearchResults = 1;
// We show up to 6 Play Store results. However, part of Play Store results may // We show up to 6 Play Store results. However, part of Play Store results may
// be filtered out because they may correspond to already installed Web apps. So // be filtered out because they may correspond to already installed Web apps. So
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/app_list/search/search_result_ranker/chip_ranker.h"
#include <algorithm>
#include <string>
#include <utility>
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/app_search_result_ranker.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/histogram_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/ranking_item_util.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/recurrence_ranker.h"
namespace app_list {
namespace {
// Apps have a boost of 8.0 + app ranker score in range [0, 1],
// hence the range of scores is [8.0, 9.0].
constexpr double kScoreHi = 9.0;
constexpr double kScoreLo = 8.0;
// Returns whether the model should be trained on this type of data.
bool ShouldTrain(RankingItemType type) {
switch (type) {
case RankingItemType::kApp:
case RankingItemType::kChip:
case RankingItemType::kZeroStateFile:
case RankingItemType::kDriveQuickAccess:
return true;
default:
return false;
}
}
double FetchScore(const std::map<std::string, float> ranks,
ChromeSearchResult* r) {
const auto it = ranks.find(NormalizeAppId(r->id()));
if (it != ranks.end())
return it->second;
return 0.0;
}
int GetNextMatchingIndex(
Mixer::SortedResults* results,
const base::RepeatingCallback<bool(const ChromeSearchResult*)>&
result_filter,
int from_index) {
int i = from_index + 1;
while (i < static_cast<int>(results->size())) {
if (result_filter.Run((*results)[i].result)) {
return i;
}
++i;
}
return -1;
}
} // namespace
ChipRanker::ChipRanker(Profile* profile) : profile_(profile) {
DCHECK(profile);
// Set up ranker model.
RecurrenceRankerConfigProto config;
config.set_min_seconds_between_saves(240u);
config.set_condition_limit(1u);
config.set_condition_decay(0.6f);
config.set_target_limit(200);
config.set_target_decay(0.9f);
config.mutable_predictor()->mutable_default_predictor();
ranker_ = std::make_unique<RecurrenceRanker>(
"", profile_->GetPath().AppendASCII("suggested_files_ranker.pb"), config,
chromeos::ProfileHelper::IsEphemeralUserProfile(profile_));
}
ChipRanker::~ChipRanker() = default;
void ChipRanker::Train(const AppLaunchData& app_launch_data) {
// ID normalisation will ensure that a file launched from the zero-state
// result list is counted as the same item as the same file launched from
// the suggestion chips.
if (ShouldTrain(app_launch_data.ranking_item_type)) {
ranker_->Record(NormalizeAppId(app_launch_data.id));
}
}
void ChipRanker::Rank(Mixer::SortedResults* results) {
std::sort(results->begin(), results->end());
const auto app_chip_filter =
base::BindRepeating([](const ChromeSearchResult* r) -> bool {
return (r->display_type() == ash::SearchResultDisplayType::kTile ||
r->display_type() == ash::SearchResultDisplayType::kChip) &&
r->is_recommendation();
});
const auto file_chip_filter =
base::BindRepeating([](const ChromeSearchResult* r) -> bool {
return r->result_type() == ash::AppListSearchResultType::kFileChip ||
r->result_type() ==
ash::AppListSearchResultType::kDriveQuickAccessChip;
});
// Use filters to find first two app chips and first file chip
int app1 = GetNextMatchingIndex(results, app_chip_filter, -1);
int app2 = GetNextMatchingIndex(results, app_chip_filter, app1);
int file = GetNextMatchingIndex(results, file_chip_filter, -1);
int prev_file = -1;
// If we couldn't find any files or couldn't find two or more apps.
if (file < 0 || app1 < 0 || app2 < 0) {
return;
}
// Fetch rankings from |ranker_|.
std::map<std::string, float> ranks = ranker_->Rank();
// Refer to class comment.
double app1_rescore = FetchScore(ranks, (*results)[app1].result);
double app2_rescore = FetchScore(ranks, (*results)[app2].result);
double file_rescore = 0.0;
double prev_file_rescore = kScoreHi;
double hi = 0.0;
double lo = 0.0;
while (file >= 0 && app1 >= 0) {
file_rescore = FetchScore(ranks, (*results)[file].result);
// File should sit above lowest of two app scores.
if (file_rescore > app2_rescore) {
// Find upper and lower bounds on score.
hi = prev_file > 0 ? (*results)[prev_file].score : kScoreHi;
lo = app2 > 0 ? (*results)[app2].score : kScoreLo;
if (prev_file_rescore > app1_rescore) {
if (file_rescore < app1_rescore)
hi = (*results)[app1].score;
else if (file_rescore > app1_rescore)
lo = (*results)[app1].score;
}
// Place new score at midpoint between hi and lo.
(*results)[file].score = lo + ((hi - lo) / 2);
prev_file = file;
file = GetNextMatchingIndex(results, file_chip_filter, file);
prev_file_rescore = file_rescore;
} else {
// File should sit below both current app scores.
app1 = app2;
app1_rescore = app2_rescore;
app2 = GetNextMatchingIndex(results, app_chip_filter, app1);
app2_rescore =
app2 < 0 ? kScoreLo : FetchScore(ranks, (*results)[app2].result);
}
}
}
void ChipRanker::SetForTest(std::unique_ptr<RecurrenceRanker> ranker) {
ranker_ = std::move(ranker);
}
} // namespace app_list
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_BROWSER_UI_APP_LIST_SEARCH_SEARCH_RESULT_RANKER_CHIP_RANKER_H_
#define CHROME_BROWSER_UI_APP_LIST_SEARCH_SEARCH_RESULT_RANKER_CHIP_RANKER_H_
#include <map>
#include <memory>
#include <string>
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/app_list/search/mixer.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/app_launch_data.h"
#include "chrome/browser/ui/app_list/search/search_result_ranker/recurrence_ranker.h"
namespace app_list {
// A ChipRanker provides a method for ranking suggestion chips in the Chrome OS
// Launcher. Given a list of SortedResults from the Mixer, the ChipRanker will
// rescore the chip items so that they are appropriately ranked, while
// preserving the original ordering of all groups of results.
//
// The ranking algorithm works as follows:
// - Start with sorting the results already scored from the Mixer
// - Take the top two app items, app1 and app2
// - For each chip in the SortedResults list:
// 1. Rank app1, app2 and chip using a Dolphin model
// 2. Adjust chip score to sit in the correct position
// relative to the two apps:
// - If chip should be first
// set chip score > app1 score
// - If chip should sit between
// set chip score > app2 score, < app1 score
// - If chip is ranked last
// take app2 and the next app item, app3, and continue
// with same file.
class ChipRanker {
public:
explicit ChipRanker(Profile* profile);
~ChipRanker();
ChipRanker(const ChipRanker&) = delete;
ChipRanker& operator=(const ChipRanker&) = delete;
// Train the ranker that compares the different result types.
void Train(const AppLaunchData& app_launch_data);
// Adjusts chip scores to fit in line with app scores using
// ranking algorithm detailed above.
void Rank(Mixer::SortedResults* results);
// Set a fake ranker for tests.
void SetForTest(std::unique_ptr<RecurrenceRanker> ranker);
// Ranker generates scores used for re-arranging items, not
// raw result scores.
std::unique_ptr<RecurrenceRanker> ranker_;
private:
Profile* profile_;
};
} // namespace app_list
#endif // CHROME_BROWSER_UI_APP_LIST_SEARCH_SEARCH_RESULT_RANKER_CHIP_RANKER_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/app_list/search/search_result_ranker/chip_ranker.h"
#include <list>
#include <vector>
#include "ash/public/cpp/app_list/app_list_types.h"
#include "base/files/file_path.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/search/chrome_search_result.h"
#include "chrome/browser/ui/app_list/search/mixer.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::ElementsAre;
using testing::UnorderedElementsAre;
using testing::WhenSorted;
namespace app_list {
namespace {
using ResultType = ash::AppListSearchResultType;
class TestSearchResult : public ChromeSearchResult {
public:
TestSearchResult(const std::string& id, ResultType type)
: instance_id_(instantiation_count++) {
set_id(id);
SetTitle(base::UTF8ToUTF16(id));
SetResultType(type);
switch (type) {
case ResultType::kFileChip:
case ResultType::kDriveQuickAccessChip:
SetDisplayType(DisplayType::kChip);
break;
case ResultType::kInstalledApp:
// Apps that should be in the chips
SetDisplayType(DisplayType::kTile);
SetIsRecommendation(true);
break;
case ResultType::kPlayStoreApp:
// Apps that shouldn't be in the chips
SetDisplayType(DisplayType::kTile);
break;
default:
SetDisplayType(DisplayType::kList);
break;
}
}
~TestSearchResult() override {}
// ChromeSearchResult overrides:
void Open(int event_flags) override {}
void InvokeAction(int action_index, int event_flags) override {}
ash::SearchResultType GetSearchResultType() const override {
return ash::SEARCH_RESULT_TYPE_BOUNDARY;
}
private:
static int instantiation_count;
int instance_id_;
DISALLOW_COPY_AND_ASSIGN(TestSearchResult);
};
int TestSearchResult::instantiation_count = 0;
MATCHER_P(HasId, id, "") {
bool match = base::UTF16ToUTF8(arg.result->title()) == id;
if (!match)
*result_listener << "HasId wants '" << id << "', but got '"
<< arg.result->title() << "'";
return match;
}
MATCHER_P(HasScore, score, "") {
const double tol = 1e-10;
bool match = abs(arg.score - score) < tol;
if (!match)
*result_listener << "HasScore wants '" << score << "', but got '"
<< arg.score << "'";
return match;
}
} // namespace
class ChipRankerTest : public testing::Test {
public:
ChipRankerTest() {
TestingProfile::Builder profile_builder;
profile_ = profile_builder.Build();
ranker_ = std::make_unique<ChipRanker>(profile_.get());
task_environment_.RunUntilIdle();
SetRankerModel();
}
~ChipRankerTest() override = default;
void SetRankerModel() {
// Set up fake ranker model.
RecurrenceRankerConfigProto config;
config.set_min_seconds_between_saves(240u);
config.set_condition_limit(100u);
config.set_condition_decay(0.99f);
config.set_target_limit(500u);
config.set_target_decay(0.99f);
config.mutable_predictor()->mutable_fake_predictor();
ranker_->SetForTest(std::make_unique<RecurrenceRanker>(
"", profile_->GetPath().AppendASCII("test_chip_ranker.pb"), config,
chromeos::ProfileHelper::IsEphemeralUserProfile(profile_.get())));
}
Mixer::SortedResults MakeSearchResults(const std::vector<std::string>& ids,
const std::vector<ResultType>& types,
const std::vector<double> scores) {
Mixer::SortedResults results;
for (int i = 0; i < static_cast<int>(ids.size()); ++i) {
results_.emplace_back(ids[i], types[i]);
results.emplace_back(&results_.back(), scores[i]);
}
return results;
}
void TrainRanker(const std::vector<std::string>& ids,
const std::vector<RankingItemType>& types,
const std::vector<int>& n,
bool reset = true) {
// Reset ranker
if (reset) {
SetRankerModel();
task_environment_.RunUntilIdle();
}
std::list<AppLaunchData> data;
for (int i = 0; i < static_cast<int>(ids.size()); ++i) {
data.emplace_back();
data.back().id = ids[i];
data.back().ranking_item_type = types[i];
}
int i = 0;
for (const auto& d : data) {
for (int j = 0; j < n[i]; ++j) {
ranker_->Train(d);
}
++i;
}
}
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<Profile> profile_;
std::unique_ptr<ChipRanker> ranker_;
// This is used only to make the ownership clear for the TestSearchResult
// objects that the return value of MakeSearchResults() contains raw pointers
// to.
std::list<TestSearchResult> results_;
};
// Check that ranking an empty list has no effect.
TEST_F(ChipRankerTest, EmptyList) {
Mixer::SortedResults results = MakeSearchResults({}, {}, {});
ranker_->Rank(&results);
EXPECT_EQ(results.size(), 0ul);
}
// Check that ranking only one app has no effect.
TEST_F(ChipRankerTest, OneAppOnly) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kFileChip, ResultType::kFileChip},
{8.9, 0.7, 0.4});
TrainRanker({"app1", "file1"},
{RankingItemType::kApp, RankingItemType::kChip}, {1, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("file2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(0.7),
HasScore(0.4))));
}
// Check that ranking only apps has no effect.
TEST_F(ChipRankerTest, AppsOnly) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "app3"},
{ResultType::kInstalledApp, ResultType::kPlayStoreApp,
ResultType::kInstalledApp},
{8.9, 8.8, 8.7});
TrainRanker({"app1", "app2"}, {RankingItemType::kApp, RankingItemType::kApp},
{1, 1});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("app3"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.8),
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({"file1", "file2"},
{RankingItemType::kChip, RankingItemType::kChip}, {1, 1});
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 =
MakeSearchResults({"app1", "app2", "omni1", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kOmnibox, ResultType::kFileChip},
{8.9, 8.7, 0.8, 0.7});
TrainRanker({"app1", "app2", "omni1", "file1"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kOmniboxGeneric, RankingItemType::kChip},
{3, 1, 1, 2});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("app2"), HasId("omni1"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.8),
HasScore(8.7), HasScore(0.8))));
}
// Check moving a file into first place.
TEST_F(ChipRankerTest, FileFirst) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kFileChip},
{8.8, 8.7, 0.9});
TrainRanker(
{"app1", "app2", "file1"},
{RankingItemType::kApp, RankingItemType::kApp, RankingItemType::kChip},
{1, 1, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("app1"),
HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.8),
HasScore(8.7))));
}
// Check moving a file between two apps.
TEST_F(ChipRankerTest, FileMid) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kFileChip},
{8.9, 8.6, 0.8});
TrainRanker(
{"app1", "app2", "file1"},
{RankingItemType::kApp, RankingItemType::kApp, RankingItemType::kChip},
{3, 1, 2});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.75),
HasScore(8.6))));
}
// Check moving a file into last place.
TEST_F(ChipRankerTest, FileLast) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kFileChip},
{8.9, 8.6, 0.4});
TrainRanker(
{"app1", "app2", "file1"},
{RankingItemType::kApp, RankingItemType::kApp, RankingItemType::kChip},
{2, 2, 1});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("file1"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.6),
HasScore(0.4))));
}
// Check alternating app and file results.
TEST_F(ChipRankerTest, ChipAlternate) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip},
{8.9, 8.6, 0.8, 0.3});
TrainRanker({"app1", "app2", "file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{4, 2, 3, 1});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("app2"), HasId("file2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.75),
HasScore(8.6), HasScore(0.3))));
}
// Check moving two files into first place.
TEST_F(ChipRankerTest, TwoFilesFirst) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip},
{8.8, 8.6, 0.9, 0.8});
TrainRanker({"app1", "app2", "file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{1, 2, 4, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("file2"),
HasId("app1"), HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.85),
HasScore(8.8), HasScore(8.6))));
}
// Check ranking a file that the ranker hasn't seen.
TEST_F(ChipRankerTest, UntrainedFile) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip},
{8.8, 8.6, 0.9});
TrainRanker({"app1", "app2"}, {RankingItemType::kApp, RankingItemType::kApp},
{2, 1});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("file1"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.8), HasScore(8.6),
HasScore(0.9))));
}
// Check that input order of apps remains the same even where ranker
// would swap them.
TEST_F(ChipRankerTest, AppMaintainOrder) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip},
{8.8, 8.6, 0.9});
TrainRanker(
{"app1", "app2", "file1"},
{RankingItemType::kApp, RankingItemType::kApp, RankingItemType::kChip},
{1, 2, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("app1"),
HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.8),
HasScore(8.6))));
}
// Check that file ordering remains the same even where ranker would
// swap them, when files are placed ahead of first app.
TEST_F(ChipRankerTest, FileMaintainOrderFirst) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip},
{8.8, 8.6, 0.9, 0.8});
TrainRanker({"app1", "app2", "file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{1, 2, 4, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("file2"),
HasId("app1"), HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.85),
HasScore(8.8), HasScore(8.6))));
}
// Check that file ordering remains the same even where ranker would
// swap them, when files are placed ahead of first app.
TEST_F(ChipRankerTest, FileMaintainOrderMid) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip},
{8.8, 8.6, 0.9, 0.8});
TrainRanker({"app1", "app2", "file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{4, 1, 2, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("file2"), HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.8), HasScore(8.7),
HasScore(8.65), HasScore(8.6))));
}
// Check that rank works on a succession of training instances.
TEST_F(ChipRankerTest, TrainMultiple) {
Mixer::SortedResults results = MakeSearchResults(
{"app1", "app2", "file1", "file2"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kDriveQuickAccessChip, ResultType::kFileChip},
{8.8, 8.6, 0.9, 0.8});
TrainRanker({"app1", "app2", "file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{5, 2, 3, 1});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("file1"),
HasId("app2"), HasId("file2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.8), HasScore(8.7),
HasScore(8.6), HasScore(0.8))));
TrainRanker({"file1", "file2"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{3, 2}, /* reset = */ false);
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("app1"),
HasId("file2"), HasId("app2"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.9), HasScore(8.8),
HasScore(8.7), HasScore(8.6))));
}
// Check ranker behaviour when multiple apps have equal scores.
TEST_F(ChipRankerTest, EqualApps) {
Mixer::SortedResults results =
MakeSearchResults({"app1", "app2", "app3", "file1"},
{ResultType::kInstalledApp, ResultType::kInstalledApp,
ResultType::kInstalledApp, ResultType::kFileChip},
{8.7, 8.7, 8.7, 0.5});
TrainRanker({"app1", "app2", "app3", "file1"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{5, 2, 1, 3});
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("app1"), HasId("app2"),
HasId("app3"), HasId("file1"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.7), HasScore(8.7),
HasScore(8.7), HasScore(8.7))));
TrainRanker({"file1"},
{RankingItemType::kApp, RankingItemType::kApp,
RankingItemType::kChip, RankingItemType::kChip},
{3}, /* reset = */ false);
ranker_->Rank(&results);
EXPECT_THAT(results, WhenSorted(ElementsAre(HasId("file1"), HasId("app1"),
HasId("app2"), HasId("app3"))));
EXPECT_THAT(results, WhenSorted(ElementsAre(HasScore(8.85), HasScore(8.7),
HasScore(8.7), HasScore(8.7))));
}
} // namespace app_list
...@@ -23,6 +23,7 @@ ZeroStateResultType ZeroStateTypeFromRankingType( ...@@ -23,6 +23,7 @@ ZeroStateResultType ZeroStateTypeFromRankingType(
case RankingItemType::kFile: case RankingItemType::kFile:
case RankingItemType::kApp: case RankingItemType::kApp:
case RankingItemType::kArcAppShortcut: case RankingItemType::kArcAppShortcut:
case RankingItemType::kChip:
return ZeroStateResultType::kUnanticipated; return ZeroStateResultType::kUnanticipated;
case RankingItemType::kOmniboxGeneric: case RankingItemType::kOmniboxGeneric:
return ZeroStateResultType::kOmniboxSearch; return ZeroStateResultType::kOmniboxSearch;
......
...@@ -33,12 +33,13 @@ RankingItemType RankingItemTypeFromSearchResult( ...@@ -33,12 +33,13 @@ RankingItemType RankingItemTypeFromSearchResult(
return RankingItemType::kIgnored; return RankingItemType::kIgnored;
case ash::AppListSearchResultType::kArcAppShortcut: case ash::AppListSearchResultType::kArcAppShortcut:
return RankingItemType::kArcAppShortcut; return RankingItemType::kArcAppShortcut;
case ash::AppListSearchResultType::kFileChip:
case ash::AppListSearchResultType::kZeroStateFile: case ash::AppListSearchResultType::kZeroStateFile:
return RankingItemType::kZeroStateFile; return RankingItemType::kZeroStateFile;
case ash::AppListSearchResultType::kDriveQuickAccessChip:
case ash::AppListSearchResultType::kDriveQuickAccess: case ash::AppListSearchResultType::kDriveQuickAccess:
return RankingItemType::kDriveQuickAccess; return RankingItemType::kDriveQuickAccess;
case ash::AppListSearchResultType::kFileChip:
case ash::AppListSearchResultType::kDriveQuickAccessChip:
return RankingItemType::kChip;
} }
} }
......
...@@ -24,6 +24,7 @@ enum class RankingItemType { ...@@ -24,6 +24,7 @@ enum class RankingItemType {
kArcAppShortcut = 5, kArcAppShortcut = 5,
kZeroStateFile = 6, kZeroStateFile = 6,
kDriveQuickAccess = 7, kDriveQuickAccess = 7,
kChip = 8,
}; };
// Convert a |ChromeSearchResult| into its |RankingItemType|. // Convert a |ChromeSearchResult| into its |RankingItemType|.
......
...@@ -4,11 +4,13 @@ ...@@ -4,11 +4,13 @@
#include "chrome/browser/ui/app_list/search/zero_state_file_provider.h" #include "chrome/browser/ui/app_list/search/zero_state_file_provider.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/files/file_util.h" #include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h" #include "base/files/scoped_temp_dir.h"
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "base/strings/utf_string_conversions.h" #include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h" #include "base/test/task_environment.h"
#include "chrome/browser/profiles/profile.h" #include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/app_list/search/zero_state_file_result.h" #include "chrome/browser/ui/app_list/search/zero_state_file_result.h"
...@@ -20,6 +22,7 @@ ...@@ -20,6 +22,7 @@
namespace app_list { namespace app_list {
namespace { namespace {
using base::test::ScopedFeatureList;
using ::file_manager::file_tasks::FileTasksObserver; using ::file_manager::file_tasks::FileTasksObserver;
using ::testing::UnorderedElementsAre; using ::testing::UnorderedElementsAre;
...@@ -57,6 +60,7 @@ class ZeroStateFileProviderTest : public testing::Test { ...@@ -57,6 +60,7 @@ class ZeroStateFileProviderTest : public testing::Test {
void Wait() { task_environment_.RunUntilIdle(); } void Wait() { task_environment_.RunUntilIdle(); }
content::BrowserTaskEnvironment task_environment_; content::BrowserTaskEnvironment task_environment_;
base::test::ScopedFeatureList scoped_feature_list_;
std::unique_ptr<Profile> profile_; std::unique_ptr<Profile> profile_;
std::unique_ptr<ZeroStateFileProvider> provider_; std::unique_ptr<ZeroStateFileProvider> provider_;
...@@ -68,7 +72,11 @@ TEST_F(ZeroStateFileProviderTest, NoResultsWithQuery) { ...@@ -68,7 +72,11 @@ TEST_F(ZeroStateFileProviderTest, NoResultsWithQuery) {
EXPECT_TRUE(provider_->results().empty()); EXPECT_TRUE(provider_->results().empty());
} }
TEST_F(ZeroStateFileProviderTest, Simple) { TEST_F(ZeroStateFileProviderTest, ResultsProvided) {
// Disable flag.
scoped_feature_list_.InitWithFeatures(
{}, {app_list_features::kEnableSuggestedFiles});
WriteFile("exists_1.txt"); WriteFile("exists_1.txt");
WriteFile("exists_2.png"); WriteFile("exists_2.png");
WriteFile("exists_3.pdf"); WriteFile("exists_3.pdf");
...@@ -85,4 +93,27 @@ TEST_F(ZeroStateFileProviderTest, Simple) { ...@@ -85,4 +93,27 @@ TEST_F(ZeroStateFileProviderTest, Simple) {
UnorderedElementsAre(Title("exists_1.txt"), Title("exists_2.png"))); UnorderedElementsAre(Title("exists_1.txt"), Title("exists_2.png")));
} }
TEST_F(ZeroStateFileProviderTest, ResultsProvidedWithChips) {
// Enable flag - with flag enabled, we expect to receive the chip
// results for each file as well, so each file should be listed twice.
scoped_feature_list_.InitWithFeatures(
{app_list_features::kEnableSuggestedFiles}, {});
WriteFile("exists_1.txt");
WriteFile("exists_2.png");
WriteFile("exists_3.pdf");
provider_->OnFilesOpened(
{OpenEvent("exists_1.txt"), OpenEvent("exists_2.png")});
provider_->OnFilesOpened({OpenEvent("nonexistant.txt")});
provider_->Start(base::string16());
Wait();
EXPECT_THAT(
provider_->results(),
UnorderedElementsAre(Title("exists_1.txt"), Title("exists_2.png"),
Title("exists_1.txt"), Title("exists_2.png")));
}
} // namespace app_list } // namespace app_list
...@@ -4416,6 +4416,7 @@ test("unit_tests") { ...@@ -4416,6 +4416,7 @@ test("unit_tests") {
"../browser/ui/app_list/search/search_result_ranker/app_launch_predictor_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/app_launch_predictor_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/app_list_launch_metrics_provider_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/app_list_launch_metrics_provider_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/app_search_result_ranker_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/app_search_result_ranker_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/chip_ranker_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/frecency_store_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/frecency_store_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/histogram_util_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/histogram_util_unittest.cc",
"../browser/ui/app_list/search/search_result_ranker/ml_app_rank_provider_unittest.cc", "../browser/ui/app_list/search/search_result_ranker/ml_app_rank_provider_unittest.cc",
......
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