Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
30
libs/search/search_quality/CMakeLists.txt
Normal file
30
libs/search/search_quality/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
project(search_quality)
|
||||
|
||||
set(SRC
|
||||
helpers.cpp
|
||||
helpers.hpp
|
||||
helpers_json.cpp
|
||||
helpers_json.hpp
|
||||
matcher.cpp
|
||||
matcher.hpp
|
||||
sample.cpp
|
||||
sample.hpp
|
||||
)
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_tests_support # TestSearchEngine in helpers.cpp
|
||||
search
|
||||
indexer
|
||||
)
|
||||
|
||||
if (NOT SKIP_QT_GUI)
|
||||
omim_add_tool_subdirectory(assessment_tool)
|
||||
endif()
|
||||
|
||||
omim_add_tool_subdirectory(features_collector_tool)
|
||||
omim_add_tool_subdirectory(samples_generation_tool)
|
||||
omim_add_tool_subdirectory(search_quality_tool)
|
||||
|
||||
omim_add_test_subdirectory(search_quality_tests)
|
||||
56
libs/search/search_quality/README.txt
Normal file
56
libs/search/search_quality/README.txt
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
This document describes how to use the tools for search quality analysis.
|
||||
|
||||
|
||||
1. Prerequisites.
|
||||
|
||||
* Get the latest version of the project (https://codeberg.org/comaps/comaps) and build it.
|
||||
|
||||
* Get the latest samples.lisp file with search queries. If you don't know
|
||||
how to get it, please, contact the search team.
|
||||
|
||||
* Install Common Lisp. Note that there are many implementations,
|
||||
but we recommend to use SBCL (http://www.sbcl.org/).
|
||||
|
||||
* Install Python 3.x and packages for data analysis (sklearn, scipy, numpy, pandas, matplotlib).
|
||||
|
||||
* Download maps necessary for search quality tests.
|
||||
For example:
|
||||
|
||||
./download-maps.sh -v 160524
|
||||
|
||||
will download all necessary maps of version 160524 to the current directory.
|
||||
|
||||
|
||||
2. This section describes how to run search engine on a set of search
|
||||
queries and how to get a CSV file with search engine output.
|
||||
|
||||
i) Run gen-samples.lisp script to get search queries with lists of
|
||||
vital or relevant responses in JSON format. For example:
|
||||
|
||||
./gen-samples.lisp < samples.lisp > samples.jsonl
|
||||
|
||||
ii) Run features_collector_tool from the build directory.
|
||||
For example:
|
||||
|
||||
features_collector_tool --mwm_path path-to-downloaded-maps \
|
||||
--json_in samples.jsonl \
|
||||
--stats_path /tmp/stats.txt \
|
||||
2>/dev/null >samples.csv
|
||||
|
||||
runs search engine on all queries from samples.jsonl, prints
|
||||
useful info to /tmp/stats.txt and generates a CSV file with
|
||||
search engine output on each query.
|
||||
|
||||
The resulting CSV file is ready for analysis, i.e. for search
|
||||
quality evaluation, ranking models learning etc. For details,
|
||||
take a look at scoring_model.py script.
|
||||
|
||||
iii) To take a quick look at what the search returns without
|
||||
launching the application, consider using search_quality_tool:
|
||||
|
||||
search_quality_tool --viewport=moscow \
|
||||
--queries_path=path-to-omim/search/search_quality/search_quality_tool/queries.txt
|
||||
--top 1 \
|
||||
2>/dev/null
|
||||
|
||||
By default, map files in path-to-omim/data are used.
|
||||
49
libs/search/search_quality/assessment_tool/CMakeLists.txt
Normal file
49
libs/search/search_quality/assessment_tool/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
project(assessment_tool)
|
||||
|
||||
set(SRC
|
||||
assessment_tool.cpp
|
||||
context.cpp
|
||||
context.hpp
|
||||
edits.cpp
|
||||
edits.hpp
|
||||
feature_info_dialog.cpp
|
||||
feature_info_dialog.hpp
|
||||
helpers.cpp
|
||||
helpers.hpp
|
||||
main_model.cpp
|
||||
main_model.hpp
|
||||
main_view.cpp
|
||||
main_view.hpp
|
||||
model.hpp
|
||||
result_view.cpp
|
||||
result_view.hpp
|
||||
results_view.cpp
|
||||
results_view.hpp
|
||||
sample_view.cpp
|
||||
sample_view.hpp
|
||||
samples_view.cpp
|
||||
samples_view.hpp
|
||||
search_request_runner.cpp
|
||||
search_request_runner.hpp
|
||||
view.hpp
|
||||
)
|
||||
|
||||
#omim_add_executable(${PROJECT_NAME} MACOSX_BUNDLE ${SRC})
|
||||
omim_add_executable(${PROJECT_NAME} ${SRC})
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES AUTOMOC ON)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_quality
|
||||
qt_common
|
||||
map
|
||||
gflags::gflags
|
||||
)
|
||||
|
||||
# if (PLATFORM_MAC)
|
||||
# set_target_properties(
|
||||
# ${PROJECT_NAME}
|
||||
# PROPERTIES
|
||||
# MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/Info.plist
|
||||
# )
|
||||
# endif()
|
||||
11
libs/search/search_quality/assessment_tool/Info.plist
Normal file
11
libs/search/search_quality/assessment_tool/Info.plist
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<plist>
|
||||
<dict>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>True</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
#include "main_model.hpp"
|
||||
#include "main_view.hpp"
|
||||
|
||||
#include "qt/qt_common/helpers.hpp"
|
||||
|
||||
#include "map/framework.hpp"
|
||||
|
||||
#include "search/search_quality/helpers.hpp"
|
||||
|
||||
#include "platform/platform_tests_support/helpers.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
DEFINE_string(resources_path, "", "Path to resources directory");
|
||||
DEFINE_string(data_path, "", "Path to data directory");
|
||||
DEFINE_string(samples_path, "", "Path to the file with samples to open on startup");
|
||||
DEFINE_uint64(num_threads, 4, "Number of search engine threads");
|
||||
|
||||
int main(int argc, char ** argv)
|
||||
{
|
||||
platform::tests_support::ChangeMaxNumberOfOpenFiles(search::search_quality::kMaxOpenFiles);
|
||||
|
||||
gflags::SetUsageMessage("Assessment tool.");
|
||||
gflags::ParseCommandLineFlags(&argc, &argv, true);
|
||||
|
||||
Platform & platform = GetPlatform();
|
||||
if (!FLAGS_resources_path.empty())
|
||||
platform.SetResourceDir(FLAGS_resources_path);
|
||||
if (!FLAGS_data_path.empty())
|
||||
platform.SetWritableDirForTests(FLAGS_data_path);
|
||||
|
||||
Q_INIT_RESOURCE(resources_common);
|
||||
QApplication app(argc, argv);
|
||||
|
||||
qt::common::SetDefaultSurfaceFormat(app.platformName());
|
||||
|
||||
FrameworkParams params;
|
||||
CHECK_GREATER(FLAGS_num_threads, 0, ());
|
||||
params.m_numSearchAPIThreads = FLAGS_num_threads;
|
||||
|
||||
Framework framework(params);
|
||||
MainView view(framework, app.primaryScreen()->geometry());
|
||||
MainModel model(framework);
|
||||
|
||||
model.SetView(view);
|
||||
view.SetModel(model);
|
||||
|
||||
view.showMaximized();
|
||||
|
||||
if (!FLAGS_samples_path.empty())
|
||||
model.Open(FLAGS_samples_path);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
181
libs/search/search_quality/assessment_tool/context.cpp
Normal file
181
libs/search/search_quality/assessment_tool/context.cpp
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
#include "search/search_quality/assessment_tool/context.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
#include "search/search_quality/matcher.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// Context -----------------------------------------------------------------------------------------
|
||||
void Context::Clear()
|
||||
{
|
||||
m_goldenMatching.clear();
|
||||
m_actualMatching.clear();
|
||||
|
||||
m_foundResults.Clear();
|
||||
m_foundResultsEdits.Clear();
|
||||
|
||||
m_nonFoundResults.clear();
|
||||
m_nonFoundResultsEdits.Clear();
|
||||
|
||||
m_sampleEdits.Clear();
|
||||
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
void Context::LoadFromSample(search::Sample const & sample)
|
||||
{
|
||||
Clear();
|
||||
m_sample = sample;
|
||||
m_sampleEdits.Reset(sample.m_useless);
|
||||
}
|
||||
|
||||
search::Sample Context::MakeSample(search::FeatureLoader & loader) const
|
||||
{
|
||||
search::Sample outSample = m_sample;
|
||||
|
||||
if (!m_initialized)
|
||||
return outSample;
|
||||
|
||||
auto const & foundEntries = m_foundResultsEdits.GetEntries();
|
||||
auto const & nonFoundEntries = m_nonFoundResultsEdits.GetEntries();
|
||||
|
||||
auto & outResults = outSample.m_results;
|
||||
outResults.clear();
|
||||
|
||||
CHECK_EQUAL(m_goldenMatching.size(), m_sample.m_results.size(), ());
|
||||
CHECK_EQUAL(m_actualMatching.size(), foundEntries.size(), ());
|
||||
CHECK_EQUAL(m_actualMatching.size(), m_foundResults.GetCount(), ());
|
||||
|
||||
// Iterates over original (loaded from the file with search samples)
|
||||
// results first.
|
||||
size_t k = 0;
|
||||
for (size_t i = 0; i < m_sample.m_results.size(); ++i)
|
||||
{
|
||||
auto const j = m_goldenMatching[i];
|
||||
|
||||
if (j == search::Matcher::kInvalidId)
|
||||
{
|
||||
auto const & entry = nonFoundEntries[k++];
|
||||
auto const deleted = entry.m_deleted;
|
||||
auto const & curr = entry.m_currRelevance;
|
||||
if (!deleted && curr)
|
||||
{
|
||||
auto result = m_sample.m_results[i];
|
||||
result.m_relevance = *curr;
|
||||
outResults.push_back(result);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundEntries[j].m_currRelevance)
|
||||
continue;
|
||||
|
||||
auto result = m_sample.m_results[i];
|
||||
result.m_relevance = *foundEntries[j].m_currRelevance;
|
||||
outResults.push_back(std::move(result));
|
||||
}
|
||||
|
||||
// Iterates over results retrieved during assessment.
|
||||
for (size_t i = 0; i < m_foundResults.GetCount(); ++i)
|
||||
{
|
||||
auto const j = m_actualMatching[i];
|
||||
if (j != search::Matcher::kInvalidId)
|
||||
{
|
||||
// This result was processed by the loop above.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!foundEntries[i].m_currRelevance)
|
||||
continue;
|
||||
|
||||
auto const & result = m_foundResults[i];
|
||||
// No need in non-feature results.
|
||||
if (result.GetResultType() != search::Result::Type::Feature)
|
||||
continue;
|
||||
|
||||
auto ft = loader.Load(result.GetFeatureID());
|
||||
CHECK(ft, ());
|
||||
outResults.push_back(search::Sample::Result::Build(*ft, *foundEntries[i].m_currRelevance));
|
||||
}
|
||||
|
||||
outSample.m_useless = m_sampleEdits.m_currUseless;
|
||||
|
||||
return outSample;
|
||||
}
|
||||
|
||||
void Context::ApplyEdits()
|
||||
{
|
||||
if (!m_initialized)
|
||||
return;
|
||||
m_foundResultsEdits.Apply();
|
||||
m_nonFoundResultsEdits.Apply();
|
||||
m_sampleEdits.Apply();
|
||||
}
|
||||
|
||||
// ContextList -------------------------------------------------------------------------------------
|
||||
ContextList::ContextList(OnResultsUpdate onResultsUpdate, OnResultsUpdate onNonFoundResultsUpdate,
|
||||
OnSampleUpdate onSampleUpdate)
|
||||
: m_onResultsUpdate(onResultsUpdate)
|
||||
, m_onNonFoundResultsUpdate(onNonFoundResultsUpdate)
|
||||
, m_onSampleUpdate(onSampleUpdate)
|
||||
{}
|
||||
|
||||
void ContextList::Resize(size_t size)
|
||||
{
|
||||
size_t const oldSize = m_contexts.size();
|
||||
|
||||
for (size_t i = size; i < oldSize; ++i)
|
||||
m_contexts[i].Clear();
|
||||
if (size < m_contexts.size())
|
||||
m_contexts.erase(m_contexts.begin() + size, m_contexts.end());
|
||||
|
||||
m_hasChanges.resize(size);
|
||||
for (size_t i = oldSize; i < size; ++i)
|
||||
{
|
||||
m_contexts.emplace_back(
|
||||
[this, i](ResultsEdits::Update const & update)
|
||||
{
|
||||
OnContextUpdated(i);
|
||||
if (m_onResultsUpdate)
|
||||
m_onResultsUpdate(i, update);
|
||||
},
|
||||
[this, i](ResultsEdits::Update const & update)
|
||||
{
|
||||
OnContextUpdated(i);
|
||||
if (m_onNonFoundResultsUpdate)
|
||||
m_onNonFoundResultsUpdate(i, update);
|
||||
}, [this, i]()
|
||||
{
|
||||
OnContextUpdated(i);
|
||||
if (m_onSampleUpdate)
|
||||
m_onSampleUpdate(i);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
vector<search::Sample> ContextList::MakeSamples(search::FeatureLoader & loader) const
|
||||
{
|
||||
vector<search::Sample> samples;
|
||||
for (auto const & context : m_contexts)
|
||||
samples.push_back(context.MakeSample(loader));
|
||||
return samples;
|
||||
}
|
||||
|
||||
void ContextList::ApplyEdits()
|
||||
{
|
||||
for (auto & context : m_contexts)
|
||||
context.ApplyEdits();
|
||||
}
|
||||
|
||||
void ContextList::OnContextUpdated(size_t index)
|
||||
{
|
||||
if (!m_hasChanges[index] && m_contexts[index].HasChanges())
|
||||
++m_numChanges;
|
||||
if (m_hasChanges[index] && !m_contexts[index].HasChanges())
|
||||
--m_numChanges;
|
||||
m_hasChanges[index] = m_contexts[index].HasChanges();
|
||||
}
|
||||
139
libs/search/search_quality/assessment_tool/context.hpp
Normal file
139
libs/search/search_quality/assessment_tool/context.hpp
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/assessment_tool/edits.hpp"
|
||||
#include "search/search_quality/matcher.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace search
|
||||
{
|
||||
class FeatureLoader;
|
||||
}
|
||||
|
||||
struct Context
|
||||
{
|
||||
enum class SearchState
|
||||
{
|
||||
Untouched,
|
||||
InQueue,
|
||||
Completed
|
||||
};
|
||||
|
||||
Context(ResultsEdits::OnUpdate onFoundResultsUpdate, ResultsEdits::OnUpdate onNonFoundResultsUpdate,
|
||||
SampleEdits::OnUpdate onSampleUpdate)
|
||||
: m_foundResultsEdits(onFoundResultsUpdate)
|
||||
, m_nonFoundResultsEdits(onNonFoundResultsUpdate)
|
||||
, m_sampleEdits(onSampleUpdate)
|
||||
{}
|
||||
|
||||
void AddNonFoundResult(search::Sample::Result const & result)
|
||||
{
|
||||
CHECK_EQUAL(m_goldenMatching.size(), m_sample.m_results.size(), ());
|
||||
|
||||
m_sample.m_results.push_back(result);
|
||||
m_goldenMatching.push_back(search::Matcher::kInvalidId);
|
||||
|
||||
m_nonFoundResults.push_back(result);
|
||||
m_nonFoundResultsEdits.Add(result.m_relevance);
|
||||
}
|
||||
|
||||
bool IsUseless() const { return m_sampleEdits.m_currUseless; }
|
||||
|
||||
bool HasChanges() const
|
||||
{
|
||||
if (m_sampleEdits.HasChanges())
|
||||
return true;
|
||||
if (!m_initialized)
|
||||
return false;
|
||||
return m_foundResultsEdits.HasChanges() || m_nonFoundResultsEdits.HasChanges();
|
||||
}
|
||||
|
||||
void Clear();
|
||||
|
||||
void LoadFromSample(search::Sample const & sample);
|
||||
|
||||
// Makes sample in accordance with uncommited edits.
|
||||
search::Sample MakeSample(search::FeatureLoader & loader) const;
|
||||
|
||||
// Commits all edits.
|
||||
void ApplyEdits();
|
||||
|
||||
search::Sample m_sample;
|
||||
search::Results m_foundResults;
|
||||
ResultsEdits m_foundResultsEdits;
|
||||
|
||||
std::vector<size_t> m_goldenMatching;
|
||||
std::vector<size_t> m_actualMatching;
|
||||
|
||||
std::vector<search::Sample::Result> m_nonFoundResults;
|
||||
ResultsEdits m_nonFoundResultsEdits;
|
||||
|
||||
SampleEdits m_sampleEdits;
|
||||
|
||||
SearchState m_searchState = SearchState::Untouched;
|
||||
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
class ContextList
|
||||
{
|
||||
public:
|
||||
class SamplesSlice
|
||||
{
|
||||
public:
|
||||
SamplesSlice() = default;
|
||||
explicit SamplesSlice(ContextList const & contexts) : m_contexts(&contexts) {}
|
||||
|
||||
bool IsValid() const { return m_contexts != nullptr; }
|
||||
|
||||
std::string GetLabel(size_t index) const { return strings::ToUtf8((*m_contexts)[index].m_sample.m_query); }
|
||||
|
||||
bool IsChanged(size_t index) const { return (*m_contexts)[index].HasChanges(); }
|
||||
|
||||
Context::SearchState GetSearchState(size_t index) const { return (*m_contexts)[index].m_searchState; }
|
||||
|
||||
bool IsUseless(size_t index) const { return (*m_contexts)[index].m_sampleEdits.m_currUseless; }
|
||||
|
||||
size_t Size() const { return m_contexts->Size(); }
|
||||
|
||||
private:
|
||||
ContextList const * m_contexts = nullptr;
|
||||
};
|
||||
|
||||
using OnResultsUpdate = std::function<void(size_t index, ResultsEdits::Update const & update)>;
|
||||
using OnSampleUpdate = std::function<void(size_t index)>;
|
||||
|
||||
ContextList(OnResultsUpdate onResultsUpdate, OnResultsUpdate onNonFoundResultsUpdate, OnSampleUpdate onSampleUpdate);
|
||||
|
||||
void Resize(size_t size);
|
||||
size_t Size() const { return m_contexts.size(); }
|
||||
|
||||
Context & operator[](size_t i) { return m_contexts[i]; }
|
||||
Context const & operator[](size_t i) const { return m_contexts[i]; }
|
||||
|
||||
bool HasChanges() const { return m_numChanges != 0; }
|
||||
|
||||
// Generates search samples in accordance with uncommited edits.
|
||||
std::vector<search::Sample> MakeSamples(search::FeatureLoader & loader) const;
|
||||
|
||||
// Commits all edits.
|
||||
void ApplyEdits();
|
||||
|
||||
private:
|
||||
void OnContextUpdated(size_t index);
|
||||
|
||||
std::vector<Context> m_contexts;
|
||||
std::vector<bool> m_hasChanges;
|
||||
size_t m_numChanges = 0;
|
||||
|
||||
OnResultsUpdate m_onResultsUpdate;
|
||||
OnResultsUpdate m_onNonFoundResultsUpdate;
|
||||
OnSampleUpdate m_onSampleUpdate;
|
||||
};
|
||||
213
libs/search/search_quality/assessment_tool/edits.cpp
Normal file
213
libs/search/search_quality/assessment_tool/edits.cpp
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
#include "search/search_quality/assessment_tool/edits.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
void UpdateNumEdits(ResultsEdits::Entry const & entry, ResultsEdits::Relevance const & r, size_t & numEdits)
|
||||
{
|
||||
if (entry.m_currRelevance != entry.m_origRelevance && r == entry.m_origRelevance)
|
||||
{
|
||||
CHECK_GREATER(numEdits, 0, ());
|
||||
--numEdits;
|
||||
}
|
||||
if (entry.m_currRelevance == entry.m_origRelevance && r != entry.m_origRelevance)
|
||||
++numEdits;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// SampleEdits -------------------------------------------------------------------------------------
|
||||
void SampleEdits::Reset(bool origUseless)
|
||||
{
|
||||
m_origUseless = origUseless;
|
||||
m_currUseless = origUseless;
|
||||
}
|
||||
|
||||
void SampleEdits::FlipUsefulness()
|
||||
{
|
||||
m_currUseless ^= true;
|
||||
if (m_onUpdate)
|
||||
m_onUpdate();
|
||||
}
|
||||
|
||||
void SampleEdits::Apply()
|
||||
{
|
||||
m_origUseless = m_currUseless;
|
||||
if (m_onUpdate)
|
||||
m_onUpdate();
|
||||
}
|
||||
|
||||
// ResultsEdits::Editor ----------------------------------------------------------------------------
|
||||
ResultsEdits::Editor::Editor(ResultsEdits & parent, size_t index) : m_parent(parent), m_index(index) {}
|
||||
|
||||
bool ResultsEdits::Editor::Set(Relevance relevance)
|
||||
{
|
||||
return m_parent.SetRelevance(m_index, relevance);
|
||||
}
|
||||
|
||||
optional<ResultsEdits::Relevance> const & ResultsEdits::Editor::Get() const
|
||||
{
|
||||
return m_parent.Get(m_index).m_currRelevance;
|
||||
}
|
||||
|
||||
bool ResultsEdits::Editor::HasChanges() const
|
||||
{
|
||||
return m_parent.HasChanges(m_index);
|
||||
}
|
||||
|
||||
ResultsEdits::Entry::Type ResultsEdits::Editor::GetType() const
|
||||
{
|
||||
return m_parent.Get(m_index).m_type;
|
||||
}
|
||||
|
||||
// ResultsEdits ------------------------------------------------------------------------------------
|
||||
void ResultsEdits::Apply()
|
||||
{
|
||||
WithObserver(Update::MakeAll(), [this]()
|
||||
{
|
||||
for (auto & entry : m_entries)
|
||||
{
|
||||
entry.m_origRelevance = entry.m_currRelevance;
|
||||
entry.m_type = Entry::Type::Loaded;
|
||||
}
|
||||
m_numEdits = 0;
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsEdits::Reset(vector<optional<ResultsEdits::Relevance>> const & relevances)
|
||||
{
|
||||
WithObserver(Update::MakeAll(), [this, &relevances]()
|
||||
{
|
||||
m_entries.resize(relevances.size());
|
||||
for (size_t i = 0; i < m_entries.size(); ++i)
|
||||
{
|
||||
auto & entry = m_entries[i];
|
||||
entry.m_origRelevance = relevances[i];
|
||||
entry.m_currRelevance = relevances[i];
|
||||
entry.m_deleted = false;
|
||||
entry.m_type = Entry::Type::Loaded;
|
||||
}
|
||||
m_numEdits = 0;
|
||||
});
|
||||
}
|
||||
|
||||
bool ResultsEdits::SetRelevance(size_t index, Relevance relevance)
|
||||
{
|
||||
return WithObserver(Update::MakeSingle(index), [this, index, relevance]()
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
|
||||
auto & entry = m_entries[index];
|
||||
|
||||
UpdateNumEdits(entry, relevance, m_numEdits);
|
||||
|
||||
entry.m_currRelevance = relevance;
|
||||
return entry.m_currRelevance != entry.m_origRelevance;
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsEdits::SetAllRelevances(Relevance relevance)
|
||||
{
|
||||
WithObserver(Update::MakeAll(), [this, relevance]()
|
||||
{
|
||||
for (auto & entry : m_entries)
|
||||
{
|
||||
UpdateNumEdits(entry, relevance, m_numEdits);
|
||||
entry.m_currRelevance = relevance;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsEdits::Add(Relevance relevance)
|
||||
{
|
||||
auto const index = m_entries.size();
|
||||
WithObserver(Update::MakeAdd(index), [&]()
|
||||
{
|
||||
m_entries.emplace_back(relevance, Entry::Type::Created);
|
||||
++m_numEdits;
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsEdits::Delete(size_t index)
|
||||
{
|
||||
return WithObserver(Update::MakeDelete(index), [this, index]()
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
|
||||
auto & entry = m_entries[index];
|
||||
CHECK(!entry.m_deleted, ());
|
||||
entry.m_deleted = true;
|
||||
switch (entry.m_type)
|
||||
{
|
||||
case Entry::Type::Loaded: ++m_numEdits; break;
|
||||
case Entry::Type::Created: --m_numEdits; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsEdits::Resurrect(size_t index)
|
||||
{
|
||||
return WithObserver(Update::MakeResurrect(index), [this, index]()
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
|
||||
auto & entry = m_entries[index];
|
||||
CHECK(entry.m_deleted, ());
|
||||
CHECK_GREATER(m_numEdits, 0, ());
|
||||
entry.m_deleted = false;
|
||||
switch (entry.m_type)
|
||||
{
|
||||
case Entry::Type::Loaded: --m_numEdits; break;
|
||||
case Entry::Type::Created: ++m_numEdits; break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ResultsEdits::Entry & ResultsEdits::GetEntry(size_t index)
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
return m_entries[index];
|
||||
}
|
||||
|
||||
ResultsEdits::Entry const & ResultsEdits::GetEntry(size_t index) const
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
return m_entries[index];
|
||||
}
|
||||
|
||||
vector<optional<ResultsEdits::Relevance>> ResultsEdits::GetRelevances() const
|
||||
{
|
||||
vector<optional<ResultsEdits::Relevance>> relevances(m_entries.size());
|
||||
for (size_t i = 0; i < m_entries.size(); ++i)
|
||||
relevances[i] = m_entries[i].m_currRelevance;
|
||||
return relevances;
|
||||
}
|
||||
|
||||
ResultsEdits::Entry const & ResultsEdits::Get(size_t index) const
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
return m_entries[index];
|
||||
}
|
||||
|
||||
void ResultsEdits::Clear()
|
||||
{
|
||||
WithObserver(Update::MakeAll(), [this]()
|
||||
{
|
||||
m_entries.clear();
|
||||
m_numEdits = 0;
|
||||
});
|
||||
}
|
||||
|
||||
bool ResultsEdits::HasChanges() const
|
||||
{
|
||||
return m_numEdits != 0;
|
||||
}
|
||||
|
||||
bool ResultsEdits::HasChanges(size_t index) const
|
||||
{
|
||||
CHECK_LESS(index, m_entries.size(), ());
|
||||
auto const & entry = m_entries[index];
|
||||
return entry.m_currRelevance != entry.m_origRelevance;
|
||||
}
|
||||
153
libs/search/search_quality/assessment_tool/edits.hpp
Normal file
153
libs/search/search_quality/assessment_tool/edits.hpp
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "base/scope_guard.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
struct SampleEdits
|
||||
{
|
||||
using OnUpdate = std::function<void()>;
|
||||
|
||||
explicit SampleEdits(OnUpdate onUpdate) : m_onUpdate(onUpdate) {}
|
||||
|
||||
void Reset(bool origUseless);
|
||||
void FlipUsefulness();
|
||||
void Apply();
|
||||
bool HasChanges() const { return m_origUseless != m_currUseless; }
|
||||
void Clear() {}
|
||||
|
||||
bool m_origUseless = false;
|
||||
bool m_currUseless = false;
|
||||
|
||||
OnUpdate m_onUpdate;
|
||||
};
|
||||
|
||||
class ResultsEdits
|
||||
{
|
||||
public:
|
||||
using Relevance = search::Sample::Result::Relevance;
|
||||
|
||||
struct Entry
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
Loaded,
|
||||
Created
|
||||
};
|
||||
|
||||
Entry() = default;
|
||||
Entry(std::optional<Relevance> relevance, Type type)
|
||||
: m_currRelevance(relevance)
|
||||
, m_origRelevance(relevance)
|
||||
, m_type(type)
|
||||
{}
|
||||
|
||||
std::optional<Relevance> m_currRelevance = {};
|
||||
std::optional<Relevance> m_origRelevance = {};
|
||||
bool m_deleted = false;
|
||||
Type m_type = Type::Loaded;
|
||||
};
|
||||
|
||||
struct Update
|
||||
{
|
||||
static auto constexpr kInvalidIndex = std::numeric_limits<size_t>::max();
|
||||
|
||||
enum class Type
|
||||
{
|
||||
Single,
|
||||
All,
|
||||
Add,
|
||||
Delete,
|
||||
Resurrect
|
||||
};
|
||||
|
||||
Update() = default;
|
||||
Update(Type type, size_t index) : m_type(type), m_index(index) {}
|
||||
|
||||
static Update MakeAll() { return {}; }
|
||||
static Update MakeSingle(size_t index) { return {Type::Single, index}; }
|
||||
static Update MakeAdd(size_t index) { return {Type::Add, index}; }
|
||||
static Update MakeDelete(size_t index) { return {Type::Delete, index}; }
|
||||
static Update MakeResurrect(size_t index) { return {Type::Resurrect, index}; }
|
||||
|
||||
Type m_type = Type::All;
|
||||
size_t m_index = kInvalidIndex;
|
||||
};
|
||||
|
||||
using OnUpdate = std::function<void(Update const & update)>;
|
||||
|
||||
class Editor
|
||||
{
|
||||
public:
|
||||
Editor(ResultsEdits & parent, size_t index);
|
||||
|
||||
// Sets relevance to |relevance|. Returns true iff |relevance|
|
||||
// differs from the original one.
|
||||
bool Set(Relevance relevance);
|
||||
std::optional<Relevance> const & Get() const;
|
||||
bool HasChanges() const;
|
||||
Entry::Type GetType() const;
|
||||
|
||||
private:
|
||||
ResultsEdits & m_parent;
|
||||
size_t m_index = 0;
|
||||
};
|
||||
|
||||
explicit ResultsEdits(OnUpdate onUpdate) : m_onUpdate(onUpdate) {}
|
||||
|
||||
void Apply();
|
||||
void Reset(std::vector<std::optional<Relevance>> const & relevances);
|
||||
|
||||
// Sets relevance at |index| to |relevance|. Returns true iff
|
||||
// |relevance| differs from the original one.
|
||||
bool SetRelevance(size_t index, Relevance relevance);
|
||||
|
||||
// Sets relevances of all entries to |relevance|.
|
||||
void SetAllRelevances(Relevance relevance);
|
||||
|
||||
// Adds a new entry.
|
||||
void Add(Relevance relevance);
|
||||
|
||||
// Marks entry at |index| as deleted.
|
||||
void Delete(size_t index);
|
||||
|
||||
// Resurrects previously deleted entry at |index|.
|
||||
void Resurrect(size_t index);
|
||||
|
||||
std::vector<Entry> const & GetEntries() const { return m_entries; }
|
||||
Entry & GetEntry(size_t index);
|
||||
Entry const & GetEntry(size_t index) const;
|
||||
size_t NumEntries() const { return m_entries.size(); }
|
||||
std::vector<std::optional<Relevance>> GetRelevances() const;
|
||||
|
||||
Entry const & Get(size_t index) const;
|
||||
|
||||
void Clear();
|
||||
bool HasChanges() const;
|
||||
bool HasChanges(size_t index) const;
|
||||
|
||||
private:
|
||||
template <typename Fn>
|
||||
std::invoke_result_t<Fn> WithObserver(Update const & update, Fn && fn)
|
||||
{
|
||||
SCOPE_GUARD(obsCall, ([this, &update]()
|
||||
{
|
||||
if (m_onUpdate)
|
||||
m_onUpdate(update);
|
||||
}));
|
||||
return fn();
|
||||
}
|
||||
|
||||
std::vector<Entry> m_entries;
|
||||
|
||||
size_t m_numEdits = 0;
|
||||
|
||||
OnUpdate m_onUpdate;
|
||||
};
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
#include "search/search_quality/assessment_tool/feature_info_dialog.hpp"
|
||||
|
||||
#include "indexer/classificator.hpp"
|
||||
#include "indexer/map_object.hpp"
|
||||
|
||||
#include "coding/string_utf8_multilang.hpp"
|
||||
|
||||
#include "geometry/latlon.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtGui/QAction>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
QLabel * MakeSelectableLabel(string const & s)
|
||||
{
|
||||
auto * result = new QLabel(QString::fromStdString(s));
|
||||
result->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
FeatureInfoDialog::FeatureInfoDialog(QWidget * parent, osm::MapObject const & mapObject,
|
||||
search::ReverseGeocoder::Address const & address, string const & locale)
|
||||
: QDialog(parent)
|
||||
{
|
||||
auto * layout = new QGridLayout();
|
||||
|
||||
{
|
||||
auto const & id = mapObject.GetID();
|
||||
CHECK(id.IsValid(), ());
|
||||
|
||||
auto * label = new QLabel("id:");
|
||||
auto * content = MakeSelectableLabel(id.GetMwmName() + ", " + strings::to_string(id.m_index));
|
||||
|
||||
AddRow(*layout, label, content);
|
||||
}
|
||||
|
||||
{
|
||||
auto * label = new QLabel("lat lon:");
|
||||
auto const ll = mapObject.GetLatLon();
|
||||
auto const ss = strings::to_string_dac(ll.m_lat, 5) + " " + strings::to_string_dac(ll.m_lon, 5);
|
||||
auto * content = MakeSelectableLabel(ss);
|
||||
|
||||
AddRow(*layout, label, content);
|
||||
}
|
||||
|
||||
{
|
||||
int8_t const localeCode = StringUtf8Multilang::GetLangIndex(locale);
|
||||
vector<int8_t> codes = {{StringUtf8Multilang::kDefaultCode, StringUtf8Multilang::kEnglishCode}};
|
||||
if (localeCode != StringUtf8Multilang::kUnsupportedLanguageCode &&
|
||||
::find(codes.begin(), codes.end(), localeCode) == codes.end())
|
||||
{
|
||||
codes.push_back(localeCode);
|
||||
}
|
||||
|
||||
for (auto const & code : codes)
|
||||
{
|
||||
string_view name;
|
||||
if (!mapObject.GetNameMultilang().GetString(code, name))
|
||||
continue;
|
||||
|
||||
auto const lang = StringUtf8Multilang::GetLangByCode(code);
|
||||
CHECK(!lang.empty(), ("Can't find lang by code:", code));
|
||||
auto * label = new QLabel(QString::fromStdString(std::string{lang} + ":"));
|
||||
auto * content = MakeSelectableLabel(std::string{name});
|
||||
|
||||
AddRow(*layout, label, content);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto const & c = classif();
|
||||
|
||||
vector<string> types;
|
||||
for (auto type : mapObject.GetTypes())
|
||||
types.push_back(c.GetReadableObjectName(type));
|
||||
|
||||
if (!types.empty())
|
||||
{
|
||||
auto * label = new QLabel("types:");
|
||||
auto * content = MakeSelectableLabel(strings::JoinStrings(types, " " /* delimiter */));
|
||||
|
||||
AddRow(*layout, label, content);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto * label = new QLabel("address:");
|
||||
auto * content = MakeSelectableLabel(address.FormatAddress());
|
||||
|
||||
AddRow(*layout, label, content);
|
||||
}
|
||||
|
||||
auto * ok = new QPushButton("OK");
|
||||
ok->setDefault(true);
|
||||
connect(ok, &QPushButton::clicked, [&]() { accept(); });
|
||||
AddRow(*layout, ok);
|
||||
|
||||
setLayout(layout);
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/reverse_geocoder.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QGridLayout>
|
||||
|
||||
namespace osm
|
||||
{
|
||||
class MapObject;
|
||||
} // namespace osm
|
||||
|
||||
class FeatureInfoDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
FeatureInfoDialog(QWidget * parent, osm::MapObject const & mapObject,
|
||||
search::ReverseGeocoder::Address const & address, std::string const & locale);
|
||||
|
||||
private:
|
||||
void AddElems(QGridLayout & layout, int row, int col, QWidget * widget) { layout.addWidget(widget, row, col); }
|
||||
|
||||
template <typename... Args>
|
||||
void AddElems(QGridLayout & layout, int row, int col, QWidget * widget, Args *... args)
|
||||
{
|
||||
AddElems(layout, row, col, widget);
|
||||
AddElems(layout, row, col + 1, args...);
|
||||
}
|
||||
|
||||
template <typename... Args>
|
||||
void AddRow(QGridLayout & layout, Args *... args)
|
||||
{
|
||||
AddElems(layout, m_numRows /* row */, 0 /* col */, args...);
|
||||
++m_numRows;
|
||||
}
|
||||
|
||||
int m_numRows = 0;
|
||||
};
|
||||
13
libs/search/search_quality/assessment_tool/helpers.cpp
Normal file
13
libs/search/search_quality/assessment_tool/helpers.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#include "search/search_quality/assessment_tool/helpers.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
QString ToQString(strings::UniString const & s)
|
||||
{
|
||||
return QString::fromUtf8(strings::ToUtf8(s).c_str());
|
||||
}
|
||||
|
||||
QString ToQString(std::string const & s)
|
||||
{
|
||||
return QString::fromUtf8(s.c_str());
|
||||
}
|
||||
28
libs/search/search_quality/assessment_tool/helpers.hpp
Normal file
28
libs/search/search_quality/assessment_tool/helpers.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <QtCore/QString>
|
||||
|
||||
namespace strings
|
||||
{
|
||||
class UniString;
|
||||
}
|
||||
|
||||
QString ToQString(strings::UniString const & s);
|
||||
QString ToQString(std::string const & s);
|
||||
|
||||
template <typename Layout, typename... Args>
|
||||
Layout * BuildLayout(Args &&... args)
|
||||
{
|
||||
return new Layout(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <typename Layout, typename... Args>
|
||||
Layout * BuildLayoutWithoutMargins(Args &&... args)
|
||||
{
|
||||
auto * layout = BuildLayout<Layout>(std::forward<Args>(args)...);
|
||||
layout->setContentsMargins(0 /* left */, 0 /* top */, 0 /* right */, 0 /* bottom */);
|
||||
return layout;
|
||||
}
|
||||
387
libs/search/search_quality/assessment_tool/main_model.cpp
Normal file
387
libs/search/search_quality/assessment_tool/main_model.cpp
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
#include "main_model.hpp"
|
||||
#include "view.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
#include "search/search_params.hpp"
|
||||
#include "search/search_quality/helpers.hpp"
|
||||
#include "search/search_quality/matcher.hpp"
|
||||
|
||||
#include "map/framework.hpp"
|
||||
|
||||
#include "geometry/algorithm.hpp"
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
using namespace std;
|
||||
|
||||
// MainModel ---------------------------------------------------------------------------------------
|
||||
MainModel::MainModel(Framework & framework)
|
||||
: m_framework(framework)
|
||||
, m_dataSource(m_framework.GetDataSource())
|
||||
, m_loader(m_dataSource)
|
||||
, m_contexts([this](size_t sampleIndex, ResultsEdits::Update const & update)
|
||||
{ OnUpdate(View::ResultType::Found, sampleIndex, update); },
|
||||
[this](size_t sampleIndex, ResultsEdits::Update const & update)
|
||||
{ OnUpdate(View::ResultType::NonFound, sampleIndex, update); },
|
||||
[this](size_t sampleIndex) { OnSampleUpdate(sampleIndex); })
|
||||
, m_runner(m_framework, m_dataSource, m_contexts,
|
||||
[this](search::Results const & results) { UpdateViewOnResults(results); }, [this](size_t index)
|
||||
{
|
||||
// Only the first parameter matters because we only change SearchStatus.
|
||||
m_view->OnSampleChanged(index, false /* isUseless */, false /* hasEdits */);
|
||||
})
|
||||
{
|
||||
search::search_quality::CheckLocale();
|
||||
}
|
||||
|
||||
void MainModel::Open(string const & path)
|
||||
{
|
||||
CHECK(m_view, ());
|
||||
|
||||
string contents;
|
||||
|
||||
{
|
||||
ifstream ifs(path);
|
||||
if (!ifs)
|
||||
{
|
||||
m_view->ShowError("Can't open file: " + path);
|
||||
return;
|
||||
}
|
||||
contents.assign(istreambuf_iterator<char>(ifs), istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
vector<search::Sample> samples;
|
||||
if (!search::Sample::DeserializeFromJSONLines(contents, samples))
|
||||
{
|
||||
m_view->ShowError("Can't parse samples: " + path);
|
||||
return;
|
||||
}
|
||||
|
||||
m_runner.ResetForegroundSearch();
|
||||
m_runner.ResetBackgroundSearch();
|
||||
|
||||
m_view->Clear();
|
||||
|
||||
m_contexts.Resize(samples.size());
|
||||
for (size_t i = 0; i < samples.size(); ++i)
|
||||
m_contexts[i].LoadFromSample(samples[i]);
|
||||
|
||||
m_path = path;
|
||||
|
||||
m_view->SetSamples(ContextList::SamplesSlice(m_contexts));
|
||||
m_selectedSample = kInvalidIndex;
|
||||
}
|
||||
|
||||
void MainModel::Save()
|
||||
{
|
||||
CHECK(HasChanges(), ());
|
||||
SaveAs(m_path);
|
||||
}
|
||||
|
||||
void MainModel::SaveAs(string const & path)
|
||||
{
|
||||
CHECK(HasChanges(), ());
|
||||
CHECK(!path.empty(), ());
|
||||
|
||||
string contents;
|
||||
search::Sample::SerializeToJSONLines(m_contexts.MakeSamples(m_loader), contents);
|
||||
|
||||
{
|
||||
ofstream ofs(path);
|
||||
if (!ofs)
|
||||
{
|
||||
m_view->ShowError("Can't open file: " + path);
|
||||
return;
|
||||
}
|
||||
copy(contents.begin(), contents.end(), ostreambuf_iterator<char>(ofs));
|
||||
}
|
||||
|
||||
m_contexts.ApplyEdits();
|
||||
m_path = path;
|
||||
}
|
||||
|
||||
void MainModel::InitiateBackgroundSearch(size_t from, size_t to)
|
||||
{
|
||||
m_runner.InitiateBackgroundSearch(from, to);
|
||||
}
|
||||
|
||||
void MainModel::OnSampleSelected(int index)
|
||||
{
|
||||
CHECK(m_threadChecker.CalledOnOriginalThread(), ());
|
||||
|
||||
CHECK_GREATER_OR_EQUAL(index, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(index), m_contexts.Size(), ());
|
||||
CHECK(m_view, ());
|
||||
|
||||
m_selectedSample = index;
|
||||
|
||||
auto & context = m_contexts[index];
|
||||
auto const & sample = context.m_sample;
|
||||
|
||||
m_view->ShowSample(index, sample, sample.m_pos, context.IsUseless(), context.HasChanges());
|
||||
|
||||
m_runner.ResetForegroundSearch();
|
||||
|
||||
if (context.m_initialized)
|
||||
{
|
||||
UpdateViewOnResults(context.m_foundResults);
|
||||
return;
|
||||
}
|
||||
|
||||
InitiateForegroundSearch(index);
|
||||
}
|
||||
|
||||
void MainModel::OnResultSelected(int index)
|
||||
{
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
auto const & context = m_contexts[m_selectedSample];
|
||||
auto const & foundResults = context.m_foundResults;
|
||||
|
||||
CHECK_GREATER_OR_EQUAL(index, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(index), foundResults.GetCount(), ());
|
||||
m_view->MoveViewportToResult(foundResults[index]);
|
||||
}
|
||||
|
||||
void MainModel::OnNonFoundResultSelected(int index)
|
||||
{
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
auto const & context = m_contexts[m_selectedSample];
|
||||
auto const & results = context.m_nonFoundResults;
|
||||
|
||||
CHECK_GREATER_OR_EQUAL(index, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(index), results.size(), ());
|
||||
m_view->MoveViewportToResult(results[index]);
|
||||
}
|
||||
|
||||
void MainModel::OnShowViewportClicked()
|
||||
{
|
||||
CHECK(m_selectedSample != kInvalidIndex, ());
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
|
||||
auto const & context = m_contexts[m_selectedSample];
|
||||
m_view->MoveViewportToRect(context.m_sample.m_viewport);
|
||||
}
|
||||
|
||||
void MainModel::OnShowPositionClicked()
|
||||
{
|
||||
CHECK(m_selectedSample != kInvalidIndex, ());
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
|
||||
static int constexpr kViewportAroundTopResultsSizeM = 100;
|
||||
static double constexpr kViewportAroundTopResultsScale = 1.2;
|
||||
static size_t constexpr kMaxTopResults = 3;
|
||||
|
||||
auto const & context = m_contexts[m_selectedSample];
|
||||
|
||||
vector<m2::PointD> points;
|
||||
if (context.m_sample.m_pos)
|
||||
points.push_back(*context.m_sample.m_pos);
|
||||
|
||||
size_t resultsAdded = 0;
|
||||
for (auto const & result : context.m_foundResults)
|
||||
{
|
||||
if (!result.HasPoint())
|
||||
continue;
|
||||
|
||||
if (resultsAdded == kMaxTopResults)
|
||||
break;
|
||||
|
||||
points.push_back(result.GetFeatureCenter());
|
||||
++resultsAdded;
|
||||
}
|
||||
|
||||
CHECK(!points.empty(), ());
|
||||
auto boundingBox = m2::ApplyCalculator(points, m2::CalculateBoundingBox());
|
||||
boundingBox.Scale(kViewportAroundTopResultsScale);
|
||||
|
||||
auto const minRect = mercator::RectByCenterXYAndSizeInMeters(boundingBox.Center(), kViewportAroundTopResultsSizeM);
|
||||
m_view->MoveViewportToRect(m2::Add(boundingBox, minRect));
|
||||
}
|
||||
|
||||
void MainModel::OnMarkAllAsRelevantClicked()
|
||||
{
|
||||
OnChangeAllRelevancesClicked(ResultsEdits::Relevance::Relevant);
|
||||
}
|
||||
|
||||
void MainModel::OnMarkAllAsIrrelevantClicked()
|
||||
{
|
||||
OnChangeAllRelevancesClicked(ResultsEdits::Relevance::Irrelevant);
|
||||
}
|
||||
|
||||
bool MainModel::HasChanges()
|
||||
{
|
||||
return m_contexts.HasChanges();
|
||||
}
|
||||
|
||||
bool MainModel::AlreadyInSamples(FeatureID const & id)
|
||||
{
|
||||
CHECK(m_selectedSample != kInvalidIndex, ());
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
|
||||
bool found = false;
|
||||
ForAnyMatchingEntry(m_contexts[m_selectedSample], id, [&](ResultsEdits & edits, size_t index)
|
||||
{
|
||||
auto const & entry = edits.GetEntry(index);
|
||||
if (!entry.m_deleted)
|
||||
found = true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
void MainModel::AddNonFoundResult(FeatureID const & id)
|
||||
{
|
||||
CHECK(m_selectedSample != kInvalidIndex, ());
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
|
||||
auto & context = m_contexts[m_selectedSample];
|
||||
|
||||
bool resurrected = false;
|
||||
ForAnyMatchingEntry(context, id, [&](ResultsEdits & edits, size_t index)
|
||||
{
|
||||
auto const & entry = edits.GetEntry(index);
|
||||
CHECK(entry.m_deleted, ());
|
||||
edits.Resurrect(index);
|
||||
resurrected = true;
|
||||
});
|
||||
if (resurrected)
|
||||
return;
|
||||
|
||||
auto ft = m_loader.Load(id);
|
||||
CHECK(ft, ("Can't load feature:", id));
|
||||
auto const result = search::Sample::Result::Build(*ft, search::Sample::Result::Relevance::Vital);
|
||||
context.AddNonFoundResult(result);
|
||||
}
|
||||
|
||||
void MainModel::FlipSampleUsefulness(int index)
|
||||
{
|
||||
CHECK_EQUAL(m_selectedSample, index, ());
|
||||
|
||||
m_contexts[index].m_sampleEdits.FlipUsefulness();
|
||||
|
||||
// Don't bother with resetting search: we cannot tell whether
|
||||
// the sample is useless without its results anyway.
|
||||
}
|
||||
|
||||
void MainModel::InitiateForegroundSearch(size_t index)
|
||||
{
|
||||
auto & context = m_contexts[index];
|
||||
auto const & sample = context.m_sample;
|
||||
|
||||
m_view->ShowSample(index, sample, sample.m_pos, context.IsUseless(), context.HasChanges());
|
||||
m_runner.InitiateForegroundSearch(index);
|
||||
m_view->OnSearchStarted();
|
||||
}
|
||||
|
||||
void MainModel::OnUpdate(View::ResultType type, size_t sampleIndex, ResultsEdits::Update const & update)
|
||||
{
|
||||
using Type = ResultsEdits::Update::Type;
|
||||
|
||||
CHECK_LESS(sampleIndex, m_contexts.Size(), ());
|
||||
auto & context = m_contexts[sampleIndex];
|
||||
|
||||
if (update.m_type == Type::Add)
|
||||
{
|
||||
CHECK_EQUAL(type, View::ResultType::NonFound, ());
|
||||
m_view->ShowNonFoundResults(context.m_nonFoundResults, context.m_nonFoundResultsEdits.GetEntries());
|
||||
m_view->SetResultsEdits(m_selectedSample, context.m_foundResultsEdits, context.m_nonFoundResultsEdits);
|
||||
}
|
||||
|
||||
m_view->OnResultChanged(sampleIndex, type, update);
|
||||
m_view->OnSampleChanged(sampleIndex, context.IsUseless(), context.HasChanges());
|
||||
m_view->OnSamplesChanged(m_contexts.HasChanges());
|
||||
|
||||
if (update.m_type == Type::Add || update.m_type == Type::Resurrect || update.m_type == Type::Delete)
|
||||
{
|
||||
CHECK(context.m_initialized, ());
|
||||
CHECK_EQUAL(type, View::ResultType::NonFound, ());
|
||||
m_view->ShowMarks(context);
|
||||
}
|
||||
}
|
||||
|
||||
void MainModel::OnSampleUpdate(size_t sampleIndex)
|
||||
{
|
||||
auto & context = m_contexts[sampleIndex];
|
||||
|
||||
m_view->OnSampleChanged(sampleIndex, context.IsUseless(), context.HasChanges());
|
||||
m_view->OnSamplesChanged(m_contexts.HasChanges());
|
||||
}
|
||||
|
||||
void MainModel::UpdateViewOnResults(search::Results const & results)
|
||||
{
|
||||
CHECK(m_threadChecker.CalledOnOriginalThread(), ());
|
||||
|
||||
m_view->AddFoundResults(results);
|
||||
|
||||
if (!results.IsEndedNormal())
|
||||
return;
|
||||
|
||||
auto & context = m_contexts[m_selectedSample];
|
||||
|
||||
m_view->ShowNonFoundResults(context.m_nonFoundResults, context.m_nonFoundResultsEdits.GetEntries());
|
||||
m_view->ShowMarks(context);
|
||||
m_view->OnResultChanged(m_selectedSample, View::ResultType::Found, ResultsEdits::Update::MakeAll());
|
||||
m_view->OnResultChanged(m_selectedSample, View::ResultType::NonFound, ResultsEdits::Update::MakeAll());
|
||||
m_view->OnSampleChanged(m_selectedSample, context.IsUseless(), context.HasChanges());
|
||||
m_view->OnSamplesChanged(m_contexts.HasChanges());
|
||||
|
||||
m_view->SetResultsEdits(m_selectedSample, context.m_foundResultsEdits, context.m_nonFoundResultsEdits);
|
||||
m_view->OnSearchCompleted();
|
||||
}
|
||||
|
||||
void MainModel::OnChangeAllRelevancesClicked(ResultsEdits::Relevance relevance)
|
||||
{
|
||||
CHECK_GREATER_OR_EQUAL(m_selectedSample, 0, ());
|
||||
CHECK_LESS(static_cast<size_t>(m_selectedSample), m_contexts.Size(), ());
|
||||
auto & context = m_contexts[m_selectedSample];
|
||||
|
||||
context.m_foundResultsEdits.SetAllRelevances(relevance);
|
||||
context.m_nonFoundResultsEdits.SetAllRelevances(relevance);
|
||||
|
||||
m_view->OnResultChanged(m_selectedSample, View::ResultType::Found, ResultsEdits::Update::MakeAll());
|
||||
m_view->OnResultChanged(m_selectedSample, View::ResultType::NonFound, ResultsEdits::Update::MakeAll());
|
||||
m_view->OnSampleChanged(m_selectedSample, context.IsUseless(), context.HasChanges());
|
||||
m_view->OnSamplesChanged(m_contexts.HasChanges());
|
||||
}
|
||||
|
||||
template <typename Fn>
|
||||
void MainModel::ForAnyMatchingEntry(Context & context, FeatureID const & id, Fn && fn)
|
||||
{
|
||||
CHECK(context.m_initialized, ());
|
||||
|
||||
auto const & foundResults = context.m_foundResults;
|
||||
CHECK_EQUAL(foundResults.GetCount(), context.m_foundResultsEdits.NumEntries(), ());
|
||||
for (size_t i = 0; i < foundResults.GetCount(); ++i)
|
||||
{
|
||||
auto const & result = foundResults[i];
|
||||
if (result.GetResultType() != search::Result::Type::Feature)
|
||||
continue;
|
||||
if (result.GetFeatureID() == id)
|
||||
return fn(context.m_foundResultsEdits, i);
|
||||
}
|
||||
|
||||
auto ft = m_loader.Load(id);
|
||||
CHECK(ft, ("Can't load feature:", id));
|
||||
search::Matcher matcher(m_loader);
|
||||
|
||||
auto const & nonFoundResults = context.m_nonFoundResults;
|
||||
CHECK_EQUAL(nonFoundResults.size(), context.m_nonFoundResultsEdits.NumEntries(), ());
|
||||
for (size_t i = 0; i < nonFoundResults.size(); ++i)
|
||||
{
|
||||
auto const & result = context.m_nonFoundResults[i];
|
||||
if (matcher.Matches(context.m_sample.m_query, result, *ft))
|
||||
return fn(context.m_nonFoundResultsEdits, i);
|
||||
}
|
||||
}
|
||||
73
libs/search/search_quality/assessment_tool/main_model.hpp
Normal file
73
libs/search/search_quality/assessment_tool/main_model.hpp
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
#pragma once
|
||||
#include "context.hpp"
|
||||
#include "edits.hpp"
|
||||
#include "model.hpp"
|
||||
#include "search_request_runner.hpp"
|
||||
#include "view.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
|
||||
#include "base/thread_checker.hpp"
|
||||
|
||||
class Framework;
|
||||
class DataSource;
|
||||
|
||||
namespace search
|
||||
{
|
||||
class Results;
|
||||
}
|
||||
|
||||
class MainModel : public Model
|
||||
{
|
||||
public:
|
||||
explicit MainModel(Framework & framework);
|
||||
|
||||
// Model overrides:
|
||||
void Open(std::string const & path) override;
|
||||
void Save() override;
|
||||
void SaveAs(std::string const & path) override;
|
||||
void InitiateBackgroundSearch(size_t const from, size_t const to) override;
|
||||
|
||||
void OnSampleSelected(int index) override;
|
||||
void OnResultSelected(int index) override;
|
||||
void OnNonFoundResultSelected(int index) override;
|
||||
void OnShowViewportClicked() override;
|
||||
void OnShowPositionClicked() override;
|
||||
void OnMarkAllAsRelevantClicked() override;
|
||||
void OnMarkAllAsIrrelevantClicked() override;
|
||||
bool HasChanges() override;
|
||||
bool AlreadyInSamples(FeatureID const & id) override;
|
||||
void AddNonFoundResult(FeatureID const & id) override;
|
||||
void FlipSampleUsefulness(int index) override;
|
||||
|
||||
private:
|
||||
static int constexpr kInvalidIndex = -1;
|
||||
|
||||
void InitiateForegroundSearch(size_t index);
|
||||
|
||||
void OnUpdate(View::ResultType type, size_t sampleIndex, ResultsEdits::Update const & update);
|
||||
void OnSampleUpdate(size_t sampleIndex);
|
||||
|
||||
void UpdateViewOnResults(search::Results const & results);
|
||||
void ShowMarks(Context const & context);
|
||||
|
||||
void OnChangeAllRelevancesClicked(ResultsEdits::Relevance relevance);
|
||||
|
||||
template <typename Fn>
|
||||
void ForAnyMatchingEntry(Context & context, FeatureID const & id, Fn && fn);
|
||||
|
||||
Framework & m_framework;
|
||||
DataSource const & m_dataSource;
|
||||
search::FeatureLoader m_loader;
|
||||
|
||||
ContextList m_contexts;
|
||||
|
||||
// Path to the last file search samples were loaded from or saved to.
|
||||
std::string m_path;
|
||||
|
||||
int m_selectedSample = kInvalidIndex;
|
||||
|
||||
SearchRequestRunner m_runner;
|
||||
|
||||
ThreadChecker m_threadChecker;
|
||||
};
|
||||
516
libs/search/search_quality/assessment_tool/main_view.cpp
Normal file
516
libs/search/search_quality/assessment_tool/main_view.cpp
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
#include "main_view.hpp"
|
||||
|
||||
#include "feature_info_dialog.hpp"
|
||||
#include "helpers.hpp"
|
||||
#include "model.hpp"
|
||||
#include "results_view.hpp"
|
||||
#include "sample_view.hpp"
|
||||
#include "samples_view.hpp"
|
||||
|
||||
#include "qt/qt_common/map_widget.hpp"
|
||||
#include "qt/qt_common/scale_slider.hpp"
|
||||
|
||||
#include "map/framework.hpp"
|
||||
#include "map/place_page_info.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/checked_cast.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <limits>
|
||||
|
||||
#include <QtCore/Qt>
|
||||
#include <QtGui/QCloseEvent>
|
||||
#include <QtGui/QIntValidator>
|
||||
#include <QtGui/QKeySequence>
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QDialogButtonBox>
|
||||
#include <QtWidgets/QDockWidget>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QFormLayout>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QToolBar>
|
||||
|
||||
using Relevance = search::Sample::Result::Relevance;
|
||||
|
||||
namespace
|
||||
{
|
||||
char const kJSON[] = "JSON Lines files (*.jsonl)";
|
||||
} // namespace
|
||||
|
||||
MainView::MainView(Framework & framework, QRect const & screenGeometry) : m_framework(framework)
|
||||
{
|
||||
setGeometry(screenGeometry);
|
||||
|
||||
setWindowTitle(tr("Assessment tool"));
|
||||
InitMapWidget();
|
||||
InitDocks();
|
||||
InitMenuBar();
|
||||
|
||||
m_framework.SetPlacePageListeners([this]()
|
||||
{
|
||||
auto const & info = m_framework.GetCurrentPlacePageInfo();
|
||||
auto const & selectedFeature = info.GetID();
|
||||
if (!selectedFeature.IsValid())
|
||||
return;
|
||||
m_selectedFeature = selectedFeature;
|
||||
|
||||
if (m_skipFeatureInfoDialog)
|
||||
{
|
||||
m_skipFeatureInfoDialog = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto mapObject = m_framework.GetMapObjectByID(selectedFeature);
|
||||
if (!mapObject.GetID().IsValid())
|
||||
return;
|
||||
|
||||
auto const address = m_framework.GetAddressAtPoint(mapObject.GetMercator());
|
||||
FeatureInfoDialog dialog(this /* parent */, mapObject, address, m_sampleLocale);
|
||||
dialog.exec();
|
||||
}, [this]() { m_selectedFeature = FeatureID(); }, {} /* onUpdate */, {} /* onSwitchFullScreenMode */);
|
||||
}
|
||||
|
||||
MainView::~MainView()
|
||||
{
|
||||
if (m_framework.GetDrapeEngine() != nullptr)
|
||||
{
|
||||
m_framework.EnterBackground();
|
||||
m_framework.DestroyDrapeEngine();
|
||||
}
|
||||
}
|
||||
|
||||
void MainView::SetSamples(ContextList::SamplesSlice const & samples)
|
||||
{
|
||||
m_samplesView->SetSamples(samples);
|
||||
m_sampleView->Clear();
|
||||
m_initiateBackgroundSearch->setEnabled(true);
|
||||
}
|
||||
|
||||
void MainView::OnSearchStarted()
|
||||
{
|
||||
m_state = State::Search;
|
||||
m_sampleView->OnSearchStarted();
|
||||
}
|
||||
|
||||
void MainView::OnSearchCompleted()
|
||||
{
|
||||
m_state = State::AfterSearch;
|
||||
m_sampleView->OnSearchCompleted();
|
||||
}
|
||||
|
||||
void MainView::ShowSample(size_t sampleIndex, search::Sample const & sample, std::optional<m2::PointD> const & position,
|
||||
bool isUseless, bool hasEdits)
|
||||
{
|
||||
m_sampleLocale = sample.m_locale;
|
||||
|
||||
MoveViewportToRect(sample.m_viewport);
|
||||
|
||||
m_sampleView->SetContents(sample, position);
|
||||
m_sampleView->show();
|
||||
|
||||
OnResultChanged(sampleIndex, ResultType::Found, ResultsEdits::Update::MakeAll());
|
||||
OnResultChanged(sampleIndex, ResultType::NonFound, ResultsEdits::Update::MakeAll());
|
||||
OnSampleChanged(sampleIndex, isUseless, hasEdits);
|
||||
}
|
||||
|
||||
void MainView::AddFoundResults(search::Results const & results)
|
||||
{
|
||||
m_sampleView->AddFoundResults(results);
|
||||
}
|
||||
|
||||
void MainView::ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries)
|
||||
{
|
||||
m_sampleView->ShowNonFoundResults(results, entries);
|
||||
}
|
||||
|
||||
void MainView::ShowMarks(Context const & context)
|
||||
{
|
||||
m_sampleView->ClearSearchResultMarks();
|
||||
m_sampleView->ShowFoundResultsMarks(context.m_foundResults);
|
||||
m_sampleView->ShowNonFoundResultsMarks(context.m_nonFoundResults, context.m_nonFoundResultsEdits.GetEntries());
|
||||
}
|
||||
|
||||
void MainView::MoveViewportToResult(search::Result const & result)
|
||||
{
|
||||
m_skipFeatureInfoDialog = true;
|
||||
m_framework.SelectSearchResult(result, false /* animation */);
|
||||
}
|
||||
|
||||
void MainView::MoveViewportToResult(search::Sample::Result const & result)
|
||||
{
|
||||
int constexpr kViewportAroundResultSizeM = 100;
|
||||
auto const rect = mercator::RectByCenterXYAndSizeInMeters(result.m_pos, kViewportAroundResultSizeM);
|
||||
MoveViewportToRect(rect);
|
||||
}
|
||||
|
||||
void MainView::MoveViewportToRect(m2::RectD const & rect)
|
||||
{
|
||||
m_framework.ShowRect(rect, -1 /* maxScale */, false /* animation */);
|
||||
}
|
||||
|
||||
void MainView::OnResultChanged(size_t sampleIndex, ResultType type, ResultsEdits::Update const & update)
|
||||
{
|
||||
m_samplesView->OnUpdate(sampleIndex);
|
||||
|
||||
if (!m_samplesView->IsSelected(sampleIndex))
|
||||
return;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case ResultType::Found: m_sampleView->GetFoundResultsView().Update(update); break;
|
||||
case ResultType::NonFound: m_sampleView->GetNonFoundResultsView().Update(update); break;
|
||||
}
|
||||
}
|
||||
|
||||
void MainView::OnSampleChanged(size_t sampleIndex, bool isUseless, bool hasEdits)
|
||||
{
|
||||
m_samplesView->OnUpdate(sampleIndex);
|
||||
if (!m_samplesView->IsSelected(sampleIndex))
|
||||
return;
|
||||
SetSampleDockTitle(isUseless, hasEdits);
|
||||
m_sampleView->OnUselessnessChanged(isUseless);
|
||||
}
|
||||
|
||||
void MainView::OnSamplesChanged(bool hasEdits)
|
||||
{
|
||||
SetSamplesDockTitle(hasEdits);
|
||||
m_save->setEnabled(hasEdits);
|
||||
m_saveAs->setEnabled(hasEdits);
|
||||
}
|
||||
|
||||
void MainView::SetResultsEdits(size_t sampleIndex, ResultsEdits & foundResultsEdits,
|
||||
ResultsEdits & nonFoundResultsEdits)
|
||||
{
|
||||
CHECK(m_samplesView->IsSelected(sampleIndex), ());
|
||||
m_sampleView->SetResultsEdits(foundResultsEdits, nonFoundResultsEdits);
|
||||
}
|
||||
|
||||
void MainView::ShowError(std::string const & msg)
|
||||
{
|
||||
QMessageBox box(QMessageBox::Critical /* icon */, tr("Error") /* title */, QString::fromStdString(msg) /* text */,
|
||||
QMessageBox::Ok /* buttons */, this /* parent */);
|
||||
box.exec();
|
||||
}
|
||||
|
||||
void MainView::Clear()
|
||||
{
|
||||
m_samplesView->Clear();
|
||||
SetSamplesDockTitle(false /* hasEdits */);
|
||||
|
||||
m_sampleView->Clear();
|
||||
SetSampleDockTitle(false /* isUseless */, false /* hasEdits */);
|
||||
|
||||
m_skipFeatureInfoDialog = false;
|
||||
m_sampleLocale.clear();
|
||||
}
|
||||
|
||||
void MainView::closeEvent(QCloseEvent * event)
|
||||
{
|
||||
if (TryToSaveEdits(tr("Save changes before closing?")) == SaveResult::Cancelled)
|
||||
event->ignore();
|
||||
else
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void MainView::OnSampleSelected(QItemSelection const & current)
|
||||
{
|
||||
CHECK(m_model, ());
|
||||
auto const indexes = current.indexes();
|
||||
for (auto const & index : indexes)
|
||||
m_model->OnSampleSelected(index.row());
|
||||
}
|
||||
|
||||
void MainView::OnResultSelected(QItemSelection const & current)
|
||||
{
|
||||
CHECK(m_model, ());
|
||||
auto const indexes = current.indexes();
|
||||
for (auto const & index : indexes)
|
||||
m_model->OnResultSelected(index.row());
|
||||
}
|
||||
|
||||
void MainView::OnNonFoundResultSelected(QItemSelection const & current)
|
||||
{
|
||||
CHECK(m_model, ());
|
||||
auto const indexes = current.indexes();
|
||||
for (auto const & index : indexes)
|
||||
m_model->OnNonFoundResultSelected(index.row());
|
||||
}
|
||||
|
||||
void MainView::InitMenuBar()
|
||||
{
|
||||
auto * bar = menuBar();
|
||||
|
||||
auto * fileMenu = bar->addMenu(tr("&File"));
|
||||
|
||||
{
|
||||
auto * open = new QAction(tr("&Open samples..."), this /* parent */);
|
||||
open->setShortcuts(QKeySequence::Open);
|
||||
open->setStatusTip(tr("Open the file with samples for assessment"));
|
||||
connect(open, &QAction::triggered, this, &MainView::Open);
|
||||
fileMenu->addAction(open);
|
||||
}
|
||||
|
||||
{
|
||||
m_save = new QAction(tr("Save samples"), this /* parent */);
|
||||
m_save->setShortcuts(QKeySequence::Save);
|
||||
m_save->setStatusTip(tr("Save the file with assessed samples"));
|
||||
m_save->setEnabled(false);
|
||||
connect(m_save, &QAction::triggered, this, &MainView::Save);
|
||||
fileMenu->addAction(m_save);
|
||||
}
|
||||
|
||||
{
|
||||
m_saveAs = new QAction(tr("Save samples as..."), this /* parent */);
|
||||
m_saveAs->setShortcuts(QKeySequence::SaveAs);
|
||||
m_saveAs->setStatusTip(tr("Save the file with assessed samples"));
|
||||
m_saveAs->setEnabled(false);
|
||||
connect(m_saveAs, &QAction::triggered, this, &MainView::SaveAs);
|
||||
fileMenu->addAction(m_saveAs);
|
||||
}
|
||||
|
||||
{
|
||||
m_initiateBackgroundSearch = new QAction(tr("Initiate background search"), this /* parent */);
|
||||
m_initiateBackgroundSearch->setShortcut(static_cast<int>(Qt::CTRL) | static_cast<int>(Qt::Key_I));
|
||||
m_initiateBackgroundSearch->setStatusTip(tr("Search in the background for the queries from a selected range"));
|
||||
m_initiateBackgroundSearch->setEnabled(false);
|
||||
connect(m_initiateBackgroundSearch, &QAction::triggered, this, &MainView::InitiateBackgroundSearch);
|
||||
fileMenu->addAction(m_initiateBackgroundSearch);
|
||||
}
|
||||
|
||||
fileMenu->addSeparator();
|
||||
|
||||
{
|
||||
auto * quit = new QAction(tr("&Quit"), this /* parent */);
|
||||
quit->setShortcuts(QKeySequence::Quit);
|
||||
quit->setStatusTip(tr("Exit the tool"));
|
||||
connect(quit, &QAction::triggered, this, &QWidget::close);
|
||||
fileMenu->addAction(quit);
|
||||
}
|
||||
|
||||
auto * viewMenu = bar->addMenu(tr("&View"));
|
||||
|
||||
{
|
||||
CHECK(m_samplesDock != nullptr, ());
|
||||
viewMenu->addAction(m_samplesDock->toggleViewAction());
|
||||
}
|
||||
|
||||
{
|
||||
CHECK(m_sampleDock != nullptr, ());
|
||||
viewMenu->addAction(m_sampleDock->toggleViewAction());
|
||||
}
|
||||
}
|
||||
|
||||
void MainView::InitMapWidget()
|
||||
{
|
||||
auto * widget = new QWidget(this /* parent */);
|
||||
auto * layout = BuildLayoutWithoutMargins<QHBoxLayout>(widget /* parent */);
|
||||
widget->setLayout(layout);
|
||||
|
||||
{
|
||||
auto * mapWidget = new qt::common::MapWidget(m_framework, false /* screenshotMode */, widget /* parent */);
|
||||
connect(mapWidget, &qt::common::MapWidget::OnContextMenuRequested,
|
||||
[this](QPoint const & p) { AddSelectedFeature(p); });
|
||||
auto * toolBar = new QToolBar(widget /* parent */);
|
||||
toolBar->setOrientation(Qt::Vertical);
|
||||
toolBar->setIconSize(QSize(32, 32));
|
||||
qt::common::ScaleSlider::Embed(Qt::Vertical, *toolBar, *mapWidget);
|
||||
|
||||
layout->addWidget(mapWidget);
|
||||
layout->addWidget(toolBar);
|
||||
}
|
||||
|
||||
setCentralWidget(widget);
|
||||
}
|
||||
|
||||
void MainView::InitDocks()
|
||||
{
|
||||
m_samplesView = new SamplesView(this /* parent */);
|
||||
|
||||
{
|
||||
auto * model = m_samplesView->selectionModel();
|
||||
connect(model, &QItemSelectionModel::selectionChanged, this, &MainView::OnSampleSelected);
|
||||
}
|
||||
|
||||
connect(m_samplesView, &SamplesView::FlipSampleUsefulness,
|
||||
[this](int index) { m_model->FlipSampleUsefulness(index); });
|
||||
|
||||
m_samplesDock = CreateDock(*m_samplesView);
|
||||
addDockWidget(Qt::LeftDockWidgetArea, m_samplesDock);
|
||||
SetSamplesDockTitle(false /* hasEdits */);
|
||||
|
||||
m_sampleView = new SampleView(this /* parent */, m_framework);
|
||||
|
||||
connect(m_sampleView, &SampleView::OnShowViewportClicked, [this]() { m_model->OnShowViewportClicked(); });
|
||||
connect(m_sampleView, &SampleView::OnShowPositionClicked, [this]() { m_model->OnShowPositionClicked(); });
|
||||
|
||||
connect(m_sampleView, &SampleView::OnMarkAllAsRelevantClicked, [this]() { m_model->OnMarkAllAsRelevantClicked(); });
|
||||
connect(m_sampleView, &SampleView::OnMarkAllAsIrrelevantClicked,
|
||||
[this]() { m_model->OnMarkAllAsIrrelevantClicked(); });
|
||||
|
||||
{
|
||||
auto const & view = m_sampleView->GetFoundResultsView();
|
||||
connect(&view, &ResultsView::OnResultSelected, [this](int index) { m_model->OnResultSelected(index); });
|
||||
}
|
||||
|
||||
{
|
||||
auto const & view = m_sampleView->GetNonFoundResultsView();
|
||||
connect(&view, &ResultsView::OnResultSelected, [this](int index) { m_model->OnNonFoundResultSelected(index); });
|
||||
}
|
||||
|
||||
m_sampleDock = CreateDock(*m_sampleView);
|
||||
connect(m_sampleDock, &QDockWidget::dockLocationChanged,
|
||||
[this](Qt::DockWidgetArea area) { m_sampleView->OnLocationChanged(area); });
|
||||
addDockWidget(Qt::RightDockWidgetArea, m_sampleDock);
|
||||
SetSampleDockTitle(false /* isUseless */, false /* hasEdits */);
|
||||
}
|
||||
|
||||
void MainView::Open()
|
||||
{
|
||||
CHECK(m_model, ());
|
||||
|
||||
if (TryToSaveEdits(tr("Save changes before opening samples?")) == SaveResult::Cancelled)
|
||||
return;
|
||||
|
||||
auto const name = QFileDialog::getOpenFileName(this /* parent */, tr("Open samples..."), QString() /* dir */, kJSON);
|
||||
auto const file = name.toStdString();
|
||||
if (file.empty())
|
||||
return;
|
||||
|
||||
m_model->Open(file);
|
||||
}
|
||||
|
||||
void MainView::Save()
|
||||
{
|
||||
m_model->Save();
|
||||
}
|
||||
|
||||
void MainView::SaveAs()
|
||||
{
|
||||
auto const name =
|
||||
QFileDialog::getSaveFileName(this /* parent */, tr("Save samples as..."), QString() /* dir */, kJSON);
|
||||
auto const file = name.toStdString();
|
||||
if (!file.empty())
|
||||
m_model->SaveAs(file);
|
||||
}
|
||||
|
||||
void MainView::InitiateBackgroundSearch()
|
||||
{
|
||||
QDialog dialog(this);
|
||||
QFormLayout form(&dialog);
|
||||
|
||||
form.addRow(new QLabel("Queries range"));
|
||||
|
||||
QValidator * validator = new QIntValidator(0, std::numeric_limits<int>::max(), this);
|
||||
|
||||
QLineEdit * lineEditFrom = new QLineEdit(&dialog);
|
||||
form.addRow(new QLabel("First"), lineEditFrom);
|
||||
lineEditFrom->setValidator(validator);
|
||||
|
||||
QLineEdit * lineEditTo = new QLineEdit(&dialog);
|
||||
form.addRow(new QLabel("Last"), lineEditTo);
|
||||
lineEditTo->setValidator(validator);
|
||||
|
||||
QDialogButtonBox buttonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, Qt::Horizontal, &dialog);
|
||||
form.addRow(&buttonBox);
|
||||
|
||||
connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
std::string const strFrom = lineEditFrom->text().toStdString();
|
||||
std::string const strTo = lineEditTo->text().toStdString();
|
||||
uint64_t from = 0;
|
||||
uint64_t to = 0;
|
||||
if (!strings::to_uint64(strFrom, from))
|
||||
{
|
||||
LOG(LERROR, ("Could not parse number from", strFrom));
|
||||
return;
|
||||
}
|
||||
if (!strings::to_uint64(strTo, to))
|
||||
{
|
||||
LOG(LERROR, ("Could not parse number from", strTo));
|
||||
return;
|
||||
}
|
||||
|
||||
m_model->InitiateBackgroundSearch(base::checked_cast<size_t>(from), base::checked_cast<size_t>(to));
|
||||
}
|
||||
|
||||
void MainView::SetSamplesDockTitle(bool hasEdits)
|
||||
{
|
||||
CHECK(m_samplesDock, ());
|
||||
if (hasEdits)
|
||||
m_samplesDock->setWindowTitle(tr("Samples *"));
|
||||
else
|
||||
m_samplesDock->setWindowTitle(tr("Samples"));
|
||||
}
|
||||
|
||||
void MainView::SetSampleDockTitle(bool isUseless, bool hasEdits)
|
||||
{
|
||||
CHECK(m_sampleDock, ());
|
||||
std::string title = "Sample";
|
||||
if (hasEdits)
|
||||
title += " *";
|
||||
if (isUseless)
|
||||
title += " (useless)";
|
||||
m_sampleDock->setWindowTitle(tr(title.data()));
|
||||
}
|
||||
|
||||
MainView::SaveResult MainView::TryToSaveEdits(QString const & msg)
|
||||
{
|
||||
CHECK(m_model, ());
|
||||
|
||||
if (!m_model->HasChanges())
|
||||
return SaveResult::NoEdits;
|
||||
|
||||
QMessageBox box(QMessageBox::Question /* icon */, tr("Save edits?") /* title */, msg /* text */,
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel /* buttons */, this /* parent */);
|
||||
auto const button = box.exec();
|
||||
switch (button)
|
||||
{
|
||||
case QMessageBox::Save: Save(); return SaveResult::Saved;
|
||||
case QMessageBox::Discard: return SaveResult::Discarded;
|
||||
case QMessageBox::Cancel: return SaveResult::Cancelled;
|
||||
}
|
||||
|
||||
CHECK(false, ());
|
||||
return SaveResult::Cancelled;
|
||||
}
|
||||
|
||||
void MainView::AddSelectedFeature(QPoint const & p)
|
||||
{
|
||||
auto const selectedFeature = m_selectedFeature;
|
||||
|
||||
if (!selectedFeature.IsValid())
|
||||
return;
|
||||
|
||||
if (m_state != State::AfterSearch)
|
||||
return;
|
||||
|
||||
if (m_model->AlreadyInSamples(selectedFeature))
|
||||
return;
|
||||
|
||||
QMenu menu;
|
||||
auto const * action = menu.addAction("Add to non-found results");
|
||||
connect(action, &QAction::triggered, [this, selectedFeature]() { m_model->AddNonFoundResult(selectedFeature); });
|
||||
menu.exec(p);
|
||||
}
|
||||
|
||||
QDockWidget * MainView::CreateDock(QWidget & widget)
|
||||
{
|
||||
auto * dock = new QDockWidget(QString(), this /* parent */, Qt::Widget);
|
||||
dock->setFeatures(QDockWidget::DockWidgetClosable | QDockWidget::DockWidgetMovable |
|
||||
QDockWidget::DockWidgetFloatable);
|
||||
dock->setWidget(&widget);
|
||||
return dock;
|
||||
}
|
||||
128
libs/search/search_quality/assessment_tool/main_view.hpp
Normal file
128
libs/search/search_quality/assessment_tool/main_view.hpp
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#pragma once
|
||||
#include "view.hpp"
|
||||
|
||||
#include "indexer/feature_decl.hpp"
|
||||
|
||||
#include <QtWidgets/QMainWindow>
|
||||
|
||||
class Framework;
|
||||
class QDockWidget;
|
||||
class QItemSelection;
|
||||
class SamplesView;
|
||||
class SampleView;
|
||||
|
||||
namespace qt
|
||||
{
|
||||
namespace common
|
||||
{
|
||||
class MapWidget;
|
||||
}
|
||||
} // namespace qt
|
||||
|
||||
class MainView
|
||||
: public QMainWindow
|
||||
, public View
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainView(Framework & framework, QRect const & screenGeometry);
|
||||
~MainView() override;
|
||||
|
||||
// View overrides:
|
||||
void SetSamples(ContextList::SamplesSlice const & samples) override;
|
||||
void OnSearchStarted() override;
|
||||
void OnSearchCompleted() override;
|
||||
void ShowSample(size_t sampleIndex, search::Sample const & sample, std::optional<m2::PointD> const & position,
|
||||
bool isUseless, bool hasEdits) override;
|
||||
|
||||
void AddFoundResults(search::Results const & results) override;
|
||||
void ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries) override;
|
||||
|
||||
void ShowMarks(Context const & context) override;
|
||||
|
||||
void MoveViewportToResult(search::Result const & result) override;
|
||||
void MoveViewportToResult(search::Sample::Result const & result) override;
|
||||
void MoveViewportToRect(m2::RectD const & rect) override;
|
||||
|
||||
void OnResultChanged(size_t sampleIndex, ResultType type, ResultsEdits::Update const & update) override;
|
||||
void SetResultsEdits(size_t sampleIndex, ResultsEdits & foundResultsResultsEdits,
|
||||
ResultsEdits & nonFoundResultsResultsEdits) override;
|
||||
void OnSampleChanged(size_t sampleIndex, bool isUseless, bool hasEdits) override;
|
||||
void OnSamplesChanged(bool hasEdits) override;
|
||||
|
||||
void ShowError(std::string const & msg) override;
|
||||
|
||||
void Clear() override;
|
||||
|
||||
protected:
|
||||
// QMainWindow overrides:
|
||||
void closeEvent(QCloseEvent * event) override;
|
||||
|
||||
private slots:
|
||||
void OnSampleSelected(QItemSelection const & current);
|
||||
void OnResultSelected(QItemSelection const & current);
|
||||
void OnNonFoundResultSelected(QItemSelection const & current);
|
||||
|
||||
private:
|
||||
enum class State
|
||||
{
|
||||
BeforeSearch,
|
||||
Search,
|
||||
AfterSearch
|
||||
};
|
||||
|
||||
friend std::string DebugPrint(State state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case State::BeforeSearch: return "BeforeSearch";
|
||||
case State::Search: return "Search";
|
||||
case State::AfterSearch: return "AfterSearch";
|
||||
}
|
||||
}
|
||||
|
||||
enum class SaveResult
|
||||
{
|
||||
NoEdits,
|
||||
Saved,
|
||||
Discarded,
|
||||
Cancelled
|
||||
};
|
||||
|
||||
void InitMapWidget();
|
||||
void InitDocks();
|
||||
void InitMenuBar();
|
||||
|
||||
void Open();
|
||||
void Save();
|
||||
void SaveAs();
|
||||
void InitiateBackgroundSearch();
|
||||
|
||||
void SetSamplesDockTitle(bool hasEdits);
|
||||
void SetSampleDockTitle(bool isUseless, bool hasEdits);
|
||||
SaveResult TryToSaveEdits(QString const & msg);
|
||||
|
||||
void AddSelectedFeature(QPoint const & p);
|
||||
|
||||
QDockWidget * CreateDock(QWidget & widget);
|
||||
|
||||
Framework & m_framework;
|
||||
|
||||
SamplesView * m_samplesView = nullptr;
|
||||
QDockWidget * m_samplesDock = nullptr;
|
||||
|
||||
SampleView * m_sampleView = nullptr;
|
||||
QDockWidget * m_sampleDock = nullptr;
|
||||
|
||||
QAction * m_save = nullptr;
|
||||
QAction * m_saveAs = nullptr;
|
||||
QAction * m_initiateBackgroundSearch = nullptr;
|
||||
|
||||
State m_state = State::BeforeSearch;
|
||||
FeatureID m_selectedFeature;
|
||||
|
||||
bool m_skipFeatureInfoDialog = false;
|
||||
std::string m_sampleLocale;
|
||||
};
|
||||
42
libs/search/search_quality/assessment_tool/model.hpp
Normal file
42
libs/search/search_quality/assessment_tool/model.hpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class View;
|
||||
struct FeatureID;
|
||||
|
||||
class Model
|
||||
{
|
||||
public:
|
||||
virtual ~Model() = default;
|
||||
|
||||
void SetView(View & view) { m_view = &view; }
|
||||
|
||||
virtual void Open(std::string const & path) = 0;
|
||||
virtual void Save() = 0;
|
||||
virtual void SaveAs(std::string const & path) = 0;
|
||||
|
||||
// Initiates the search in the background on all samples
|
||||
// in the 1-based range [|from|, |to|], both ends included.
|
||||
// Another background search that may currently be running will be cancelled
|
||||
// but the results for already completed requests will not be discarded.
|
||||
//
|
||||
// Does nothing if the range is invalid.
|
||||
virtual void InitiateBackgroundSearch(size_t from, size_t to) = 0;
|
||||
|
||||
virtual void OnSampleSelected(int index) = 0;
|
||||
virtual void OnResultSelected(int index) = 0;
|
||||
virtual void OnNonFoundResultSelected(int index) = 0;
|
||||
virtual void OnShowViewportClicked() = 0;
|
||||
virtual void OnShowPositionClicked() = 0;
|
||||
virtual void OnMarkAllAsRelevantClicked() = 0;
|
||||
virtual void OnMarkAllAsIrrelevantClicked() = 0;
|
||||
virtual bool HasChanges() = 0;
|
||||
|
||||
virtual bool AlreadyInSamples(FeatureID const & id) = 0;
|
||||
virtual void AddNonFoundResult(FeatureID const & id) = 0;
|
||||
virtual void FlipSampleUsefulness(int index) = 0;
|
||||
|
||||
protected:
|
||||
View * m_view = nullptr;
|
||||
};
|
||||
185
libs/search/search_quality/assessment_tool/result_view.cpp
Normal file
185
libs/search/search_quality/assessment_tool/result_view.cpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#include "search/search_quality/assessment_tool/result_view.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/assessment_tool/helpers.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QRadioButton>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
QLabel * CreateLabel(QWidget & parent)
|
||||
{
|
||||
QLabel * label = new QLabel(&parent);
|
||||
label->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
void SetText(QLabel & label, string const & text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
label.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
label.setText(ToQString(text));
|
||||
label.show();
|
||||
}
|
||||
|
||||
string GetResultType(search::Sample::Result const & result)
|
||||
{
|
||||
return strings::JoinStrings(result.m_types, ", ");
|
||||
}
|
||||
|
||||
string GetResultType(search::Result const & result)
|
||||
{
|
||||
return (result.GetResultType() == search::Result::Type::Feature ? result.GetLocalizedFeatureType() : "");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ResultView::ResultView(string const & name, string const & type, string const & address, QWidget & parent)
|
||||
: QWidget(&parent)
|
||||
{
|
||||
Init();
|
||||
SetContents(name, type, address);
|
||||
setEnabled(false);
|
||||
setObjectName("result");
|
||||
}
|
||||
|
||||
ResultView::ResultView(search::Result const & result, QWidget & parent)
|
||||
: ResultView(result.GetString(), GetResultType(result), result.GetAddress(), parent)
|
||||
{}
|
||||
|
||||
ResultView::ResultView(search::Sample::Result const & result, QWidget & parent)
|
||||
: ResultView(strings::ToUtf8(result.m_name), GetResultType(result), {} /* address */, parent)
|
||||
{}
|
||||
|
||||
void ResultView::SetEditor(ResultsEdits::Editor && editor)
|
||||
{
|
||||
m_editor = make_unique<ResultsEdits::Editor>(std::move(editor));
|
||||
|
||||
UpdateRelevanceRadioButtons();
|
||||
|
||||
setEnabled(true);
|
||||
}
|
||||
|
||||
void ResultView::Update()
|
||||
{
|
||||
if (!m_editor)
|
||||
{
|
||||
setStyleSheet("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_editor->GetType() == ResultsEdits::Entry::Type::Created)
|
||||
setStyleSheet("#result {background: rgba(173, 223, 173, 50%)}");
|
||||
else if (m_editor->HasChanges())
|
||||
setStyleSheet("#result {background: rgba(255, 255, 200, 50%)}");
|
||||
else
|
||||
setStyleSheet("");
|
||||
|
||||
UpdateRelevanceRadioButtons();
|
||||
}
|
||||
|
||||
void ResultView::Init()
|
||||
{
|
||||
auto * layout = new QVBoxLayout(this /* parent */);
|
||||
layout->setSizeConstraint(QLayout::SetMinimumSize);
|
||||
setLayout(layout);
|
||||
|
||||
m_name = CreateLabel(*this /* parent */);
|
||||
layout->addWidget(m_name);
|
||||
|
||||
m_type = CreateLabel(*this /* parent */);
|
||||
m_type->setStyleSheet("QLabel { font-size : 8pt }");
|
||||
layout->addWidget(m_type);
|
||||
|
||||
m_address = CreateLabel(*this /* parent */);
|
||||
m_address->setStyleSheet("QLabel { color : green ; font-size : 8pt }");
|
||||
layout->addWidget(m_address);
|
||||
|
||||
{
|
||||
auto * group = new QWidget(this /* parent */);
|
||||
auto * groupLayout = new QHBoxLayout(group /* parent */);
|
||||
group->setLayout(groupLayout);
|
||||
|
||||
m_harmful = CreateRatioButton("Harmful", *groupLayout);
|
||||
m_irrelevant = CreateRatioButton("Irrelevant", *groupLayout);
|
||||
m_relevant = CreateRatioButton("Relevant", *groupLayout);
|
||||
m_vital = CreateRatioButton("Vital", *groupLayout);
|
||||
|
||||
layout->addWidget(group);
|
||||
}
|
||||
}
|
||||
|
||||
void ResultView::SetContents(string const & name, string const & type, string const & address)
|
||||
{
|
||||
SetText(*m_name, name);
|
||||
SetText(*m_type, type);
|
||||
SetText(*m_address, address);
|
||||
|
||||
m_harmful->setChecked(false);
|
||||
m_irrelevant->setChecked(false);
|
||||
m_relevant->setChecked(false);
|
||||
m_vital->setChecked(false);
|
||||
}
|
||||
|
||||
QRadioButton * ResultView::CreateRatioButton(string const & text, QLayout & layout)
|
||||
{
|
||||
QRadioButton * radio = new QRadioButton(QString::fromStdString(text), this /* parent */);
|
||||
layout.addWidget(radio);
|
||||
|
||||
connect(radio, &QRadioButton::toggled, this, &ResultView::OnRelevanceChanged);
|
||||
return radio;
|
||||
}
|
||||
|
||||
void ResultView::OnRelevanceChanged()
|
||||
{
|
||||
if (!m_editor)
|
||||
return;
|
||||
|
||||
auto relevance = Relevance::Irrelevant;
|
||||
if (m_harmful->isChecked())
|
||||
relevance = Relevance::Harmful;
|
||||
else if (m_irrelevant->isChecked())
|
||||
relevance = Relevance::Irrelevant;
|
||||
else if (m_relevant->isChecked())
|
||||
relevance = Relevance::Relevant;
|
||||
else if (m_vital->isChecked())
|
||||
relevance = Relevance::Vital;
|
||||
|
||||
m_editor->Set(relevance);
|
||||
}
|
||||
|
||||
void ResultView::UpdateRelevanceRadioButtons()
|
||||
{
|
||||
if (!m_editor)
|
||||
return;
|
||||
|
||||
m_harmful->setChecked(false);
|
||||
m_irrelevant->setChecked(false);
|
||||
m_relevant->setChecked(false);
|
||||
m_vital->setChecked(false);
|
||||
|
||||
auto const & r = m_editor->Get();
|
||||
|
||||
if (!r)
|
||||
return;
|
||||
|
||||
switch (*r)
|
||||
{
|
||||
case Relevance::Harmful: m_harmful->setChecked(true); break;
|
||||
case Relevance::Irrelevant: m_irrelevant->setChecked(true); break;
|
||||
case Relevance::Relevant: m_relevant->setChecked(true); break;
|
||||
case Relevance::Vital: m_vital->setChecked(true); break;
|
||||
}
|
||||
}
|
||||
51
libs/search/search_quality/assessment_tool/result_view.hpp
Normal file
51
libs/search/search_quality/assessment_tool/result_view.hpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/assessment_tool/edits.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
|
||||
class QLabel;
|
||||
class QRadioButton;
|
||||
|
||||
namespace search
|
||||
{
|
||||
class Result;
|
||||
}
|
||||
|
||||
class ResultView : public QWidget
|
||||
{
|
||||
public:
|
||||
using Relevance = search::Sample::Result::Relevance;
|
||||
|
||||
ResultView(search::Result const & result, QWidget & parent);
|
||||
ResultView(search::Sample::Result const & result, QWidget & parent);
|
||||
|
||||
void SetEditor(ResultsEdits::Editor && editor);
|
||||
|
||||
void Update();
|
||||
|
||||
private:
|
||||
ResultView(std::string const & name, std::string const & type, std::string const & address, QWidget & parent);
|
||||
|
||||
void Init();
|
||||
void SetContents(std::string const & name, std::string const & type, std::string const & address);
|
||||
|
||||
QRadioButton * CreateRatioButton(std::string const & label, QLayout & layout);
|
||||
void OnRelevanceChanged();
|
||||
void UpdateRelevanceRadioButtons();
|
||||
|
||||
QLabel * m_name = nullptr;
|
||||
QLabel * m_type = nullptr;
|
||||
QLabel * m_address = nullptr;
|
||||
|
||||
QRadioButton * m_harmful = nullptr;
|
||||
QRadioButton * m_irrelevant = nullptr;
|
||||
QRadioButton * m_relevant = nullptr;
|
||||
QRadioButton * m_vital = nullptr;
|
||||
|
||||
std::unique_ptr<ResultsEdits::Editor> m_editor;
|
||||
};
|
||||
108
libs/search/search_quality/assessment_tool/results_view.cpp
Normal file
108
libs/search/search_quality/assessment_tool/results_view.cpp
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#include "search/search_quality/assessment_tool/results_view.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/assessment_tool/result_view.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <QtWidgets/QListWidgetItem>
|
||||
|
||||
ResultsView::ResultsView(QWidget & parent) : QListWidget(&parent)
|
||||
{
|
||||
setAlternatingRowColors(true);
|
||||
|
||||
connect(selectionModel(), &QItemSelectionModel::selectionChanged, [&](QItemSelection const & current)
|
||||
{
|
||||
auto const indexes = current.indexes();
|
||||
for (auto const & index : indexes)
|
||||
emit OnResultSelected(index.row());
|
||||
});
|
||||
|
||||
connect(this, &ResultsView::itemClicked, [&](QListWidgetItem * item)
|
||||
{
|
||||
auto const index = indexFromItem(item);
|
||||
emit OnResultSelected(index.row());
|
||||
});
|
||||
}
|
||||
|
||||
void ResultsView::Add(search::Result const & result)
|
||||
{
|
||||
AddImpl(result, false /* hidden */);
|
||||
if (result.HasPoint())
|
||||
m_hasResultsWithPoints = true;
|
||||
}
|
||||
|
||||
void ResultsView::Add(search::Sample::Result const & result, ResultsEdits::Entry const & entry)
|
||||
{
|
||||
AddImpl(result, entry.m_deleted /* hidden */);
|
||||
}
|
||||
|
||||
ResultView & ResultsView::Get(size_t i)
|
||||
{
|
||||
CHECK_LESS(i, Size(), ());
|
||||
return *m_results[i];
|
||||
}
|
||||
|
||||
ResultView const & ResultsView::Get(size_t i) const
|
||||
{
|
||||
CHECK_LESS(i, Size(), ());
|
||||
return *m_results[i];
|
||||
}
|
||||
|
||||
void ResultsView::Update(ResultsEdits::Update const & update)
|
||||
{
|
||||
switch (update.m_type)
|
||||
{
|
||||
case ResultsEdits::Update::Type::Single:
|
||||
{
|
||||
CHECK_LESS(update.m_index, m_results.size(), ());
|
||||
m_results[update.m_index]->Update();
|
||||
break;
|
||||
}
|
||||
case ResultsEdits::Update::Type::All:
|
||||
{
|
||||
for (auto * result : m_results)
|
||||
result->Update();
|
||||
break;
|
||||
}
|
||||
case ResultsEdits::Update::Type::Add:
|
||||
{
|
||||
CHECK_LESS(update.m_index, m_results.size(), ());
|
||||
m_results[update.m_index]->Update();
|
||||
break;
|
||||
}
|
||||
case ResultsEdits::Update::Type::Delete:
|
||||
{
|
||||
auto const index = update.m_index;
|
||||
CHECK_LESS(index, Size(), ());
|
||||
item(static_cast<int>(index))->setHidden(true);
|
||||
break;
|
||||
}
|
||||
case ResultsEdits::Update::Type::Resurrect:
|
||||
auto const index = update.m_index;
|
||||
CHECK_LESS(index, Size(), ());
|
||||
item(static_cast<int>(index))->setHidden(false);
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
void ResultsView::Clear()
|
||||
{
|
||||
m_results.clear();
|
||||
m_hasResultsWithPoints = false;
|
||||
clear();
|
||||
}
|
||||
|
||||
template <typename Result>
|
||||
void ResultsView::AddImpl(Result const & result, bool hidden)
|
||||
{
|
||||
auto * item = new QListWidgetItem(this /* parent */);
|
||||
item->setHidden(hidden);
|
||||
addItem(item);
|
||||
|
||||
auto * view = new ResultView(result, *this /* parent */);
|
||||
item->setSizeHint(view->minimumSizeHint());
|
||||
setItemWidget(item, view);
|
||||
|
||||
m_results.push_back(view);
|
||||
}
|
||||
46
libs/search/search_quality/assessment_tool/results_view.hpp
Normal file
46
libs/search/search_quality/assessment_tool/results_view.hpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/assessment_tool/edits.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include <QtWidgets/QListWidget>
|
||||
|
||||
class QWidget;
|
||||
class ResultView;
|
||||
|
||||
namespace search
|
||||
{
|
||||
class Result;
|
||||
}
|
||||
|
||||
class ResultsView : public QListWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ResultsView(QWidget & parent);
|
||||
|
||||
void Add(search::Result const & result);
|
||||
void Add(search::Sample::Result const & result, ResultsEdits::Entry const & entry);
|
||||
|
||||
ResultView & Get(size_t i);
|
||||
ResultView const & Get(size_t i) const;
|
||||
void Update(ResultsEdits::Update const & update);
|
||||
|
||||
size_t Size() const { return m_results.size(); }
|
||||
bool HasResultsWithPoints() const { return m_hasResultsWithPoints; }
|
||||
|
||||
void Clear();
|
||||
|
||||
signals:
|
||||
void OnResultSelected(int index);
|
||||
|
||||
private:
|
||||
template <typename Result>
|
||||
void AddImpl(Result const & result, bool hidden);
|
||||
|
||||
std::vector<ResultView *> m_results;
|
||||
bool m_hasResultsWithPoints = false;
|
||||
};
|
||||
387
libs/search/search_quality/assessment_tool/sample_view.cpp
Normal file
387
libs/search/search_quality/assessment_tool/sample_view.cpp
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
#include "sample_view.hpp"
|
||||
|
||||
#include "helpers.hpp"
|
||||
#include "result_view.hpp"
|
||||
#include "results_view.hpp"
|
||||
|
||||
#include "qt/qt_common/spinner.hpp"
|
||||
|
||||
#include "map/bookmark_manager.hpp"
|
||||
#include "map/framework.hpp"
|
||||
#include "map/search_mark.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include <QtGui/QStandardItem>
|
||||
#include <QtGui/QStandardItemModel>
|
||||
#include <QtWidgets/QComboBox>
|
||||
#include <QtWidgets/QGroupBox>
|
||||
#include <QtWidgets/QHBoxLayout>
|
||||
#include <QtWidgets/QHeaderView>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QListWidget>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtWidgets/QVBoxLayout>
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename Layout>
|
||||
Layout * BuildSubLayout(QLayout & mainLayout, QWidget & parent, QWidget ** box)
|
||||
{
|
||||
*box = new QWidget(&parent);
|
||||
auto * subLayout = BuildLayoutWithoutMargins<Layout>(*box /* parent */);
|
||||
(*box)->setLayout(subLayout);
|
||||
mainLayout.addWidget(*box);
|
||||
return subLayout;
|
||||
}
|
||||
|
||||
template <typename Layout>
|
||||
Layout * BuildSubLayout(QLayout & mainLayout, QWidget & parent)
|
||||
{
|
||||
QWidget * box = nullptr;
|
||||
return BuildSubLayout<Layout>(mainLayout, parent, &box);
|
||||
}
|
||||
|
||||
void SetVerticalStretch(QWidget & widget, int stretch)
|
||||
{
|
||||
auto policy = widget.sizePolicy();
|
||||
policy.setVerticalStretch(stretch);
|
||||
widget.setSizePolicy(policy);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SampleView::SampleView(QWidget * parent, Framework & framework) : QWidget(parent), m_framework(framework)
|
||||
{
|
||||
m_framework.GetBookmarkManager().GetEditSession().SetIsVisible(UserMark::Type::SEARCH, true);
|
||||
m_framework.GetBookmarkManager().GetEditSession().SetIsVisible(UserMark::Type::COLORED, true);
|
||||
|
||||
auto * mainLayout = BuildLayout<QVBoxLayout>(this /* parent */);
|
||||
|
||||
// When the dock for SampleView is attached to the right side of the
|
||||
// screen, we don't need left margin, because of zoom in/zoom out
|
||||
// slider. In other cases, it's better to keep left margin as is.
|
||||
m_defaultMargins = mainLayout->contentsMargins();
|
||||
m_rightAreaMargins = m_defaultMargins;
|
||||
m_rightAreaMargins.setLeft(0);
|
||||
|
||||
{
|
||||
m_query = new QLabel(this /* parent */);
|
||||
m_query->setToolTip(tr("Query text"));
|
||||
m_query->setWordWrap(true);
|
||||
m_query->hide();
|
||||
mainLayout->addWidget(m_query);
|
||||
}
|
||||
|
||||
{
|
||||
m_langs = new QLabel(this /* parent */);
|
||||
m_langs->setToolTip(tr("Query input language"));
|
||||
m_langs->hide();
|
||||
mainLayout->addWidget(m_langs);
|
||||
}
|
||||
|
||||
{
|
||||
auto * layout = BuildSubLayout<QHBoxLayout>(*mainLayout, *this /* parent */);
|
||||
|
||||
m_showViewport = new QPushButton(tr("Show viewport"), this /* parent */);
|
||||
connect(m_showViewport, &QPushButton::clicked, [this]() { emit OnShowViewportClicked(); });
|
||||
layout->addWidget(m_showViewport);
|
||||
|
||||
m_showPosition = new QPushButton(tr("Show position"), this /* parent */);
|
||||
connect(m_showPosition, &QPushButton::clicked, [this]() { emit OnShowPositionClicked(); });
|
||||
layout->addWidget(m_showPosition);
|
||||
}
|
||||
|
||||
{
|
||||
auto * layout = BuildSubLayout<QVBoxLayout>(*mainLayout, *this /* parent */, &m_relatedQueriesBox);
|
||||
SetVerticalStretch(*m_relatedQueriesBox, 1 /* stretch */);
|
||||
|
||||
layout->addWidget(new QLabel(tr("Related queries")));
|
||||
|
||||
m_relatedQueries = new QListWidget();
|
||||
layout->addWidget(m_relatedQueries);
|
||||
}
|
||||
|
||||
{
|
||||
auto * layout = BuildSubLayout<QHBoxLayout>(*mainLayout, *this /* parent */);
|
||||
|
||||
m_markAllAsRelevant = new QPushButton(tr("Mark all as Relevant"), this /* parent */);
|
||||
connect(m_markAllAsRelevant, &QPushButton::clicked, [this]() { emit OnMarkAllAsRelevantClicked(); });
|
||||
layout->addWidget(m_markAllAsRelevant);
|
||||
|
||||
m_markAllAsIrrelevant = new QPushButton(tr("Mark all as Irrelevant"), this /* parent */);
|
||||
connect(m_markAllAsIrrelevant, &QPushButton::clicked, [this]() { emit OnMarkAllAsIrrelevantClicked(); });
|
||||
layout->addWidget(m_markAllAsIrrelevant);
|
||||
}
|
||||
|
||||
{
|
||||
m_uselessnessLabel = new QLabel(this /* parent */);
|
||||
m_uselessnessLabel->setText(tr("Sample is marked as useless"));
|
||||
m_uselessnessLabel->hide();
|
||||
mainLayout->addWidget(m_uselessnessLabel);
|
||||
}
|
||||
|
||||
{
|
||||
auto * layout = BuildSubLayout<QVBoxLayout>(*mainLayout, *this /* parent */, &m_foundResultsBox);
|
||||
SetVerticalStretch(*m_foundResultsBox, 4 /* stretch */);
|
||||
|
||||
{
|
||||
auto * subLayout = BuildSubLayout<QHBoxLayout>(*layout, *this /* parent */);
|
||||
subLayout->addWidget(new QLabel(tr("Found results")));
|
||||
|
||||
m_spinner = new Spinner();
|
||||
subLayout->addWidget(&m_spinner->AsWidget());
|
||||
}
|
||||
|
||||
m_foundResults = new ResultsView(*this /* parent */);
|
||||
layout->addWidget(m_foundResults);
|
||||
}
|
||||
|
||||
{
|
||||
auto * layout = BuildSubLayout<QVBoxLayout>(*mainLayout, *this /* parent */, &m_nonFoundResultsBox);
|
||||
SetVerticalStretch(*m_nonFoundResultsBox, 2 /* stretch */);
|
||||
|
||||
layout->addWidget(new QLabel(tr("Non found results")));
|
||||
|
||||
m_nonFoundResults = new ResultsView(*this /* parent */);
|
||||
m_nonFoundResults->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_nonFoundResults, &ResultsView::customContextMenuRequested, [&](QPoint pos)
|
||||
{
|
||||
pos = m_nonFoundResults->mapToGlobal(pos);
|
||||
|
||||
auto const items = m_nonFoundResults->selectedItems();
|
||||
for (auto const * item : items)
|
||||
{
|
||||
int const row = m_nonFoundResults->row(item);
|
||||
|
||||
QMenu menu;
|
||||
auto const * action = menu.addAction("Remove result");
|
||||
connect(action, &QAction::triggered, [this, row]() { OnRemoveNonFoundResult(row); });
|
||||
|
||||
menu.exec(pos);
|
||||
}
|
||||
});
|
||||
layout->addWidget(m_nonFoundResults);
|
||||
}
|
||||
|
||||
setLayout(mainLayout);
|
||||
|
||||
Clear();
|
||||
}
|
||||
|
||||
void SampleView::SetContents(search::Sample const & sample, std::optional<m2::PointD> const & position)
|
||||
{
|
||||
if (!sample.m_query.empty())
|
||||
{
|
||||
m_query->setText(ToQString(sample.m_query));
|
||||
m_query->show();
|
||||
}
|
||||
if (!sample.m_locale.empty())
|
||||
{
|
||||
m_langs->setText(ToQString(sample.m_locale));
|
||||
m_langs->show();
|
||||
}
|
||||
m_showViewport->setEnabled(true);
|
||||
|
||||
m_relatedQueries->clear();
|
||||
for (auto const & query : sample.m_relatedQueries)
|
||||
m_relatedQueries->addItem(ToQString(query));
|
||||
if (m_relatedQueries->count() != 0)
|
||||
m_relatedQueriesBox->show();
|
||||
|
||||
ClearAllResults();
|
||||
m_position = position;
|
||||
if (m_position)
|
||||
ShowUserPosition(*m_position);
|
||||
else
|
||||
HideUserPosition();
|
||||
}
|
||||
|
||||
void SampleView::OnSearchStarted()
|
||||
{
|
||||
m_spinner->Show();
|
||||
m_showPosition->setEnabled(false);
|
||||
|
||||
m_markAllAsRelevant->setEnabled(false);
|
||||
m_markAllAsIrrelevant->setEnabled(false);
|
||||
}
|
||||
|
||||
void SampleView::OnSearchCompleted()
|
||||
{
|
||||
m_spinner->Hide();
|
||||
auto const resultsAvailable = m_foundResults->HasResultsWithPoints();
|
||||
if (m_position)
|
||||
{
|
||||
if (resultsAvailable)
|
||||
m_showPosition->setText(tr("Show position and top results"));
|
||||
else
|
||||
m_showPosition->setText(tr("Show position"));
|
||||
m_showPosition->setEnabled(true);
|
||||
}
|
||||
else if (resultsAvailable)
|
||||
{
|
||||
m_showPosition->setText(tr("Show results"));
|
||||
m_showPosition->setEnabled(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_showPosition->setEnabled(false);
|
||||
}
|
||||
|
||||
m_markAllAsRelevant->setEnabled(resultsAvailable);
|
||||
m_markAllAsIrrelevant->setEnabled(resultsAvailable);
|
||||
}
|
||||
|
||||
void SampleView::AddFoundResults(search::Results const & results)
|
||||
{
|
||||
/// @todo Should clear previous m_foundResults.
|
||||
for (auto const & res : results)
|
||||
m_foundResults->Add(res);
|
||||
}
|
||||
|
||||
void SampleView::ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries)
|
||||
{
|
||||
CHECK_EQUAL(results.size(), entries.size(), ());
|
||||
|
||||
m_nonFoundResults->Clear();
|
||||
|
||||
bool allDeleted = true;
|
||||
for (size_t i = 0; i < results.size(); ++i)
|
||||
{
|
||||
m_nonFoundResults->Add(results[i], entries[i]);
|
||||
if (!entries[i].m_deleted)
|
||||
allDeleted = false;
|
||||
}
|
||||
if (!allDeleted)
|
||||
m_nonFoundResultsBox->show();
|
||||
}
|
||||
|
||||
void SampleView::ShowFoundResultsMarks(search::Results const & results)
|
||||
{
|
||||
/// @todo Should clear previous _found_ results marks, but keep _nonfound_ if any.
|
||||
m_framework.FillSearchResultsMarks(false /* clear */, results);
|
||||
}
|
||||
|
||||
void SampleView::ShowNonFoundResultsMarks(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries)
|
||||
|
||||
{
|
||||
CHECK_EQUAL(results.size(), entries.size(), ());
|
||||
|
||||
auto editSession = m_framework.GetBookmarkManager().GetEditSession();
|
||||
editSession.SetIsVisible(UserMark::Type::SEARCH, true);
|
||||
|
||||
for (size_t i = 0; i < results.size(); ++i)
|
||||
{
|
||||
auto const & result = results[i];
|
||||
auto const & entry = entries[i];
|
||||
if (entry.m_deleted)
|
||||
continue;
|
||||
|
||||
auto * mark = editSession.CreateUserMark<SearchMarkPoint>(result.m_pos);
|
||||
mark->SetNotFoundType();
|
||||
}
|
||||
}
|
||||
|
||||
void SampleView::ClearSearchResultMarks()
|
||||
{
|
||||
m_framework.GetBookmarkManager().GetEditSession().ClearGroup(UserMark::Type::SEARCH);
|
||||
}
|
||||
|
||||
void SampleView::ClearAllResults()
|
||||
{
|
||||
m_foundResults->Clear();
|
||||
m_nonFoundResults->Clear();
|
||||
m_nonFoundResultsBox->hide();
|
||||
ClearSearchResultMarks();
|
||||
}
|
||||
|
||||
void SampleView::SetResultsEdits(ResultsEdits & resultsResultsEdits, ResultsEdits & nonFoundResultsEdits)
|
||||
{
|
||||
SetResultsEdits(*m_foundResults, resultsResultsEdits);
|
||||
SetResultsEdits(*m_nonFoundResults, nonFoundResultsEdits);
|
||||
m_nonFoundResultsEdits = &nonFoundResultsEdits;
|
||||
}
|
||||
|
||||
void SampleView::OnUselessnessChanged(bool isUseless)
|
||||
{
|
||||
if (isUseless)
|
||||
{
|
||||
m_uselessnessLabel->show();
|
||||
m_foundResultsBox->hide();
|
||||
m_nonFoundResultsBox->hide();
|
||||
m_markAllAsRelevant->hide();
|
||||
m_markAllAsIrrelevant->hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
m_uselessnessLabel->hide();
|
||||
m_foundResultsBox->show();
|
||||
m_markAllAsRelevant->show();
|
||||
m_markAllAsIrrelevant->show();
|
||||
}
|
||||
}
|
||||
|
||||
void SampleView::Clear()
|
||||
{
|
||||
m_query->hide();
|
||||
m_langs->hide();
|
||||
|
||||
m_showViewport->setEnabled(false);
|
||||
m_showPosition->setEnabled(false);
|
||||
|
||||
m_markAllAsRelevant->setEnabled(false);
|
||||
m_markAllAsIrrelevant->setEnabled(false);
|
||||
|
||||
m_relatedQueriesBox->hide();
|
||||
|
||||
ClearAllResults();
|
||||
HideUserPosition();
|
||||
m_position = std::nullopt;
|
||||
OnSearchCompleted();
|
||||
}
|
||||
|
||||
void SampleView::OnLocationChanged(Qt::DockWidgetArea area)
|
||||
{
|
||||
if (area == Qt::RightDockWidgetArea)
|
||||
layout()->setContentsMargins(m_rightAreaMargins);
|
||||
else
|
||||
layout()->setContentsMargins(m_defaultMargins);
|
||||
}
|
||||
|
||||
void SampleView::SetResultsEdits(ResultsView & results, ResultsEdits & edits)
|
||||
{
|
||||
size_t const numRelevances = edits.GetRelevances().size();
|
||||
CHECK_EQUAL(results.Size(), numRelevances, ());
|
||||
for (size_t i = 0; i < numRelevances; ++i)
|
||||
results.Get(i).SetEditor(ResultsEdits::Editor(edits, i));
|
||||
}
|
||||
|
||||
void SampleView::OnRemoveNonFoundResult(int row)
|
||||
{
|
||||
m_nonFoundResultsEdits->Delete(row);
|
||||
}
|
||||
|
||||
void SampleView::ShowUserPosition(m2::PointD const & position)
|
||||
{
|
||||
// Clear the old position.
|
||||
HideUserPosition();
|
||||
|
||||
auto es = m_framework.GetBookmarkManager().GetEditSession();
|
||||
auto mark = es.CreateUserMark<ColoredMarkPoint>(position);
|
||||
mark->SetColor(dp::Color(200, 100, 240, 255) /* purple */);
|
||||
mark->SetRadius(8.0f);
|
||||
m_positionMarkId = mark->GetId();
|
||||
}
|
||||
|
||||
void SampleView::HideUserPosition()
|
||||
{
|
||||
if (m_positionMarkId == kml::kInvalidMarkId)
|
||||
return;
|
||||
|
||||
m_framework.GetBookmarkManager().GetEditSession().DeleteUserMark(m_positionMarkId);
|
||||
m_positionMarkId = kml::kInvalidMarkId;
|
||||
}
|
||||
102
libs/search/search_quality/assessment_tool/sample_view.hpp
Normal file
102
libs/search/search_quality/assessment_tool/sample_view.hpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
#include "edits.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
|
||||
#include "kml/type_utils.hpp"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QtCore/QMargins>
|
||||
#include <QtWidgets/QWidget>
|
||||
|
||||
class Framework;
|
||||
class QLabel;
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
class ResultsView;
|
||||
class Spinner;
|
||||
|
||||
class SampleView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using Relevance = search::Sample::Result::Relevance;
|
||||
|
||||
SampleView(QWidget * parent, Framework & framework);
|
||||
|
||||
void SetContents(search::Sample const & sample, std::optional<m2::PointD> const & position);
|
||||
void OnSearchStarted();
|
||||
void OnSearchCompleted();
|
||||
|
||||
void AddFoundResults(search::Results const & results);
|
||||
void ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries);
|
||||
|
||||
void ShowFoundResultsMarks(search::Results const & results);
|
||||
void ShowNonFoundResultsMarks(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries);
|
||||
void ClearSearchResultMarks();
|
||||
|
||||
void SetResultsEdits(ResultsEdits & resultsResultsEdits, ResultsEdits & nonFoundResultsResultsEdits);
|
||||
|
||||
void OnUselessnessChanged(bool isUseless);
|
||||
|
||||
void Clear();
|
||||
|
||||
ResultsView & GetFoundResultsView() { return *m_foundResults; }
|
||||
ResultsView & GetNonFoundResultsView() { return *m_nonFoundResults; }
|
||||
|
||||
void OnLocationChanged(Qt::DockWidgetArea area);
|
||||
|
||||
signals:
|
||||
void OnShowViewportClicked();
|
||||
void OnShowPositionClicked();
|
||||
void OnMarkAllAsRelevantClicked();
|
||||
void OnMarkAllAsIrrelevantClicked();
|
||||
|
||||
private:
|
||||
void ClearAllResults();
|
||||
void SetResultsEdits(ResultsView & results, ResultsEdits & edits);
|
||||
void OnRemoveNonFoundResult(int row);
|
||||
|
||||
void ShowUserPosition(m2::PointD const & position);
|
||||
void HideUserPosition();
|
||||
|
||||
Framework & m_framework;
|
||||
|
||||
Spinner * m_spinner = nullptr;
|
||||
|
||||
QLabel * m_query = nullptr;
|
||||
QLabel * m_langs = nullptr;
|
||||
|
||||
QListWidget * m_relatedQueries = nullptr;
|
||||
QWidget * m_relatedQueriesBox = nullptr;
|
||||
|
||||
QPushButton * m_showViewport = nullptr;
|
||||
QPushButton * m_showPosition = nullptr;
|
||||
|
||||
QPushButton * m_markAllAsRelevant = nullptr;
|
||||
QPushButton * m_markAllAsIrrelevant = nullptr;
|
||||
|
||||
QLabel * m_uselessnessLabel = nullptr;
|
||||
|
||||
ResultsView * m_foundResults = nullptr;
|
||||
QWidget * m_foundResultsBox = nullptr;
|
||||
|
||||
ResultsView * m_nonFoundResults = nullptr;
|
||||
QWidget * m_nonFoundResultsBox = nullptr;
|
||||
|
||||
ResultsEdits * m_nonFoundResultsEdits = nullptr;
|
||||
|
||||
QMargins m_rightAreaMargins;
|
||||
QMargins m_defaultMargins;
|
||||
|
||||
kml::MarkId m_positionMarkId = kml::kInvalidMarkId;
|
||||
|
||||
std::optional<m2::PointD> m_position;
|
||||
};
|
||||
77
libs/search/search_quality/assessment_tool/samples_view.cpp
Normal file
77
libs/search/search_quality/assessment_tool/samples_view.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#include "search/search_quality/assessment_tool/samples_view.hpp"
|
||||
|
||||
#include "search/search_quality/assessment_tool/helpers.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <QtGui/QAction>
|
||||
#include <QtGui/QContextMenuEvent>
|
||||
#include <QtGui/QStandardItem>
|
||||
#include <QtWidgets/QHeaderView>
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
// SamplesView::Model ------------------------------------------------------------------------------
|
||||
SamplesView::Model::Model(QWidget * parent) : QStandardItemModel(0 /* rows */, 1 /* columns */, parent) {}
|
||||
|
||||
QVariant SamplesView::Model::data(QModelIndex const & index, int role) const
|
||||
{
|
||||
auto const row = index.row();
|
||||
|
||||
if (role == Qt::DisplayRole && m_samples.IsValid())
|
||||
return QString::fromStdString(m_samples.GetLabel(row));
|
||||
if (role == Qt::BackgroundRole && m_samples.IsValid())
|
||||
{
|
||||
if (m_samples.IsChanged(row))
|
||||
return QBrush(QColor(0xFF, 0xFF, 0xC8));
|
||||
|
||||
if (m_samples.GetSearchState(row) == Context::SearchState::InQueue)
|
||||
return QBrush(QColor(0xFF, 0xCC, 0x66));
|
||||
|
||||
if (m_samples.GetSearchState(row) == Context::SearchState::Completed)
|
||||
return QBrush(QColor(0xCA, 0xFE, 0xDB));
|
||||
|
||||
return QBrush(Qt::transparent);
|
||||
}
|
||||
return QStandardItemModel::data(index, role);
|
||||
}
|
||||
|
||||
// SamplesView -------------------------------------------------------------------------------------
|
||||
SamplesView::SamplesView(QWidget * parent) : QTableView(parent)
|
||||
{
|
||||
setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
|
||||
{
|
||||
auto * header = horizontalHeader();
|
||||
header->setStretchLastSection(true /* stretch */);
|
||||
header->hide();
|
||||
}
|
||||
|
||||
m_model = new Model(this /* parent */);
|
||||
// TODO: Do not invoke virtual functions from constructor.
|
||||
setModel(m_model);
|
||||
}
|
||||
|
||||
bool SamplesView::IsSelected(size_t index) const
|
||||
{
|
||||
return selectionModel()->isRowSelected(base::checked_cast<int>(index), QModelIndex());
|
||||
}
|
||||
|
||||
void SamplesView::contextMenuEvent(QContextMenuEvent * event)
|
||||
{
|
||||
QModelIndex modelIndex = selectionModel()->currentIndex();
|
||||
if (!modelIndex.isValid())
|
||||
return;
|
||||
|
||||
int const index = modelIndex.row();
|
||||
bool const isUseless = m_model->SampleIsUseless(index);
|
||||
|
||||
QMenu menu(this);
|
||||
auto const text = std::string(isUseless ? "unmark" : "mark") + " sample as useless";
|
||||
auto const * action = menu.addAction(text.c_str());
|
||||
connect(action, &QAction::triggered, [this, index]() { emit FlipSampleUsefulness(index); });
|
||||
menu.exec(event->globalPos());
|
||||
}
|
||||
64
libs/search/search_quality/assessment_tool/samples_view.hpp
Normal file
64
libs/search/search_quality/assessment_tool/samples_view.hpp
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/assessment_tool/context.hpp"
|
||||
|
||||
#include "base/checked_cast.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include <QtGui/QStandardItemModel>
|
||||
#include <QtWidgets/QMainWindow>
|
||||
#include <QtWidgets/QTableView>
|
||||
|
||||
class SamplesView : public QTableView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SamplesView(QWidget * parent);
|
||||
|
||||
void SetSamples(ContextList::SamplesSlice const & samples) { m_model->SetSamples(samples); }
|
||||
bool IsSelected(size_t index) const;
|
||||
void OnUpdate(size_t index) { m_model->OnUpdate(index); }
|
||||
void Clear() { m_model->SetSamples(ContextList::SamplesSlice{}); }
|
||||
|
||||
// QMainWindow overrides:
|
||||
void contextMenuEvent(QContextMenuEvent * event) override;
|
||||
|
||||
signals:
|
||||
void FlipSampleUsefulness(int index);
|
||||
|
||||
private:
|
||||
class Model : public QStandardItemModel
|
||||
{
|
||||
public:
|
||||
explicit Model(QWidget * parent);
|
||||
|
||||
void SetSamples(ContextList::SamplesSlice const & samples)
|
||||
{
|
||||
removeRows(0, rowCount());
|
||||
m_samples = samples;
|
||||
if (m_samples.IsValid())
|
||||
insertRows(0, base::checked_cast<int>(m_samples.Size()));
|
||||
}
|
||||
|
||||
void OnUpdate(size_t index)
|
||||
{
|
||||
auto const ix = createIndex(base::checked_cast<int>(index) /* row */, 0 /* column */);
|
||||
|
||||
// Need to refresh view when some item is updated.
|
||||
emit dataChanged(ix, ix);
|
||||
}
|
||||
|
||||
bool SampleIsUseless(int index) const { return m_samples.IsUseless(index); }
|
||||
|
||||
// QStandardItemModel overrides:
|
||||
QVariant data(QModelIndex const & index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
private:
|
||||
ContextList::SamplesSlice m_samples;
|
||||
};
|
||||
|
||||
Model * m_model = nullptr;
|
||||
};
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
#include "search/search_quality/assessment_tool/search_request_runner.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
using namespace std;
|
||||
|
||||
SearchRequestRunner::SearchRequestRunner(Framework & framework, DataSource const & dataSource, ContextList & contexts,
|
||||
UpdateViewOnResults && updateViewOnResults,
|
||||
UpdateSampleSearchState && updateSampleSearchState)
|
||||
: m_framework(framework)
|
||||
, m_dataSource(dataSource)
|
||||
, m_contexts(contexts)
|
||||
, m_updateViewOnResults(std::move(updateViewOnResults))
|
||||
, m_updateSampleSearchState(std::move(updateSampleSearchState))
|
||||
{}
|
||||
|
||||
void SearchRequestRunner::InitiateForegroundSearch(size_t index)
|
||||
{
|
||||
RunRequest(index, false /* background */, m_foregroundTimestamp);
|
||||
}
|
||||
|
||||
void SearchRequestRunner::InitiateBackgroundSearch(size_t from, size_t to)
|
||||
{
|
||||
// 1 <= from <= to <= m_contexts.Size().
|
||||
if (from < 1 || from > to || to > m_contexts.Size())
|
||||
{
|
||||
LOG(LINFO, ("Could not initiate search in the range", from, to, "Total samples:", m_contexts.Size()));
|
||||
return;
|
||||
}
|
||||
|
||||
ResetBackgroundSearch();
|
||||
|
||||
// Convert to 0-based.
|
||||
--from;
|
||||
--to;
|
||||
m_backgroundFirstIndex = from;
|
||||
m_backgroundLastIndex = to;
|
||||
m_backgroundNumProcessed = 0;
|
||||
|
||||
for (size_t index = from; index <= to; ++index)
|
||||
{
|
||||
if (m_contexts[index].m_searchState == Context::SearchState::Untouched)
|
||||
{
|
||||
m_contexts[index].m_searchState = Context::SearchState::InQueue;
|
||||
m_backgroundQueue.push(index);
|
||||
m_updateSampleSearchState(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK(m_contexts[index].m_searchState == Context::SearchState::Completed, ());
|
||||
++m_backgroundNumProcessed;
|
||||
LOG(LINFO, ("Using results from an earlier search for request number", index + 1));
|
||||
if (m_backgroundNumProcessed == to - from + 1)
|
||||
PrintBackgroundSearchStats();
|
||||
}
|
||||
}
|
||||
|
||||
size_t const numThreads = m_framework.GetSearchAPI().GetEngine().GetNumThreads();
|
||||
for (size_t i = 0; i < numThreads; ++i)
|
||||
RunNextBackgroundRequest(m_backgroundTimestamp);
|
||||
}
|
||||
|
||||
void SearchRequestRunner::ResetForegroundSearch()
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
|
||||
++m_foregroundTimestamp;
|
||||
if (auto handle = m_foregroundQueryHandle.lock())
|
||||
handle->Cancel();
|
||||
}
|
||||
|
||||
void SearchRequestRunner::ResetBackgroundSearch()
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
|
||||
++m_backgroundTimestamp;
|
||||
|
||||
queue<size_t>().swap(m_backgroundQueue);
|
||||
m_backgroundNumProcessed = 0;
|
||||
|
||||
bool cancelledAny = false;
|
||||
for (auto const & entry : m_backgroundQueryHandles)
|
||||
{
|
||||
auto handle = entry.second.lock();
|
||||
if (handle)
|
||||
{
|
||||
handle->Cancel();
|
||||
cancelledAny = true;
|
||||
}
|
||||
}
|
||||
m_backgroundQueryHandles.clear();
|
||||
|
||||
if (cancelledAny)
|
||||
{
|
||||
for (size_t index = m_backgroundFirstIndex; index <= m_backgroundLastIndex; ++index)
|
||||
{
|
||||
if (m_contexts[index].m_searchState == Context::SearchState::InQueue)
|
||||
{
|
||||
m_contexts[index].m_searchState = Context::SearchState::Untouched;
|
||||
m_updateSampleSearchState(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_backgroundFirstIndex = kInvalidIndex;
|
||||
m_backgroundLastIndex = kInvalidIndex;
|
||||
}
|
||||
|
||||
void SearchRequestRunner::RunNextBackgroundRequest(size_t timestamp)
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
|
||||
if (m_backgroundQueue.empty())
|
||||
return;
|
||||
|
||||
size_t index = m_backgroundQueue.front();
|
||||
m_backgroundQueue.pop();
|
||||
|
||||
RunRequest(index, true /* background */, timestamp);
|
||||
}
|
||||
|
||||
void SearchRequestRunner::RunRequest(size_t index, bool background, size_t timestamp)
|
||||
{
|
||||
CHECK_THREAD_CHECKER(m_threadChecker, ());
|
||||
|
||||
auto const & context = m_contexts[index];
|
||||
auto const & sample = context.m_sample;
|
||||
|
||||
search::SearchParams params;
|
||||
sample.FillSearchParams(params);
|
||||
params.m_onResults = [=, this](search::Results const & results)
|
||||
{
|
||||
vector<optional<ResultsEdits::Relevance>> relevances;
|
||||
vector<size_t> goldenMatching;
|
||||
vector<size_t> actualMatching;
|
||||
|
||||
if (results.IsEndedNormal())
|
||||
{
|
||||
// Can't use MainModel's m_loader here due to thread-safety issues.
|
||||
search::FeatureLoader loader(m_dataSource);
|
||||
search::Matcher matcher(loader);
|
||||
|
||||
vector<search::Result> const actual(results.begin(), results.end());
|
||||
matcher.Match(sample, actual, goldenMatching, actualMatching);
|
||||
relevances.resize(actual.size());
|
||||
for (size_t i = 0; i < goldenMatching.size(); ++i)
|
||||
{
|
||||
auto const j = goldenMatching[i];
|
||||
if (j != search::Matcher::kInvalidId)
|
||||
{
|
||||
CHECK_LESS(j, relevances.size(), ());
|
||||
relevances[j] = sample.m_results[i].m_relevance;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LINFO, ("Request number", index + 1, "has been processed in the", background ? "background" : "foreground"));
|
||||
}
|
||||
|
||||
GetPlatform().RunTask(Platform::Thread::Gui,
|
||||
[this, background, timestamp, index, results, relevances, goldenMatching, actualMatching]
|
||||
{
|
||||
size_t const latestTimestamp = background ? m_backgroundTimestamp : m_foregroundTimestamp;
|
||||
if (timestamp != latestTimestamp)
|
||||
return;
|
||||
|
||||
auto & context = m_contexts[index];
|
||||
|
||||
context.m_foundResults = results;
|
||||
|
||||
if (results.IsEndMarker())
|
||||
{
|
||||
if (results.IsEndedNormal())
|
||||
context.m_searchState = Context::SearchState::Completed;
|
||||
else
|
||||
context.m_searchState = Context::SearchState::Untouched;
|
||||
m_updateSampleSearchState(index);
|
||||
}
|
||||
|
||||
if (results.IsEndedNormal())
|
||||
{
|
||||
if (!context.m_initialized)
|
||||
{
|
||||
context.m_foundResultsEdits.Reset(relevances);
|
||||
context.m_goldenMatching = goldenMatching;
|
||||
context.m_actualMatching = actualMatching;
|
||||
|
||||
{
|
||||
vector<optional<ResultsEdits::Relevance>> relevances;
|
||||
|
||||
auto & nonFound = context.m_nonFoundResults;
|
||||
CHECK(nonFound.empty(), ());
|
||||
for (size_t i = 0; i < context.m_goldenMatching.size(); ++i)
|
||||
{
|
||||
auto const j = context.m_goldenMatching[i];
|
||||
if (j != search::Matcher::kInvalidId)
|
||||
continue;
|
||||
nonFound.push_back(context.m_sample.m_results[i]);
|
||||
relevances.emplace_back(nonFound.back().m_relevance);
|
||||
}
|
||||
context.m_nonFoundResultsEdits.Reset(relevances);
|
||||
}
|
||||
|
||||
context.m_initialized = true;
|
||||
}
|
||||
|
||||
if (background)
|
||||
{
|
||||
++m_backgroundNumProcessed;
|
||||
m_backgroundQueryHandles.erase(index);
|
||||
if (m_backgroundNumProcessed == m_backgroundLastIndex - m_backgroundFirstIndex + 1)
|
||||
PrintBackgroundSearchStats();
|
||||
else
|
||||
RunNextBackgroundRequest(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
if (!background)
|
||||
m_updateViewOnResults(results);
|
||||
});
|
||||
};
|
||||
|
||||
auto & engine = m_framework.GetSearchAPI().GetEngine();
|
||||
if (background)
|
||||
m_backgroundQueryHandles[index] = engine.Search(std::move(params));
|
||||
else
|
||||
m_foregroundQueryHandle = engine.Search(std::move(params));
|
||||
}
|
||||
|
||||
void SearchRequestRunner::PrintBackgroundSearchStats() const
|
||||
{
|
||||
LOG(LINFO, ("All requests from", m_backgroundFirstIndex + 1, "to", m_backgroundLastIndex + 1, "have been processed"));
|
||||
|
||||
vector<size_t> vitals;
|
||||
vitals.reserve(m_backgroundLastIndex - m_backgroundFirstIndex + 1);
|
||||
for (size_t index = m_backgroundFirstIndex; index <= m_backgroundLastIndex; ++index)
|
||||
{
|
||||
auto const & entries = m_contexts[index].m_foundResultsEdits.GetEntries();
|
||||
bool const foundVital = any_of(entries.begin(), entries.end(), [](ResultsEdits::Entry const & e)
|
||||
{ return e.m_currRelevance == search::Sample::Result::Relevance::Vital; });
|
||||
if (foundVital)
|
||||
vitals.emplace_back(index + 1);
|
||||
}
|
||||
|
||||
LOG(LINFO, ("Vital results found:", vitals.size(), "out of", m_backgroundLastIndex - m_backgroundFirstIndex + 1));
|
||||
LOG(LINFO, ("Vital results found for these queries (1-based):", vitals));
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/assessment_tool/context.hpp"
|
||||
|
||||
#include "map/framework.hpp"
|
||||
|
||||
#include "base/thread_checker.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
|
||||
// A proxy for SearchAPI/SearchEngine.
|
||||
// This class updates the Model's |m_contexts| directly (from the main thread) and updates
|
||||
// the View via the |m_updateViewOnResults| and |m_updateSampleSearchState| callbacks.
|
||||
class SearchRequestRunner
|
||||
{
|
||||
public:
|
||||
using UpdateViewOnResults = std::function<void(search::Results const & results)>;
|
||||
using UpdateSampleSearchState = std::function<void(size_t index)>;
|
||||
|
||||
SearchRequestRunner(Framework & framework, DataSource const & dataSource, ContextList & contexts,
|
||||
UpdateViewOnResults && updateViewOnResults, UpdateSampleSearchState && updateSampleSearchState);
|
||||
|
||||
void InitiateForegroundSearch(size_t index);
|
||||
|
||||
void InitiateBackgroundSearch(size_t from, size_t to);
|
||||
|
||||
void ResetForegroundSearch();
|
||||
|
||||
void ResetBackgroundSearch();
|
||||
|
||||
private:
|
||||
static size_t constexpr kInvalidIndex = std::numeric_limits<size_t>::max();
|
||||
|
||||
// Tries to run the unprocessed request with the smallest index, if there is one.
|
||||
void RunNextBackgroundRequest(size_t timestamp);
|
||||
|
||||
void RunRequest(size_t index, bool background, size_t timestamp);
|
||||
|
||||
void PrintBackgroundSearchStats() const;
|
||||
|
||||
Framework & m_framework;
|
||||
|
||||
DataSource const & m_dataSource;
|
||||
|
||||
ContextList & m_contexts;
|
||||
|
||||
UpdateViewOnResults m_updateViewOnResults;
|
||||
UpdateSampleSearchState m_updateSampleSearchState;
|
||||
|
||||
std::weak_ptr<search::ProcessorHandle> m_foregroundQueryHandle;
|
||||
std::map<size_t, std::weak_ptr<search::ProcessorHandle>> m_backgroundQueryHandles;
|
||||
|
||||
size_t m_foregroundTimestamp = 0;
|
||||
size_t m_backgroundTimestamp = 0;
|
||||
|
||||
size_t m_backgroundFirstIndex = kInvalidIndex;
|
||||
size_t m_backgroundLastIndex = kInvalidIndex;
|
||||
std::queue<size_t> m_backgroundQueue;
|
||||
size_t m_backgroundNumProcessed = 0;
|
||||
|
||||
ThreadChecker m_threadChecker;
|
||||
};
|
||||
69
libs/search/search_quality/assessment_tool/view.hpp
Normal file
69
libs/search/search_quality/assessment_tool/view.hpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#pragma once
|
||||
#include "context.hpp"
|
||||
#include "edits.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ResultsEdits;
|
||||
class Model;
|
||||
|
||||
class View
|
||||
{
|
||||
public:
|
||||
enum class ResultType
|
||||
{
|
||||
Found,
|
||||
NonFound
|
||||
};
|
||||
|
||||
virtual ~View() = default;
|
||||
|
||||
void SetModel(Model & model) { m_model = &model; }
|
||||
|
||||
virtual void SetSamples(ContextList::SamplesSlice const & samples) = 0;
|
||||
virtual void OnSearchStarted() = 0;
|
||||
virtual void OnSearchCompleted() = 0;
|
||||
virtual void ShowSample(size_t index, search::Sample const & sample, std::optional<m2::PointD> const & position,
|
||||
bool isUseless, bool hasEdits) = 0;
|
||||
|
||||
virtual void AddFoundResults(search::Results const & results) = 0;
|
||||
virtual void ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
|
||||
std::vector<ResultsEdits::Entry> const & entries) = 0;
|
||||
|
||||
virtual void ShowMarks(Context const & context) = 0;
|
||||
|
||||
virtual void MoveViewportToResult(search::Result const & result) = 0;
|
||||
virtual void MoveViewportToResult(search::Sample::Result const & result) = 0;
|
||||
virtual void MoveViewportToRect(m2::RectD const & rect) = 0;
|
||||
|
||||
virtual void OnResultChanged(size_t sampleIndex, ResultType type, ResultsEdits::Update const & update) = 0;
|
||||
virtual void SetResultsEdits(size_t index, ResultsEdits & foundResultsEdits, ResultsEdits & nonFoundResultsEdits) = 0;
|
||||
virtual void OnSampleChanged(size_t sampleIndex, bool isUseless, bool hasEdits) = 0;
|
||||
virtual void OnSamplesChanged(bool hasEdits) = 0;
|
||||
|
||||
virtual void ShowError(std::string const & msg) = 0;
|
||||
|
||||
virtual void Clear() = 0;
|
||||
|
||||
protected:
|
||||
Model * m_model = nullptr;
|
||||
};
|
||||
|
||||
inline std::string DebugPrint(View::ResultType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case View::ResultType::Found: return "Found";
|
||||
case View::ResultType::NonFound: return "NonFound";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
333
libs/search/search_quality/clusterize-tag-values.lisp
Executable file
333
libs/search/search_quality/clusterize-tag-values.lisp
Executable file
|
|
@ -0,0 +1,333 @@
|
|||
#|
|
||||
exec /usr/bin/env sbcl --noinform --quit --eval "(defparameter *script-name* \"$0\")" --load "$0" --end-toplevel-options "$@"
|
||||
|#
|
||||
|
||||
;;; This script clusterizes values from the taginfo database and
|
||||
;;; prints information about clusters.
|
||||
|
||||
;;; Silently loads sqlite.
|
||||
(with-open-file (*standard-output* "/dev/null"
|
||||
:direction :output
|
||||
:if-exists :supersede)
|
||||
(ql:quickload "sqlite"))
|
||||
|
||||
(defun latin-char-p (char)
|
||||
(or (and (char>= char #\a) (char<= char #\z))
|
||||
(and (char>= char #\A) (char<= char #\Z))))
|
||||
|
||||
(defun starts-with (text prefix)
|
||||
"Returns non-nil if text starts with prefix."
|
||||
(and (>= (length text) (length prefix))
|
||||
(loop for u being the element of text
|
||||
for v being the element of prefix
|
||||
always (char= u v))))
|
||||
|
||||
(defun get-postcode-pattern (postcode fn)
|
||||
"Simplifies postcode in the following way:
|
||||
* all latin letters are replaced by 'A'
|
||||
* all digits are replaced by 'N'
|
||||
* hyphens and dots are replaced by a space
|
||||
* other characters are capitalized
|
||||
|
||||
This format follows https://en.wikipedia.org/wiki/List_of_postal_codes.
|
||||
"
|
||||
(let ((pattern (map 'string #'(lambda (c) (cond ((latin-char-p c) #\A)
|
||||
((digit-char-p c) #\N)
|
||||
((or (char= #\- c) (char= #\. c)) #\Space)
|
||||
(T c)))
|
||||
(string-upcase postcode))))
|
||||
(funcall fn postcode pattern)))
|
||||
|
||||
|
||||
(defun get-phone-or-flat-pattern (phone fn)
|
||||
"Simplifies phone or flat numbers in the following way:
|
||||
* all letters are replaced by 'A'
|
||||
* all digits are replaced by 'N'
|
||||
* other characters are capitalized
|
||||
"
|
||||
(let ((pattern (map 'string #'(lambda (c) (cond ((alpha-char-p c) #\A)
|
||||
((digit-char-p c) #\N)
|
||||
(T c)))
|
||||
(string-upcase phone))))
|
||||
(funcall fn phone pattern)))
|
||||
|
||||
(defun group-by (cmp list)
|
||||
"cmp -> [a] -> [[a]]
|
||||
|
||||
Groups equal adjacent elements of the list. Equality is checked with cmp.
|
||||
"
|
||||
(let ((buckets
|
||||
(reduce #'(lambda (buckets cur)
|
||||
(cond ((null buckets) (cons (list cur) nil))
|
||||
((funcall cmp (caar buckets) cur)
|
||||
(cons (cons cur (car buckets)) (cdr buckets)))
|
||||
(T (cons (list cur) buckets))))
|
||||
list :initial-value nil)))
|
||||
(reverse (mapcar #'reverse buckets))))
|
||||
|
||||
(defun split-by (fn list)
|
||||
"fn -> [a] -> [[a]]
|
||||
|
||||
Splits list by separators, where separators are defined by fn
|
||||
predicate.
|
||||
"
|
||||
(loop for e in list
|
||||
with buckets = nil
|
||||
for prev-sep = T then cur-sep
|
||||
for cur-sep = (funcall fn e)
|
||||
do (cond (cur-sep T)
|
||||
(prev-sep (push (list e) buckets))
|
||||
(T (push e (car buckets))))
|
||||
finally (return (reverse (mapcar #'reverse buckets)))))
|
||||
|
||||
(defun split-string-by (fn string)
|
||||
"fn -> string -> [string]
|
||||
|
||||
Splits string by separators, where separators are defined by fn
|
||||
predicate.
|
||||
"
|
||||
(mapcar #'(lambda (list) (concatenate 'string list))
|
||||
(split-by fn (concatenate 'list string))))
|
||||
|
||||
(defun drop-while (fn list)
|
||||
(cond ((null list) nil)
|
||||
((funcall fn (car list)) (drop-while fn (cdr list)))
|
||||
(T list)))
|
||||
|
||||
(defun take-while (fn list)
|
||||
(if (null list)
|
||||
nil
|
||||
(loop for value in list
|
||||
while (funcall fn value)
|
||||
collecting value)))
|
||||
|
||||
(defparameter *building-synonyms*
|
||||
'("building" "bldg" "bld" "bl" "unit" "block" "blk"
|
||||
"корпус" "корп" "кор" "литер" "лит" "строение" "стр" "блок" "бл"))
|
||||
|
||||
(defparameter *house-number-seps* '(#\Space #\Tab #\" #\\ #\( #\) #\. #\# #\~))
|
||||
(defparameter *house-number-groups-seps* '(#\, #\| #\; #\+))
|
||||
|
||||
(defun building-synonym-p (s)
|
||||
(find s *building-synonyms* :test #'string=))
|
||||
|
||||
(defun short-building-synonym-p (s)
|
||||
(or (string= "к" s) (string= "с" s)))
|
||||
|
||||
(defstruct token value type)
|
||||
|
||||
(defun get-char-type (c)
|
||||
(cond ((digit-char-p c) :number)
|
||||
((find c *house-number-seps* :test #'char=) :separator)
|
||||
((find c *house-number-groups-seps* :test #'char=) :group-separator)
|
||||
((char= c #\-) :hyphen)
|
||||
((char= c #\/) :slash)
|
||||
(T :string)))
|
||||
|
||||
(defun transform-string-token (fn value)
|
||||
"Transforms building token value into one or more tokens in
|
||||
accordance to its value. For example, 'литA' is transformed to
|
||||
tokens 'лит' (building part) and 'А' (letter).
|
||||
"
|
||||
(flet ((emit (value type) (funcall fn value type)))
|
||||
(cond ((building-synonym-p value)
|
||||
(emit value :building-part))
|
||||
((and (= 4 (length value))
|
||||
(starts-with value "лит"))
|
||||
(emit (subseq value 0 3) :building-part)
|
||||
(emit (subseq value 3) :letter))
|
||||
((and (= 2 (length value))
|
||||
(short-building-synonym-p (subseq value 0 1)))
|
||||
(emit (subseq value 0 1) :building-part)
|
||||
(emit (subseq value 1) :letter))
|
||||
((= 1 (length value))
|
||||
(emit value (if (short-building-synonym-p value)
|
||||
:letter-or-building-part
|
||||
:letter)))
|
||||
(T (emit value :string)))))
|
||||
|
||||
(defun tokenize-house-number (house-number)
|
||||
"house-number => [token]"
|
||||
(let ((parts (group-by #'(lambda (lhs rhs)
|
||||
(eq (get-char-type lhs) (get-char-type rhs)))
|
||||
(string-downcase house-number)))
|
||||
(tokens nil))
|
||||
(flet ((add-token (value type) (push (make-token :value value :type type) tokens)))
|
||||
(dolist (part parts)
|
||||
(let ((value (concatenate 'string part))
|
||||
(type (get-char-type (car part))))
|
||||
(case type
|
||||
(:string (transform-string-token #'add-token value))
|
||||
(:separator T)
|
||||
(otherwise (add-token value type)))))
|
||||
(loop for prev = nil then curr
|
||||
for curr in tokens
|
||||
do (when (eq :letter-or-building-part (token-type curr))
|
||||
(cond ((null prev)
|
||||
(setf (token-type curr) :letter))
|
||||
((eq :number (token-type prev))
|
||||
(setf (token-type curr) :building-part)))))
|
||||
(reverse tokens))))
|
||||
|
||||
(defun house-number-with-optional-suffix-p (tokens)
|
||||
(case (length tokens)
|
||||
(1 (eq (token-type (first tokens)) :number))
|
||||
(2 (let ((first-type (token-type (first tokens)))
|
||||
(second-type (token-type (second tokens))))
|
||||
(and (eq first-type :number)
|
||||
(or (eq second-type :string)
|
||||
(eq second-type :letter)
|
||||
(eq second-type :letter-or-building-part)))))
|
||||
(otherwise nil)))
|
||||
|
||||
(defun get-house-number-sub-numbers (house-number)
|
||||
"house-number => [[token]]
|
||||
|
||||
As house-number can be actually a collection of separated house
|
||||
numbers, this function returns a list of possible house numbers.
|
||||
Current implementation splits house number if and only if
|
||||
house-number matches the following rule:
|
||||
|
||||
NUMBERS ::= (NUMBER STRING-SUFFIX?) | (NUMBER STRING-SUFFIX?) SEP NUMBERS
|
||||
"
|
||||
(let* ((tokens (tokenize-house-number house-number))
|
||||
(groups (split-by #'(lambda (token) (eq :group-separator (token-type token))) tokens)))
|
||||
(if (every #'house-number-with-optional-suffix-p groups)
|
||||
groups
|
||||
(list tokens))))
|
||||
|
||||
(defun join-house-number-tokens (tokens)
|
||||
"Joins token values with spaces."
|
||||
(format nil "~{~a~^ ~}" (mapcar #'token-value tokens)))
|
||||
|
||||
(defun join-house-number-parse (tokens)
|
||||
"Joins parsed house number tokens with spaces."
|
||||
(format nil "~{~a~^ ~}"
|
||||
(mapcar #'(lambda (token)
|
||||
(let ((token-type (token-type token))
|
||||
(token-value (token-value token)))
|
||||
(case token-type
|
||||
(:number "N")
|
||||
(:building-part "B")
|
||||
(:letter "L")
|
||||
(:letter-or-building-part "U")
|
||||
(:string "S")
|
||||
((:hyphen :slash :group-separator) token-value)
|
||||
(otherwise (assert NIL NIL (format nil "Unknown token type: ~a"
|
||||
token-type))))))
|
||||
tokens)))
|
||||
|
||||
(defun get-house-number-pattern (house-number fn)
|
||||
(dolist (number (get-house-number-sub-numbers house-number))
|
||||
(let ((house-number (join-house-number-tokens number))
|
||||
(pattern (join-house-number-parse number)))
|
||||
(funcall fn house-number pattern))))
|
||||
|
||||
(defun get-house-number-strings (house-number fn)
|
||||
"Returns all strings from the house number."
|
||||
(dolist (number (get-house-number-sub-numbers house-number))
|
||||
(dolist (string (mapcar #'token-value
|
||||
(remove-if-not #'(lambda (token)
|
||||
(case (token-type token)
|
||||
((:string
|
||||
:letter
|
||||
:letter-or-building-part
|
||||
:building-part) T)
|
||||
(otherwise nil)))
|
||||
number)))
|
||||
(funcall fn string string))))
|
||||
|
||||
(defstruct type-settings
|
||||
pattern-simplifier
|
||||
field-name)
|
||||
|
||||
(defparameter *value-type-settings*
|
||||
`(:postcode ,(make-type-settings :pattern-simplifier #'get-postcode-pattern
|
||||
:field-name "addr:postcode")
|
||||
:phone ,(make-type-settings :pattern-simplifier #'get-phone-or-flat-pattern
|
||||
:field-name "contact:phone")
|
||||
:flat ,(make-type-settings :pattern-simplifier #'get-phone-or-flat-pattern
|
||||
:field-name "addr:flats")
|
||||
:house-number ,(make-type-settings :pattern-simplifier #'get-house-number-pattern
|
||||
:field-name "addr:housenumber")
|
||||
:house-number-strings ,(make-type-settings
|
||||
:pattern-simplifier #'get-house-number-strings
|
||||
:field-name "addr:housenumber")))
|
||||
|
||||
(defstruct cluster
|
||||
"A cluster of values with the same pattern, i.e. all six-digits
|
||||
series or all four-digits-two-letters series."
|
||||
(key "") (num-samples 0) (samples nil))
|
||||
|
||||
(defun add-sample (cluster sample &optional (count 1))
|
||||
"Adds a value sample to a cluster of samples."
|
||||
(push sample (cluster-samples cluster))
|
||||
(incf (cluster-num-samples cluster) count))
|
||||
|
||||
(defparameter *seps* '(#\Space #\Tab #\Newline #\Backspace #\Return #\Rubout #\Linefeed #\"))
|
||||
|
||||
(defun trim (string)
|
||||
"Removes leading and trailing garbage from a string."
|
||||
(string-trim *seps* string))
|
||||
|
||||
(defun get-pattern-clusters (values simplifier)
|
||||
"Constructs a list of clusters by a list of values."
|
||||
(let ((table (make-hash-table :test #'equal))
|
||||
(clusters nil))
|
||||
(loop for (value count) in values
|
||||
do (funcall simplifier (trim value)
|
||||
#'(lambda (value pattern)
|
||||
(let ((cluster (gethash pattern table (make-cluster :key pattern))))
|
||||
(add-sample cluster value count)
|
||||
(setf (gethash pattern table) cluster)))))
|
||||
(maphash #'(lambda (pattern cluster)
|
||||
(declare (ignore pattern))
|
||||
(push cluster clusters))
|
||||
table)
|
||||
clusters))
|
||||
|
||||
(defun make-keyword (name) (values (intern (string-upcase name) "KEYWORD")))
|
||||
|
||||
(when (/= 3 (length *posix-argv*))
|
||||
(format t "Usage: ~a value path-to-taginfo-db.db~%" *script-name*)
|
||||
(format t "~%value can be one of the following:~%")
|
||||
(format t "~{ ~a~%~}"
|
||||
(loop for field in *value-type-settings* by #'cddr collecting (string-downcase field)))
|
||||
(exit :code -1))
|
||||
|
||||
(defparameter *value-type* (second *posix-argv*))
|
||||
(defparameter *db-path* (third *posix-argv*))
|
||||
|
||||
(defparameter *type-settings* (getf *value-type-settings* (make-keyword *value-type*)))
|
||||
|
||||
(defparameter *values*
|
||||
(sqlite:with-open-database (db *db-path*)
|
||||
(let ((query (format nil "select value, count_all from tags where key=\"~a\";"
|
||||
(type-settings-field-name *type-settings*))))
|
||||
(sqlite:execute-to-list db query))))
|
||||
|
||||
(defparameter *clusters*
|
||||
(sort (get-pattern-clusters *values* (type-settings-pattern-simplifier *type-settings*))
|
||||
#'(lambda (lhs rhs) (> (cluster-num-samples lhs)
|
||||
(cluster-num-samples rhs)))))
|
||||
|
||||
(defparameter *total*
|
||||
(loop for cluster in *clusters*
|
||||
summing (cluster-num-samples cluster)))
|
||||
|
||||
(format t "Total: ~a~%" *total*)
|
||||
(loop for cluster in *clusters*
|
||||
for prev-prefix-sum = 0 then curr-prefix-sum
|
||||
for curr-prefix-sum = (+ prev-prefix-sum (cluster-num-samples cluster))
|
||||
do (let ((key (cluster-key cluster))
|
||||
(num-samples (cluster-num-samples cluster))
|
||||
(samples (cluster-samples cluster)))
|
||||
; Prints number of values in a cluster, accumulated
|
||||
; percent of values clustered so far, simplified version
|
||||
; of a value and examples of values.
|
||||
(format t "~a (~2$%) ~a [~{~a~^, ~}~:[~;, ...~]]~%"
|
||||
num-samples
|
||||
(* 100 (/ curr-prefix-sum *total*))
|
||||
key
|
||||
(subseq samples 0 (min (length samples) 5))
|
||||
(> num-samples 5))))
|
||||
89
libs/search/search_quality/download-maps.sh
Executable file
89
libs/search/search_quality/download-maps.sh
Executable file
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Downloads all maps necessary for learning to rank to the current
|
||||
# directory.
|
||||
|
||||
ALL=
|
||||
VERSION=
|
||||
BASE="http://direct.mapswithme.com/direct"
|
||||
|
||||
display_usage() {
|
||||
echo "Usage: $0 -v [version] -a -h"
|
||||
echo " -v version of maps to download"
|
||||
echo " -a download all maps of the specified version"
|
||||
echo " -c continue getting partially-downloaded files; new files on the server must be exactly the same as the ones in the previous attempt"
|
||||
echo " -h display this message"
|
||||
}
|
||||
|
||||
while getopts ":acv:h" opt
|
||||
do
|
||||
case "$opt" in
|
||||
a) ALL=1
|
||||
;;
|
||||
c) RESUME_PARTIAL="-c"
|
||||
;;
|
||||
v) VERSION="$OPTARG"
|
||||
;;
|
||||
h) display_usage
|
||||
exit -1
|
||||
;;
|
||||
\?) echo "Invalid option: -$OPTARG" 1>&2
|
||||
display_usage
|
||||
exit -1
|
||||
;;
|
||||
:) echo "Option -$OPTARG requires an argument" 1>&2
|
||||
display_usage
|
||||
exit -1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$VERSION" ]
|
||||
then
|
||||
echo "Version of maps is not specified." 1>&2
|
||||
exit -1
|
||||
fi
|
||||
|
||||
if ! curl "$BASE/" 2>/dev/null |
|
||||
sed -n 's/^.*href="\(.*\)\/".*$/\1/p' |
|
||||
grep -v "^../$" | grep -q "$VERSION"
|
||||
then
|
||||
echo "Invalid version: $VERSION" 1>&2
|
||||
exit -1
|
||||
fi
|
||||
|
||||
NAMES=("Australia_Brisbane.mwm"
|
||||
"Belarus_Hrodna*.mwm"
|
||||
"Belarus_Minsk*.mwm"
|
||||
"Canada_Ontario_London.mwm"
|
||||
"Canada_Quebek_Montreal.mwm"
|
||||
"Germany_*.mwm"
|
||||
"Russia_*.mwm"
|
||||
"UK_England_*.mwm"
|
||||
"US_California_*.mwm"
|
||||
"US_Maryland_*.mwm")
|
||||
|
||||
DIR="$BASE/$VERSION"
|
||||
|
||||
if [ "$ALL" ]
|
||||
then
|
||||
echo "Downloading all maps..."
|
||||
|
||||
files=$(curl "$DIR/" 2>/dev/null | sed -n 's/^.*href="\(.*\.mwm\)".*$/\1/p')
|
||||
|
||||
set -e
|
||||
set -x
|
||||
for file in $files
|
||||
do
|
||||
wget $RESUME_PARTIAL -np -nd "$DIR/$file"
|
||||
done
|
||||
else
|
||||
echo "Downloading maps..."
|
||||
|
||||
set -e
|
||||
set -x
|
||||
for name in ${NAMES[@]}
|
||||
do
|
||||
wget $RESUME_PARTIAL -r -np -nd -A "$name" "$DIR/"
|
||||
done
|
||||
fi
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
project(features_collector_tool)
|
||||
|
||||
set(SRC features_collector_tool.cpp)
|
||||
|
||||
omim_add_executable(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_quality
|
||||
search_tests_support
|
||||
gflags::gflags
|
||||
)
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
#include "search/search_quality/helpers.hpp"
|
||||
#include "search/search_quality/matcher.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "search/search_tests_support/test_search_engine.hpp"
|
||||
#include "search/search_tests_support/test_search_request.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
#include "search/ranking_info.hpp"
|
||||
#include "search/result.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/data_source.hpp"
|
||||
|
||||
#include "platform/platform_tests_support/helpers.hpp"
|
||||
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
using namespace search::search_quality;
|
||||
using namespace search::tests_support;
|
||||
using namespace search;
|
||||
using namespace std;
|
||||
|
||||
DEFINE_int32(num_threads, 1, "Number of search engine threads");
|
||||
DEFINE_string(data_path, "", "Path to data directory (resources dir)");
|
||||
DEFINE_string(mwm_path, "", "Path to mwm files (writable dir)");
|
||||
DEFINE_string(stats_path, "", "Path to store stats about queries results (default: stderr)");
|
||||
DEFINE_string(json_in, "", "Path to the json file with samples (default: stdin)");
|
||||
|
||||
struct Stats
|
||||
{
|
||||
// Indexes of not-found VITAL or RELEVANT results.
|
||||
vector<size_t> m_notFound;
|
||||
};
|
||||
|
||||
void GetContents(istream & is, string & contents)
|
||||
{
|
||||
string line;
|
||||
while (getline(is, line))
|
||||
{
|
||||
contents.append(line);
|
||||
contents.push_back('\n');
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayStats(ostream & os, vector<Sample> const & samples, vector<Stats> const & stats)
|
||||
{
|
||||
auto const n = samples.size();
|
||||
ASSERT_EQUAL(stats.size(), n, ());
|
||||
|
||||
size_t numWarnings = 0;
|
||||
for (auto const & stat : stats)
|
||||
if (!stat.m_notFound.empty())
|
||||
++numWarnings;
|
||||
|
||||
if (numWarnings == 0)
|
||||
{
|
||||
os << "All " << stats.size() << " queries are OK." << endl;
|
||||
return;
|
||||
}
|
||||
|
||||
os << numWarnings << " warnings." << endl;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (stats[i].m_notFound.empty())
|
||||
continue;
|
||||
os << "Query #" << i + 1 << " \"" << strings::ToUtf8(samples[i].m_query) << "\":" << endl;
|
||||
for (auto const & j : stats[i].m_notFound)
|
||||
os << "Not found: " << DebugPrint(samples[i].m_results[j]) << endl;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
platform::tests_support::ChangeMaxNumberOfOpenFiles(kMaxOpenFiles);
|
||||
CheckLocale();
|
||||
|
||||
gflags::SetUsageMessage("Features collector tool.");
|
||||
gflags::ParseCommandLineFlags(&argc, &argv, true);
|
||||
|
||||
SetPlatformDirs(FLAGS_data_path, FLAGS_mwm_path);
|
||||
|
||||
classificator::Load();
|
||||
|
||||
FrozenDataSource dataSource;
|
||||
InitDataSource(dataSource, "" /* mwmListPath */);
|
||||
|
||||
auto engine = InitSearchEngine(dataSource, "en" /* locale */, FLAGS_num_threads);
|
||||
engine->InitAffiliations();
|
||||
|
||||
vector<Sample> samples;
|
||||
{
|
||||
string lines;
|
||||
if (FLAGS_json_in.empty())
|
||||
{
|
||||
GetContents(cin, lines);
|
||||
}
|
||||
else
|
||||
{
|
||||
ifstream ifs(FLAGS_json_in);
|
||||
if (!ifs.is_open())
|
||||
{
|
||||
cerr << "Can't open input json file." << endl;
|
||||
return -1;
|
||||
}
|
||||
GetContents(ifs, lines);
|
||||
}
|
||||
|
||||
if (!Sample::DeserializeFromJSONLines(lines, samples))
|
||||
{
|
||||
cerr << "Can't parse input json file." << endl;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
vector<Stats> stats(samples.size());
|
||||
FeatureLoader loader(dataSource);
|
||||
Matcher matcher(loader);
|
||||
|
||||
vector<unique_ptr<TestSearchRequest>> requests;
|
||||
requests.reserve(samples.size());
|
||||
|
||||
for (auto const & sample : samples)
|
||||
{
|
||||
search::SearchParams params;
|
||||
sample.FillSearchParams(params);
|
||||
params.m_batchSize = 100;
|
||||
params.m_maxNumResults = 300;
|
||||
requests.push_back(make_unique<TestSearchRequest>(*engine, params));
|
||||
requests.back()->Start();
|
||||
}
|
||||
|
||||
cout << "SampleId,";
|
||||
RankingInfo::PrintCSVHeader(cout);
|
||||
cout << ",Relevance" << endl;
|
||||
for (size_t i = 0; i < samples.size(); ++i)
|
||||
{
|
||||
requests[i]->Wait();
|
||||
auto const & sample = samples[i];
|
||||
auto const & results = requests[i]->Results();
|
||||
|
||||
vector<size_t> goldenMatching;
|
||||
vector<size_t> actualMatching;
|
||||
matcher.Match(sample, results, goldenMatching, actualMatching);
|
||||
|
||||
for (size_t j = 0; j < results.size(); ++j)
|
||||
{
|
||||
if (results[j].GetResultType() != Result::Type::Feature)
|
||||
continue;
|
||||
if (actualMatching[j] == Matcher::kInvalidId)
|
||||
continue;
|
||||
|
||||
auto const & info = results[j].GetRankingInfo();
|
||||
cout << i << ",";
|
||||
info.ToCSV(cout);
|
||||
|
||||
auto const relevance = sample.m_results[actualMatching[j]].m_relevance;
|
||||
cout << "," << DebugPrint(relevance) << endl;
|
||||
}
|
||||
|
||||
auto & s = stats[i];
|
||||
for (size_t j = 0; j < goldenMatching.size(); ++j)
|
||||
{
|
||||
auto const wasNotFound = goldenMatching[j] == Matcher::kInvalidId ||
|
||||
goldenMatching[j] >= search::SearchParams::kDefaultNumResultsEverywhere;
|
||||
auto const isRelevant = sample.m_results[j].m_relevance == Sample::Result::Relevance::Relevant ||
|
||||
sample.m_results[j].m_relevance == Sample::Result::Relevance::Vital;
|
||||
if (wasNotFound && isRelevant)
|
||||
s.m_notFound.push_back(j);
|
||||
}
|
||||
requests[i].reset();
|
||||
}
|
||||
|
||||
if (FLAGS_stats_path.empty())
|
||||
{
|
||||
cerr << string(34, '=') << " Statistics " << string(34, '=') << endl;
|
||||
DisplayStats(cerr, samples, stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
ofstream ofs(FLAGS_stats_path);
|
||||
if (!ofs.is_open())
|
||||
{
|
||||
cerr << "Can't open output file for stats." << endl;
|
||||
return -1;
|
||||
}
|
||||
DisplayStats(ofs, samples, stats);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
135
libs/search/search_quality/gen-samples.lisp
Executable file
135
libs/search/search_quality/gen-samples.lisp
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
#|
|
||||
exec /usr/bin/env sbcl --noinform --quit --load "$0" --end-toplevel-options "$@"
|
||||
|#
|
||||
|
||||
;;; Silently loads :cl-json.
|
||||
(with-open-file (*standard-output* "/dev/null"
|
||||
:direction :output
|
||||
:if-exists :supersede)
|
||||
(ql:quickload "cl-json"))
|
||||
|
||||
(defparameter *samples* nil)
|
||||
|
||||
(defparameter *minx* -180)
|
||||
(defparameter *maxx* 180)
|
||||
(defparameter *miny* -180)
|
||||
(defparameter *maxy* 180)
|
||||
|
||||
(defun deg-to-rad (deg) (/ (* deg pi) 180))
|
||||
(defun rad-to-deg (rad) (/ (* rad 180) pi))
|
||||
|
||||
(defun clamp (value min max)
|
||||
(cond ((< value min) min)
|
||||
((> value max) max)
|
||||
(t value)))
|
||||
|
||||
(defun clamp-x (x) (clamp x *minx* *maxx*))
|
||||
(defun clamp-y (y) (clamp y *miny* *maxy*))
|
||||
|
||||
(defun lon-to-x (lon) lon)
|
||||
(defun lat-to-y (lat)
|
||||
(let* ((sinx (sin (deg-to-rad (clamp lat -86.0 86.0))))
|
||||
(result (rad-to-deg (* 0.5
|
||||
(log (/ (+ 1.0 sinx)
|
||||
(- 1.0 sinx)))))))
|
||||
(clamp-y result)))
|
||||
|
||||
(defclass pos () ((x :initarg :x)
|
||||
(y :initarg :y)))
|
||||
(defclass viewport () ((minx :initarg :minx)
|
||||
(miny :initarg :miny)
|
||||
(maxx :initarg :maxx)
|
||||
(maxy :initarg :maxy)))
|
||||
|
||||
(defun position-x-y (x y)
|
||||
(assert (and (>= x *minx*) (<= x *maxx*)))
|
||||
(assert (and (>= y *miny*) (<= y *maxy*)))
|
||||
(make-instance 'pos :x x :y y))
|
||||
|
||||
(defun position-lat-lon (lat lon)
|
||||
(position-x-y (lon-to-x lon) (lat-to-y lat)))
|
||||
|
||||
(defun viewport (&key minx miny maxx maxy)
|
||||
(assert (<= minx maxx))
|
||||
(assert (<= miny maxy))
|
||||
(make-instance 'viewport :minx minx :maxx maxx :miny miny :maxy maxy))
|
||||
|
||||
(defclass result ()
|
||||
((name :initarg :name)
|
||||
(relevancy :initarg :relevancy)
|
||||
(types :initarg :types)
|
||||
(position :initarg :position)
|
||||
(house-number :initarg :house-number)))
|
||||
|
||||
(defun make-result (relevancy name types position &key (house-number ""))
|
||||
(make-instance 'result
|
||||
:name name
|
||||
:relevancy relevancy
|
||||
:types types
|
||||
:position position
|
||||
:house-number house-number))
|
||||
|
||||
(defmacro vital (&rest args)
|
||||
`(make-result 'vital ,@args))
|
||||
|
||||
(defmacro relevant (&rest args)
|
||||
`(make-result 'relevant ,@args))
|
||||
|
||||
(defmacro irrelevant (&rest args)
|
||||
`(make-result 'irrelevant ,@args))
|
||||
|
||||
(defmacro harmful (&rest args)
|
||||
`(make-result 'harmful ,@args))
|
||||
|
||||
(defclass sample ()
|
||||
((query :initarg :query)
|
||||
(locale :initarg :locale)
|
||||
(position :initarg :position)
|
||||
(viewport :initarg :viewport)
|
||||
(results :initarg :results)))
|
||||
|
||||
(defun make-sample (query locale position viewport results)
|
||||
(make-instance 'sample
|
||||
:query query
|
||||
:locale locale
|
||||
:position position
|
||||
:viewport viewport
|
||||
:results results))
|
||||
|
||||
(defmacro with-gensyms ((&rest syms) &rest body)
|
||||
`(let ,(loop for sym in syms
|
||||
collecting `(,sym (gensym)))
|
||||
,@body))
|
||||
|
||||
(defmacro defsample (&rest args)
|
||||
`(push (make-sample ,@args) *samples*))
|
||||
|
||||
(defmacro scoped-samples ((locale position viewport) &rest body)
|
||||
(with-gensyms (ls ps vs)
|
||||
`(let ((,ls ,locale)
|
||||
(,ps ,position)
|
||||
(,vs ,viewport))
|
||||
(flet ((def (query results)
|
||||
(defsample query ,ls ,ps ,vs results)))
|
||||
,@body))))
|
||||
|
||||
(defun power-set (seq)
|
||||
(unless seq (return-from power-set '(())))
|
||||
(let ((x (car seq))
|
||||
(ps (power-set (cdr seq))))
|
||||
(concatenate 'list ps (mapcar #'(lambda (xs) (cons x xs)) ps))))
|
||||
|
||||
(defun join-strings (strings)
|
||||
"Joins a list of strings with spaces between them."
|
||||
(with-output-to-string (s)
|
||||
(format s "~{~a~^ ~}" strings)))
|
||||
|
||||
;;; Loads samples specification from standard input.
|
||||
(load *standard-input*)
|
||||
|
||||
(format *error-output* "Num samples: ~a~%" (length *samples*))
|
||||
(format *error-output* "Num results: ~a~%"
|
||||
(loop for sample in *samples*
|
||||
summing (length (slot-value sample 'results))))
|
||||
|
||||
(format t "~{~a~%~}" (mapcar #'json:encode-json-to-string (reverse *samples*)))
|
||||
157
libs/search/search_quality/helpers.cpp
Normal file
157
libs/search/search_quality/helpers.cpp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#include "search/search_quality/helpers.hpp"
|
||||
|
||||
#include "indexer/data_source.hpp"
|
||||
|
||||
#include "platform/local_country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/reader.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
namespace search
|
||||
{
|
||||
namespace search_quality
|
||||
{
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
uint64_t ReadVersionFromHeader(platform::LocalCountryFile const & mwm)
|
||||
{
|
||||
vector<string> const kSpecialFiles = {WORLD_FILE_NAME, WORLD_COASTS_FILE_NAME};
|
||||
for (auto const & name : kSpecialFiles)
|
||||
if (mwm.GetCountryName() == name)
|
||||
return mwm.GetVersion();
|
||||
|
||||
return version::MwmVersion::Read(FilesContainerR(mwm.GetPath(MapFileType::Map))).GetVersion();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void CheckLocale()
|
||||
{
|
||||
string const kJson = "{\"coord\":123.456}";
|
||||
string const kErrorMsg = "Bad locale. Consider setting LC_ALL=C";
|
||||
|
||||
double coord;
|
||||
{
|
||||
base::Json root(kJson.c_str());
|
||||
FromJSONObject(root.get(), "coord", coord);
|
||||
}
|
||||
|
||||
string line;
|
||||
{
|
||||
auto root = base::NewJSONObject();
|
||||
ToJSONObject(*root, "coord", coord);
|
||||
|
||||
unique_ptr<char, JSONFreeDeleter> buffer(json_dumps(root.get(), JSON_COMPACT));
|
||||
|
||||
line.append(buffer.get());
|
||||
}
|
||||
|
||||
CHECK_EQUAL(line, kJson, (kErrorMsg));
|
||||
|
||||
{
|
||||
string const kTest = "123.456";
|
||||
double value;
|
||||
VERIFY(strings::to_double(kTest, value), (kTest));
|
||||
CHECK_EQUAL(strings::to_string(value), kTest, (kErrorMsg));
|
||||
}
|
||||
}
|
||||
|
||||
void ReadStringsFromFile(string const & path, vector<string> & result)
|
||||
{
|
||||
ifstream stream(path.c_str());
|
||||
CHECK(stream.is_open(), ("Can't open", path));
|
||||
|
||||
string s;
|
||||
while (getline(stream, s))
|
||||
{
|
||||
strings::Trim(s);
|
||||
if (!s.empty())
|
||||
result.emplace_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
void SetPlatformDirs(string const & dataPath, string const & mwmPath)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
|
||||
if (!dataPath.empty())
|
||||
platform.SetResourceDir(dataPath);
|
||||
|
||||
if (!mwmPath.empty())
|
||||
platform.SetWritableDirForTests(mwmPath);
|
||||
|
||||
LOG(LINFO, ("writable dir =", platform.WritableDir()));
|
||||
LOG(LINFO, ("resources dir =", platform.ResourcesDir()));
|
||||
}
|
||||
|
||||
void InitViewport(string viewportName, m2::RectD & viewport)
|
||||
{
|
||||
map<string, m2::RectD> const kViewports = {{"default", m2::RectD(m2::PointD(0.0, 0.0), m2::PointD(1.0, 1.0))},
|
||||
{"moscow", mercator::RectByCenterLatLonAndSizeInMeters(55.7, 37.7, 5000)},
|
||||
{"london", mercator::RectByCenterLatLonAndSizeInMeters(51.5, 0.0, 5000)},
|
||||
{"zurich", mercator::RectByCenterLatLonAndSizeInMeters(47.4, 8.5, 5000)}};
|
||||
|
||||
auto it = kViewports.find(viewportName);
|
||||
if (it == kViewports.end())
|
||||
{
|
||||
LOG(LINFO, ("Unknown viewport name:", viewportName, "; setting to default"));
|
||||
viewportName = "default";
|
||||
it = kViewports.find(viewportName);
|
||||
}
|
||||
CHECK(it != kViewports.end(), ());
|
||||
viewport = it->second;
|
||||
LOG(LINFO, ("Viewport is set to:", viewportName, DebugPrint(viewport)));
|
||||
}
|
||||
|
||||
void InitDataSource(FrozenDataSource & dataSource, string const & mwmListPath)
|
||||
{
|
||||
vector<platform::LocalCountryFile> mwms;
|
||||
if (!mwmListPath.empty())
|
||||
{
|
||||
vector<string> availableMwms;
|
||||
ReadStringsFromFile(mwmListPath, availableMwms);
|
||||
for (auto const & countryName : availableMwms)
|
||||
mwms.emplace_back(GetPlatform().WritableDir(), platform::CountryFile(countryName), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
platform::FindAllLocalMapsAndCleanup(numeric_limits<int64_t>::max() /* the latest version */, mwms);
|
||||
}
|
||||
|
||||
LOG(LINFO, ("Initializing the data source with the following mwms:"));
|
||||
for (auto & mwm : mwms)
|
||||
{
|
||||
mwm.SyncWithDisk();
|
||||
LOG(LINFO, (mwm.GetCountryName(), ReadVersionFromHeader(mwm)));
|
||||
dataSource.RegisterMap(mwm);
|
||||
}
|
||||
LOG(LINFO, ());
|
||||
}
|
||||
|
||||
unique_ptr<search::tests_support::TestSearchEngine> InitSearchEngine(DataSource & dataSource, string const & locale,
|
||||
size_t numThreads)
|
||||
{
|
||||
search::Engine::Params params;
|
||||
params.m_locale = locale;
|
||||
params.m_numThreads = base::checked_cast<size_t>(numThreads);
|
||||
|
||||
return make_unique<search::tests_support::TestSearchEngine>(dataSource, params);
|
||||
}
|
||||
} // namespace search_quality
|
||||
} // namespace search
|
||||
35
libs/search/search_quality/helpers.hpp
Normal file
35
libs/search/search_quality/helpers.hpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_tests_support/test_search_engine.hpp"
|
||||
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class DataSource;
|
||||
class FrozenDataSource;
|
||||
|
||||
namespace search
|
||||
{
|
||||
namespace search_quality
|
||||
{
|
||||
// todo(@m) We should not need that much.
|
||||
size_t constexpr kMaxOpenFiles = 4000;
|
||||
|
||||
void CheckLocale();
|
||||
|
||||
void ReadStringsFromFile(std::string const & path, std::vector<std::string> & result);
|
||||
|
||||
void SetPlatformDirs(std::string const & dataPath, std::string const & mwmPath);
|
||||
|
||||
void InitViewport(std::string viewportName, m2::RectD & viewport);
|
||||
|
||||
void InitDataSource(FrozenDataSource & dataSource, std::string const & mwmListPath);
|
||||
|
||||
std::unique_ptr<search::tests_support::TestSearchEngine> InitSearchEngine(DataSource & dataSource,
|
||||
std::string const & locale,
|
||||
size_t numThreads);
|
||||
} // namespace search_quality
|
||||
} // namespace search
|
||||
105
libs/search/search_quality/helpers_json.cpp
Normal file
105
libs/search/search_quality/helpers_json.cpp
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#include "search/search_quality/helpers_json.hpp"
|
||||
|
||||
namespace m2
|
||||
{
|
||||
using std::string;
|
||||
|
||||
namespace
|
||||
{
|
||||
void ParsePoint(json_t * root, m2::PointD & point)
|
||||
{
|
||||
FromJSONObject(root, "x", point.x);
|
||||
FromJSONObject(root, "y", point.y);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void FromJSONObject(json_t * root, char const * field, RectD & rect)
|
||||
{
|
||||
json_t * r = base::GetJSONObligatoryField(root, field);
|
||||
double minX, minY, maxX, maxY;
|
||||
FromJSONObject(r, "minx", minX);
|
||||
FromJSONObject(r, "miny", minY);
|
||||
FromJSONObject(r, "maxx", maxX);
|
||||
FromJSONObject(r, "maxy", maxY);
|
||||
rect.setMinX(minX);
|
||||
rect.setMinY(minY);
|
||||
rect.setMaxX(maxX);
|
||||
rect.setMaxY(maxY);
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, char const * field, RectD const & rect)
|
||||
{
|
||||
auto json = base::NewJSONObject();
|
||||
ToJSONObject(*json, "minx", rect.minX());
|
||||
ToJSONObject(*json, "miny", rect.minY());
|
||||
ToJSONObject(*json, "maxx", rect.maxX());
|
||||
ToJSONObject(*json, "maxy", rect.maxY());
|
||||
json_object_set_new(&root, field, json.release());
|
||||
}
|
||||
|
||||
void FromJSONObject(json_t * root, string const & field, RectD & rect)
|
||||
{
|
||||
FromJSONObject(root, field.c_str(), rect);
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, string const & field, RectD const & rect)
|
||||
{
|
||||
ToJSONObject(root, field.c_str(), rect);
|
||||
}
|
||||
|
||||
void FromJSONObject(json_t * root, char const * field, PointD & point)
|
||||
{
|
||||
json_t * p = base::GetJSONObligatoryField(root, field);
|
||||
ParsePoint(p, point);
|
||||
}
|
||||
|
||||
void FromJSONObject(json_t * root, string const & field, PointD & point)
|
||||
{
|
||||
FromJSONObject(root, field.c_str(), point);
|
||||
}
|
||||
|
||||
void FromJSONObjectOptional(json_t * root, char const * field, std::optional<PointD> & point)
|
||||
{
|
||||
json_t * p = base::GetJSONOptionalField(root, field);
|
||||
if (!p || base::JSONIsNull(p))
|
||||
{
|
||||
point = std::nullopt;
|
||||
return;
|
||||
}
|
||||
|
||||
PointD parsed;
|
||||
ParsePoint(p, parsed);
|
||||
point = parsed;
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, char const * field, PointD const & point)
|
||||
{
|
||||
auto json = base::NewJSONObject();
|
||||
ToJSONObject(*json, "x", point.x);
|
||||
ToJSONObject(*json, "y", point.y);
|
||||
json_object_set_new(&root, field, json.release());
|
||||
}
|
||||
|
||||
void FromJSONObjectOptional(json_t * root, string const & field, std::optional<PointD> & point)
|
||||
{
|
||||
FromJSONObjectOptional(root, field.c_str(), point);
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, string const & field, PointD const & point)
|
||||
{
|
||||
ToJSONObject(root, field.c_str(), point);
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, char const * field, std::optional<PointD> const & point)
|
||||
{
|
||||
if (point)
|
||||
ToJSONObject(root, field, *point);
|
||||
else
|
||||
ToJSONObject(root, field, base::NewJSONNull());
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, string const & field, std::optional<PointD> const & point)
|
||||
{
|
||||
ToJSONObject(root, field.c_str(), point);
|
||||
}
|
||||
} // namespace m2
|
||||
27
libs/search/search_quality/helpers_json.hpp
Normal file
27
libs/search/search_quality/helpers_json.hpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
namespace m2
|
||||
{
|
||||
void FromJSONObject(json_t * root, char const * field, RectD & rect);
|
||||
void ToJSONObject(json_t & root, char const * field, RectD const & rect);
|
||||
void FromJSONObject(json_t * root, std::string const & field, RectD & rect);
|
||||
void ToJSONObject(json_t & root, std::string const & field, RectD const & rect);
|
||||
|
||||
void FromJSONObject(json_t * root, char const * field, PointD & point);
|
||||
void FromJSONObjectOptional(json_t * root, char const * field, std::optional<PointD> & point);
|
||||
void FromJSONObject(json_t * root, std::string const & field, PointD & point);
|
||||
void FromJSONObjectOptional(json_t * root, std::string const & field, std::optional<PointD> & point);
|
||||
|
||||
void ToJSONObject(json_t & root, char const * field, PointD const & point);
|
||||
void ToJSONObject(json_t & root, std::string const & field, PointD const & point);
|
||||
void ToJSONObject(json_t & root, char const * field, std::optional<PointD> const & point);
|
||||
void ToJSONObject(json_t & root, std::string const & field, std::optional<PointD> const & point);
|
||||
} // namespace m2
|
||||
203
libs/search/search_quality/matcher.cpp
Normal file
203
libs/search/search_quality/matcher.cpp
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
#include "search/search_quality/matcher.hpp"
|
||||
|
||||
#include "search/feature_loader.hpp"
|
||||
#include "search/house_numbers_matcher.hpp"
|
||||
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_algo.hpp"
|
||||
#include "indexer/road_shields_parser.hpp"
|
||||
#include "indexer/search_string_utils.hpp"
|
||||
|
||||
#include "base/control_flow.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace search
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename Iter>
|
||||
bool StartsWithHouseNumber(Iter beg, Iter end)
|
||||
{
|
||||
using namespace search::house_numbers;
|
||||
|
||||
std::string s;
|
||||
for (auto it = beg; it != end; ++it)
|
||||
{
|
||||
if (!s.empty())
|
||||
s.append(" ");
|
||||
s.append(*it);
|
||||
if (LooksLikeHouseNumber(s, false /* isPrefix */))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// todo(@m) This function looks very slow.
|
||||
template <typename Iter>
|
||||
bool EndsWithHouseNumber(Iter beg, Iter end)
|
||||
{
|
||||
using namespace search::house_numbers;
|
||||
|
||||
if (beg == end)
|
||||
return false;
|
||||
|
||||
std::string s;
|
||||
for (auto it = --end;; --it)
|
||||
{
|
||||
if (s.empty())
|
||||
s = *it;
|
||||
else
|
||||
s = *it + " " + s;
|
||||
if (LooksLikeHouseNumber(s, false /* isPrefix */))
|
||||
return true;
|
||||
if (it == beg)
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> NormalizeAndTokenizeAsUtf8(std::string_view str)
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
ForEachNormalizedToken(str, [&res](strings::UniString const & token) { res.push_back(strings::ToUtf8(token)); });
|
||||
return res;
|
||||
}
|
||||
|
||||
bool StreetMatches(std::string_view name, std::vector<std::string> const & queryTokens)
|
||||
{
|
||||
auto const nameTokens = NormalizeAndTokenizeAsUtf8(name);
|
||||
if (nameTokens.empty())
|
||||
return false;
|
||||
|
||||
for (size_t i = 0; i + nameTokens.size() <= queryTokens.size(); ++i)
|
||||
{
|
||||
bool found = true;
|
||||
for (size_t j = 0; j < nameTokens.size(); ++j)
|
||||
{
|
||||
if (queryTokens[i + j] != nameTokens[j])
|
||||
{
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
continue;
|
||||
|
||||
if (!EndsWithHouseNumber(queryTokens.begin(), queryTokens.begin() + i) &&
|
||||
!StartsWithHouseNumber(queryTokens.begin() + i + nameTokens.size(), queryTokens.end()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Matcher::Matcher(FeatureLoader & loader) : m_loader(loader) {}
|
||||
|
||||
void Matcher::Match(Sample const & goldenSample, std::vector<Result> const & actual,
|
||||
std::vector<size_t> & goldenMatching, std::vector<size_t> & actualMatching)
|
||||
{
|
||||
auto const & golden = goldenSample.m_results;
|
||||
|
||||
auto const n = golden.size();
|
||||
auto const m = actual.size();
|
||||
|
||||
goldenMatching.assign(n, kInvalidId);
|
||||
actualMatching.assign(m, kInvalidId);
|
||||
|
||||
// TODO (@y, @m): use Kuhn algorithm here for maximum matching.
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
{
|
||||
if (goldenMatching[i] != kInvalidId)
|
||||
continue;
|
||||
auto const & g = golden[i];
|
||||
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
{
|
||||
if (actualMatching[j] != kInvalidId)
|
||||
continue;
|
||||
|
||||
auto const & a = actual[j];
|
||||
if (Matches(goldenSample.m_query, g, a))
|
||||
{
|
||||
goldenMatching[i] = j;
|
||||
actualMatching[j] = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Matcher::Matches(strings::UniString const & query, Sample::Result const & golden, search::Result const & actual)
|
||||
{
|
||||
if (actual.GetResultType() != Result::Type::Feature)
|
||||
return false;
|
||||
|
||||
auto ft = m_loader.Load(actual.GetFeatureID());
|
||||
if (!ft)
|
||||
return false;
|
||||
|
||||
return Matches(query, golden, *ft);
|
||||
}
|
||||
|
||||
bool Matcher::Matches(strings::UniString const & query, Sample::Result const & golden, FeatureType & ft)
|
||||
{
|
||||
auto const queryTokens = NormalizeAndTokenizeAsUtf8(ToUtf8(query));
|
||||
|
||||
bool nameMatches = false;
|
||||
|
||||
// The golden result may have an empty name. What is more likely, though, is that the
|
||||
// sample was not obtained as a result of a previous run of our search_quality tools.
|
||||
// Probably it originates from a third-party source.
|
||||
if (golden.m_name.empty())
|
||||
{
|
||||
if (ft.GetGeomType() == feature::GeomType::Line)
|
||||
{
|
||||
for (auto const & name : ftypes::GetRoadShieldsNames(ft))
|
||||
{
|
||||
if (StreetMatches(name, queryTokens))
|
||||
{
|
||||
nameMatches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Don't try to guess: it's enough to match by distance.
|
||||
// |ft| with GeomType::Point here is usually a POI and |ft| with GeomType::Area is a building.
|
||||
nameMatches = true;
|
||||
}
|
||||
}
|
||||
|
||||
ft.ForEachName([&queryTokens, &ft, &golden, &nameMatches](int8_t /* lang */, std::string_view name)
|
||||
{
|
||||
if (NormalizeAndSimplifyString(ToUtf8(golden.m_name)) == NormalizeAndSimplifyString(name))
|
||||
{
|
||||
nameMatches = true;
|
||||
return base::ControlFlow::Break;
|
||||
}
|
||||
|
||||
if (golden.m_name.empty() && ft.GetGeomType() == feature::GeomType::Line && StreetMatches(name, queryTokens))
|
||||
{
|
||||
nameMatches = true;
|
||||
return base::ControlFlow::Break;
|
||||
}
|
||||
|
||||
return base::ControlFlow::Continue;
|
||||
});
|
||||
|
||||
bool houseNumberMatches = true;
|
||||
std::string const & hn = ft.GetHouseNumber();
|
||||
if (!golden.m_houseNumber.empty() && !hn.empty())
|
||||
houseNumberMatches = golden.m_houseNumber == hn;
|
||||
|
||||
/// @todo Where are 50 meters came from?
|
||||
return (nameMatches && houseNumberMatches && feature::GetMinDistanceMeters(ft, golden.m_pos) < 50.0);
|
||||
}
|
||||
} // namespace search
|
||||
41
libs/search/search_quality/matcher.hpp
Normal file
41
libs/search/search_quality/matcher.hpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "search/result.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
class FeatureType;
|
||||
|
||||
namespace search
|
||||
{
|
||||
class FeatureLoader;
|
||||
|
||||
class Matcher
|
||||
{
|
||||
public:
|
||||
inline static size_t constexpr kInvalidId = std::numeric_limits<size_t>::max();
|
||||
|
||||
explicit Matcher(FeatureLoader & loader);
|
||||
|
||||
// Matches the results loaded from |goldenSample| with |actual| results
|
||||
// found by the search engine using the params from the Sample.
|
||||
// goldenMatching[i] is the index of the result in |actual| that matches
|
||||
// the sample result number i.
|
||||
// actualMatching[j] is the index of the sample in |golden| that matches
|
||||
// the golden result number j.
|
||||
void Match(Sample const & goldenSample, std::vector<Result> const & actual, std::vector<size_t> & goldenMatching,
|
||||
std::vector<size_t> & actualMatching);
|
||||
|
||||
bool Matches(strings::UniString const & query, Sample::Result const & golden, Result const & actual);
|
||||
bool Matches(strings::UniString const & query, Sample::Result const & golden, FeatureType & ft);
|
||||
|
||||
private:
|
||||
FeatureLoader & m_loader;
|
||||
};
|
||||
} // namespace search
|
||||
200
libs/search/search_quality/merge_samples.py
Normal file
200
libs/search/search_quality/merge_samples.py
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
import argparse
|
||||
import copy
|
||||
import json
|
||||
|
||||
|
||||
def read_samples(path):
|
||||
with open(path, "r") as f:
|
||||
return [json.loads(line) for line in f]
|
||||
|
||||
|
||||
def doubles_equal(a, b):
|
||||
EPS = 1e-6
|
||||
return abs(a - b) < EPS
|
||||
|
||||
|
||||
def points_equal(a, b):
|
||||
return doubles_equal(a["x"], b["x"]) and doubles_equal(a["y"], b["y"])
|
||||
|
||||
|
||||
def rects_equal(a, b):
|
||||
for key in ["minx", "miny", "maxx", "maxy"]:
|
||||
if not doubles_equal(a[key], b[key]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def samples_equal(a, b):
|
||||
"""
|
||||
Returns whether two samples originate from the same unassessed sample,
|
||||
i.e. found results and uselessness are not taken into account.
|
||||
"""
|
||||
for field in ["query", "locale"]:
|
||||
if a[field] != b[field]:
|
||||
return False
|
||||
|
||||
pos_key = "position"
|
||||
bad_point = {"x": -1, "y": -1}
|
||||
a_pos = a[pos_key] or bad_point
|
||||
b_pos = b[pos_key] or bad_point
|
||||
if not points_equal(a_pos, b_pos):
|
||||
return False
|
||||
|
||||
vp_key = "viewport"
|
||||
if not rects_equal(a[vp_key], b[vp_key]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def results_equal(a, b):
|
||||
"""
|
||||
Returns whether two found results are equal before assessing.
|
||||
Does not take relevance into account.
|
||||
"""
|
||||
pos_key = "position"
|
||||
name_key = "name"
|
||||
hn_key = "houseNumber"
|
||||
types_key = "types"
|
||||
if a[name_key] != b[name_key]:
|
||||
return False
|
||||
if not points_equal(a[pos_key], b[pos_key]):
|
||||
return False
|
||||
if a[hn_key] != b[hn_key]:
|
||||
return False
|
||||
if sorted(a[types_key]) != sorted(b[types_key]):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def greedily_match_results(a, b):
|
||||
match_to_a = [-1] * len(a)
|
||||
match_to_b = [-1] * len(b)
|
||||
for i in range(len(a)):
|
||||
if match_to_a[i] >= 0:
|
||||
continue
|
||||
for j in range(len(b)):
|
||||
if match_to_b[j] >= 0:
|
||||
continue
|
||||
if results_equal(a[i], b[j]):
|
||||
match_to_a[i] = j
|
||||
match_to_b[j] = i
|
||||
break
|
||||
return match_to_a, match_to_b
|
||||
|
||||
|
||||
def merge_relevancies(a, b):
|
||||
"""
|
||||
The second element of the returned tuple is True
|
||||
iff the relevancies are compatible and can be merged.
|
||||
Two relevancies are incompatible if one of them is a
|
||||
"strong" one, i.e. either "harmful" or "vital", and
|
||||
the other is "weak" and has the opposite sign ("relevant"
|
||||
and "irrelevant" respectively).
|
||||
|
||||
If a and b are compatible, then the first element of the tuple is
|
||||
the resulting relevance. The less intuitive merge results are:
|
||||
strong, weak -> strong
|
||||
relevant, irrelevant -> relevant
|
||||
"""
|
||||
RELEVANCIES = ["harmful", "irrelevant", "relevant", "vital"]
|
||||
id_a = RELEVANCIES.index(a)
|
||||
id_b = RELEVANCIES.index(b)
|
||||
if id_a > id_b:
|
||||
a, b = b, a
|
||||
id_a, id_b = id_b, id_a
|
||||
if id_a == 0 and id_b <= 1:
|
||||
return a, True
|
||||
if id_a == 1 and id_b <= 2:
|
||||
return b, True
|
||||
if id_a > 1:
|
||||
return b, True
|
||||
return a, False
|
||||
|
||||
|
||||
def merge_two_samples(a, b, line_number):
|
||||
if not samples_equal(a, b):
|
||||
raise Exception(f"Tried to merge two non-equivalent samples, line {line_number}")
|
||||
|
||||
useless_key = "useless"
|
||||
useless_a = a.get(useless_key, False)
|
||||
useless_b = b.get(useless_key, False)
|
||||
|
||||
if useless_a != useless_b:
|
||||
print(line_number, "useless:", useless_a, useless_b)
|
||||
|
||||
if useless_a and not useless_b:
|
||||
return b
|
||||
if useless_b:
|
||||
return a
|
||||
|
||||
# Both are not useless.
|
||||
|
||||
res_key = "results"
|
||||
match_to_a, match_to_b = greedily_match_results(a[res_key], b[res_key])
|
||||
|
||||
lst = [x for x in match_to_a if x < 0]
|
||||
|
||||
rel_key = "relevancy"
|
||||
|
||||
c = copy.deepcopy(a)
|
||||
c[res_key] = []
|
||||
|
||||
for i, j in enumerate(match_to_a):
|
||||
if j < 0:
|
||||
continue
|
||||
res = a[res_key][i]
|
||||
ra = a[res_key][i][rel_key]
|
||||
rb = b[res_key][j][rel_key]
|
||||
rc, ok = merge_relevancies(ra, rb)
|
||||
res[rel_key] = rc
|
||||
if not ok:
|
||||
print(line_number, ra, rb, a[res_key][i]["name"])
|
||||
continue
|
||||
c[res_key].append(res)
|
||||
|
||||
# Add all unmatched results as is.
|
||||
def add_unmatched(x, match_to_x):
|
||||
c[res_key].extend(
|
||||
x[res_key][i]
|
||||
for i in range(len(match_to_x))
|
||||
if match_to_x[i] < 0
|
||||
)
|
||||
|
||||
add_unmatched(a, match_to_a)
|
||||
add_unmatched(b, match_to_b)
|
||||
|
||||
return c
|
||||
|
||||
|
||||
def merge_two_files(path0, path1, path_out):
|
||||
"""
|
||||
Merges two .jsonl files. The files must contain the same number
|
||||
of samples, one JSON-encoded sample per line. The first sample
|
||||
of the first file will be merged with the first sample of the second
|
||||
file, etc.
|
||||
"""
|
||||
a = read_samples(path0)
|
||||
b = read_samples(path1)
|
||||
if len(a) != len(b):
|
||||
raise Exception(f"Different sizes of samples are not supported: {len(a)} and {len(b)}")
|
||||
|
||||
result = [merge_two_samples(a[i], b[i], i+1) for i in range(len(a))]
|
||||
|
||||
with open(path_out, "w") as f:
|
||||
for sample in result:
|
||||
f.write(json.dumps(sample) + "\n")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Utilities to merge assessed samples from different assessors")
|
||||
parser.add_argument("--input0", required=True, dest="input0", help="Path to the first input file")
|
||||
parser.add_argument("--input1", required=True, dest="input1", help="Path to the second input file")
|
||||
parser.add_argument("--output", required=True, dest="output", help="Path to the output file")
|
||||
args = parser.parse_args()
|
||||
|
||||
merge_two_files(args.input0, args.input1, args.output)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
307
libs/search/search_quality/sample.cpp
Normal file
307
libs/search/search_quality/sample.cpp
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "search/search_params.hpp"
|
||||
#include "search/search_quality/helpers_json.hpp"
|
||||
|
||||
#include "indexer/feature.hpp"
|
||||
#include "indexer/feature_algo.hpp"
|
||||
#include "indexer/feature_data.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
|
||||
namespace search
|
||||
{
|
||||
using namespace base;
|
||||
using namespace std;
|
||||
|
||||
namespace
|
||||
{
|
||||
bool LessRect(m2::RectD const & lhs, m2::RectD const & rhs)
|
||||
{
|
||||
if (lhs.minX() != rhs.minX())
|
||||
return lhs.minX() < rhs.minX();
|
||||
if (lhs.minY() != rhs.minY())
|
||||
return lhs.minY() < rhs.minY();
|
||||
if (lhs.maxX() != rhs.maxX())
|
||||
return lhs.maxX() < rhs.maxX();
|
||||
if (lhs.maxY() != rhs.maxY())
|
||||
return lhs.maxY() < rhs.maxY();
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Less(vector<T> lhs, vector<T> rhs)
|
||||
{
|
||||
sort(lhs.begin(), lhs.end());
|
||||
sort(rhs.begin(), rhs.end());
|
||||
return lhs < rhs;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool Equal(vector<T> lhs, vector<T> rhs)
|
||||
{
|
||||
sort(lhs.begin(), lhs.end());
|
||||
sort(rhs.begin(), rhs.end());
|
||||
return lhs == rhs;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
Sample::Result Sample::Result::Build(FeatureType & ft, Relevance relevance)
|
||||
{
|
||||
Sample::Result r;
|
||||
r.m_pos = feature::GetCenter(ft);
|
||||
r.m_name = strings::MakeUniString(ft.GetReadableName());
|
||||
r.m_houseNumber = ft.GetHouseNumber();
|
||||
r.m_types = feature::TypesHolder(ft).ToObjectNames();
|
||||
r.m_relevance = relevance;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool Sample::Result::operator<(Sample::Result const & rhs) const
|
||||
{
|
||||
if (m_pos != rhs.m_pos)
|
||||
return m_pos < rhs.m_pos;
|
||||
if (m_name != rhs.m_name)
|
||||
return m_name < rhs.m_name;
|
||||
if (m_houseNumber != rhs.m_houseNumber)
|
||||
return m_houseNumber < rhs.m_houseNumber;
|
||||
if (m_relevance != rhs.m_relevance)
|
||||
return m_relevance < rhs.m_relevance;
|
||||
return Less(m_types, rhs.m_types);
|
||||
}
|
||||
|
||||
bool Sample::Result::operator==(Sample::Result const & rhs) const
|
||||
{
|
||||
// Note: Strict equality for points and viewports.
|
||||
return m_pos == rhs.m_pos && m_name == rhs.m_name && m_houseNumber == rhs.m_houseNumber &&
|
||||
Equal(m_types, rhs.m_types) && m_relevance == rhs.m_relevance;
|
||||
}
|
||||
|
||||
bool Sample::DeserializeFromJSON(string const & jsonStr)
|
||||
{
|
||||
try
|
||||
{
|
||||
base::Json root(jsonStr.c_str());
|
||||
DeserializeFromJSONImpl(root.get());
|
||||
return true;
|
||||
}
|
||||
catch (base::Json::Exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't parse sample:", e.Msg(), jsonStr));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
base::JSONPtr Sample::SerializeToJSON() const
|
||||
{
|
||||
auto json = base::NewJSONObject();
|
||||
SerializeToJSONImpl(*json);
|
||||
return json;
|
||||
}
|
||||
|
||||
bool Sample::operator<(Sample const & rhs) const
|
||||
{
|
||||
if (m_query != rhs.m_query)
|
||||
return m_query < rhs.m_query;
|
||||
if (m_locale != rhs.m_locale)
|
||||
return m_locale < rhs.m_locale;
|
||||
if (m_pos != rhs.m_pos)
|
||||
return m_pos < rhs.m_pos;
|
||||
if (m_viewport != rhs.m_viewport)
|
||||
return LessRect(m_viewport, rhs.m_viewport);
|
||||
if (!Equal(m_results, rhs.m_results))
|
||||
return Less(m_results, rhs.m_results);
|
||||
return Less(m_relatedQueries, rhs.m_relatedQueries);
|
||||
}
|
||||
|
||||
bool Sample::operator==(Sample const & rhs) const
|
||||
{
|
||||
return !(*this < rhs) && !(rhs < *this);
|
||||
}
|
||||
|
||||
// static
|
||||
bool Sample::DeserializeFromJSONLines(string const & lines, vector<Sample> & samples)
|
||||
{
|
||||
istringstream is(lines);
|
||||
string line;
|
||||
vector<Sample> result;
|
||||
|
||||
while (getline(is, line))
|
||||
{
|
||||
if (line.empty())
|
||||
continue;
|
||||
|
||||
Sample sample;
|
||||
if (!sample.DeserializeFromJSON(line))
|
||||
return false;
|
||||
result.emplace_back(std::move(sample));
|
||||
}
|
||||
|
||||
samples.insert(samples.end(), result.begin(), result.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
// static
|
||||
void Sample::SerializeToJSONLines(vector<Sample> const & samples, string & lines)
|
||||
{
|
||||
for (auto const & sample : samples)
|
||||
{
|
||||
unique_ptr<char, JSONFreeDeleter> buffer(json_dumps(sample.SerializeToJSON().get(), JSON_COMPACT));
|
||||
lines.append(buffer.get());
|
||||
lines.push_back('\n');
|
||||
}
|
||||
}
|
||||
|
||||
void Sample::DeserializeFromJSONImpl(json_t * root)
|
||||
{
|
||||
FromJSONObject(root, "query", m_query);
|
||||
FromJSONObject(root, "locale", m_locale);
|
||||
FromJSONObjectOptional(root, "position", m_pos);
|
||||
FromJSONObject(root, "viewport", m_viewport);
|
||||
FromJSONObjectOptional(root, "results", m_results);
|
||||
FromJSONObjectOptional(root, "related_queries", m_relatedQueries);
|
||||
FromJSONObjectOptionalField(root, "useless", m_useless);
|
||||
}
|
||||
|
||||
void Sample::SerializeToJSONImpl(json_t & root) const
|
||||
{
|
||||
ToJSONObject(root, "query", m_query);
|
||||
ToJSONObject(root, "locale", m_locale);
|
||||
ToJSONObject(root, "position", m_pos);
|
||||
ToJSONObject(root, "viewport", m_viewport);
|
||||
ToJSONObject(root, "results", m_results);
|
||||
ToJSONObject(root, "related_queries", m_relatedQueries);
|
||||
if (m_useless)
|
||||
ToJSONObject(root, "useless", m_useless);
|
||||
}
|
||||
|
||||
void Sample::FillSearchParams(search::SearchParams & params) const
|
||||
{
|
||||
params.m_query = strings::ToUtf8(m_query);
|
||||
params.m_inputLocale = m_locale;
|
||||
params.m_viewport = m_viewport;
|
||||
params.m_mode = Mode::Everywhere;
|
||||
params.m_position = m_pos.value_or(m2::PointD());
|
||||
params.m_needAddress = true;
|
||||
params.m_suggestsEnabled = false;
|
||||
params.m_needHighlighting = false;
|
||||
params.m_useDebugInfo = true; // for RankingInfo printing
|
||||
}
|
||||
|
||||
void FromJSONObject(json_t * root, char const * field, Sample::Result::Relevance & relevance)
|
||||
{
|
||||
string r;
|
||||
FromJSONObject(root, field, r);
|
||||
if (r == "harmful")
|
||||
relevance = search::Sample::Result::Relevance::Harmful;
|
||||
else if (r == "irrelevant")
|
||||
relevance = search::Sample::Result::Relevance::Irrelevant;
|
||||
else if (r == "relevant")
|
||||
relevance = search::Sample::Result::Relevance::Relevant;
|
||||
else if (r == "vital")
|
||||
relevance = search::Sample::Result::Relevance::Vital;
|
||||
else
|
||||
CHECK(false, ("Unknown relevance:", r));
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, char const * field, Sample::Result::Relevance relevance)
|
||||
{
|
||||
using Relevance = Sample::Result::Relevance;
|
||||
|
||||
string r;
|
||||
switch (relevance)
|
||||
{
|
||||
case Relevance::Harmful: r = "harmful"; break;
|
||||
case Relevance::Irrelevant: r = "irrelevant"; break;
|
||||
case Relevance::Relevant: r = "relevant"; break;
|
||||
case Relevance::Vital: r = "vital"; break;
|
||||
}
|
||||
|
||||
json_object_set_new(&root, field, json_string(r.c_str()));
|
||||
}
|
||||
|
||||
void FromJSONObject(json_t * root, string const & field, Sample::Result::Relevance & relevance)
|
||||
{
|
||||
FromJSONObject(root, field.c_str(), relevance);
|
||||
}
|
||||
|
||||
void ToJSONObject(json_t & root, string const & field, Sample::Result::Relevance relevance)
|
||||
{
|
||||
ToJSONObject(root, field.c_str(), relevance);
|
||||
}
|
||||
|
||||
void FromJSON(json_t * root, Sample::Result & result)
|
||||
{
|
||||
FromJSONObject(root, "position", result.m_pos);
|
||||
FromJSONObject(root, "name", result.m_name);
|
||||
FromJSONObject(root, "houseNumber", result.m_houseNumber);
|
||||
FromJSONObject(root, "types", result.m_types);
|
||||
FromJSONObject(root, "relevancy", result.m_relevance);
|
||||
}
|
||||
|
||||
base::JSONPtr ToJSON(Sample::Result const & result)
|
||||
{
|
||||
auto root = base::NewJSONObject();
|
||||
ToJSONObject(*root, "position", result.m_pos);
|
||||
ToJSONObject(*root, "name", result.m_name);
|
||||
ToJSONObject(*root, "houseNumber", result.m_houseNumber);
|
||||
ToJSONObject(*root, "types", result.m_types);
|
||||
ToJSONObject(*root, "relevancy", result.m_relevance);
|
||||
return root;
|
||||
}
|
||||
|
||||
string DebugPrint(Sample::Result::Relevance r)
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case Sample::Result::Relevance::Harmful: return "Harmful";
|
||||
case Sample::Result::Relevance::Irrelevant: return "Irrelevant";
|
||||
case Sample::Result::Relevance::Relevant: return "Relevant";
|
||||
case Sample::Result::Relevance::Vital: return "Vital";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
string DebugPrint(Sample::Result const & r)
|
||||
{
|
||||
ostringstream oss;
|
||||
oss << "relevance: " << DebugPrint(r.m_relevance) << " ";
|
||||
oss << "name: " << DebugPrint(r.m_name) << " ";
|
||||
oss << "house number: " << r.m_houseNumber << " ";
|
||||
oss << "pos: " << DebugPrint(r.m_pos) << " ";
|
||||
oss << "types: [";
|
||||
for (size_t i = 0; i < r.m_types.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
oss << " ";
|
||||
oss << r.m_types[i];
|
||||
}
|
||||
oss << "]";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
string DebugPrint(Sample const & s)
|
||||
{
|
||||
ostringstream oss;
|
||||
oss << "[";
|
||||
oss << "query: " << DebugPrint(s.m_query) << ", ";
|
||||
oss << "locale: " << s.m_locale << ", ";
|
||||
oss << "pos: " << (s.m_pos ? DebugPrint(*s.m_pos) : "null") << ", ";
|
||||
oss << "viewport: " << DebugPrint(s.m_viewport) << ", ";
|
||||
oss << "results: [";
|
||||
for (size_t i = 0; i < s.m_results.size(); ++i)
|
||||
{
|
||||
if (i > 0)
|
||||
oss << ", ";
|
||||
oss << DebugPrint(s.m_results[i]);
|
||||
}
|
||||
oss << "]";
|
||||
return oss.str();
|
||||
}
|
||||
} // namespace search
|
||||
109
libs/search/search_quality/sample.hpp
Normal file
109
libs/search/search_quality/sample.hpp
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class FeatureType;
|
||||
|
||||
namespace search
|
||||
{
|
||||
struct SearchParams;
|
||||
|
||||
struct Sample
|
||||
{
|
||||
struct Result
|
||||
{
|
||||
enum class Relevance
|
||||
{
|
||||
// A result that should not be present and it's hard (for the user)
|
||||
// to explain why it is there, i.e. it is a waste of time even
|
||||
// to try to understand what this result is about.
|
||||
Harmful,
|
||||
|
||||
// A result that is irrelevant to the query but at
|
||||
// least it is easy to explain why it showed up.
|
||||
Irrelevant,
|
||||
|
||||
// A result that is relevant to the query.
|
||||
Relevant,
|
||||
|
||||
// A result that definetely should be present, preferably
|
||||
// at a position close to the beginning.
|
||||
Vital
|
||||
};
|
||||
|
||||
static Result Build(FeatureType & ft, Relevance relevance);
|
||||
|
||||
bool operator<(Result const & rhs) const;
|
||||
|
||||
bool operator==(Result const & rhs) const;
|
||||
|
||||
m2::PointD m_pos = m2::PointD(0, 0);
|
||||
|
||||
strings::UniString m_name;
|
||||
std::string m_houseNumber;
|
||||
std::vector<std::string> m_types; // OMaps types, not OSM types.
|
||||
Relevance m_relevance = Relevance::Irrelevant;
|
||||
};
|
||||
|
||||
bool DeserializeFromJSON(std::string const & jsonStr);
|
||||
base::JSONPtr SerializeToJSON() const;
|
||||
|
||||
static bool DeserializeFromJSONLines(std::string const & lines, std::vector<Sample> & samples);
|
||||
static void SerializeToJSONLines(std::vector<Sample> const & samples, std::string & lines);
|
||||
|
||||
bool operator<(Sample const & rhs) const;
|
||||
|
||||
bool operator==(Sample const & rhs) const;
|
||||
|
||||
void DeserializeFromJSONImpl(json_t * root);
|
||||
void SerializeToJSONImpl(json_t & root) const;
|
||||
|
||||
void FillSearchParams(search::SearchParams & params) const;
|
||||
|
||||
strings::UniString m_query;
|
||||
std::string m_locale;
|
||||
std::optional<m2::PointD> m_pos;
|
||||
m2::RectD m_viewport = m2::RectD(0, 0, 0, 0);
|
||||
std::vector<Result> m_results;
|
||||
std::vector<strings::UniString> m_relatedQueries;
|
||||
|
||||
// A useless sample is usually a result of the user exploring
|
||||
// the search engine without a clear search intent or a sample
|
||||
// that cannot be assessed properly using only the data available
|
||||
// to the engine (for example, related queries may help a lot but
|
||||
// are not expected to be available, or local knowledge of the area
|
||||
// is needed).
|
||||
// More examples:
|
||||
// * A sample whose requests is precisely about a particular street
|
||||
// in a particular city is useless if the assessor is sure that
|
||||
// there is no such street in this city.
|
||||
// * On the other hand, if there is such a street (or, more often,
|
||||
// a building) as indicated by other data sources but the engine
|
||||
// still could not find it because of its absense in our
|
||||
// data, the sample is NOT useless.
|
||||
bool m_useless = false;
|
||||
};
|
||||
|
||||
void FromJSONObject(json_t * root, char const * field, Sample::Result::Relevance & relevance);
|
||||
void ToJSONObject(json_t & root, char const * field, Sample::Result::Relevance relevance);
|
||||
void FromJSONObject(json_t * root, std::string const & field, Sample::Result::Relevance & relevance);
|
||||
void ToJSONObject(json_t & root, std::string const & field, Sample::Result::Relevance relevance);
|
||||
|
||||
void FromJSON(json_t * root, Sample::Result & result);
|
||||
base::JSONPtr ToJSON(Sample::Result const & result);
|
||||
|
||||
std::string DebugPrint(Sample::Result::Relevance r);
|
||||
|
||||
std::string DebugPrint(Sample::Result const & r);
|
||||
|
||||
std::string DebugPrint(Sample const & s);
|
||||
} // namespace search
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
project(samples_generation_tool)
|
||||
|
||||
set(SRC samples_generation_tool.cpp)
|
||||
|
||||
omim_add_executable(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_quality
|
||||
search_tests_support
|
||||
gflags::gflags
|
||||
)
|
||||
|
|
@ -0,0 +1,458 @@
|
|||
#include "search/search_quality/helpers.hpp"
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "search/categories_cache.hpp"
|
||||
#include "search/house_numbers_matcher.hpp"
|
||||
#include "search/mwm_context.hpp"
|
||||
#include "search/reverse_geocoder.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/data_source.hpp"
|
||||
#include "indexer/feature_algo.hpp"
|
||||
#include "indexer/ftypes_matcher.hpp"
|
||||
|
||||
#include "platform/platform_tests_support/helpers.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring> // strlen
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
|
||||
using namespace search::search_quality;
|
||||
using namespace search;
|
||||
using namespace std;
|
||||
|
||||
double constexpr kMaxDistanceToObjectM = 7500.0;
|
||||
double constexpr kMinViewportSizeM = 100.0;
|
||||
double constexpr kMaxViewportSizeM = 5000.0;
|
||||
size_t constexpr kMaxSamplesPerMwm = 20;
|
||||
|
||||
DEFINE_string(data_path, "", "Path to data directory (resources dir).");
|
||||
DEFINE_string(mwm_path, "", "Path to mwm files (writable dir).");
|
||||
DEFINE_string(out_buildings_path, "buildings.json", "Path to output file for buildings samples.");
|
||||
DEFINE_string(out_cafes_path, "cafes.json", "Path to output file for cafes samples.");
|
||||
DEFINE_double(max_distance_to_object, kMaxDistanceToObjectM, "Maximal distance from user position to object (meters).");
|
||||
DEFINE_double(min_viewport_size, kMinViewportSizeM, "Minimal size of viewport (meters).");
|
||||
DEFINE_double(max_viewport_size, kMaxViewportSizeM, "Maximal size of viewport (meters).");
|
||||
DEFINE_uint64(max_samples_per_mwm, kMaxSamplesPerMwm,
|
||||
"Maximal number of samples of each type (buildings/cafes) per mwm.");
|
||||
DEFINE_bool(add_misprints, false, "Add random misprints.");
|
||||
DEFINE_bool(add_cafe_address, false, "Add address.");
|
||||
DEFINE_bool(add_cafe_type, false, "Add cafe type (restaurant/cafe/bar) in local language.");
|
||||
|
||||
std::random_device g_rd;
|
||||
mt19937 g_rng(g_rd());
|
||||
|
||||
enum class RequestType
|
||||
{
|
||||
Building,
|
||||
Cafe
|
||||
};
|
||||
|
||||
bool GetRandomBool()
|
||||
{
|
||||
std::uniform_int_distribution<> dis(0, 1);
|
||||
return dis(g_rng) == 0;
|
||||
}
|
||||
|
||||
// Leaves first letter as is.
|
||||
void EraseRandom(strings::UniString & uni)
|
||||
{
|
||||
if (uni.size() <= 1)
|
||||
return;
|
||||
|
||||
uniform_int_distribution<size_t> dis(1, uni.size() - 1);
|
||||
auto const it = uni.begin() + dis(g_rng);
|
||||
uni.erase(it, it + 1);
|
||||
}
|
||||
|
||||
// Leaves first letter as is.
|
||||
void SwapRandom(strings::UniString & uni)
|
||||
{
|
||||
if (uni.size() <= 2)
|
||||
return;
|
||||
|
||||
uniform_int_distribution<size_t> dis(1, uni.size() - 2);
|
||||
auto const index = dis(g_rng);
|
||||
swap(uni[index], uni[index + 1]);
|
||||
}
|
||||
|
||||
void AddRandomMisprint(strings::UniString & str)
|
||||
{
|
||||
// todo(@t.yan): Disable for hieroglyphs, consider implementing InsertRandom.
|
||||
if (GetRandomBool())
|
||||
return EraseRandom(str);
|
||||
return SwapRandom(str);
|
||||
}
|
||||
|
||||
void AddMisprints(string & str)
|
||||
{
|
||||
if (!FLAGS_add_misprints)
|
||||
return;
|
||||
|
||||
auto tokens = strings::Tokenize(str, " -&");
|
||||
str.clear();
|
||||
for (size_t i = 0; i < tokens.size(); ++i)
|
||||
{
|
||||
auto const & token = tokens[i];
|
||||
auto uni = strings::MakeUniString(token);
|
||||
if (uni.size() > 4 && g_rng() % 4)
|
||||
AddRandomMisprint(uni);
|
||||
if (uni.size() > 8 && g_rng() % 4)
|
||||
AddRandomMisprint(uni);
|
||||
|
||||
str += strings::ToUtf8(uni);
|
||||
if (i != tokens.size() - 1)
|
||||
str += " ";
|
||||
}
|
||||
}
|
||||
|
||||
map<string, vector<string>> const kStreetSynonyms = {{"улица", {"ул", "у"}},
|
||||
{"проспект", {"пр-т", "пр", "пркт", "прт", "пр-кт"}},
|
||||
{"переулок", {"пер"}},
|
||||
{"проезд", {"пр-д", "пр", "прд"}},
|
||||
{"аллея", {"ал"}},
|
||||
{"бульвар", {"б-р", "бр"}},
|
||||
{"набережная", {"наб", "наб-я"}},
|
||||
{"шоссе", {"шос"}},
|
||||
{"вyлица", {"вул"}},
|
||||
{"площадь", {"пл", "площ"}},
|
||||
{"тупик", {"туп"}},
|
||||
{"street", {"str", "st"}},
|
||||
{"avenue", {"ave", "av"}},
|
||||
{"boulevard", {"bld", "blv", "bv", "blvd"}},
|
||||
{"drive", {"dr"}},
|
||||
{"highway", {"hw", "hwy"}},
|
||||
{"road", {"rd"}},
|
||||
{"square", {"sq"}}};
|
||||
|
||||
void ModifyStreet(string & str)
|
||||
{
|
||||
auto tokens = strings::Tokenize<std::string>(str, " -&");
|
||||
str.clear();
|
||||
|
||||
auto const isStreetSynonym = [](string const & s) { return kStreetSynonyms.find(s) != kStreetSynonyms.end(); };
|
||||
|
||||
auto const synonymIt = find_if(tokens.begin(), tokens.end(), isStreetSynonym);
|
||||
if (synonymIt != tokens.end() && find_if(synonymIt + 1, tokens.end(), isStreetSynonym) == tokens.end())
|
||||
{
|
||||
// Only one street synonym.
|
||||
if (GetRandomBool())
|
||||
{
|
||||
if (GetRandomBool())
|
||||
{
|
||||
auto const & synonyms = kStreetSynonyms.at(*synonymIt);
|
||||
uniform_int_distribution<size_t> dis(0, synonyms.size() - 1);
|
||||
*synonymIt = synonyms[dis(g_rng)];
|
||||
}
|
||||
else
|
||||
{
|
||||
tokens.erase(synonymIt);
|
||||
}
|
||||
}
|
||||
// Else leave as is.
|
||||
}
|
||||
|
||||
if (tokens.empty())
|
||||
return;
|
||||
|
||||
str = strings::JoinStrings(tokens, " ");
|
||||
AddMisprints(str);
|
||||
}
|
||||
|
||||
void ModifyHouse(uint8_t lang, string & str)
|
||||
{
|
||||
if (str.empty())
|
||||
return;
|
||||
|
||||
if (lang == StringUtf8Multilang::GetLangIndex("ru") && isdigit(str[0]))
|
||||
{
|
||||
uniform_int_distribution<size_t> dis(0, 4);
|
||||
auto const r = dis(g_rng);
|
||||
if (r == 0)
|
||||
str = "д " + str;
|
||||
if (r == 1)
|
||||
str = "д" + str;
|
||||
if (r == 2)
|
||||
str = "дом " + str;
|
||||
if (r == 3)
|
||||
str = "д. " + str;
|
||||
// Else leave housenumber as is.
|
||||
}
|
||||
}
|
||||
|
||||
m2::PointD GenerateNearbyPosition(m2::PointD const & point)
|
||||
{
|
||||
auto const maxDistance = FLAGS_max_distance_to_object;
|
||||
uniform_real_distribution<double> dis(-maxDistance, maxDistance);
|
||||
return mercator::GetSmPoint(point, dis(g_rng) /* dX */, dis(g_rng) /* dY */);
|
||||
}
|
||||
|
||||
m2::RectD GenerateNearbyViewport(m2::PointD const & point)
|
||||
{
|
||||
uniform_real_distribution<double> dis(FLAGS_min_viewport_size, FLAGS_max_viewport_size);
|
||||
return mercator::RectByCenterXYAndSizeInMeters(GenerateNearbyPosition(point), dis(g_rng));
|
||||
}
|
||||
|
||||
bool GetBuildingInfo(FeatureType & ft, search::ReverseGeocoder const & coder, string & street)
|
||||
{
|
||||
std::string const & hn = ft.GetHouseNumber();
|
||||
if (hn.empty() || !search::house_numbers::LooksLikeHouseNumber(hn, false /* prefix */))
|
||||
return false;
|
||||
|
||||
street = coder.GetFeatureStreetName(ft);
|
||||
if (street.empty())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GetCafeInfo(FeatureType & ft, search::ReverseGeocoder const & coder, string & street, uint32_t & cafeType,
|
||||
string_view & name)
|
||||
{
|
||||
if (!ft.HasName())
|
||||
return false;
|
||||
|
||||
if (!ft.GetNames().GetString(StringUtf8Multilang::kDefaultCode, name))
|
||||
return false;
|
||||
|
||||
for (auto const t : feature::TypesHolder(ft))
|
||||
{
|
||||
if (ftypes::IsEatChecker::Instance()(t))
|
||||
{
|
||||
cafeType = t;
|
||||
street = coder.GetFeatureStreetName(ft);
|
||||
return !street.empty();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string ModifyAddress(string street, string house, uint8_t lang)
|
||||
{
|
||||
if (street.empty() || house.empty())
|
||||
return "";
|
||||
|
||||
ModifyStreet(street);
|
||||
ModifyHouse(lang, house);
|
||||
|
||||
if (GetRandomBool())
|
||||
return street + " " + house;
|
||||
|
||||
return house + " " + street;
|
||||
}
|
||||
|
||||
string CombineRandomly(string const & mandatory, string const & optional)
|
||||
{
|
||||
if (optional.empty() || GetRandomBool())
|
||||
return mandatory;
|
||||
if (GetRandomBool())
|
||||
return optional + " " + mandatory;
|
||||
return mandatory + " " + optional;
|
||||
}
|
||||
|
||||
void ModifyCafe(string const & name, string const & type, string & out)
|
||||
{
|
||||
out = FLAGS_add_cafe_type ? CombineRandomly(name, type) : name;
|
||||
AddMisprints(out);
|
||||
}
|
||||
|
||||
string_view GetLocalizedCafeType(unordered_map<uint32_t, StringUtf8Multilang> const & typesTranslations, uint32_t type,
|
||||
uint8_t lang)
|
||||
{
|
||||
auto const it = typesTranslations.find(type);
|
||||
if (it == typesTranslations.end())
|
||||
return {};
|
||||
string_view translation;
|
||||
if (it->second.GetString(lang, translation))
|
||||
return translation;
|
||||
it->second.GetString(StringUtf8Multilang::kEnglishCode, translation);
|
||||
return translation;
|
||||
}
|
||||
|
||||
optional<Sample> GenerateRequest(FeatureType & ft, search::ReverseGeocoder const & coder,
|
||||
unordered_map<uint32_t, StringUtf8Multilang> const & typesTranslations,
|
||||
vector<int8_t> const & mwmLangCodes, RequestType requestType)
|
||||
{
|
||||
string street;
|
||||
string cafeStr;
|
||||
auto const lang = !mwmLangCodes.empty() ? mwmLangCodes[0] : StringUtf8Multilang::kEnglishCode;
|
||||
|
||||
switch (requestType)
|
||||
{
|
||||
case RequestType::Building:
|
||||
{
|
||||
if (!GetBuildingInfo(ft, coder, street))
|
||||
return {};
|
||||
break;
|
||||
}
|
||||
case RequestType::Cafe:
|
||||
{
|
||||
uint32_t type;
|
||||
string_view name;
|
||||
if (!GetCafeInfo(ft, coder, street, type, name))
|
||||
return {};
|
||||
|
||||
auto const cafeType = GetLocalizedCafeType(typesTranslations, type, lang);
|
||||
ModifyCafe(std::string(name), std::string(cafeType), cafeStr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto const featureCenter = feature::GetCenter(ft);
|
||||
auto const address = ModifyAddress(std::move(street), ft.GetHouseNumber(), lang);
|
||||
auto query = address;
|
||||
if (!cafeStr.empty())
|
||||
query = FLAGS_add_cafe_address ? CombineRandomly(cafeStr, address) : cafeStr;
|
||||
|
||||
Sample sample;
|
||||
sample.m_query = strings::MakeUniString(query);
|
||||
sample.m_locale = StringUtf8Multilang::GetLangByCode(lang);
|
||||
sample.m_pos = GenerateNearbyPosition(featureCenter);
|
||||
sample.m_viewport = GenerateNearbyViewport(featureCenter);
|
||||
sample.m_results.push_back(Sample::Result::Build(ft, Sample::Result::Relevance::Vital));
|
||||
|
||||
return sample;
|
||||
}
|
||||
|
||||
unordered_map<uint32_t, StringUtf8Multilang> ParseStrings()
|
||||
{
|
||||
auto const stringsFile = base::JoinPath(GetPlatform().ResourcesDir(), "strings", "types_strings.txt");
|
||||
ifstream s(stringsFile);
|
||||
CHECK(s.is_open(), ("Cannot open", stringsFile));
|
||||
|
||||
// Skip the first [[Types]] line.
|
||||
string line;
|
||||
getline(s, line);
|
||||
|
||||
uint32_t type = 0;
|
||||
auto const typePrefixSize = strlen("[type.");
|
||||
auto const typePostfixSize = strlen("]");
|
||||
unordered_map<uint32_t, StringUtf8Multilang> typesTranslations;
|
||||
while (s.good())
|
||||
{
|
||||
getline(s, line);
|
||||
strings::Trim(line);
|
||||
|
||||
// Allow for comments starting with '#' character.
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
// New type.
|
||||
if (line[0] == '[')
|
||||
{
|
||||
CHECK_GREATER(line.size(), typePrefixSize + typePostfixSize, (line));
|
||||
auto typeString = line.substr(typePrefixSize, line.size() - typePrefixSize - typePostfixSize);
|
||||
auto const tokens = strings::Tokenize(typeString, ".");
|
||||
type = classif().GetTypeByPath(tokens);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can not get initial type by types_strings key for some types like
|
||||
// "amenity parking multi-storey" which is stored like amenity.parking.multi.storey
|
||||
// and can not be tokenized correctly.
|
||||
if (type == 0)
|
||||
continue;
|
||||
auto const pos = line.find("=");
|
||||
CHECK_NOT_EQUAL(pos, string::npos, ());
|
||||
auto lang = line.substr(0, pos);
|
||||
strings::Trim(lang);
|
||||
auto translation = line.substr(pos + 1);
|
||||
strings::Trim(translation);
|
||||
typesTranslations[type].AddString(lang, translation);
|
||||
}
|
||||
}
|
||||
return typesTranslations;
|
||||
}
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
platform::tests_support::ChangeMaxNumberOfOpenFiles(kMaxOpenFiles);
|
||||
|
||||
gflags::SetUsageMessage("Samples generation tool.");
|
||||
gflags::ParseCommandLineFlags(&argc, &argv, true);
|
||||
|
||||
SetPlatformDirs(FLAGS_data_path, FLAGS_mwm_path);
|
||||
|
||||
ofstream buildingsOut;
|
||||
buildingsOut.open(FLAGS_out_buildings_path);
|
||||
CHECK(buildingsOut.is_open(), ("Can't open output file", FLAGS_out_buildings_path));
|
||||
|
||||
ofstream cafesOut;
|
||||
cafesOut.open(FLAGS_out_cafes_path);
|
||||
CHECK(cafesOut.is_open(), ("Can't open output file", FLAGS_out_cafes_path));
|
||||
|
||||
classificator::Load();
|
||||
FrozenDataSource dataSource;
|
||||
InitDataSource(dataSource, "" /* mwmListPath */);
|
||||
search::ReverseGeocoder const coder(dataSource);
|
||||
auto const typesTranslations = ParseStrings();
|
||||
|
||||
vector<shared_ptr<MwmInfo>> mwmInfos;
|
||||
dataSource.GetMwmsInfo(mwmInfos);
|
||||
for (auto const & mwmInfo : mwmInfos)
|
||||
{
|
||||
MwmSet::MwmId const mwmId(mwmInfo);
|
||||
LOG(LINFO, ("Start generation for", mwmId));
|
||||
|
||||
vector<int8_t> mwmLangCodes;
|
||||
mwmInfo->GetRegionData().GetLanguages(mwmLangCodes);
|
||||
|
||||
auto handle = dataSource.GetMwmHandleById(mwmId);
|
||||
auto & value = *handle.GetValue();
|
||||
// WorldCoasts.
|
||||
if (!value.HasSearchIndex())
|
||||
continue;
|
||||
|
||||
MwmContext const mwmContext(std::move(handle));
|
||||
base::Cancellable const cancellable;
|
||||
FeaturesLoaderGuard g(dataSource, mwmId);
|
||||
|
||||
auto generate = [&](ftypes::BaseChecker const & checker, RequestType type, ofstream & out)
|
||||
{
|
||||
CategoriesCache cache(checker, cancellable);
|
||||
auto features = cache.Get(mwmContext);
|
||||
|
||||
vector<uint32_t> fids;
|
||||
features.ForEach([&fids](uint64_t fid) { fids.push_back(base::asserted_cast<uint32_t>(fid)); });
|
||||
shuffle(fids.begin(), fids.end(), g_rng);
|
||||
|
||||
size_t numSamples = 0;
|
||||
for (auto const fid : fids)
|
||||
{
|
||||
if (numSamples >= FLAGS_max_samples_per_mwm)
|
||||
break;
|
||||
|
||||
auto ft = g.GetFeatureByIndex(fid);
|
||||
CHECK(ft, ());
|
||||
auto const sample = GenerateRequest(*ft, coder, typesTranslations, mwmLangCodes, type);
|
||||
if (sample)
|
||||
{
|
||||
string json;
|
||||
Sample::SerializeToJSONLines({*sample}, json);
|
||||
out << json;
|
||||
++numSamples;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generate(ftypes::IsBuildingChecker::Instance(), RequestType::Building, buildingsOut);
|
||||
generate(ftypes::IsEatChecker::Instance(), RequestType::Cafe, cafesOut);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
342
libs/search/search_quality/scoring_model.py
Executable file
342
libs/search/search_quality/scoring_model.py
Executable file
|
|
@ -0,0 +1,342 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from math import exp, log
|
||||
from scipy.stats import pearsonr, t
|
||||
from sklearn import svm
|
||||
from sklearn.model_selection import GridSearchCV, KFold
|
||||
from sklearn.utils import resample
|
||||
import argparse
|
||||
import collections
|
||||
import itertools
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import random
|
||||
import sys
|
||||
|
||||
|
||||
MAX_DISTANCE_METERS = 2e6
|
||||
MAX_RANK = 255.0
|
||||
MAX_POPULARITY = 255.0
|
||||
RELEVANCES = {'Harmful': -3, 'Irrelevant': 0, 'Relevant': 1, 'Vital': 3}
|
||||
NAME_SCORES = ['Zero', 'Substring', 'Prefix', 'Full Match']
|
||||
SEARCH_TYPES = ['SUBPOI', 'COMPLEX_POI', 'Building', 'Street', 'Unclassified', 'Village', 'City', 'State', 'Country']
|
||||
RESULT_TYPES = ['TransportMajor', 'TransportLocal', 'Eat', 'Hotel', 'Attraction', 'Service', 'General']
|
||||
FEATURES = ['DistanceToPivot', 'Rank', 'Popularity', 'Rating', 'FalseCats', 'ErrorsMade', 'MatchedFraction',
|
||||
'AllTokensUsed', 'ExactCountryOrCapital'] + NAME_SCORES + SEARCH_TYPES + RESULT_TYPES
|
||||
|
||||
BOOTSTRAP_ITERATIONS = 10000
|
||||
|
||||
|
||||
def transform_name_score(value, categories_match):
|
||||
if categories_match == 1:
|
||||
return 'Zero'
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def normalize_data(data):
|
||||
transform_distance = lambda v: min(v, MAX_DISTANCE_METERS) / MAX_DISTANCE_METERS
|
||||
|
||||
data['DistanceToPivot'] = data['DistanceToPivot'].apply(transform_distance)
|
||||
data['Rank'] = data['Rank'].apply(lambda v: v / MAX_RANK)
|
||||
data['Popularity'] = data['Popularity'].apply(lambda v: v / MAX_POPULARITY)
|
||||
data['Relevance'] = data['Relevance'].apply(lambda v: RELEVANCES[v])
|
||||
|
||||
cats = data['PureCats'].combine(data['FalseCats'], max)
|
||||
|
||||
# TODO (@y, @m): do forward/backward/subset selection of features
|
||||
# instead of this merging. It would be great to conduct PCA on
|
||||
# the features too.
|
||||
data['NameScore'] = data['NameScore'].combine(cats, transform_name_score)
|
||||
|
||||
# Adds dummy variables to data for NAME_SCORES.
|
||||
for ns in NAME_SCORES:
|
||||
data[ns] = data['NameScore'].apply(lambda v: int(ns == v))
|
||||
|
||||
# Adds dummy variables to data for SEARCH_TYPES.
|
||||
|
||||
# We unify BUILDING with COMPLEX_POI and SUBPOI here, as we don't have enough
|
||||
# training data to distinguish between them. Remove following
|
||||
# line as soon as the model will be changed or we will have enough
|
||||
# training data.
|
||||
data['SearchType'] = data['SearchType'].apply(lambda v: v if v != 'Building' and v != 'COMPLEX_POI' else 'SUBPOI')
|
||||
for st in SEARCH_TYPES:
|
||||
data[st] = data['SearchType'].apply(lambda v: int(st == v))
|
||||
|
||||
# Adds dummy variables to data for RESULT_TYPES.
|
||||
for rt in RESULT_TYPES:
|
||||
data[rt] = data['ResultType'].apply(lambda v: int(rt == v))
|
||||
|
||||
|
||||
def compute_ndcg(relevances):
|
||||
"""
|
||||
Computes NDCG (Normalized Discounted Cumulative Gain) for a given
|
||||
array of scores.
|
||||
"""
|
||||
|
||||
dcg = sum(r / log(2 + i, 2) for i, r in enumerate(relevances))
|
||||
dcg_norm = sum(r / log(2 + i, 2) for i, r in enumerate(sorted(relevances, reverse=True)))
|
||||
return dcg / dcg_norm if dcg_norm != 0 else 0
|
||||
|
||||
|
||||
def compute_ndcgs_without_ws(data):
|
||||
"""
|
||||
Computes NDCG (Normalized Discounted Cumulative Gain) for a given
|
||||
data. Returns an array of ndcg scores in the shape [num groups of
|
||||
features].
|
||||
"""
|
||||
|
||||
grouped = data.groupby(data['SampleId'], sort=False).groups
|
||||
|
||||
ndcgs = []
|
||||
for id in grouped:
|
||||
indices = grouped[id]
|
||||
relevances = np.array(data.ix[indices]['Relevance'])
|
||||
ndcgs.append(compute_ndcg(relevances))
|
||||
|
||||
return ndcgs
|
||||
|
||||
|
||||
def compute_ndcgs_for_ws(data, ws):
|
||||
"""
|
||||
Computes NDCG (Normalized Discounted Cumulative Gain) for a given
|
||||
data and an array of coeffs in a linear model. Returns an array of
|
||||
ndcg scores in the shape [num groups of features].
|
||||
"""
|
||||
|
||||
data_scores = np.array([np.dot(data.ix[i][FEATURES], ws) for i in data.index])
|
||||
grouped = data.groupby(data['SampleId'], sort=False).groups
|
||||
|
||||
ndcgs = []
|
||||
for id in grouped:
|
||||
indices = grouped[id]
|
||||
|
||||
relevances = np.array(data.ix[indices]['Relevance'])
|
||||
scores = data_scores[indices]
|
||||
|
||||
# Reoders relevances in accordance with decreasing scores.
|
||||
relevances = relevances[scores.argsort()[::-1]]
|
||||
ndcgs.append(compute_ndcg(relevances))
|
||||
|
||||
return ndcgs
|
||||
|
||||
|
||||
def transform_data(data):
|
||||
"""
|
||||
By a given data computes x and y that can be used as an input to a
|
||||
linear SVM.
|
||||
"""
|
||||
|
||||
grouped = data.groupby(data['SampleId'], sort=False)
|
||||
|
||||
xs, ys = [], []
|
||||
|
||||
# k is used to create a balanced samples set for better linear
|
||||
# separation.
|
||||
k = 1
|
||||
for _, group in grouped:
|
||||
features, relevances = group[FEATURES], group['Relevance']
|
||||
|
||||
n, total = len(group), 0
|
||||
for _, (i, j) in enumerate(itertools.combinations(range(n), 2)):
|
||||
dr = relevances.iloc[j] - relevances.iloc[i]
|
||||
y = np.sign(dr)
|
||||
if y == 0:
|
||||
continue
|
||||
|
||||
x = np.array(features.iloc[j]) - np.array(features.iloc[i])
|
||||
|
||||
# Need to multiply x by average drop in NDCG when i-th and
|
||||
# j-th are exchanged.
|
||||
x *= abs(dr * (1 / log(j + 2, 2) - 1 / log(i + 2, 2)))
|
||||
|
||||
# This is needed to prevent disbalance in classes sizes.
|
||||
if y != k:
|
||||
x = np.negative(x)
|
||||
y = -y
|
||||
|
||||
xs.append(x)
|
||||
ys.append(y)
|
||||
total += 1
|
||||
k = -k
|
||||
|
||||
# Scales this group of features to equalize different search
|
||||
# queries.
|
||||
for i in range(-1, -total, -1):
|
||||
xs[i] = xs[i] / total
|
||||
return xs, ys
|
||||
|
||||
|
||||
def show_pearson_statistics(xs, ys, features):
|
||||
"""
|
||||
Shows info about Pearson coefficient between features and
|
||||
relevancy.
|
||||
"""
|
||||
|
||||
print('***** Correlation table *****')
|
||||
print('H0 - feature not is correlated with relevancy')
|
||||
print('H1 - feature is correlated with relevancy')
|
||||
print()
|
||||
|
||||
cs, ncs = [], []
|
||||
for i, f in enumerate(features):
|
||||
zs = [x[i] for x in xs]
|
||||
(c, p) = pearsonr(zs, ys)
|
||||
|
||||
correlated = p < 0.05
|
||||
print('{}: pearson={:.3f}, P(H1)={}'.format(f, c, 1 - p))
|
||||
if correlated:
|
||||
cs.append(f)
|
||||
else:
|
||||
ncs.append(f)
|
||||
|
||||
print()
|
||||
print('Correlated:', cs)
|
||||
print('Non-correlated:', ncs)
|
||||
|
||||
|
||||
def raw_output(features, ws):
|
||||
"""
|
||||
Prints feature-coeff pairs to the standard output.
|
||||
"""
|
||||
|
||||
print('{:<20}{}'.format('Feature', 'Value'))
|
||||
print()
|
||||
for f, w in zip(features, ws):
|
||||
print('{:<20}{:.5f}'.format(f, w))
|
||||
|
||||
|
||||
def print_const(name, value):
|
||||
print('double constexpr k{} = {:.7f};'.format(name, value))
|
||||
|
||||
|
||||
def print_array(name, size, values):
|
||||
print('double constexpr {}[{}] = {{'.format(name, size))
|
||||
print(',\n'.join(' {:.7f} /* {} */'.format(w, f) for (f, w) in values))
|
||||
print('};')
|
||||
|
||||
def cpp_output(features, ws):
|
||||
"""
|
||||
Prints feature-coeff pairs in the C++-compatible format.
|
||||
"""
|
||||
|
||||
ns, st, rt = [], [], []
|
||||
|
||||
for f, w in zip(features, ws):
|
||||
if f in NAME_SCORES:
|
||||
ns.append((f, w))
|
||||
elif f in SEARCH_TYPES:
|
||||
st.append((f, w))
|
||||
elif f in RESULT_TYPES:
|
||||
rt.append((f, w))
|
||||
else:
|
||||
print_const(f, w)
|
||||
print_array('kNameScore', 'NameScore::NAME_SCORE_COUNT', ns)
|
||||
print_array('kType', 'Model::TYPE_COUNT', st)
|
||||
print_array('kResultType', 'base::Underlying(ResultType::Count)', rt)
|
||||
|
||||
|
||||
def show_bootstrap_statistics(clf, X, y, features):
|
||||
num_features = len(features)
|
||||
|
||||
coefs = []
|
||||
for i in range(num_features):
|
||||
coefs.append([])
|
||||
|
||||
for _ in range(BOOTSTRAP_ITERATIONS):
|
||||
X_sample, y_sample = resample(X, y)
|
||||
clf.fit(X_sample, y_sample)
|
||||
for i, c in enumerate(get_normalized_coefs(clf)):
|
||||
coefs[i].append(c)
|
||||
|
||||
subpoi_index = features.index('SUBPOI')
|
||||
poi_index = features.index('COMPLEX_POI')
|
||||
building_index = features.index('Building')
|
||||
coefs[building_index] = coefs[subpoi_index]
|
||||
coefs[poi_index] = coefs[subpoi_index]
|
||||
|
||||
intervals = []
|
||||
|
||||
print()
|
||||
print('***** Bootstrap statistics *****')
|
||||
print('{:<20}{:<20}{:<10}{:<10}'.format('Feature', '95% interval', 't-value', 'Pr(>|t|)'))
|
||||
print()
|
||||
for i, cs in enumerate(coefs):
|
||||
values = np.array(cs)
|
||||
lo = np.percentile(values, 2.5)
|
||||
hi = np.percentile(values, 97.5)
|
||||
interval = '({:.3f}, {:.3f})'.format(lo, hi)
|
||||
tv = np.mean(values) / np.std(values)
|
||||
pr = (1.0 - t.cdf(x=abs(tv), df=len(values))) * 0.5
|
||||
|
||||
stv = '{:.3f}'.format(tv)
|
||||
spr = '{:.3f}'.format(pr)
|
||||
print('{:<20}{:<20}{:<10}{:<10}'.format(features[i], interval, stv, spr))
|
||||
|
||||
|
||||
def get_normalized_coefs(clf):
|
||||
ws = clf.coef_[0]
|
||||
max_w = max(abs(w) for w in ws)
|
||||
return np.divide(ws, max_w)
|
||||
|
||||
|
||||
def main(args):
|
||||
data = pd.read_csv(sys.stdin)
|
||||
|
||||
# Drop categorial requests cause we use different ranking model for them.
|
||||
data.drop(data[data['IsCategorialRequest'] == 1].index, inplace=True)
|
||||
data.reset_index(inplace=True, drop=True)
|
||||
data.drop(columns=['IsCategorialRequest', 'HasName'], inplace=True)
|
||||
|
||||
normalize_data(data)
|
||||
|
||||
ndcgs = compute_ndcgs_without_ws(data);
|
||||
print('Current NDCG: {:.3f}, std: {:.3f}'.format(np.mean(ndcgs), np.std(ndcgs)))
|
||||
print()
|
||||
|
||||
xs, ys = transform_data(data)
|
||||
|
||||
clf = svm.LinearSVC(random_state=args.seed)
|
||||
cv = KFold(n_splits=5, shuffle=True, random_state=args.seed)
|
||||
|
||||
# "C" stands for the regularizer constant.
|
||||
grid = {'C': np.power(10.0, np.arange(-5, 6))}
|
||||
gs = GridSearchCV(clf, grid, scoring='roc_auc', cv=cv)
|
||||
gs.fit(xs, ys)
|
||||
|
||||
print('Best params: {}'.format(gs.best_params_))
|
||||
|
||||
ws = get_normalized_coefs(gs.best_estimator_)
|
||||
|
||||
# Following code restores coeffs for merged features.
|
||||
ws[FEATURES.index('Building')] = ws[FEATURES.index('SUBPOI')]
|
||||
ws[FEATURES.index('COMPLEX_POI')] = ws[FEATURES.index('SUBPOI')]
|
||||
|
||||
ndcgs = compute_ndcgs_for_ws(data, ws)
|
||||
|
||||
print('NDCG mean: {:.3f}, std: {:.3f}'.format(np.mean(ndcgs), np.std(ndcgs)))
|
||||
print('ROC AUC: {:.3f}'.format(gs.best_score_))
|
||||
|
||||
if args.pearson:
|
||||
print()
|
||||
show_pearson_statistics(xs, ys, FEATURES)
|
||||
|
||||
print()
|
||||
print('***** Linear model weights *****')
|
||||
if args.cpp:
|
||||
cpp_output(FEATURES, ws)
|
||||
else:
|
||||
raw_output(FEATURES, ws)
|
||||
|
||||
if args.bootstrap:
|
||||
show_bootstrap_statistics(clf, xs, ys, FEATURES)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--seed', help='random seed', type=int)
|
||||
parser.add_argument('--pearson', help='show pearson statistics', action='store_true')
|
||||
parser.add_argument('--cpp', help='generate output in the C++ format', action='store_true')
|
||||
parser.add_argument('--bootstrap', help='show bootstrap confidence intervals', action='store_true')
|
||||
args = parser.parse_args()
|
||||
main(args)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
project(search_quality_tests)
|
||||
|
||||
set(SRC
|
||||
benchmark_tests.cpp
|
||||
real_mwm_tests.cpp
|
||||
sample_test.cpp
|
||||
)
|
||||
|
||||
omim_add_test(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_quality
|
||||
)
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "search/search_tests_support/helpers.hpp"
|
||||
|
||||
namespace benchmark_tests
|
||||
{
|
||||
|
||||
using BenchmarkFixture = search::tests_support::SearchTest;
|
||||
|
||||
UNIT_CLASS_TEST(BenchmarkFixture, Smoke)
|
||||
{
|
||||
RegisterLocalMapsInViewport(mercator::Bounds::FullRect());
|
||||
|
||||
SetViewport({50.1052, 8.6868}, 10000); // Frankfurt am Main
|
||||
|
||||
auto request = MakeRequest("b");
|
||||
LOG(LINFO, (request->ResponseTime().count()));
|
||||
}
|
||||
|
||||
} // namespace benchmark_tests
|
||||
1426
libs/search/search_quality/search_quality_tests/real_mwm_tests.cpp
Normal file
1426
libs/search/search_quality/search_quality_tests/real_mwm_tests.cpp
Normal file
File diff suppressed because it is too large
Load diff
174
libs/search/search_quality/search_quality_tests/sample_test.cpp
Normal file
174
libs/search/search_quality/search_quality_tests/sample_test.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "search/search_quality/sample.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace sample_test
|
||||
{
|
||||
using search::Sample;
|
||||
|
||||
class SampleTest
|
||||
{
|
||||
public:
|
||||
SampleTest() { Init(); }
|
||||
|
||||
protected:
|
||||
void Init();
|
||||
|
||||
Sample m_cuba;
|
||||
Sample m_riga;
|
||||
Sample m_tula;
|
||||
};
|
||||
|
||||
void SampleTest::Init()
|
||||
{
|
||||
m_cuba.m_query = strings::MakeUniString("cuba");
|
||||
m_cuba.m_locale = "en";
|
||||
m_cuba.m_pos = m2::PointD{37.618706, 99.53730574302003};
|
||||
m_cuba.m_viewport = {37.1336, 67.1349, 38.0314, 67.7348};
|
||||
Sample::Result cubaRes;
|
||||
cubaRes.m_name = strings::MakeUniString("Cuba");
|
||||
cubaRes.m_relevance = Sample::Result::Relevance::Relevant;
|
||||
cubaRes.m_types.push_back("place-country");
|
||||
cubaRes.m_pos = {-80.832886, 15.521132748163712};
|
||||
cubaRes.m_houseNumber = "";
|
||||
m_cuba.m_results = {cubaRes};
|
||||
m_cuba.m_relatedQueries = {strings::MakeUniString("Cuba Libre"), strings::MakeUniString("Patria o Muerte")};
|
||||
|
||||
m_riga.m_query = strings::MakeUniString("riga");
|
||||
m_riga.m_locale = "en";
|
||||
m_riga.m_pos = m2::PointD{37.65376, 98.51110651930014};
|
||||
m_riga.m_viewport = {37.5064, 67.0476, 37.7799, 67.304};
|
||||
Sample::Result rigaRes;
|
||||
rigaRes.m_name = strings::MakeUniString("Rīga");
|
||||
rigaRes.m_relevance = Sample::Result::Relevance::Vital;
|
||||
rigaRes.m_types.push_back("place-city-capital-2");
|
||||
rigaRes.m_pos = {24.105186, 107.7819569220319};
|
||||
rigaRes.m_houseNumber = "";
|
||||
m_riga.m_results = {rigaRes, rigaRes};
|
||||
|
||||
m_tula.m_query = strings::MakeUniString("tula");
|
||||
m_tula.m_locale = "en";
|
||||
m_tula.m_pos = {};
|
||||
m_tula.m_viewport = {37.5064, 67.0476, 37.7799, 67.304};
|
||||
}
|
||||
|
||||
UNIT_CLASS_TEST(SampleTest, Smoke)
|
||||
{
|
||||
auto const jsonStr = R"EOF(
|
||||
{
|
||||
"query": "cuba",
|
||||
"locale": "en",
|
||||
"position": {
|
||||
"x": 37.618706,
|
||||
"y": 99.53730574302003
|
||||
},
|
||||
"viewport": {
|
||||
"minx": 37.1336,
|
||||
"miny": 67.1349,
|
||||
"maxx": 38.0314,
|
||||
"maxy": 67.7348
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"name": "Cuba",
|
||||
"relevancy": "relevant",
|
||||
"types": [
|
||||
"place-country"
|
||||
],
|
||||
"position": {
|
||||
"x": -80.832886,
|
||||
"y": 15.521132748163712
|
||||
},
|
||||
"houseNumber": ""
|
||||
}
|
||||
],
|
||||
"related_queries": ["Cuba Libre", "Patria o Muerte"]
|
||||
}
|
||||
)EOF";
|
||||
|
||||
Sample s;
|
||||
TEST(s.DeserializeFromJSON(jsonStr), ());
|
||||
TEST_EQUAL(s, m_cuba, ());
|
||||
}
|
||||
|
||||
UNIT_CLASS_TEST(SampleTest, BadViewport)
|
||||
{
|
||||
auto const jsonStr = R"EOF(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"houseNumber": "",
|
||||
"position": {
|
||||
"y": 15.521132748163712,
|
||||
"x": -80.832886
|
||||
},
|
||||
"types": [
|
||||
"place-country"
|
||||
],
|
||||
"relevancy": "relevant",
|
||||
"name": "Cuba"
|
||||
}
|
||||
],
|
||||
"viewport": {
|
||||
"maxy": 67.7348,
|
||||
"maxx": 38.0314,
|
||||
},
|
||||
"position": {
|
||||
"y": 99.53730574302003,
|
||||
"x": 37.618706
|
||||
},
|
||||
"locale": "en",
|
||||
"query": "cuba"
|
||||
}
|
||||
)EOF";
|
||||
|
||||
Sample s;
|
||||
TEST(!s.DeserializeFromJSON(jsonStr), ());
|
||||
}
|
||||
|
||||
UNIT_CLASS_TEST(SampleTest, Arrays)
|
||||
{
|
||||
std::string lines;
|
||||
lines.append(
|
||||
R"({"query": "cuba", "locale": "en", "position": {"x": 37.618706, "y": 99.53730574302003}, "viewport": {"minx": 37.1336, "miny": 67.1349, "maxx": 38.0314, "maxy": 67.7348}, "results": [{"name": "Cuba", "relevancy": "relevant", "types": ["place-country"], "position": {"x": -80.832886, "y": 15.521132748163712}, "houseNumber": ""}], "related_queries": ["Patria o Muerte", "Cuba Libre"]})");
|
||||
lines.append("\n");
|
||||
|
||||
lines.append(
|
||||
R"({"query": "riga", "locale": "en", "position": {"x": 37.65376, "y": 98.51110651930014}, "viewport": {"minx": 37.5064, "miny": 67.0476, "maxx": 37.7799, "maxy": 67.304}, "results": [{"name": "R\u012bga", "relevancy": "vital", "types": ["place-city-capital-2"], "position": {"x": 24.105186, "y": 107.7819569220319}, "houseNumber": ""}, {"name": "R\u012bga", "relevancy": "vital", "types": ["place-city-capital-2"], "position": {"x": 24.105186, "y": 107.7819569220319}, "houseNumber": ""}]})");
|
||||
lines.append("\n");
|
||||
|
||||
lines.append(
|
||||
R"({"query": "tula", "locale": "en", "viewport": {"minx": 37.5064, "miny": 67.0476, "maxx": 37.7799, "maxy": 67.304}})");
|
||||
lines.append("\n");
|
||||
|
||||
std::vector<Sample> samples;
|
||||
TEST(Sample::DeserializeFromJSONLines(lines, samples), ());
|
||||
|
||||
std::vector<Sample> expected = {m_cuba, m_riga, m_tula};
|
||||
|
||||
std::sort(samples.begin(), samples.end());
|
||||
std::sort(expected.begin(), expected.end());
|
||||
|
||||
TEST_EQUAL(samples, expected, ());
|
||||
}
|
||||
|
||||
UNIT_CLASS_TEST(SampleTest, SerDes)
|
||||
{
|
||||
std::vector<Sample> expected = {m_cuba, m_riga, m_tula};
|
||||
|
||||
std::string lines;
|
||||
Sample::SerializeToJSONLines(expected, lines);
|
||||
|
||||
std::vector<Sample> actual;
|
||||
TEST(Sample::DeserializeFromJSONLines(lines, actual), ());
|
||||
|
||||
std::sort(expected.begin(), expected.end());
|
||||
std::sort(actual.begin(), actual.end());
|
||||
TEST_EQUAL(expected, actual, ());
|
||||
}
|
||||
} // namespace sample_test
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
project(search_quality_tool)
|
||||
|
||||
set(SRC search_quality_tool.cpp)
|
||||
|
||||
omim_add_executable(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
search_tests_support
|
||||
search_quality
|
||||
gflags::gflags
|
||||
)
|
||||
41
libs/search/search_quality/search_quality_tool/queries.txt
Normal file
41
libs/search/search_quality/search_quality_tool/queries.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
wifi
|
||||
hotel
|
||||
Москва
|
||||
London
|
||||
кафе москва
|
||||
улица ленина
|
||||
улица Ленина дом 1
|
||||
ленина 1
|
||||
лесная 12
|
||||
ул. лесная д. 13
|
||||
улица Лесная д14
|
||||
лесная 15/1
|
||||
Ленинградский проспект 39
|
||||
Ленинградский проспект 39с79
|
||||
Ленинградский проспект 39-79
|
||||
Ленинградский проспект 39 стр. 79
|
||||
Senieji Trakai
|
||||
Trakų str
|
||||
IKEA Москва
|
||||
Красноармейская улица
|
||||
улица набережная
|
||||
квартира 44
|
||||
Tehama street
|
||||
tehama 4th street
|
||||
Улица 8 Марта
|
||||
4-я улица 8 Марта
|
||||
орехово-зуево лапина улица 78
|
||||
поварово i
|
||||
поварово 1
|
||||
чехова улица завод
|
||||
чехов улица лермонтова
|
||||
чехов улица чехова
|
||||
депутатский
|
||||
6 route des jeunes Genève
|
||||
москва 4 2 останкинская
|
||||
москва 2 4 останкинская
|
||||
улица Константина Симонова
|
||||
фрезерная 1, 2/1, стр. 1
|
||||
фрезерная 1, д. 2/1, стр. 1
|
||||
фрезерная 1, д. 2/1, стр. 10
|
||||
хиславичи улица толстого 19
|
||||
|
|
@ -0,0 +1,377 @@
|
|||
#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 += math::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() && 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue