summaryrefslogtreecommitdiff
path: root/lib/mu-query-match-deciders.cc
blob: 999d609386d15ee34c022cf51df1ce46fb873ab7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/*
** Copyright (C) 2020-2022 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, Field::Id::MessageId)
			       .value_or(*opt_string(doc, Field::Id::Path))};
		if (!decider_info_.message_ids.emplace(std::move(msgid)).second)
			qm.flags |= QueryMatch::Flags::Duplicate;

		const auto path{opt_string(doc, 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, Field::Id::ThreadId)};
		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, Field::Id id) const noexcept {
		const auto value_no{field_from_id(id).value_no()};
		std::string val = xapian_try([&] { return doc.get_value(value_no); }, std::string{""});
		if (val.empty())
			return Nothing;
		else
			return Some(std::move(val));
	}
};

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 they are
	 * 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;

		return should_include(it.first->second);
	}
};

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);
}