// 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