[search] [assessment-tool] Samples can now be marked as useless.

This commit is contained in:
Maxim Pimenov 2019-04-12 14:54:48 +03:00 committed by Tatiana Yan
parent b4e8f38c7c
commit 861bc09eeb
15 changed files with 200 additions and 32 deletions

View file

@ -21,9 +21,18 @@ void Context::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;
@ -93,6 +102,8 @@ search::Sample Context::MakeSample(search::FeatureLoader & loader) const
outResults.push_back(search::Sample::Result::Build(*ft, *foundEntries[i].m_currRelevance));
}
outSample.m_useless = m_sampleEdits.m_currUseless;
return outSample;
}
@ -102,12 +113,15 @@ void Context::ApplyEdits()
return;
m_foundResultsEdits.Apply();
m_nonFoundResultsEdits.Apply();
m_sampleEdits.Apply();
}
// ContextList -------------------------------------------------------------------------------------
ContextList::ContextList(OnUpdate onResultsUpdate, OnUpdate onNonFoundResultsUpdate)
ContextList::ContextList(OnUpdate onResultsUpdate, OnUpdate onNonFoundResultsUpdate,
OnSampleUpdate onSampleUpdate)
: m_onResultsUpdate(onResultsUpdate)
, m_onNonFoundResultsUpdate(onNonFoundResultsUpdate)
, m_onSampleUpdate(onSampleUpdate)
{
}
@ -133,6 +147,11 @@ void ContextList::Resize(size_t size)
OnContextUpdated(i);
if (m_onNonFoundResultsUpdate)
m_onNonFoundResultsUpdate(i, update);
},
[this, i]() {
OnContextUpdated(i);
if (m_onSampleUpdate)
m_onSampleUpdate(i);
});
}
}

View file

@ -26,8 +26,11 @@ struct Context
Completed
};
Context(Edits::OnUpdate onFoundResultsUpdate, Edits::OnUpdate onNonFoundResultsUpdate)
: m_foundResultsEdits(onFoundResultsUpdate), m_nonFoundResultsEdits(onNonFoundResultsUpdate)
Context(Edits::OnUpdate onFoundResultsUpdate, Edits::OnUpdate onNonFoundResultsUpdate,
SampleEdits::OnUpdate onSampleUpdate)
: m_foundResultsEdits(onFoundResultsUpdate)
, m_nonFoundResultsEdits(onNonFoundResultsUpdate)
, m_sampleEdits(onSampleUpdate)
{
}
@ -42,8 +45,12 @@ struct Context
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();
@ -51,6 +58,8 @@ struct Context
void Clear();
void LoadFromSample(search::Sample const & sample);
// Makes sample in accordance with uncommited edits.
search::Sample MakeSample(search::FeatureLoader & loader) const;
@ -67,6 +76,8 @@ struct Context
std::vector<search::Sample::Result> m_nonFoundResults;
Edits m_nonFoundResultsEdits;
SampleEdits m_sampleEdits;
SearchState m_searchState = SearchState::Untouched;
bool m_initialized = false;
@ -95,6 +106,8 @@ public:
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:
@ -102,8 +115,10 @@ public:
};
using OnUpdate = std::function<void(size_t index, Edits::Update const & update)>;
using OnSampleUpdate = std::function<void(size_t index)>;
ContextList(OnUpdate onResultsUpdate, OnUpdate onNonFoundResultsUpdate);
ContextList(OnUpdate onResultsUpdate, OnUpdate onNonFoundResultsUpdate,
OnSampleUpdate onSampleUpdate);
void Resize(size_t size);
size_t Size() const { return m_contexts.size(); }
@ -128,4 +143,5 @@ private:
OnUpdate m_onResultsUpdate;
OnUpdate m_onNonFoundResultsUpdate;
OnSampleUpdate m_onSampleUpdate;
};

View file

@ -12,6 +12,38 @@
#include <boost/optional.hpp>
struct SampleEdits
{
using OnUpdate = std::function<void()>;
SampleEdits(OnUpdate onUpdate) : m_onUpdate(onUpdate) {}
void Reset(bool origUseless)
{
m_origUseless = origUseless;
m_currUseless = origUseless;
}
void FlipUsefulness()
{
m_currUseless ^= true;
if (m_onUpdate)
m_onUpdate();
}
void Apply() { m_origUseless = m_currUseless; }
bool HasChanges() const { return m_origUseless != m_currUseless; }
void Clear() {}
bool m_origUseless = false;
bool m_currUseless = false;
OnUpdate m_onUpdate;
};
// todo(@m) Rename to ResultsEdits?
class Edits
{
public:

View file

@ -36,12 +36,13 @@ MainModel::MainModel(Framework & framework)
},
[this](size_t sampleIndex, Edits::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) {
// The second parameter does not matter, we only change SearchStatus.
m_view->OnSampleChanged(index, false /* hasEdits */);
// Only the first parameter matters because we only change SearchStatus.
m_view->OnSampleChanged(index, false /* isUseless */, false /* hasEdits */);
})
{
search::CheckLocale();
@ -77,11 +78,8 @@ void MainModel::Open(string const & path)
m_contexts.Resize(samples.size());
for (size_t i = 0; i < samples.size(); ++i)
{
auto & context = m_contexts[i];
context.Clear();
context.m_sample = samples[i];
}
m_contexts[i].LoadFromSample(samples[i]);
m_path = path;
m_view->SetSamples(ContextList::SamplesSlice(m_contexts));
@ -134,7 +132,7 @@ void MainModel::OnSampleSelected(int index)
auto & context = m_contexts[index];
auto const & sample = context.m_sample;
m_view->ShowSample(index, sample, sample.m_pos, context.HasChanges());
m_view->ShowSample(index, sample, sample.m_pos, context.IsUseless(), context.HasChanges());
m_runner.ResetForegroundSearch();
m_numShownResults = 0;
@ -271,12 +269,22 @@ void MainModel::AddNonFoundResult(FeatureID const & id)
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.HasChanges());
m_view->ShowSample(index, sample, sample.m_pos, context.IsUseless(), context.HasChanges());
m_runner.InitiateForegroundSearch(index);
m_view->OnSearchStarted();
}
@ -299,7 +307,7 @@ void MainModel::OnUpdate(View::ResultType type, size_t sampleIndex, Edits::Updat
}
m_view->OnResultChanged(sampleIndex, type, update);
m_view->OnSampleChanged(sampleIndex, context.HasChanges());
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 ||
@ -311,6 +319,14 @@ void MainModel::OnUpdate(View::ResultType type, size_t sampleIndex, Edits::Updat
}
}
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(), ());
@ -329,7 +345,8 @@ void MainModel::UpdateViewOnResults(search::Results const & results)
m_view->ShowMarks(context);
m_view->OnResultChanged(m_selectedSample, View::ResultType::Found, Edits::Update::MakeAll());
m_view->OnResultChanged(m_selectedSample, View::ResultType::NonFound, Edits::Update::MakeAll());
m_view->OnSampleChanged(m_selectedSample, context.HasChanges());
m_view->OnSampleChanged(m_selectedSample, context.IsUseless(), context.HasChanges());
m_view->OnSamplesChanged(m_contexts.HasChanges());
m_view->SetEdits(m_selectedSample, context.m_foundResultsEdits, context.m_nonFoundResultsEdits);
m_view->OnSearchCompleted();
@ -346,7 +363,7 @@ void MainModel::OnChangeAllRelevancesClicked(Edits::Relevance relevance)
m_view->OnResultChanged(m_selectedSample, View::ResultType::Found, Edits::Update::MakeAll());
m_view->OnResultChanged(m_selectedSample, View::ResultType::NonFound, Edits::Update::MakeAll());
m_view->OnSampleChanged(m_selectedSample, context.HasChanges());
m_view->OnSampleChanged(m_selectedSample, context.IsUseless(), context.HasChanges());
m_view->OnSamplesChanged(m_contexts.HasChanges());
}

View file

@ -47,6 +47,7 @@ public:
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;
@ -54,6 +55,7 @@ private:
void InitiateForegroundSearch(size_t index);
void OnUpdate(View::ResultType type, size_t sampleIndex, Edits::Update const & update);
void OnSampleUpdate(size_t sampleIndex);
void UpdateViewOnResults(search::Results const & results);
void ShowMarks(Context const & context);

View file

@ -110,7 +110,8 @@ void MainView::OnSearchCompleted()
}
void MainView::ShowSample(size_t sampleIndex, search::Sample const & sample,
boost::optional<m2::PointD> const & position, bool hasEdits)
boost::optional<m2::PointD> const & position, bool isUseless,
bool hasEdits)
{
m_sampleLocale = sample.m_locale;
@ -121,7 +122,7 @@ void MainView::ShowSample(size_t sampleIndex, search::Sample const & sample,
OnResultChanged(sampleIndex, ResultType::Found, Edits::Update::MakeAll());
OnResultChanged(sampleIndex, ResultType::NonFound, Edits::Update::MakeAll());
OnSampleChanged(sampleIndex, hasEdits);
OnSampleChanged(sampleIndex, isUseless, hasEdits);
}
void MainView::AddFoundResults(search::Results::ConstIter begin, search::Results::ConstIter end)
@ -190,12 +191,13 @@ void MainView::OnResultChanged(size_t sampleIndex, ResultType type, Edits::Updat
}
}
void MainView::OnSampleChanged(size_t sampleIndex, bool hasEdits)
void MainView::OnSampleChanged(size_t sampleIndex, bool isUseless, bool hasEdits)
{
m_samplesView->OnUpdate(sampleIndex);
if (!m_samplesView->IsSelected(sampleIndex))
return;
SetSampleDockTitle(hasEdits);
SetSampleDockTitle(isUseless, hasEdits);
m_sampleView->OnUselessnessChanged(isUseless);
}
void MainView::OnSamplesChanged(bool hasEdits)
@ -225,7 +227,7 @@ void MainView::Clear()
SetSamplesDockTitle(false /* hasEdits */);
m_sampleView->Clear();
SetSampleDockTitle(false /* hasEdits */);
SetSampleDockTitle(false /* isUseless */, false /* hasEdits */);
m_skipFeatureInfoDialog = false;
m_sampleLocale.clear();
@ -361,6 +363,9 @@ void MainView::InitDocks()
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 */);
@ -393,7 +398,7 @@ void MainView::InitDocks()
connect(m_sampleDock, &QDockWidget::dockLocationChanged,
[this](Qt::DockWidgetArea area) { m_sampleView->OnLocationChanged(area); });
addDockWidget(Qt::RightDockWidgetArea, m_sampleDock);
SetSampleDockTitle(false /* hasEdits */);
SetSampleDockTitle(false /* isUseless */, false /* hasEdits */);
}
void MainView::Open()
@ -479,13 +484,15 @@ void MainView::SetSamplesDockTitle(bool hasEdits)
m_samplesDock->setWindowTitle(tr("Samples"));
}
void MainView::SetSampleDockTitle(bool hasEdits)
void MainView::SetSampleDockTitle(bool isUseless, bool hasEdits)
{
CHECK(m_sampleDock, ());
std::string title = "Sample";
if (hasEdits)
m_sampleDock->setWindowTitle(tr("Sample *"));
else
m_sampleDock->setWindowTitle(tr("Sample"));
title += " *";
if (isUseless)
title += " (useless)";
m_sampleDock->setWindowTitle(tr(title.data()));
}
MainView::SaveResult MainView::TryToSaveEdits(QString const & msg)

View file

@ -33,7 +33,8 @@ public:
void OnSearchStarted() override;
void OnSearchCompleted() override;
void ShowSample(size_t sampleIndex, search::Sample const & sample,
boost::optional<m2::PointD> const & position, bool hasEdits) override;
boost::optional<m2::PointD> const & position, bool isUseless,
bool hasEdits) override;
void AddFoundResults(search::Results::ConstIter begin, search::Results::ConstIter end) override;
void ShowNonFoundResults(std::vector<search::Sample::Result> const & results,
@ -53,7 +54,7 @@ public:
void OnResultChanged(size_t sampleIndex, ResultType type, Edits::Update const & update) override;
void SetEdits(size_t sampleIndex, Edits & foundResultsEdits,
Edits & nonFoundResultsEdits) override;
void OnSampleChanged(size_t sampleIndex, bool hasEdits) override;
void OnSampleChanged(size_t sampleIndex, bool isUseless, bool hasEdits) override;
void OnSamplesChanged(bool hasEdits) override;
void ShowError(std::string const & msg) override;
@ -105,7 +106,7 @@ private:
void InitiateBackgroundSearch();
void SetSamplesDockTitle(bool hasEdits);
void SetSampleDockTitle(bool hasEdits);
void SetSampleDockTitle(bool isUseless, bool hasEdits);
SaveResult TryToSaveEdits(QString const & msg);
void AddSelectedFeature(QPoint const & p);

View file

@ -35,6 +35,7 @@ public:
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;

View file

@ -101,6 +101,7 @@ SampleView::SampleView(QWidget * parent, Framework & framework)
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();
@ -121,6 +122,13 @@ SampleView::SampleView(QWidget * parent, Framework & framework)
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);
@ -305,6 +313,25 @@ void SampleView::SetEdits(Edits & resultsEdits, Edits & 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();

View file

@ -44,6 +44,8 @@ public:
void SetEdits(Edits & resultsEdits, Edits & nonFoundResultsEdits);
void OnUselessnessChanged(bool isUseless);
void Clear();
ResultsView & GetFoundResultsView() { return *m_foundResults; }
@ -81,6 +83,8 @@ private:
QPushButton * m_markAllAsRelevant = nullptr;
QPushButton * m_markAllAsIrrelevant = nullptr;
QLabel * m_uselessnessLabel = nullptr;
ResultsView * m_foundResults = nullptr;
QWidget * m_foundResultsBox = nullptr;

View file

@ -3,9 +3,15 @@
#include "search/search_quality/assessment_tool/helpers.hpp"
#include "base/assert.hpp"
#include "base/logging.hpp"
#include <string>
#include <QtGui/QContextMenuEvent>
#include <QtGui/QStandardItem>
#include <QtWidgets/QAction>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QMenu>
// SamplesView::Model ------------------------------------------------------------------------------
SamplesView::Model::Model(QWidget * parent)
@ -55,3 +61,19 @@ 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());
}

View file

@ -8,10 +8,13 @@
#include <vector>
#include <QtGui/QStandardItemModel>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QTableView>
class SamplesView : public QTableView
{
Q_OBJECT
public:
explicit SamplesView(QWidget * parent);
@ -20,6 +23,12 @@ public:
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
{
@ -42,6 +51,8 @@ private:
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;

View file

@ -34,7 +34,8 @@ public:
virtual void OnSearchStarted() = 0;
virtual void OnSearchCompleted() = 0;
virtual void ShowSample(size_t index, search::Sample const & sample,
boost::optional<m2::PointD> const & position, bool hasEdits) = 0;
boost::optional<m2::PointD> const & position, bool isUseless,
bool hasEdits) = 0;
virtual void AddFoundResults(search::Results::ConstIter begin,
search::Results::ConstIter end) = 0;
@ -55,7 +56,7 @@ public:
virtual void OnResultChanged(size_t sampleIndex, ResultType type,
Edits::Update const & update) = 0;
virtual void SetEdits(size_t index, Edits & foundResultsEdits, Edits & nonFoundResultsEdits) = 0;
virtual void OnSampleChanged(size_t sampleIndex, bool hasEdits) = 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;

View file

@ -168,6 +168,7 @@ void Sample::DeserializeFromJSONImpl(json_t * root)
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
@ -178,6 +179,8 @@ void Sample::SerializeToJSONImpl(json_t & root) const
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

View file

@ -76,6 +76,11 @@ struct Sample
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 it contains.
bool m_useless = false;
};
void FromJSONObject(json_t * root, char const * field,