chromium/ash/webui/help_app_ui/search/search_handler.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 "ash/webui/help_app_ui/search/search_handler.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ash/components/local_search_service/public/cpp/local_search_service_proxy.h"

namespace ash::help_app {
namespace {

// The path to save the help app persistence within the user's cryptohome.
constexpr char kHelpAppDir[] = "help_app/";

// The end result of a search. Logged once per time a search finishes.
// Not logged if the search is canceled by a new search starting. These values
// persist to logs. Entries should not be renumbered and numeric values should
// never be reused.
enum class SearchResultStatus {
  // There is no cache update, and the index is still empty, so the Search
  // Handler is not ready to handle searches.
  kNotReadyAndEmptyIndex = 0,
  // Not ready and the cache status is updating. This should be far less common
  // than kNotReadyAndEmptyIndex.
  kNotReadyAndUpdating = 1,
  // Ready and the LSS response status is Success.
  kReadyAndSuccess = 2,
  // Ready and the LSS response status is EmptyIndex. This can happen for
  // languages with no localized content to add to the search index.
  kReadyAndEmptyIndex = 3,
  // Ready and the LSS response status is something other than Success or
  // EmptyIndex.
  kReadyAndOtherStatus = 4,
  kMaxValue = kReadyAndOtherStatus,
};

// The current cache status of the search handler. We should only load from disk
// if the cache is empty, and should only provide search service if the cache is
// ready.
enum class CacheStatus {
  // The cache is empty.
  kEmpty = 0,
  // The cache is updating.
  kUpdating = 1,
  // The cache is updated and ready for search.
  kReady = 2,
};

// Use this in OnFindComplete.
void LogSearchResultStatus(SearchResultStatus state) {
  base::UmaHistogramEnumeration("Discover.SearchHandler.SearchResultStatus",
                                state);
}

// Order search results by relevance score. Higher relevance first.
bool CompareSearchResults(const mojom::SearchResultPtr& first,
                          const mojom::SearchResultPtr& second) {
  return first->relevance_score > second->relevance_score;
}

}  // namespace

SearchHandler::SearchHandler(
    SearchTagRegistry* search_tag_registry,
    local_search_service::LocalSearchServiceProxy* local_search_service_proxy)
    : search_tag_registry_(search_tag_registry),
      cache_status_(CacheStatus::kEmpty),
      construction_time_(base::TimeTicks::Now()) {
  local_search_service_proxy->GetIndex(
      local_search_service::IndexId::kHelpAppLauncher,
      local_search_service::Backend::kLinearMap,
      index_remote_.BindNewPipeAndPassReceiver());
  DCHECK(index_remote_.is_bound());

  search_tag_registry_->AddObserver(this);

  // Set the search params to make fuzzy and prefix matching stricter.
  // This reduces the number of irrelevant search results.
  index_remote_->SetSearchParams(
      {
          /*relevance_threshold=*/0.73,  // The threshold used by linear map.
          /*prefix_threshold=*/0.8,
          /*fuzzy_threshold=*/0.85,
      },
      base::OnceCallback<void()>());
}

SearchHandler::~SearchHandler() {
  search_tag_registry_->RemoveObserver(this);
}

void SearchHandler::BindInterface(
    mojo::PendingReceiver<mojom::SearchHandler> pending_receiver) {
  receivers_.Add(this, std::move(pending_receiver));
}

void SearchHandler::Search(const std::u16string& query,
                           uint32_t max_num_results,
                           SearchCallback callback) {
  // Search for 5x the maximum set of results. If there are many matches for
  // a query, it may be the case that |index_| returns some matches with higher
  // SearchResultDefaultRank values later in the list. Requesting up to 5x the
  // maximum number ensures that such results will be returned and can be ranked
  // accordingly when sorted.
  uint32_t max_local_search_service_results = 5 * max_num_results;

  // Reject the search request if the cache is not ready yet.
  if (cache_status_ != CacheStatus::kReady) {
    LogSearchResultStatus(cache_status_ == CacheStatus::kEmpty
                              ? SearchResultStatus::kNotReadyAndEmptyIndex
                              : SearchResultStatus::kNotReadyAndUpdating);
    std::move(callback).Run({});
    return;
  }
  index_remote_->Find(query, max_local_search_service_results,
                      base::BindOnce(&SearchHandler::OnFindComplete,
                                     weak_ptr_factory_.GetWeakPtr(),
                                     std::move(callback), max_num_results));
}

void SearchHandler::Update(std::vector<mojom::SearchConceptPtr> concepts,
                           UpdateCallback callback) {
  // Temporarily disable the search before the update is complete to avoid
  // data inconsistency.
  cache_status_ = CacheStatus::kUpdating;
  // Update the persistence if the path is available.
  if (persistence_) {
    persistence_->UpdateSearchConcepts(concepts);
  }

  if (concepts.empty()) {
    // Trying to update with an empty list causes an error in the LSS.
    cache_status_ = CacheStatus::kReady;
    std::move(callback).Run();
    return;
  }

  search_tag_registry_->ClearAndUpdate(std::move(concepts),
                                       std::move(callback));
}

void SearchHandler::Observe(
    mojo::PendingRemote<mojom::SearchResultsObserver> observer) {
  observers_.Add(std::move(observer));
}

void SearchHandler::OnProfileDirAvailable(const base::FilePath& profile_dir) {
  persistence_ = std::make_unique<SearchConcept>(
      profile_dir.AppendASCII(kHelpAppDir).AppendASCII("persistence.pb"));

  // Attempt to read from persistence.
  persistence_->GetSearchConcepts(
      base::BindOnce(&SearchHandler::OnPersistenceReadComplete,
                     weak_ptr_factory_.GetWeakPtr()));
}

void SearchHandler::OnRegistryUpdated() {
  cache_status_ = CacheStatus::kReady;
  if (!construction_time_.is_null()) {
    base::UmaHistogramTimes("Discover.SearchHandler.SearchAvailableTime",
                            base::TimeTicks::Now() - construction_time_);
    // Reset as we only care about the first time the search is available.
    construction_time_ = base::TimeTicks();
  }
  for (auto& observer : observers_)
    observer->OnSearchResultAvailabilityChanged();
}

std::vector<mojom::SearchResultPtr> SearchHandler::GenerateSearchResultsArray(
    const std::vector<local_search_service::Result>&
        local_search_service_results,
    uint32_t max_num_results) const {
  std::vector<mojom::SearchResultPtr> search_results;
  for (const auto& result : local_search_service_results) {
    if (search_results.size() == max_num_results) {
      break;
    }
    mojom::SearchResultPtr result_ptr = ResultToSearchResult(result);
    if (result_ptr)
      search_results.push_back(std::move(result_ptr));
  }
  std::sort(search_results.begin(), search_results.end(), CompareSearchResults);

  return search_results;
}

void SearchHandler::OnFindComplete(
    SearchCallback callback,
    uint32_t max_num_results,
    local_search_service::ResponseStatus response_status,
    const std::optional<std::vector<local_search_service::Result>>&
        local_search_service_results) {
  if (response_status != local_search_service::ResponseStatus::kSuccess) {
    LogSearchResultStatus(
        response_status == local_search_service::ResponseStatus::kEmptyIndex
            ? SearchResultStatus::kReadyAndEmptyIndex
            : SearchResultStatus::kReadyAndOtherStatus);
    std::move(callback).Run({});
    return;
  }
  LogSearchResultStatus(SearchResultStatus::kReadyAndSuccess);

  std::move(callback).Run(GenerateSearchResultsArray(
      local_search_service_results.value(), max_num_results));
}

void SearchHandler::OnPersistenceReadComplete(
    std::vector<mojom::SearchConceptPtr> concepts) {
  // Only update from persistence if the cache is empty.
  if (cache_status_ != CacheStatus::kEmpty) {
    return;
  }
  cache_status_ = CacheStatus::kUpdating;
  if (concepts.empty()) {
    // Trying to update with an empty list causes an error in the LSS.
    cache_status_ = CacheStatus::kReady;
    return;
  }

  search_tag_registry_->Update(concepts, base::DoNothing());
}

mojom::SearchResultPtr SearchHandler::ResultToSearchResult(
    const local_search_service::Result& result) const {
  const auto& metadata = search_tag_registry_->GetTagMetadata(result.id);
  // This should not happen because there isn't a way to remove metadata.
  if (&metadata == &SearchTagRegistry::not_found_) {
    return nullptr;
  }

  // Empty locale because we assume the locale always matches the system locale.
  return mojom::SearchResult::New(
      /*id=*/result.id,
      /*title=*/metadata.title,
      /*main_category=*/metadata.main_category,
      /*url_path_with_parameters=*/metadata.url_path_with_parameters,
      /*locale=*/"",
      /*relevance_score=*/result.score);
}

}  // namespace ash::help_app