mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-28 17:03:38 +00:00
To expand with full Organic Maps and Maps.ME commits history run: git remote add om-historic [om-historic.git repo url] git fetch --tags om-historic git replace squashed-history historic-commits
391 lines
12 KiB
C++
391 lines
12 KiB
C++
#include "search/search_quality/helpers.hpp"
|
|
|
|
#include "search/search_tests_support/test_search_engine.hpp"
|
|
#include "search/search_tests_support/test_search_request.hpp"
|
|
|
|
#include "search/ranking_info.hpp"
|
|
#include "search/result.hpp"
|
|
#include "search/search_params.hpp"
|
|
|
|
#include "indexer/classificator_loader.hpp"
|
|
#include "indexer/data_source.hpp"
|
|
#include "indexer/mwm_set.hpp"
|
|
|
|
#include "platform/platform_tests_support/helpers.hpp"
|
|
|
|
#include "platform/country_file.hpp"
|
|
#include "platform/platform.hpp"
|
|
|
|
#include "geometry/mercator.hpp"
|
|
#include "geometry/point2d.hpp"
|
|
|
|
#include "base/file_name_utils.hpp"
|
|
#include "base/logging.hpp"
|
|
#include "base/stl_helpers.hpp"
|
|
#include "base/string_utils.hpp"
|
|
#include "base/timer.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <gflags/gflags.h>
|
|
|
|
using namespace search::search_quality;
|
|
using namespace search::tests_support;
|
|
using namespace search;
|
|
using namespace std::chrono;
|
|
using namespace std;
|
|
|
|
DEFINE_string(data_path, "", "Path to data directory (resources dir)");
|
|
DEFINE_string(locale, "en", "Locale of all the search queries");
|
|
DEFINE_int32(num_threads, 1, "Number of search engine threads");
|
|
DEFINE_string(mwm_list_path, "",
|
|
"Path to a file containing the names of available mwms, one per line");
|
|
DEFINE_string(mwm_path, "", "Path to mwm files (writable dir)");
|
|
DEFINE_string(queries_path, "", "Path to the file with queries");
|
|
DEFINE_int32(top, 1, "Number of top results to show for every query");
|
|
DEFINE_string(viewport, "", "Viewport to use when searching (default, moscow, london, zurich)");
|
|
DEFINE_string(check_completeness, "", "Path to the file with completeness data");
|
|
DEFINE_string(ranking_csv_file, "", "File ranking info will be exported to");
|
|
|
|
string const kDefaultQueriesPathSuffix =
|
|
"/../search/search_quality/search_quality_tool/queries.txt";
|
|
string const kEmptyResult = "<empty>";
|
|
|
|
// Unlike strings::Tokenize, this function allows for empty tokens.
|
|
void Split(string const & s, char delim, vector<string> & parts)
|
|
{
|
|
istringstream iss(s);
|
|
string part;
|
|
while (getline(iss, part, delim))
|
|
parts.push_back(part);
|
|
}
|
|
|
|
struct CompletenessQuery
|
|
{
|
|
DECLARE_EXCEPTION(MalformedQueryException, RootException);
|
|
|
|
explicit CompletenessQuery(string && s)
|
|
{
|
|
s.append(" ");
|
|
|
|
vector<string> parts;
|
|
Split(s, ';', parts);
|
|
if (parts.size() != 7)
|
|
{
|
|
MYTHROW(MalformedQueryException,
|
|
("Can't split", s, ", found", parts.size(), "part(s):", parts));
|
|
}
|
|
|
|
auto const idx = parts[0].find(':');
|
|
if (idx == string::npos)
|
|
MYTHROW(MalformedQueryException, ("Could not find \':\':", s));
|
|
|
|
string mwmName = parts[0].substr(0, idx);
|
|
string const kMwmSuffix = ".mwm";
|
|
if (!mwmName.ends_with(kMwmSuffix))
|
|
MYTHROW(MalformedQueryException, ("Bad mwm name:", s));
|
|
|
|
string const featureIdStr = parts[0].substr(idx + 1);
|
|
uint64_t featureId;
|
|
if (!strings::to_uint64(featureIdStr, featureId))
|
|
MYTHROW(MalformedQueryException, ("Bad feature id:", s));
|
|
|
|
string const type = parts[1];
|
|
double lon, lat;
|
|
if (!strings::to_double(parts[2].c_str(), lon) || !strings::to_double(parts[3].c_str(), lat))
|
|
MYTHROW(MalformedQueryException, ("Bad lon-lat:", s));
|
|
|
|
string const city = parts[4];
|
|
string const street = parts[5];
|
|
string const house = parts[6];
|
|
|
|
mwmName = mwmName.substr(0, mwmName.size() - kMwmSuffix.size());
|
|
string country = mwmName;
|
|
replace(country.begin(), country.end(), '_', ' ');
|
|
|
|
m_query = country + " " + city + " " + street + " " + house + " ";
|
|
m_mwmName = mwmName;
|
|
m_featureId = static_cast<uint32_t>(featureId);
|
|
m_lat = lat;
|
|
m_lon = lon;
|
|
}
|
|
|
|
string m_query;
|
|
unique_ptr<TestSearchRequest> m_request;
|
|
string m_mwmName;
|
|
uint32_t m_featureId = 0;
|
|
double m_lat = 0;
|
|
double m_lon = 0;
|
|
};
|
|
|
|
string MakePrefixFree(string const & query) { return query + " "; }
|
|
|
|
// If n == 1, prints the query and the top result separated by a tab.
|
|
// Otherwise, prints the query on a separate line
|
|
// and then prints n top results on n lines starting with tabs.
|
|
void PrintTopResults(string const & query, vector<Result> const & results, size_t n,
|
|
double elapsedSeconds)
|
|
{
|
|
cout << query;
|
|
char timeBuf[100];
|
|
snprintf(timeBuf, sizeof(timeBuf), "\t[%.3fs]", elapsedSeconds);
|
|
if (n > 1)
|
|
cout << timeBuf;
|
|
for (size_t i = 0; i < n; ++i)
|
|
{
|
|
if (n > 1)
|
|
cout << endl;
|
|
cout << "\t";
|
|
if (i < results.size())
|
|
// todo(@m) Print more information: coordinates, viewport, etc.
|
|
cout << results[i].GetString();
|
|
else
|
|
cout << kEmptyResult;
|
|
}
|
|
if (n == 1)
|
|
cout << timeBuf;
|
|
cout << endl;
|
|
}
|
|
|
|
void CalcStatistics(vector<double> const & a, double & avg, double & maximum, double & var,
|
|
double & stdDev)
|
|
{
|
|
avg = 0;
|
|
maximum = 0;
|
|
var = 0;
|
|
stdDev = 0;
|
|
|
|
for (auto const x : a)
|
|
{
|
|
avg += static_cast<double>(x);
|
|
maximum = max(maximum, static_cast<double>(x));
|
|
}
|
|
|
|
double n = static_cast<double>(a.size());
|
|
if (a.size() > 0)
|
|
avg /= n;
|
|
|
|
for (double const x : a)
|
|
var += base::Pow2(x - avg);
|
|
if (a.size() > 1)
|
|
var /= n - 1;
|
|
stdDev = sqrt(var);
|
|
}
|
|
|
|
// Returns the position of the result that is expected to be found by geocoder completeness
|
|
// tests in the |result| vector or -1 if it does not occur there.
|
|
int FindResult(DataSource & dataSource, string const & mwmName, uint32_t const featureId,
|
|
double const lat, double const lon, vector<Result> const & results)
|
|
{
|
|
CHECK_LESS_OR_EQUAL(results.size(), numeric_limits<int>::max(), ());
|
|
auto const mwmId = dataSource.GetMwmIdByCountryFile(platform::CountryFile(mwmName));
|
|
FeatureID const expectedFeatureId(mwmId, featureId);
|
|
for (size_t i = 0; i < results.size(); ++i)
|
|
{
|
|
auto const & r = results[i];
|
|
if (r.GetFeatureID() == expectedFeatureId)
|
|
return static_cast<int>(i);
|
|
}
|
|
|
|
// Another attempt. If the queries are stale, feature id is useless.
|
|
// However, some information may be recovered from (lat, lon).
|
|
double const kEps = 1e-2;
|
|
for (size_t i = 0; i < results.size(); ++i)
|
|
{
|
|
auto const & r = results[i];
|
|
if (r.HasPoint() &&
|
|
base::AlmostEqualAbs(r.GetFeatureCenter(), mercator::FromLatLon(lat, lon), kEps))
|
|
{
|
|
double const dist =
|
|
mercator::DistanceOnEarth(r.GetFeatureCenter(), mercator::FromLatLon(lat, lon));
|
|
LOG(LDEBUG, ("dist =", dist));
|
|
return static_cast<int>(i);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// Reads queries in the format
|
|
// CountryName.mwm:featureId;type;lon;lat;city;street;<housenumber or housename>
|
|
// from |path|, executes them against the |engine| with viewport set to |viewport|
|
|
// and reports the number of queries whose expected result is among the returned results.
|
|
// Exact feature id is expected, but a close enough (lat, lon) is good too.
|
|
void CheckCompleteness(string const & path, DataSource & dataSource, TestSearchEngine & engine,
|
|
m2::RectD const & viewport, string const & locale)
|
|
{
|
|
base::ScopedLogAbortLevelChanger const logAbortLevel(LCRITICAL);
|
|
|
|
ifstream stream(path.c_str());
|
|
CHECK(stream.is_open(), ("Can't open", path));
|
|
|
|
base::Timer timer;
|
|
|
|
uint32_t totalQueries = 0;
|
|
uint32_t malformedQueries = 0;
|
|
uint32_t expectedResultsFound = 0;
|
|
uint32_t expectedResultsTop1 = 0;
|
|
|
|
// todo(@m) Process the queries on the fly and do not keep them.
|
|
vector<CompletenessQuery> queries;
|
|
|
|
string line;
|
|
while (getline(stream, line))
|
|
{
|
|
++totalQueries;
|
|
try
|
|
{
|
|
CompletenessQuery q(std::move(line));
|
|
q.m_request =
|
|
make_unique<TestSearchRequest>(engine, q.m_query, locale, Mode::Everywhere, viewport);
|
|
queries.push_back(std::move(q));
|
|
}
|
|
catch (CompletenessQuery::MalformedQueryException & e)
|
|
{
|
|
LOG(LERROR, (e.what()));
|
|
++malformedQueries;
|
|
}
|
|
}
|
|
|
|
for (auto & q : queries)
|
|
{
|
|
q.m_request->Run();
|
|
|
|
LOG(LDEBUG, (q.m_query, q.m_request->Results()));
|
|
int pos = FindResult(dataSource, q.m_mwmName, q.m_featureId, q.m_lat, q.m_lon,
|
|
q.m_request->Results());
|
|
if (pos >= 0)
|
|
++expectedResultsFound;
|
|
if (pos == 0)
|
|
++expectedResultsTop1;
|
|
}
|
|
|
|
double const expectedResultsFoundPercentage =
|
|
totalQueries == 0
|
|
? 0
|
|
: 100.0 * static_cast<double>(expectedResultsFound) / static_cast<double>(totalQueries);
|
|
double const expectedResultsTop1Percentage =
|
|
totalQueries == 0
|
|
? 0
|
|
: 100.0 * static_cast<double>(expectedResultsTop1) / static_cast<double>(totalQueries);
|
|
|
|
cout << "Time spent on checking completeness: " << timer.ElapsedSeconds() << "s." << endl;
|
|
cout << "Total queries: " << totalQueries << endl;
|
|
cout << "Malformed queries: " << malformedQueries << endl;
|
|
cout << "Expected results found: " << expectedResultsFound << " ("
|
|
<< expectedResultsFoundPercentage << "%)." << endl;
|
|
cout << "Expected results found in the top1 slot: " << expectedResultsTop1 << " ("
|
|
<< expectedResultsTop1Percentage << "%)." << endl;
|
|
}
|
|
|
|
void RunRequests(TestSearchEngine & engine, m2::RectD const & viewport, string queriesPath,
|
|
string const & locale, string const & rankingCSVFile, size_t top)
|
|
{
|
|
vector<string> queries;
|
|
{
|
|
if (queriesPath.empty())
|
|
queriesPath = base::JoinPath(GetPlatform().WritableDir(), kDefaultQueriesPathSuffix);
|
|
ReadStringsFromFile(queriesPath, queries);
|
|
}
|
|
|
|
vector<unique_ptr<TestSearchRequest>> requests;
|
|
for (size_t i = 0; i < queries.size(); ++i)
|
|
{
|
|
// todo(@m) Add a bool flag to search with prefixes?
|
|
requests.emplace_back(make_unique<TestSearchRequest>(engine, MakePrefixFree(queries[i]), locale,
|
|
Mode::Everywhere, viewport));
|
|
}
|
|
|
|
ofstream csv;
|
|
bool dumpCSV = false;
|
|
if (!rankingCSVFile.empty())
|
|
{
|
|
csv.open(rankingCSVFile);
|
|
if (!csv.is_open())
|
|
LOG(LERROR, ("Can't open file for CSV dump:", rankingCSVFile));
|
|
else
|
|
dumpCSV = true;
|
|
}
|
|
|
|
if (dumpCSV)
|
|
{
|
|
RankingInfo::PrintCSVHeader(csv);
|
|
csv << endl;
|
|
}
|
|
|
|
vector<double> responseTimes(queries.size());
|
|
for (size_t i = 0; i < queries.size(); ++i)
|
|
{
|
|
requests[i]->Run();
|
|
auto rt = duration_cast<milliseconds>(requests[i]->ResponseTime()).count();
|
|
responseTimes[i] = static_cast<double>(rt) / 1000;
|
|
PrintTopResults(MakePrefixFree(queries[i]), requests[i]->Results(), top, responseTimes[i]);
|
|
|
|
if (dumpCSV)
|
|
{
|
|
for (auto const & result : requests[i]->Results())
|
|
{
|
|
result.GetRankingInfo().ToCSV(csv);
|
|
csv << endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
double averageTime;
|
|
double maxTime;
|
|
double varianceTime;
|
|
double stdDevTime;
|
|
CalcStatistics(responseTimes, averageTime, maxTime, varianceTime, stdDevTime);
|
|
|
|
cout << fixed << setprecision(3);
|
|
cout << endl;
|
|
cout << "Maximum response time: " << maxTime << "s" << endl;
|
|
cout << "Average response time: " << averageTime << "s"
|
|
<< " (std. dev. " << stdDevTime << "s)" << endl;
|
|
}
|
|
|
|
int main(int argc, char * argv[])
|
|
{
|
|
platform::tests_support::ChangeMaxNumberOfOpenFiles(kMaxOpenFiles);
|
|
CheckLocale();
|
|
|
|
gflags::SetUsageMessage("Search quality tests.");
|
|
gflags::ParseCommandLineFlags(&argc, &argv, true);
|
|
|
|
SetPlatformDirs(FLAGS_data_path, FLAGS_mwm_path);
|
|
|
|
classificator::Load();
|
|
|
|
FrozenDataSource dataSource;
|
|
InitDataSource(dataSource, FLAGS_mwm_list_path);
|
|
|
|
auto engine = InitSearchEngine(dataSource, FLAGS_locale, FLAGS_num_threads);
|
|
engine->InitAffiliations();
|
|
|
|
m2::RectD viewport;
|
|
InitViewport(FLAGS_viewport, viewport);
|
|
|
|
ios_base::sync_with_stdio(false);
|
|
|
|
if (!FLAGS_check_completeness.empty())
|
|
{
|
|
CheckCompleteness(FLAGS_check_completeness, dataSource, *engine, viewport, FLAGS_locale);
|
|
return 0;
|
|
}
|
|
|
|
RunRequests(*engine, viewport, FLAGS_queries_path, FLAGS_locale, FLAGS_ranking_csv_file,
|
|
static_cast<size_t>(FLAGS_top));
|
|
return 0;
|
|
}
|