1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qhelpenginecore.h"
5#include "qhelpsearchengine.h"
6#include "qhelpsearchquerywidget.h"
7#include "qhelpsearchresultwidget.h"
8
9#include "qhelpsearchindexreader_p.h"
10#include "qhelpsearchindexreader_default_p.h"
11#include "qhelpsearchindexwriter_default_p.h"
12
13#include <QtCore/QDir>
14#include <QtCore/QFile>
15#include <QtCore/QFileInfo>
16#include <QtCore/QVariant>
17#include <QtCore/QThread>
18#include <QtCore/QPointer>
19#include <QtCore/QTimer>
20
21QT_BEGIN_NAMESPACE
22
23using namespace fulltextsearch::qt;
24
25class QHelpSearchResultData : public QSharedData
26{
27public:
28 QUrl m_url;
29 QString m_title;
30 QString m_snippet;
31};
32
33/*!
34 \class QHelpSearchResult
35 \since 5.9
36 \inmodule QtHelp
37 \brief The QHelpSearchResult class provides the data associated with the
38 search result.
39
40 The QHelpSearchResult object is a data object that describes a single search result.
41 The vector of search result objects is returned by QHelpSearchEngine::searchResults().
42 The description of the search result contains the document title and URL
43 that the search input matched. It also contains the snippet from
44 the document content containing the best match of the search input.
45 \sa QHelpSearchEngine
46*/
47
48/*!
49 Constructs a new empty QHelpSearchResult.
50*/
51QHelpSearchResult::QHelpSearchResult()
52 : d(new QHelpSearchResultData)
53{
54}
55
56/*!
57 Constructs a copy of \a other.
58*/
59QHelpSearchResult::QHelpSearchResult(const QHelpSearchResult &other)
60 : d(other.d)
61{
62}
63
64/*!
65 Constructs the search result containing \a url, \a title and \a snippet
66 as the description of the result.
67*/
68QHelpSearchResult::QHelpSearchResult(const QUrl &url, const QString &title, const QString &snippet)
69 : d(new QHelpSearchResultData)
70{
71 d->m_url = url;
72 d->m_title = title;
73 d->m_snippet = snippet;
74}
75
76/*!
77 Destroys the search result.
78*/
79QHelpSearchResult::~QHelpSearchResult()
80{
81}
82
83/*!
84 Assigns \a other to this search result and returns a reference to this search result.
85*/
86QHelpSearchResult &QHelpSearchResult::operator=(const QHelpSearchResult &other)
87{
88 d = other.d;
89 return *this;
90}
91
92/*!
93 Returns the document title of the search result.
94*/
95QString QHelpSearchResult::title() const
96{
97 return d->m_title;
98}
99
100/*!
101 Returns the document URL of the search result.
102*/
103QUrl QHelpSearchResult::url() const
104{
105 return d->m_url;
106}
107
108/*!
109 Returns the document snippet containing the search phrase of the search result.
110*/
111QString QHelpSearchResult::snippet() const
112{
113 return d->m_snippet;
114}
115
116
117class QHelpSearchEnginePrivate : public QObject
118{
119 Q_OBJECT
120
121signals:
122 void indexingStarted();
123 void indexingFinished();
124
125 void searchingStarted();
126 void searchingFinished(int searchResultCount);
127
128private:
129 QHelpSearchEnginePrivate(QHelpEngineCore *helpEngine)
130 : helpEngine(helpEngine)
131 {
132 }
133
134 ~QHelpSearchEnginePrivate()
135 {
136 delete indexReader;
137 delete indexWriter;
138 }
139
140 int searchResultCount() const
141 {
142 return indexReader ? indexReader->searchResultCount() : 0;
143 }
144
145 QList<QHelpSearchResult> searchResults(int start, int end) const
146 {
147 return indexReader ?
148 indexReader->searchResults(start, end) :
149 QList<QHelpSearchResult>();
150 }
151
152 void updateIndex(bool reindex = false)
153 {
154 if (helpEngine.isNull())
155 return;
156
157 if (!QFile::exists(fileName: QFileInfo(helpEngine->collectionFile()).path()))
158 return;
159
160 if (!indexWriter) {
161 indexWriter = new QHelpSearchIndexWriter();
162
163 connect(sender: indexWriter, signal: &QHelpSearchIndexWriter::indexingStarted,
164 context: this, slot: &QHelpSearchEnginePrivate::indexingStarted);
165 connect(sender: indexWriter, signal: &QHelpSearchIndexWriter::indexingFinished,
166 context: this, slot: &QHelpSearchEnginePrivate::indexingFinished);
167 }
168
169 indexWriter->cancelIndexing();
170 indexWriter->updateIndex(collectionFile: helpEngine->collectionFile(),
171 indexFilesFolder: indexFilesFolder(), reindex);
172 }
173
174 void cancelIndexing()
175 {
176 if (indexWriter)
177 indexWriter->cancelIndexing();
178 }
179
180 void search(const QString &searchInput)
181 {
182 if (helpEngine.isNull())
183 return;
184
185 if (!QFile::exists(fileName: QFileInfo(helpEngine->collectionFile()).path()))
186 return;
187
188 if (!indexReader) {
189 indexReader = new QHelpSearchIndexReaderDefault();
190 connect(sender: indexReader, signal: &fulltextsearch::QHelpSearchIndexReader::searchingStarted,
191 context: this, slot: &QHelpSearchEnginePrivate::searchingStarted);
192 connect(sender: indexReader, signal: &fulltextsearch::QHelpSearchIndexReader::searchingFinished,
193 context: this, slot: &QHelpSearchEnginePrivate::searchingFinished);
194 }
195
196 m_searchInput = searchInput;
197 indexReader->cancelSearching();
198 indexReader->search(collectionFile: helpEngine->collectionFile(), indexFilesFolder: indexFilesFolder(),
199 searchInput, usesFilterEngine: helpEngine->usesFilterEngine());
200 }
201
202 void cancelSearching()
203 {
204 if (indexReader)
205 indexReader->cancelSearching();
206 }
207
208 QString indexFilesFolder() const
209 {
210 QString indexFilesFolder = QLatin1String(".fulltextsearch");
211 if (helpEngine && !helpEngine->collectionFile().isEmpty()) {
212 QFileInfo fi(helpEngine->collectionFile());
213 indexFilesFolder = fi.absolutePath() + QDir::separator()
214 + QLatin1Char('.')
215 + fi.fileName().left(n: fi.fileName().lastIndexOf(s: QLatin1String(".qhc")));
216 }
217 return indexFilesFolder;
218 }
219
220private:
221 friend class QHelpSearchEngine;
222
223 bool m_isIndexingScheduled = false;
224
225 QHelpSearchQueryWidget *queryWidget = nullptr;
226 QHelpSearchResultWidget *resultWidget = nullptr;
227
228 fulltextsearch::QHelpSearchIndexReader *indexReader = nullptr;
229 QHelpSearchIndexWriter *indexWriter = nullptr;
230
231 QPointer<QHelpEngineCore> helpEngine;
232
233 QString m_searchInput;
234};
235
236/*!
237 \class QHelpSearchQuery
238 \deprecated
239 \since 4.4
240 \inmodule QtHelp
241 \brief The QHelpSearchQuery class contains the field name and the associated
242 search term.
243
244 The QHelpSearchQuery class contains the field name and the associated search
245 term. Depending on the field the search term might get split up into separate
246 terms to be parsed differently by the search engine.
247
248 \note This class has been deprecated in favor of QString.
249
250 \sa QHelpSearchQueryWidget
251*/
252
253/*!
254 \fn QHelpSearchQuery::QHelpSearchQuery()
255
256 Constructs a new empty QHelpSearchQuery.
257*/
258
259/*!
260 \fn QHelpSearchQuery::QHelpSearchQuery(FieldName field, const QStringList &wordList)
261
262 Constructs a new QHelpSearchQuery and initializes it with the given \a field and \a wordList.
263*/
264
265/*!
266 \enum QHelpSearchQuery::FieldName
267 This enum type specifies the field names that are handled by the search engine.
268
269 \value DEFAULT the default field provided by the search widget, several terms should be
270 split and stored in the word list except search terms enclosed in quotes.
271 \value FUZZY \deprecated Terms should be split in separate
272 words and passed to the search engine.
273 \value WITHOUT \deprecated Terms should be split in separate
274 words and passed to the search engine.
275 \value PHRASE \deprecated Terms should not be split in separate words.
276 \value ALL \deprecated Terms should be split in separate
277 words and passed to the search engine
278 \value ATLEAST \deprecated Terms should be split in separate
279 words and passed to the search engine
280*/
281
282/*!
283 \class QHelpSearchEngine
284 \since 4.4
285 \inmodule QtHelp
286 \brief The QHelpSearchEngine class provides access to widgets reusable
287 to integrate fulltext search as well as to index and search documentation.
288
289 Before the search engine can be used, one has to instantiate at least a
290 QHelpEngineCore object that needs to be passed to the search engines constructor.
291 This is required as the search engine needs to be connected to the help
292 engines setupFinished() signal to know when it can start to index documentation.
293
294 After starting the indexing process the signal indexingStarted() is emitted and
295 on the end of the indexing process the indexingFinished() is emitted. To stop
296 the indexing one can call cancelIndexing().
297
298 When the indexing process has finished, the search engine can be used to
299 search through the index for a given term using the search() function. When
300 the search input is passed to the search engine, the searchingStarted()
301 signal is emitted. When the search finishes, the searchingFinished() signal
302 is emitted. The search process can be stopped by calling cancelSearching().
303
304 If the search succeeds, searchingFinished() is called with the search result
305 count to fetch the search results from the search engine. Calling the
306 searchResults() function with a range returns a list of QHelpSearchResult
307 objects within the range. The results consist of the document title and URL,
308 as well as a snippet from the document that contains the best match for the
309 search input.
310
311 To display the given search results use the QHelpSearchResultWidget or build up your own one if you need
312 more advanced functionality. Note that the QHelpSearchResultWidget can not be instantiated
313 directly, you must retrieve the widget from the search engine in use as all connections will be
314 established for you by the widget itself.
315*/
316
317/*!
318 \fn void QHelpSearchEngine::indexingStarted()
319
320 This signal is emitted when indexing process is started.
321*/
322
323/*!
324 \fn void QHelpSearchEngine::indexingFinished()
325
326 This signal is emitted when the indexing process is complete.
327*/
328
329/*!
330 \fn void QHelpSearchEngine::searchingStarted()
331
332 This signal is emitted when the search process is started.
333*/
334
335/*!
336 \fn void QHelpSearchEngine::searchingFinished(int searchResultCount)
337
338 This signal is emitted when the search process is complete.
339 The search result count is stored in \a searchResultCount.
340*/
341
342/*!
343 Constructs a new search engine with the given \a parent. The search engine
344 uses the given \a helpEngine to access the documentation that needs to be indexed.
345 The QHelpEngine's setupFinished() signal is automatically connected to the
346 QHelpSearchEngine's indexing function, so that new documentation will be indexed
347 after the signal is emitted.
348*/
349QHelpSearchEngine::QHelpSearchEngine(QHelpEngineCore *helpEngine, QObject *parent)
350 : QObject(parent)
351{
352 d = new QHelpSearchEnginePrivate(helpEngine);
353
354 connect(sender: helpEngine, signal: &QHelpEngineCore::setupFinished,
355 context: this, slot: &QHelpSearchEngine::scheduleIndexDocumentation);
356
357 connect(sender: d, signal: &QHelpSearchEnginePrivate::indexingStarted,
358 context: this, slot: &QHelpSearchEngine::indexingStarted);
359 connect(sender: d, signal: &QHelpSearchEnginePrivate::indexingFinished,
360 context: this, slot: &QHelpSearchEngine::indexingFinished);
361 connect(sender: d, signal: &QHelpSearchEnginePrivate::searchingStarted,
362 context: this, slot: &QHelpSearchEngine::searchingStarted);
363 connect(sender: d, signal: &QHelpSearchEnginePrivate::searchingFinished,
364 context: this, slot: &QHelpSearchEngine::searchingFinished);
365}
366
367/*!
368 Destructs the search engine.
369*/
370QHelpSearchEngine::~QHelpSearchEngine()
371{
372 delete d;
373}
374
375/*!
376 Returns a widget to use as input widget. Depending on your search engine
377 configuration you will get a different widget with more or less subwidgets.
378*/
379QHelpSearchQueryWidget* QHelpSearchEngine::queryWidget()
380{
381 if (!d->queryWidget)
382 d->queryWidget = new QHelpSearchQueryWidget();
383
384 return d->queryWidget;
385}
386
387/*!
388 Returns a widget that can hold and display the search results.
389*/
390QHelpSearchResultWidget* QHelpSearchEngine::resultWidget()
391{
392 if (!d->resultWidget)
393 d->resultWidget = new QHelpSearchResultWidget(this);
394
395 return d->resultWidget;
396}
397
398#if QT_DEPRECATED_SINCE(5, 9)
399/*!
400 \deprecated
401 Use searchResultCount() instead.
402*/
403int QHelpSearchEngine::hitsCount() const
404{
405 return d->searchResultCount();
406}
407
408/*!
409 \since 4.6
410 \deprecated
411 Use searchResultCount() instead.
412*/
413int QHelpSearchEngine::hitCount() const
414{
415 return d->searchResultCount();
416}
417#endif // QT_DEPRECATED_SINCE(5, 9)
418
419/*!
420 \since 5.9
421 Returns the number of results the search engine found.
422*/
423int QHelpSearchEngine::searchResultCount() const
424{
425 return d->searchResultCount();
426}
427
428#if QT_DEPRECATED_SINCE(5, 9)
429/*!
430 \typedef QHelpSearchEngine::SearchHit
431 \deprecated
432
433 Use QHelpSearchResult instead.
434
435 Typedef for QPair<QString, QString>.
436 The values of that pair are the documentation file path and the page title.
437
438 \sa hits()
439*/
440
441/*!
442 \deprecated
443 Use searchResults() instead.
444*/
445QList<QHelpSearchEngine::SearchHit> QHelpSearchEngine::hits(int start, int end) const
446{
447 QList<QHelpSearchEngine::SearchHit> hits;
448 for (const QHelpSearchResult &result : searchResults(start, end))
449 hits.append(t: qMakePair(value1: result.url().toString(), value2: result.title()));
450 return hits;
451}
452#endif // QT_DEPRECATED_SINCE(5, 9)
453
454/*!
455 \since 5.9
456 Returns a list of search results within the range from the index
457 specified by \a start to the index specified by \a end.
458*/
459QList<QHelpSearchResult> QHelpSearchEngine::searchResults(int start, int end) const
460{
461 return d->searchResults(start, end);
462}
463
464/*!
465 \since 5.9
466 Returns the phrase that was last searched for.
467*/
468QString QHelpSearchEngine::searchInput() const
469{
470 return d->m_searchInput;
471}
472
473#if QT_DEPRECATED_SINCE(5, 9)
474/*!
475 \deprecated
476 \since 4.5
477 Use searchInput() instead.
478*/
479QList<QHelpSearchQuery> QHelpSearchEngine::query() const
480{
481 return QList<QHelpSearchQuery>() << QHelpSearchQuery(QHelpSearchQuery::DEFAULT,
482 d->m_searchInput.split(sep: QChar::Space));
483}
484#endif // QT_DEPRECATED_SINCE(5, 9)
485
486/*!
487 Forces the search engine to reindex all documentation files.
488*/
489void QHelpSearchEngine::reindexDocumentation()
490{
491 d->updateIndex(reindex: true);
492}
493
494/*!
495 Stops the indexing process.
496*/
497void QHelpSearchEngine::cancelIndexing()
498{
499 d->cancelIndexing();
500}
501
502/*!
503 Stops the search process.
504*/
505void QHelpSearchEngine::cancelSearching()
506{
507 d->cancelSearching();
508}
509
510/*!
511 \since 5.9
512 Starts the search process using the given search phrase \a searchInput.
513
514 The phrase may consist of several words. By default, the search engine returns
515 the list of documents that contain all the specified words.
516 The phrase may contain any combination of the logical operators AND, OR, and
517 NOT. The operator must be written in all capital letters, otherwise it will
518 be considered a part of the search phrase.
519
520 If double quotation marks are used to group the words,
521 the search engine will search for an exact match of the quoted phrase.
522
523 For more information about the text query syntax,
524 see \l {https://sqlite.org/fts5.html#full_text_query_syntax}
525 {SQLite FTS5 Extension}.
526*/
527void QHelpSearchEngine::search(const QString &searchInput)
528{
529 d->search(searchInput);
530}
531
532#if QT_DEPRECATED_SINCE(5, 9)
533/*!
534 \deprecated
535 Use search(const QString &searchInput) instead.
536*/
537void QHelpSearchEngine::search(const QList<QHelpSearchQuery> &queryList)
538{
539 if (queryList.isEmpty())
540 return;
541
542 d->search(searchInput: queryList.first().wordList.join(sep: QChar::Space));
543}
544#endif // QT_DEPRECATED_SINCE(5, 9)
545
546/*!
547 \internal
548*/
549void QHelpSearchEngine::scheduleIndexDocumentation()
550{
551 if (d->m_isIndexingScheduled)
552 return;
553
554 d->m_isIndexingScheduled = true;
555 QTimer::singleShot(interval: 0, receiver: this, slot: &QHelpSearchEngine::indexDocumentation);
556}
557
558void QHelpSearchEngine::indexDocumentation()
559{
560 d->m_isIndexingScheduled = false;
561 d->updateIndex();
562}
563
564QT_END_NAMESPACE
565
566#include "qhelpsearchengine.moc"
567

source code of qttools/src/assistant/help/qhelpsearchengine.cpp