chromium/chrome/browser/ash/app_list/search/ranking/keyword_ranker_unittest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/app_list/search/ranking/keyword_ranker.h"

#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/scoring.h"
#include "chrome/browser/ash/app_list/search/test/ranking_test_util.h"
#include "chrome/browser/ash/app_list/search/test/test_result.h"
#include "chrome/browser/ash/app_list/search/types.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace app_list::test {
namespace {

constexpr double kEps = 1e-3;

// Helper function that check if the results' multiplier is right.
void ExpectKeywordMultiplier(const ResultsMap& results,
                             ProviderType provider,
                             double keyword_multiplier) {
  const auto it = results.find(provider);
  if (it == results.end()) {
    return;
  }

  for (const auto& result : it->second) {
    EXPECT_NEAR(result->scoring().keyword_multiplier(), keyword_multiplier,
                kEps);
  }
}

}  // namespace

class KeywordRankerTest : public RankerTestBase {};

/*********************** Exact String Matching Tests ***********************/

// Test the input query does not contains keyword that match any providers.
TEST_F(KeywordRankerTest, NoMatchedKeywords) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  KeywordRanker ranker;
  ranker.Start(u"ABC", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);

  // As no detection of keywords for this result type, therefore the keyword
  // multiplier is down-weighted.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.0);
}

// Test the input query only match one provider.
TEST_F(KeywordRankerTest, OneMatchedKeyword) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  results[ResultType::kHelpApp] =
      MakeScoredResults({"help_a", "help_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"explore", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kHelpApp);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kHelpApp].size(), 2u);

  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.0);
  ExpectKeywordMultiplier(results, ResultType::kHelpApp, 1.25);
}

// Test the input query match multiple provider.
TEST_F(KeywordRankerTest, KeywordMatchesMultipleProviders) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});
  results[ResultType::kDriveSearch] =
      MakeScoredResults({"drive_a", "drive_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"file", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);
  ranker.UpdateResultRanks(results, ProviderType::kDriveSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 3u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);
  ASSERT_EQ(results[ResultType::kDriveSearch].size(), 2u);

  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.0);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.25);
  ExpectKeywordMultiplier(results, ResultType::kDriveSearch, 1.25);
}

/*********************** Fuzzy String Matching Tests ***********************/

// Test that when the input query fuzzy matched one provider's keyword.
TEST_F(KeywordRankerTest, OneTokenMatchOneProvider) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kOmnibox] = MakeScoredResults({"omnibox_a"}, {0.7});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"seach", categories);
  ranker.UpdateResultRanks(results, ProviderType::kOmnibox);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kOmnibox].size(), 1u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Since fuzzy matching, kOmnibox keyword_multiplier will be more than 1 but
  // less than 1.25.
  ExpectKeywordMultiplier(results, ResultType::kOmnibox, 1.199);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);
}

// Test that when one provider have multiple matched token, it will take the max
// score.
TEST_F(KeywordRankerTest, MultipleTokensMatchOneProvider) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kOmnibox] = MakeScoredResults({"omnibox_a"}, {0.7});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"seach search", categories);
  ranker.UpdateResultRanks(results, ProviderType::kOmnibox);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kOmnibox].size(), 1u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Since there exist exact match, therefore kOmnibox's keyword_multiplier will
  // be 1.25.
  ExpectKeywordMultiplier(results, ResultType::kOmnibox, 1.25);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);
}

// Test that the one token fuzzy matched multiple providers.
TEST_F(KeywordRankerTest, OneTokenMatchMultipleProviders) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});
  results[ResultType::kDriveSearch] =
      MakeScoredResults({"drive_a", "drive_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"fil", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);
  ranker.UpdateResultRanks(results, ProviderType::kDriveSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 3u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);
  ASSERT_EQ(results[ResultType::kDriveSearch].size(), 2u);

  // The misspell of "file" will affect both kFileSearch and kDriveSearch.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.0);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.212);
  ExpectKeywordMultiplier(results, ResultType::kDriveSearch, 1.212);
}

// Test that multiple tokens fuzzy matched/exact matched multiple providers.
TEST_F(KeywordRankerTest, MultipleTokensMatchMultipleProviders) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});
  results[ResultType::kDriveSearch] =
      MakeScoredResults({"drive_a", "drive_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"fil drive app", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);
  ranker.UpdateResultRanks(results, ProviderType::kDriveSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 3u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);
  ASSERT_EQ(results[ResultType::kDriveSearch].size(), 2u);

  // The misspell of "file" will affect both kFileSearch and kDriveSearch.
  // However, token "drive" is exact match for kDriveSearch, hence kDriveSearch
  // will have a higher keyword_multiplier.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.25);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.212);
  ExpectKeywordMultiplier(results, ResultType::kDriveSearch, 1.25);
}

/*********************** Train Tests ***********************/

// Test Train() by checking if the keyword_multiplier_ has been boosted up to a
// certain number as the boost_factor_ increased.
TEST_F(KeywordRankerTest, TestTrainBoostFactorIncrease) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.5});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"app", categories);

  // Set the ftrl score of each result from the providers.
  SetFtrlScore(results, ProviderType::kInstalledApp, {0.7, 0.5});
  SetFtrlScore(results, ProviderType::kFileSearch, {0.7, 0.5});

  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of results and the keyword_multiplier of the results.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Only kInstalledApp's keyword matched in the input. Train() does not happen
  // yet.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.25);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);

  // Train with a boosted result being selected. There are 2 results which score
  // higher than the selected result. Out of the 2 results only 1 is
  // non-boosting. The boost_factor_ will be increase by half of itself.
  ranker.Train(MakeLaunchData("app_b"));
  Wait();

  ranker.Start(u"app", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Since boost factor being boost up, keyword_multiplier_ should be higher
  // too. That is 1.25 + (0.5 - 0.25)*0.5 = 1.375.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.375);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);
}

// Test Train() by checking if the keyword_multiplier_ has been decreased to a
// certain number as the boost_factor_ decreased.
TEST_F(KeywordRankerTest, TestTrainBoostFactorDecrease) {
  // Simulate a query starting.
  ResultsMap results;
  CategoriesList categories;

  // Input the results scores and UpdateResultRanks.
  results[ResultType::kInstalledApp] =
      MakeScoredResults({"app_a", "app_b"}, {0.7, 0.4});
  results[ResultType::kFileSearch] =
      MakeScoredResults({"local_a", "local_b"}, {0.7, 0.5});

  KeywordRanker ranker;
  ranker.Start(u"app", categories);

  // Set the ftrl score of each result from the providers.
  SetFtrlScore(results, ProviderType::kInstalledApp, {0.7, 0.4});
  SetFtrlScore(results, ProviderType::kFileSearch, {0.7, 0.5});

  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of results and the keyword_multiplier of the results.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Only kInstalledApp's keyword matched in the input. Train() does not happen
  // yet.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.25);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);

  // Train with a non boosted result being selected. There are 2 results which
  // score higher than the selected result. Out of the 2 results only 1 is
  // boosting. The boost_factor_ will be decrease by half of itself.
  ranker.Train(MakeLaunchData("local_b"));
  Wait();

  ranker.Start(u"app", categories);
  ranker.UpdateResultRanks(results, ProviderType::kInstalledApp);
  ranker.UpdateResultRanks(results, ProviderType::kFileSearch);

  // Check the number of result and the keyword_multiplier of the result.
  ASSERT_EQ(results.size(), 2u);
  ASSERT_EQ(results[ResultType::kInstalledApp].size(), 2u);
  ASSERT_EQ(results[ResultType::kFileSearch].size(), 2u);

  // Since boost factor being boost up, keyword_multiplier_ should be higher
  // too. That is 1.25 - 0.25*0.5 = 1.125.
  ExpectKeywordMultiplier(results, ResultType::kInstalledApp, 1.125);
  ExpectKeywordMultiplier(results, ResultType::kFileSearch, 1.0);
}

}  // namespace app_list::test