Akonadi

searchmanager.cpp
1/*
2 SPDX-FileCopyrightText: 2010 Volker Krause <vkrause@kde.org>
3 SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "searchmanager.h"
9#include "abstractsearchplugin.h"
10#include "akonadiserver_search_debug.h"
11
12#include "agentsearchengine.h"
13#include "akonadi.h"
14#include "handler/searchhelper.h"
15#include "notificationmanager.h"
16#include "searchrequest.h"
17#include "searchtaskmanager.h"
18#include "storage/datastore.h"
19#include "storage/querybuilder.h"
20#include "storage/selectquerybuilder.h"
21#include "storage/transaction.h"
22
23#include "private/protocol_p.h"
24
25#include <QDBusConnection>
26#include <QDir>
27#include <QPluginLoader>
28#include <QTimer>
29
30#include <memory>
31
33
34using namespace Akonadi;
35using namespace Akonadi::Server;
36
37Q_DECLARE_METATYPE(Collection)
38
39SearchManager::SearchManager(const QStringList &searchEngines, SearchTaskManager &agentSearchManager)
40 : AkThread(QStringLiteral("SearchManager"), AkThread::ManualStart, QThread::InheritPriority)
41 , mAgentSearchManager(agentSearchManager)
42 , mEngineNames(searchEngines)
43 , mSearchUpdateTimer(nullptr)
44{
45 qRegisterMetaType<Collection>();
46
47 // We load search plugins (as in QLibrary::load()) in the main thread so that
48 // static initialization happens in the QApplication thread
49 loadSearchPlugins();
50
51 // Register to DBus on the main thread connection - otherwise we don't appear
52 // on the service.
54 conn.registerObject(QStringLiteral("/SearchManager"), this, QDBusConnection::ExportAllSlots);
55
56 // Delay-call init()
57 startThread();
58}
59
60void SearchManager::init()
61{
62 AkThread::init();
63
64 mEngines.reserve(mEngineNames.size());
65 for (const QString &engineName : std::as_const(mEngineNames)) {
66 if (engineName == QLatin1StringView("Agent")) {
67 mEngines.append(new AgentSearchEngine);
68 } else {
69 qCCritical(AKONADISERVER_SEARCH_LOG) << "Unknown search engine type: " << engineName;
70 }
71 }
72
73 initSearchPlugins();
74
75 // The timer will tick 15 seconds after last change notification. If a new notification
76 // is delivered in the meantime, the timer is reset
77 mSearchUpdateTimer = new QTimer(this);
78 mSearchUpdateTimer->setInterval(15 * 1000);
79 mSearchUpdateTimer->setSingleShot(true);
80 connect(mSearchUpdateTimer, &QTimer::timeout, this, &SearchManager::searchUpdateTimeout);
81}
82
83void SearchManager::quit()
84{
86 conn.unregisterObject(QStringLiteral("/SearchManager"), QDBusConnection::UnregisterTree);
87 conn.disconnectFromBus(conn.name());
88
89 // Make sure all children are deleted within context of this thread
90 qDeleteAll(children());
91
92 qDeleteAll(mEngines);
93 qDeleteAll(mPlugins);
94 /*
95 * FIXME: Unloading plugin messes up some global statics from client libs
96 * and causes crash on Akonadi shutdown (below main). Keeping the plugins
97 * loaded is not really a big issue as this is only invoked on server shutdown
98 * anyway, so we are not leaking any memory.
99 Q_FOREACH (QPluginLoader *loader, mPluginLoaders) {
100 loader->unload();
101 delete loader;
102 }
103 */
104
105 AkThread::quit();
106}
107
108SearchManager::~SearchManager()
109{
110 quitThread();
111}
112
114{
115 mAgentSearchManager.registerInstance(id);
116}
117
119{
120 mAgentSearchManager.unregisterInstance(id);
121}
122
124{
125 return mPlugins;
126}
127
128void SearchManager::loadSearchPlugins()
129{
130 QStringList loadedPlugins;
131 const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN"));
132 if (!pluginOverride.isEmpty()) {
133 qCInfo(AKONADISERVER_SEARCH_LOG) << "Overriding the search plugins with: " << pluginOverride;
134 }
135
137 for (const QString &pluginDir : dirs) {
138 const QString path(pluginDir + QStringLiteral("/pim6/akonadi"));
139 QDir dir(path);
140 const QStringList fileNames = dir.entryList(QDir::Files);
141 qCDebug(AKONADISERVER_SEARCH_LOG) << "SEARCH MANAGER: searching in " << path << ":" << fileNames;
142 for (const QString &fileName : fileNames) {
143 const QString filePath = path % QLatin1Char('/') % fileName;
144 std::unique_ptr<QPluginLoader> loader(new QPluginLoader(filePath));
145 const QVariantMap metadata = loader->metaData().value(QStringLiteral("MetaData")).toVariant().toMap();
146 if (metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1StringView("SearchPlugin")) {
147 continue;
148 }
149
150 const QString libraryName = metadata.value(QStringLiteral("X-Akonadi-Library")).toString();
151 if (loadedPlugins.contains(libraryName)) {
152 qCDebug(AKONADISERVER_SEARCH_LOG) << "Already loaded one version of this plugin, skipping: " << libraryName;
153 continue;
154 }
155
156 // When search plugin override is active, ignore all plugins except for the override
157 if (!pluginOverride.isEmpty()) {
158 if (libraryName != pluginOverride) {
159 qCDebug(AKONADISERVER_SEARCH_LOG) << libraryName << "skipped because of AKONADI_OVERRIDE_SEARCHPLUGIN";
160 continue;
161 }
162
163 // When there's no override, only load plugins enabled by default
164 } else if (!metadata.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool()) {
165 continue;
166 }
167
168 if (!loader->load()) {
169 qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << libraryName << ":" << loader->errorString();
170 continue;
171 }
172
173 mPluginLoaders << loader.release();
174 loadedPlugins << libraryName;
175 }
176 }
177}
178
179void SearchManager::initSearchPlugins()
180{
181 for (QPluginLoader *loader : std::as_const(mPluginLoaders)) {
182 if (!loader->load()) {
183 qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << loader->fileName() << ":" << loader->errorString();
184 continue;
185 }
186
188 if (!plugin) {
189 qCCritical(AKONADISERVER_SEARCH_LOG) << loader->fileName() << "is not a valid Akonadi search plugin";
190 continue;
191 }
192
193 qCDebug(AKONADISERVER_SEARCH_LOG) << "SearchManager: loaded search plugin" << loader->fileName();
194 mPlugins << plugin;
195 }
196}
197
198void SearchManager::scheduleSearchUpdate()
199{
200 // Reset if the timer is active (use QueuedConnection to invoke start() from
201 // the thread the QTimer lives in instead of caller's thread, otherwise crashes
202 // and weird things can happen.
203 QMetaObject::invokeMethod(mSearchUpdateTimer, qOverload<>(&QTimer::start), Qt::QueuedConnection);
204}
205
206void SearchManager::searchUpdateTimeout()
207{
208 // Get all search collections, that is subcollections of "Search", which always has ID 1
209 const Collection::List collections = Collection::retrieveFiltered(Collection::parentIdFullColumnName(), 1);
210 for (const Collection &collection : collections) {
211 updateSearchAsync(collection);
212 }
213}
214
216{
218 this,
219 [this, collection]() {
220 updateSearchImpl(collection);
221 },
223}
224
226{
227 mLock.lock();
228 if (mUpdatingCollections.contains(collection.id())) {
229 mLock.unlock();
230 return;
231 // FIXME: If another thread already requested an update, we return to the caller before the
232 // search update is performed; this contradicts the docs
233 }
234 mUpdatingCollections.insert(collection.id());
235 mLock.unlock();
237 this,
238 [this, collection]() {
239 updateSearchImpl(collection);
240 },
242 mLock.lock();
243 mUpdatingCollections.remove(collection.id());
244 mLock.unlock();
245}
246
247void SearchManager::updateSearchImpl(const Collection &collection)
248{
249 if (collection.queryString().size() >= 32768) {
250 qCWarning(AKONADISERVER_SEARCH_LOG) << "The query is at least 32768 chars long, which is the maximum size supported by the akonadi db schema. The "
251 "query is therefore most likely truncated and will not be executed.";
252 return;
253 }
254 if (collection.queryString().isEmpty()) {
255 return;
256 }
257
258 const QStringList queryAttributes = collection.queryAttributes().split(QLatin1Char(' '));
259 const bool remoteSearch = queryAttributes.contains(QLatin1StringView(AKONADI_PARAM_REMOTE));
260 bool recursive = queryAttributes.contains(QLatin1StringView(AKONADI_PARAM_RECURSIVE));
261
262 QStringList queryMimeTypes;
263 const QList<MimeType> mimeTypes = collection.mimeTypes();
264 queryMimeTypes.reserve(mimeTypes.count());
265
266 for (const MimeType &mt : mimeTypes) {
267 queryMimeTypes << mt.name();
268 }
269
270 QList<qint64> queryAncestors;
271 if (collection.queryCollections().isEmpty()) {
272 queryAncestors << 0;
273 recursive = true;
274 } else {
275 const QStringList collectionIds = collection.queryCollections().split(QLatin1Char(' '));
276 queryAncestors.reserve(collectionIds.count());
277 for (const QString &colId : collectionIds) {
278 queryAncestors << colId.toLongLong();
279 }
280 }
281
282 // Always query the given collections
283 QList<qint64> queryCollections = queryAncestors;
284
285 if (recursive) {
286 // Resolve subcollections if necessary
287 queryCollections += SearchHelper::matchSubcollectionsByMimeType(queryAncestors, queryMimeTypes);
288 }
289
290 // This happens if we try to search a virtual collection in recursive mode (because virtual collections are excluded from listCollectionsRecursive)
291 if (queryCollections.isEmpty()) {
292 qCDebug(AKONADISERVER_SEARCH_LOG) << "No collections to search, you're probably trying to search a virtual collection.";
293 return;
294 }
295
296 // Query all plugins for search results
297 const QByteArray id = "searchUpdate-" + QByteArray::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch());
298 SearchRequest request(id, *this, mAgentSearchManager);
299 request.setCollections(queryCollections);
300 request.setMimeTypes(queryMimeTypes);
301 request.setQuery(collection.queryString());
302 request.setRemoteSearch(remoteSearch);
303 request.setStoreResults(true);
304 request.setProperty("SearchCollection", QVariant::fromValue(collection));
305 connect(&request, &SearchRequest::resultsAvailable, this, &SearchManager::searchUpdateResultsAvailable);
306 request.exec(); // blocks until all searches are done
307
308 const QSet<qint64> results = request.results();
309
310 // Get all items in the collection
311 QueryBuilder qb(CollectionPimItemRelation::tableName());
312 qb.addColumn(CollectionPimItemRelation::rightColumn());
313 qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id());
314 if (!qb.exec()) {
315 return;
316 }
317
318 Transaction transaction(DataStore::self(), QStringLiteral("UPDATE SEARCH"));
319
320 // Unlink all items that were not in search results from the collection
321 QVariantList toRemove;
322 while (qb.query().next()) {
323 const qint64 id = qb.query().value(0).toLongLong();
324 if (!results.contains(id)) {
325 toRemove << id;
326 Collection::removePimItem(collection.id(), id);
327 }
328 }
329
330 if (!transaction.commit()) {
331 return;
332 }
333
334 if (!toRemove.isEmpty()) {
336 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, toRemove);
337 if (!qb.exec()) {
338 return;
339 }
340
341 const QList<PimItem> removedItems = qb.result();
342 DataStore::self()->notificationCollector()->itemsUnlinked(removedItems, collection);
343 }
344
345 qCInfo(AKONADISERVER_SEARCH_LOG) << "Search update for collection" << collection.name() << "(" << collection.id() << ") finished:"
346 << "all results: " << results.count() << ", removed results:" << toRemove.count();
347}
348
349void SearchManager::searchUpdateResultsAvailable(const QSet<qint64> &results)
350{
351 const auto collection = sender()->property("SearchCollection").value<Collection>();
352 qCDebug(AKONADISERVER_SEARCH_LOG) << "searchUpdateResultsAvailable" << collection.id() << results.count() << "results";
353
354 QSet<qint64> newMatches = results;
355 QSet<qint64> existingMatches;
356 {
357 QueryBuilder qb(CollectionPimItemRelation::tableName());
358 qb.addColumn(CollectionPimItemRelation::rightColumn());
359 qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id());
360 if (!qb.exec()) {
361 return;
362 }
363
364 while (qb.query().next()) {
365 const qint64 id = qb.query().value(0).toLongLong();
366 if (newMatches.contains(id)) {
367 existingMatches << id;
368 }
369 }
370 }
371
372 qCDebug(AKONADISERVER_SEARCH_LOG) << "Got" << newMatches.count() << "results, out of which" << existingMatches.count() << "are already in the collection";
373
374 newMatches = newMatches - existingMatches;
375 if (newMatches.isEmpty()) {
376 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (fast path)";
377 return;
378 }
379
380 Transaction transaction(DataStore::self(), QStringLiteral("PUSH SEARCH RESULTS"), !DataStore::self()->inTransaction());
381
382 // First query all the IDs we got from search plugin/agent against the DB.
383 // This will remove IDs that no longer exist in the DB.
384 constexpr int maximumParametersSize = 1000;
385 QVariantList newMatchesVariant;
386 newMatchesVariant.reserve(maximumParametersSize);
387 QList<PimItem> items;
388
389 for (qint64 id : std::as_const(newMatches)) {
390 newMatchesVariant << id;
391 if (newMatchesVariant.size() >= maximumParametersSize) {
393 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant);
394 if (!qb.exec()) {
395 return;
396 }
397
398 items << qb.result();
399
400 newMatchesVariant.clear();
401 }
402 }
403
404 if (!newMatchesVariant.isEmpty()) {
406 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant);
407 if (!qb.exec()) {
408 return;
409 }
410
411 items << qb.result();
412 }
413
414 if (items.count() != newMatches.count()) {
415 qCDebug(AKONADISERVER_SEARCH_LOG) << "Search backend returned" << (newMatches.count() - items.count()) << "results that no longer exist in Akonadi.";
416 qCDebug(AKONADISERVER_SEARCH_LOG) << "Please reindex collection" << collection.id();
417 // TODO: Request the reindexing directly from here
418 }
419
420 if (items.isEmpty()) {
421 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (no existing result)";
422 return;
423 }
424
425 for (const auto &item : items) {
426 Collection::addPimItem(collection.id(), item.id());
427 }
428
429 if (!transaction.commit()) {
430 qCWarning(AKONADISERVER_SEARCH_LOG) << "Failed to commit search results transaction";
431 return;
432 }
433
434 DataStore::self()->notificationCollector()->itemsLinked(items, collection);
435 // Force collector to dispatch the notification now
437
438 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results:" << items.count();
439}
440
441#include "moc_searchmanager.cpp"
3rd party applications can install a search plugin for Akonadi server to provide access to their sear...
Represents a collection of PIM items.
Definition collection.h:62
Search engine for distributing searches to agents.
static DataStore * self()
Per thread singleton.
NotificationCollector * notificationCollector()
Returns the notification collector of this DataStore object.
Part of the DataStore, collects change notifications and emits them after the current transaction has...
void itemsUnlinked(const PimItem::List &items, const Collection &collection)
Notify about unlinked items.
bool dispatchNotifications()
Trigger sending of collected notifications.
void itemsLinked(const PimItem::List &items, const Collection &collection)
Notify about linked items.
Helper class to construct arbitrary SQL queries.
void addValueCondition(const QString &column, Query::CompareOperator op, const QVariant &value, ConditionType type=WhereCondition)
Add a WHERE or HAVING condition which compares a column with a given value.
bool exec()
Executes the query, returns true on success.
QSqlQuery & query()
Returns the query, only valid after exec().
void addColumn(const QString &col)
Adds the given column to a select query.
SearchManager creates and deletes persistent searches for all currently active search engines.
virtual void registerInstance(const QString &id)
This is called via D-Bus from AgentManager to register an agent with search interface.
virtual QList< AbstractSearchPlugin * > searchPlugins() const
Returns currently available search plugins.
virtual void updateSearch(const Collection &collection)
Updates the search query synchronously.
virtual void unregisterInstance(const QString &id)
This is called via D-Bus from AgentManager to unregister an agent with search interface.
virtual void updateSearchAsync(const Collection &collection)
Updates the search query asynchronously.
Helper class for creating and executing database SELECT queries.
QList< T > result()
Returns the result of this SELECT query.
Helper class for DataStore transaction handling.
Definition transaction.h:23
Helper integration between Akonadi and Qt.
QByteArray number(double n, char format, int precision)
QStringList libraryPaths()
QDateTime currentDateTimeUtc()
void disconnectFromBus(const QString &name)
QString name() const const
bool registerObject(const QString &path, QObject *object, RegisterOptions options)
QDBusConnection sessionBus()
void unregisterObject(const QString &path, UnregisterMode mode)
qsizetype count() const const
bool isEmpty() const const
void reserve(qsizetype size)
qsizetype size() const const
T value(const Key &key, const T &defaultValue) const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
void lock()
void unlock()
const QObjectList & children() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
QObject * sender() const const
bool contains(const QSet< T > &other) const const
qsizetype count() const const
iterator insert(const T &value)
bool isEmpty() const const
bool remove(const T &value)
bool next()
QVariant value(const QString &name) const const
QString fromLatin1(QByteArrayView str)
bool isEmpty() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QueuedConnection
void setInterval(int msec)
void setSingleShot(bool singleShot)
void start()
void timeout()
QVariant fromValue(T &&value)
qlonglong toLongLong(bool *ok) const const
T value() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 4 2024 16:31:59 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.