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

// Copyright 2022 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/sorting.h"

#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ash/app_list/search/common/search_result_util.h"

namespace app_list {

namespace {

bool CategoryAffectsScore(const ChromeSearchResult* result) {
  return result->display_type() == ash::SearchResultDisplayType::kList;
}

}  // namespace

void SortCategories(CategoriesList& categories) {
  // Sort categories first by burn-in iteration number, then by score.
  std::sort(categories.begin(), categories.end(),
            [](const auto& a, const auto& b) {
              const int a_burn_in = a.burn_in_iteration;
              const int b_burn_in = b.burn_in_iteration;
              if (a_burn_in != b_burn_in) {
                // Sort order: 0, 1, 2, 3, ... then -1.
                // The effect of this is to sort by arrival order, with unseen
                // categories ranked last.
                // N.B. (a ^ b) < 0 checks for opposite sign.
                return (a_burn_in ^ b_burn_in) < 0 ? a_burn_in > b_burn_in
                                                   : a_burn_in < b_burn_in;
              }
              // Special-case the search and assistant category, which should
              // be sorted last, when burn-in iteration numbers are equal.
              if (a.category == Category::kSearchAndAssistant) {
                return false;
              }
              if (b.category == Category::kSearchAndAssistant) {
                return true;
              }
              return a.score > b.score;
            });
}

void SortResults(
    std::vector<raw_ptr<ChromeSearchResult, VectorExperimental>>& results,
    const CategoriesList& categories) {
  std::sort(
      results.begin(), results.end(),
      [&](const ChromeSearchResult* a, const ChromeSearchResult* b) {
        const int a_best_match_rank = a->scoring().best_match_rank();
        const int b_best_match_rank = b->scoring().best_match_rank();
        if (a_best_match_rank != b_best_match_rank) {
          // First, sort by best match. All best matches are brought to
          // the front of the list and ordered by best_match_rank.
          //
          // The following gives sort order:
          //    0, 1, 2, ... (best matches) then -1 (non-best matches).
          // N.B. (a ^ b) < 0 checks for opposite sign.
          return (a_best_match_rank ^ b_best_match_rank) < 0
                     ? a_best_match_rank > b_best_match_rank
                     : a_best_match_rank < b_best_match_rank;
        }

        const bool ignore_categories =
            !CategoryAffectsScore(a) || !CategoryAffectsScore(b);
        if (a_best_match_rank == -1 && !ignore_categories &&
            a->category() != b->category()) {
          // Next, sort by categories, except for within best match.
          // |categories_| has been sorted above so the first category in
          // |categories_| should be ranked more highly.
          for (const auto& category : categories) {
            if (category.category == a->category()) {
              return true;
            }
            if (category.category == b->category()) {
              return false;
            }
          }
          // Any category associated with a result should also be present
          // in |categories_|.
          NOTREACHED_IN_MIGRATION();
          return false;
        }

        if (a->scoring().burn_in_iteration() !=
            b->scoring().burn_in_iteration()) {
          // Next, sort by burn-in iteration number. This has no effect on
          // results which arrive pre-burn-in. For post-burn-in results
          // for a given category, later-arriving results are placed below
          // earlier-arriving results.
          // This happens before sorting on display_score, as a trade-off
          // between ranking accuracy and UX pop-in mitigation.
          return a->scoring().burn_in_iteration() <
                 b->scoring().burn_in_iteration();
        }

        if (a->scoring().continue_rank() != b->scoring().continue_rank()) {
          return a->scoring().continue_rank() > b->scoring().continue_rank();
        }

        // Sort by display score.
        if (a->display_score() != b->display_score()) {
          return a->display_score() > b->display_score();
        }

        // Last, sort file results by filepath as it is unique. For the other
        // results, try to sort by title. It's our best effort to stabilize the
        // result ordering to avoid result flipping.
        if (a->category() == ash::AppListSearchResultCategory::kFiles) {
          return a->filePath().value() < b->filePath().value();
        } else {
          // Some providers set `title_vector` and the others sets `title`.
          if (!a->title().empty()) {
            return a->title() < b->title();
          } else {
            return StringFromTextVector(a->title_text_vector()) <
                   StringFromTextVector(b->title_text_vector());
          }
        }
      });
}

}  // namespace app_list