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 | |
21 | QT_BEGIN_NAMESPACE |
22 | |
23 | using namespace fulltextsearch::qt; |
24 | |
25 | class QHelpSearchResultData : public QSharedData |
26 | { |
27 | public: |
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 | */ |
51 | QHelpSearchResult::QHelpSearchResult() |
52 | : d(new QHelpSearchResultData) |
53 | { |
54 | } |
55 | |
56 | /*! |
57 | Constructs a copy of \a other. |
58 | */ |
59 | QHelpSearchResult::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 | */ |
68 | QHelpSearchResult::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 | */ |
79 | QHelpSearchResult::~QHelpSearchResult() |
80 | { |
81 | } |
82 | |
83 | /*! |
84 | Assigns \a other to this search result and returns a reference to this search result. |
85 | */ |
86 | QHelpSearchResult &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 | */ |
95 | QString QHelpSearchResult::title() const |
96 | { |
97 | return d->m_title; |
98 | } |
99 | |
100 | /*! |
101 | Returns the document URL of the search result. |
102 | */ |
103 | QUrl 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 | */ |
111 | QString QHelpSearchResult::snippet() const |
112 | { |
113 | return d->m_snippet; |
114 | } |
115 | |
116 | |
117 | class QHelpSearchEnginePrivate : public QObject |
118 | { |
119 | Q_OBJECT |
120 | |
121 | signals: |
122 | void indexingStarted(); |
123 | void indexingFinished(); |
124 | |
125 | void searchingStarted(); |
126 | void searchingFinished(int searchResultCount); |
127 | |
128 | private: |
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 | |
220 | private: |
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 | */ |
349 | QHelpSearchEngine::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 | */ |
370 | QHelpSearchEngine::~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 | */ |
379 | QHelpSearchQueryWidget* 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 | */ |
390 | QHelpSearchResultWidget* 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 | */ |
403 | int QHelpSearchEngine::hitsCount() const |
404 | { |
405 | return d->searchResultCount(); |
406 | } |
407 | |
408 | /*! |
409 | \since 4.6 |
410 | \deprecated |
411 | Use searchResultCount() instead. |
412 | */ |
413 | int 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 | */ |
423 | int 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 | */ |
445 | QList<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 | */ |
459 | QList<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 | */ |
468 | QString 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 | */ |
479 | QList<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 | */ |
489 | void QHelpSearchEngine::reindexDocumentation() |
490 | { |
491 | d->updateIndex(reindex: true); |
492 | } |
493 | |
494 | /*! |
495 | Stops the indexing process. |
496 | */ |
497 | void QHelpSearchEngine::cancelIndexing() |
498 | { |
499 | d->cancelIndexing(); |
500 | } |
501 | |
502 | /*! |
503 | Stops the search process. |
504 | */ |
505 | void 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 | */ |
527 | void 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 | */ |
537 | void 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 | */ |
549 | void 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 | |
558 | void QHelpSearchEngine::indexDocumentation() |
559 | { |
560 | d->m_isIndexingScheduled = false; |
561 | d->updateIndex(); |
562 | } |
563 | |
564 | QT_END_NAMESPACE |
565 | |
566 | #include "qhelpsearchengine.moc" |
567 | |