chromium/chrome/browser/ash/app_list/search/ranking/filtering_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/filtering_ranker.h"

#include "ash/public/cpp/app_list/app_list_config.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/flat_set.h"
#include "chrome/browser/ash/app_list/search/chrome_search_result.h"
#include "chrome/browser/ash/app_list/search/omnibox/omnibox_util.h"
#include "chrome/browser/ash/app_list/search/ranking/constants.h"
#include "chromeos/crosapi/mojom/launcher_search.mojom.h"

namespace app_list {
namespace {

using CrosApiSearchResult = ::crosapi::mojom::SearchResult;

constexpr auto kRestrictedAnswerTypes =
    base::MakeFixedFlatSet<CrosApiSearchResult::AnswerType>({
        CrosApiSearchResult::AnswerType::kDefaultAnswer,
        CrosApiSearchResult::AnswerType::kDictionary,
        CrosApiSearchResult::AnswerType::kTranslation,
    });

// Given `higher_priority` and `lower_priority` result types, deduplicate
// results between the two result types in `results` based on their id,
// preserving the ones in `higher_priority`.
//
// Note this only deduplicates results whose ids are present in both result
// types; if two results of one result type have the same id, they will not be
// deduplicated.
void DeduplicateResults(ResultsMap& results,
                        ResultType higher_priority,
                        ResultType lower_priority) {
  const auto first_it = results.find(higher_priority);
  const auto second_it = results.find(lower_priority);
  if (first_it == results.end() || second_it == results.end()) {
    return;
  }
  const auto& first_results = first_it->second;
  const auto& second_results = second_it->second;

  base::flat_set<std::string> first_ids;
  for (const auto& result : first_results) {
    if (result->result_type() == higher_priority) {
      first_ids.insert(result->id());
    }
  }

  for (auto& result : second_results) {
    if (first_ids.contains(result->id())) {
      result->scoring().set_filtered(true);
    }
  }
}

void DeduplicateDriveFilesAndTabs(ResultsMap& results) {
  const auto omnibox_it = results.find(ProviderType::kOmnibox);
  const auto drive_it = results.find(ProviderType::kDriveSearch);
  if (omnibox_it == results.end() || drive_it == results.end()) {
    return;
  }
  const auto& omnibox_results = omnibox_it->second;
  const auto& drive_results = drive_it->second;

  base::flat_set<std::string> drive_tab_ids;
  for (const auto& result : omnibox_results) {
    if (result->result_type() == ResultType::kOpenTab && result->DriveId()) {
      drive_tab_ids.insert(result->DriveId().value());
    }
  }

  for (auto& result : drive_results) {
    const auto& drive_id = result->DriveId();
    if (drive_id && drive_tab_ids.contains(drive_id.value())) {
      result->scoring().set_filtered(true);
    }
  }
}

void FilterOmniboxResults(ResultsMap& results, const std::u16string& query) {
  // We currently only filter omnibox results. So if we don't have any yet,
  // early exit.
  const auto it = results.find(ProviderType::kOmnibox);
  if (it == results.end()) {
    return;
  }

  auto& omnibox_results = results[ProviderType::kOmnibox];

  // Some answer result types overtrigger on short queries, so these will be
  // filtered out be default.
  if (query.size() <= kMinQueryLengthForCommonAnswers) {
    for (auto& omnibox_result : omnibox_results) {
      if (omnibox_result->display_type() == DisplayType::kAnswerCard &&
          kRestrictedAnswerTypes.contains(omnibox_result->answer_type())) {
        omnibox_result->scoring().set_filtered(true);
      }
    }
  }

  // Compute the total number of results. If we have fewer than can fit in the
  // UI, early exit.
  static const int max_search_results =
      ash::SharedAppListConfig::instance().max_search_results();
  int total_results = 0;
  for (const auto& type_results : results) {
    total_results += type_results.second.size();
  }
  if (total_results <= max_search_results) {
    return;
  }

  // Sort the list of omnibox results best-to-worst.
  std::sort(omnibox_results.begin(), omnibox_results.end(),
            [](const auto& a, const auto& b) {
              return a->relevance() > b->relevance();
            });

  // Filter all results after the |kMaxOmniboxResults|th one out of the UI,
  // but never remove best matches  or answer cards.
  for (size_t i = kMaxOmniboxResults; i < omnibox_results.size(); ++i) {
    auto& scoring = omnibox_results[i]->scoring();
    if (scoring.best_match_rank() == -1 &&
        omnibox_results[i]->display_type() != DisplayType::kAnswerCard) {
      scoring.set_filtered(true);
    }
  }
}

}  //  namespace

FilteringRanker::FilteringRanker() = default;

FilteringRanker::~FilteringRanker() = default;

void FilteringRanker::Start(const std::u16string& query,
                            CategoriesList& categories) {
  last_query_ = query;
}

void FilteringRanker::UpdateResultRanks(ResultsMap& results,
                                        ProviderType provider) {
  // Do not filter for zero-state.
  if (last_query_.empty()) {
    return;
  }
  FilterOmniboxResults(results, last_query_);
  DeduplicateDriveFilesAndTabs(results);
  // TODO(crbug.com/40218201): Verify that game URLs match the omnibox stripped
  // URL once game URLs are finalized.
  DeduplicateResults(results, ResultType::kGames, ResultType::kOmnibox);
  DeduplicateResults(results, ResultType::kImageSearch,
                     ResultType::kFileSearch);
}

}  // namespace app_list