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

// Copyright 2021 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/best_match_ranker.h"

#include <cmath>
#include <vector>

#include "base/containers/flat_set.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/ranking/constants.h"
#include "chrome/browser/ash/app_list/search/search_features.h"
#include "chrome/browser/ash/app_list/search/types.h"

namespace app_list {
namespace {

// Returns true if the |type| provider's results should never be a best match.
bool ShouldIgnoreProvider(ProviderType type) {
  switch (type) {
      // Continue providers:
    case ProviderType::kZeroStateApp:
    case ProviderType::kZeroStateFile:
    case ProviderType::kZeroStateDrive:
    case ProviderType::kDesksAdminTemplate:
      // Low-intent providers:
    case ProviderType::kPlayStoreReinstallApp:
    case ProviderType::kPlayStoreApp:
    case ProviderType::kAssistantText:
      // Deprecated providers:
    case ProviderType::kLauncher:
    case ProviderType::kAnswerCard:
      // Internal results:
    case ProviderType::kUnknown:
    case ProviderType::kInternalPrivacyInfo:
    // In development:
    case ProviderType::kImageSearch:
    case ProviderType::kHelpApp:
      return true;
    case ProviderType::kInternalApp:
    case ProviderType::kArcAppShortcut:
    case ProviderType::kDriveSearch:
    case ProviderType::kGames:
    case ProviderType::kZeroStateHelpApp:
    case ProviderType::kFileSearch:
    case ProviderType::kInstalledApp:
    case ProviderType::kInstantApp:
    case ProviderType::kOmnibox:
    case ProviderType::kPersonalization:
    case ProviderType::kOpenTab:
    case ProviderType::kOsSettings:
    case ProviderType::kSystemInfo:
    case ProviderType::kAppShortcutV2:
      return false;
    case ProviderType::kKeyboardShortcut:
      // Allows key shortcut results to appear in best match if flag enabled,
      // otherwise disable it by default to avoid possible over-triggering.
      if (search_features::IskLauncherKeyShortcutInBestMatchEnabled()) {
        return false;
      }
      return true;
  }
}

bool ShouldIgnoreResult(const ChromeSearchResult* result) {
  // Ignore:
  // - answer results: ANSWER and CALCULATOR.
  // - 'magnifying glass' results: WEB_QUERY, SEARCH_SUGGEST,
  //   SEARCH_SUGGEST_PERSONALIZED.
  //
  // This is determined using the omnibox metrics type, which is currently
  // the only type that gives sufficient granularity.
  return result->metrics_type() == ash::OMNIBOX_ANSWER ||
         result->metrics_type() == ash::OMNIBOX_CALCULATOR ||
         result->metrics_type() == ash::OMNIBOX_WEB_QUERY ||
         result->metrics_type() == ash::OMNIBOX_SEARCH_SUGGEST ||
         result->metrics_type() == ash::OMNIBOX_SEARCH_SUGGEST_ENTITY ||
         result->metrics_type() == ash::OMNIBOX_SUGGEST_PERSONALIZED;
}

}  // namespace

BestMatchRanker::BestMatchRanker() = default;

BestMatchRanker::~BestMatchRanker() = default;

void BestMatchRanker::Start(const std::u16string& query,
                            CategoriesList& categories) {
  is_pre_burnin_ = true;
  best_matches_.clear();
}

void BestMatchRanker::OnBurnInPeriodElapsed() {
  is_pre_burnin_ = false;
}

void BestMatchRanker::UpdateResultRanks(ResultsMap& results,
                                        ProviderType provider) {
  // Skip results from providers that are never included in the top matches.
  if (ShouldIgnoreProvider(provider)) {
    return;
  }

  // Remove invalidated weak pointers from best_matches_
  for (auto iter = best_matches_.begin(); iter != best_matches_.end();) {
    if (!(*iter)) {
      // C++11: the return value of erase(iter) is an iterator pointing to the
      // next element in the container.
      iter = best_matches_.erase(iter);
    } else {
      ++iter;
    }
  }

  const bool use_relevance_sort_only = is_pre_burnin_ || best_matches_.empty();

  // Insert into |best_matches_| any new results from this provider that meet
  // the best match threshold.
  const auto it = results.find(provider);
  DCHECK(it != results.end());

  base::flat_set<std::string> seen_ids;
  for (const auto result : best_matches_) {
    seen_ids.insert(result->id());
  }

  for (const auto& result : it->second) {
    if (ShouldIgnoreResult(result.get())) {
      continue;
    }

    if (seen_ids.find(result->id()) != seen_ids.end()) {
      // Omnibox provider can return more than once. Don't add duplicate results
      // to best_matches_.
      continue;
    }
    Scoring& scoring = result->scoring();

    double threshold = kBestMatchThreshold;
    if (search_features::IsLauncherKeywordExtractionScoringEnabled()) {
      threshold = kBestMatchThresholdWithKeywordRanking;
    }

    if (scoring.BestMatchScore() >= threshold) {
      best_matches_.push_back(result->GetWeakPtr());
    }
  }

  // If we have no best matches, there is no ranking work to do. Return early.
  if (best_matches_.empty()) {
    return;
  }

  // Sort best_matches_:
  if (use_relevance_sort_only) {
    // Pre-burn-in, or post-burn-in where we are receiving best matches for the
    // first time, simply sort by relevance.
    std::sort(best_matches_.begin(), best_matches_.end(),
              [](const auto& a, const auto& b) {
                return a->scoring().BestMatchScore() >
                       b->scoring().BestMatchScore();
              });
  } else {
    // Post-burn-in, where best matches exist from previous provider returns,
    // sort by result score but stabilize the top-ranked best match result. We
    // achieve this by first sorting by best match rank, and then sorting a
    // post-fix of the vector by relevance score. This is important because we
    // cannot rely on particular numbered ranks (e.g. 0) being present, as
    // results can technically be destroyed at any time.
    std::sort(best_matches_.begin(), best_matches_.end(),
              [](const auto& a, const auto& b) {
                const int a_rank = a->scoring().best_match_rank();
                const int b_rank = b->scoring().best_match_rank();
                // Sort order: 0, 1, 2, 3, ... then -1.
                // N.B. (a ^ b) < 0 checks for opposite sign.
                return (a_rank ^ b_rank) < 0 ? a_rank > b_rank
                                             : a_rank < b_rank;
              });
    std::sort(best_matches_.begin() + kNumBestMatchesToStabilize,
              best_matches_.end(), [](const auto& a, const auto& b) {
                return a->scoring().normalized_relevance() >
                       b->scoring().normalized_relevance();
              });
  }

  // For the first kNumBestMatches of best_matches_, renumber their best match
  // rank.
  for (size_t i = 0; i < std::min(kNumBestMatches, best_matches_.size()); ++i) {
    best_matches_[i]->scoring().set_best_match_rank(i);
    best_matches_[i]->SetBestMatch(true);
  }

  // For the excess elements of best_matches_, revoke their best match status,
  // and remove them from the vector.
  if (best_matches_.size() > kNumBestMatches) {
    for (size_t i = kNumBestMatches; i < best_matches_.size(); ++i) {
      best_matches_[i]->scoring().set_best_match_rank(-1);
      best_matches_[i]->SetBestMatch(false);
    }
    best_matches_.resize(kNumBestMatches);
  }
}

}  // namespace app_list