mu/lib/mu-query-match-deciders.cc

232 lines
8.8 KiB
C++

/*
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/
#include "mu-query-match-deciders.hh"
#include "mu-query-results.hh"
#include "utils/mu-option.hh"
using namespace Mu;
// We use a MatchDecider to gather information about the matches, and decide
// whether to include them in the results.
//
// Note that to include the "related" messages, we need _two_ queries; the first
// one to get the initial matches (called the Leader-Query) and a Related-Query, to get
// the Leader matches + all messages that have a thread-id seen in the Leader
// matches.
//
// We use the MatchDecider to gather information and use it for both queries.
struct MatchDecider: public Xapian::MatchDecider {
MatchDecider (QueryFlags qflags, DeciderInfo& info):
qflags_{qflags}, decider_info_{info}
{}
/**
* Update the match structure with unreadable/duplicate flags
*
* @param doc a Xapian document.
*
* @return a new QueryMatch object
*/
QueryMatch make_query_match (const Xapian::Document& doc) const {
QueryMatch qm{};
auto msgid {opt_string(doc, MU_MSG_FIELD_ID_MSGID)
.value_or(*opt_string(doc, MU_MSG_FIELD_ID_PATH))};
if (!decider_info_.message_ids.emplace(std::move(msgid)).second)
qm.flags |= QueryMatch::Flags::Duplicate;
const auto path{opt_string(doc, MU_MSG_FIELD_ID_PATH)};
if (!path || ::access(path->c_str(), R_OK) != 0)
qm.flags |= QueryMatch::Flags::Unreadable;
return qm;
}
/**
* Should this message be included in the results?
*
* @param qm a query match
*
* @return true or false
*/
bool should_include (const QueryMatch& qm) const {
if (any_of(qflags_ & QueryFlags::SkipDuplicates) &&
any_of(qm.flags & QueryMatch::Flags::Duplicate))
return false;
if (any_of(qflags_ & QueryFlags::SkipUnreadable) &&
any_of(qm.flags & QueryMatch::Flags::Unreadable))
return false;
return true;
}
/**
* Gather thread ids from this match.
*
* @param doc the document (message)
*
*/
void gather_thread_ids(const Xapian::Document& doc) const {
auto thread_id{opt_string(doc, MU_MSG_FIELD_ID_THREAD_ID)};
if (thread_id)
decider_info_.thread_ids.emplace(std::move(*thread_id));
}
protected:
const QueryFlags qflags_;
DeciderInfo& decider_info_;
private:
Option<std::string> opt_string(const Xapian::Document& doc, MuMsgFieldId id) const noexcept try {
auto&& val{doc.get_value(id)};
return val.empty() ? Nothing : Some(val);
} MU_XAPIAN_CATCH_BLOCK_RETURN (Nothing);
};
struct MatchDeciderLeader final: public MatchDecider {
MatchDeciderLeader (QueryFlags qflags, DeciderInfo& info):
MatchDecider(qflags, info)
{}
/**
* operator()
*
* This receives the documents considered during a Xapian query, and
* is to return either true (keep) or false (ignore)
*
* We use this to potentiallly avoid certain messages (documents):
* - with QueryFlags::SkipUnreadable this will return false for message
* that are not readable in the file-system
* - with QueryFlags::SkipDuplicates this will return false for messages
* whose message-id was seen before.
*
* Even if we do not skip these messages entirely, we remember whether
* they were unreadable/duplicate (in the QueryMatch::Flags), so we can
* quickly find that info when doing the second 'related' query.
*
* The "leader" query. Matches here get the Leader flag unless their
* duplicates / unreadable. We check the duplicate/readable status
* regardless of whether SkipDuplicates/SkipUnreadable was passed
* (to gather that information); however those flags
* affect our true/false verdict.
*
* @param doc xapian document
*
* @return true or false
*/
bool operator() (const Xapian::Document& doc) const override {
// by definition, we haven't seen the docid before,
// so no need to search
auto it = decider_info_.matches.emplace(doc.get_docid(),
make_query_match(doc));
it.first->second.flags |= QueryMatch::Flags::Leader;
if (should_include(it.first->second)) {
// if (any_of(qflags_ & QueryFlags::GatherThreadIds))
// gather_thread_ids(doc);
return true;
}
return false;
}
};
std::unique_ptr<Xapian::MatchDecider>
Mu::make_leader_decider (QueryFlags qflags, DeciderInfo& info)
{
return std::make_unique<MatchDeciderLeader>(qflags, info);
}
struct MatchDeciderRelated final: public MatchDecider {
MatchDeciderRelated(QueryFlags qflags, DeciderInfo& info):
MatchDecider(qflags, info) {}
/**
* operator()
*
* This receives the documents considered during a Xapian query, and
* is to return either true (keep) or false (ignore)
*
* We use this to potentially avoid certain messages (documents):
* - with QueryFlags::SkipUnreadable this will return false for message
* that are not readable in the file-system
* - with QueryFlags::SkipDuplicates this will return false for messages
* whose message-id was seen before.
*
* Unlike in the "leader" decider (scroll up), we don't need to remember
* messages we won't include.
*
* @param doc xapian document
*
* @return true or false
*/
bool operator() (const Xapian::Document& doc) const override {
// we may have seen this match in the "Leader" query.
const auto it = decider_info_.matches.find(doc.get_docid());
if (it != decider_info_.matches.end())
return should_include(it->second);
auto qm{make_query_match(doc)};
if (should_include(qm)) {
qm.flags = QueryMatch::Flags::Related;
decider_info_.matches.emplace(doc.get_docid(), std::move(qm));
return true;
} else
return false; // nope.
}
};
std::unique_ptr<Xapian::MatchDecider>
Mu::make_related_decider (QueryFlags qflags, DeciderInfo& info)
{
return std::make_unique<MatchDeciderRelated>(qflags, info);
}
struct MatchDeciderThread final: public MatchDecider {
MatchDeciderThread(QueryFlags qflags, DeciderInfo& info):
MatchDecider{qflags, info} {}
/**
* operator()
*
* This receives the documents considered during a Xapian query, and
* is to return either true (keep) or false (ignore)
*
* Only include documents that earlier checks have decided to include.
*
* @param doc xapian document
*
* @return true or false
*/
bool operator() (const Xapian::Document& doc) const override {
// we may have seen this match in the "Leader" query,
// or in the second (unbuounded) related query;
const auto it{decider_info_.matches.find(doc.get_docid())};
return it != decider_info_.matches.end() && !it->second.thread_path.empty();
}
};
std::unique_ptr<Xapian::MatchDecider>
Mu::make_thread_decider (QueryFlags qflags, DeciderInfo& info)
{
return std::make_unique<MatchDeciderThread>(qflags, info);
}