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