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 "qhelpfilterengine.h"
6#include "qhelpsearchindexreader_default_p.h"
7
8#include <QtCore/QSet>
9#include <QtSql/QSqlDatabase>
10#include <QtSql/QSqlQuery>
11
12QT_BEGIN_NAMESPACE
13
14namespace fulltextsearch {
15namespace qt {
16
17void Reader::setIndexPath(const QString &path)
18{
19 m_indexPath = path;
20 m_namespaceAttributes.clear();
21 m_filterEngineNamespaceList.clear();
22 m_useFilterEngine = false;
23}
24
25void Reader::addNamespaceAttributes(const QString &namespaceName, const QStringList &attributes)
26{
27 m_namespaceAttributes.insert(key: namespaceName, value: attributes);
28}
29
30void Reader::setFilterEngineNamespaceList(const QStringList &namespaceList)
31{
32 m_useFilterEngine = true;
33 m_filterEngineNamespaceList = namespaceList;
34}
35
36static QString namespacePlaceholders(const QMultiMap<QString, QStringList> &namespaces)
37{
38 QString placeholders;
39 const auto &namespaceList = namespaces.uniqueKeys();
40 bool firstNS = true;
41 for (const QString &ns : namespaceList) {
42 if (firstNS)
43 firstNS = false;
44 else
45 placeholders += QLatin1String(" OR ");
46 placeholders += QLatin1String("(namespace = ?");
47
48 const QList<QStringList> &attributeSets = namespaces.values(key: ns);
49 bool firstAS = true;
50 for (const QStringList &attributeSet : attributeSets) {
51 if (!attributeSet.isEmpty()) {
52 if (firstAS) {
53 firstAS = false;
54 placeholders += QLatin1String(" AND (");
55 } else {
56 placeholders += QLatin1String(" OR ");
57 }
58 placeholders += QLatin1String("attributes = ?");
59 }
60 }
61 if (!firstAS)
62 placeholders += QLatin1Char(')'); // close "AND ("
63 placeholders += QLatin1Char(')');
64 }
65 return placeholders;
66}
67
68static void bindNamespacesAndAttributes(QSqlQuery *query, const QMultiMap<QString, QStringList> &namespaces)
69{
70 const auto &namespaceList = namespaces.uniqueKeys();
71 for (const QString &ns : namespaceList) {
72 query->addBindValue(val: ns);
73
74 const QList<QStringList> &attributeSets = namespaces.values(key: ns);
75 for (const QStringList &attributeSet : attributeSets) {
76 if (!attributeSet.isEmpty())
77 query->addBindValue(val: attributeSet.join(sep: QLatin1Char('|')));
78 }
79 }
80}
81
82static QString namespacePlaceholders(const QStringList &namespaceList)
83{
84 QString placeholders;
85 bool firstNS = true;
86 for (int i = namespaceList.size(); i; --i) {
87 if (firstNS)
88 firstNS = false;
89 else
90 placeholders += QLatin1String(" OR ");
91 placeholders += QLatin1String("namespace = ?");
92 }
93 return placeholders;
94}
95
96static void bindNamespacesAndAttributes(QSqlQuery *query, const QStringList &namespaceList)
97{
98 for (const QString &ns : namespaceList)
99 query->addBindValue(val: ns);
100}
101
102QList<QHelpSearchResult> Reader::queryTable(const QSqlDatabase &db,
103 const QString &tableName,
104 const QString &searchInput) const
105{
106 const QString nsPlaceholders = m_useFilterEngine
107 ? namespacePlaceholders(namespaceList: m_filterEngineNamespaceList)
108 : namespacePlaceholders(namespaces: m_namespaceAttributes);
109 QSqlQuery query(db);
110 query.prepare(query: QLatin1String("SELECT url, title, snippet(") + tableName +
111 QLatin1String(", -1, '<b>', '</b>', '...', '10') FROM ") + tableName +
112 QLatin1String(" WHERE (") + nsPlaceholders +
113 QLatin1String(") AND ") + tableName +
114 QLatin1String(" MATCH ? ORDER BY rank"));
115 m_useFilterEngine
116 ? bindNamespacesAndAttributes(query: &query, namespaceList: m_filterEngineNamespaceList)
117 : bindNamespacesAndAttributes(query: &query, namespaces: m_namespaceAttributes);
118 query.addBindValue(val: searchInput);
119 query.exec();
120
121 QList<QHelpSearchResult> results;
122
123 while (query.next()) {
124 const QString &url = query.value(name: QLatin1String("url")).toString();
125 const QString &title = query.value(name: QLatin1String("title")).toString();
126 const QString &snippet = query.value(i: 2).toString();
127 results.append(t: QHelpSearchResult(url, title, snippet));
128 }
129
130 return results;
131}
132
133void Reader::searchInDB(const QString &searchInput)
134{
135 const QString &uniqueId = QHelpGlobal::uniquifyConnectionName(name: QLatin1String("QHelpReader"), pointer: this);
136 {
137 QSqlDatabase db = QSqlDatabase::addDatabase(type: QLatin1String("QSQLITE"), connectionName: uniqueId);
138 db.setConnectOptions(QLatin1String("QSQLITE_OPEN_READONLY"));
139 db.setDatabaseName(m_indexPath + QLatin1String("/fts"));
140
141 if (db.open()) {
142 const QList<QHelpSearchResult> titleResults = queryTable(db,
143 tableName: QLatin1String("titles"), searchInput);
144 const QList<QHelpSearchResult> contentResults = queryTable(db,
145 tableName: QLatin1String("contents"), searchInput);
146
147 // merge results form title and contents searches
148 m_searchResults = QList<QHelpSearchResult>();
149
150 QSet<QUrl> urls;
151
152 for (const QHelpSearchResult &result : titleResults) {
153 const QUrl &url = result.url();
154 if (!urls.contains(value: url)) {
155 urls.insert(value: url);
156 m_searchResults.append(t: result);
157 }
158 }
159
160 for (const QHelpSearchResult &result : contentResults) {
161 const QUrl &url = result.url();
162 if (!urls.contains(value: url)) {
163 urls.insert(value: url);
164 m_searchResults.append(t: result);
165 }
166 }
167 }
168 }
169 QSqlDatabase::removeDatabase(connectionName: uniqueId);
170}
171
172QList<QHelpSearchResult> Reader::searchResults() const
173{
174 return m_searchResults;
175}
176
177static bool attributesMatchFilter(const QStringList &attributes,
178 const QStringList &filter)
179{
180 for (const QString &attribute : filter) {
181 if (!attributes.contains(str: attribute, cs: Qt::CaseInsensitive))
182 return false;
183 }
184
185 return true;
186}
187
188void QHelpSearchIndexReaderDefault::run()
189{
190 QMutexLocker lock(&m_mutex);
191
192 if (m_cancel)
193 return;
194
195 const QString searchInput = m_searchInput;
196 const QString collectionFile = m_collectionFile;
197 const QString indexPath = m_indexFilesFolder;
198 const bool usesFilterEngine = m_usesFilterEngine;
199
200 lock.unlock();
201
202 QHelpEngineCore engine(collectionFile, nullptr);
203 if (!engine.setupData())
204 return;
205
206 emit searchingStarted();
207
208 // setup the reader
209 m_reader.setIndexPath(indexPath);
210
211 if (usesFilterEngine) {
212 m_reader.setFilterEngineNamespaceList(
213 engine.filterEngine()->namespacesForFilter(
214 filterName: engine.filterEngine()->activeFilter()));
215 } else {
216 const QStringList &registeredDocs = engine.registeredDocumentations();
217 const QStringList &currentFilter = engine.filterAttributes(filterName: engine.currentFilter());
218
219 for (const QString &namespaceName : registeredDocs) {
220 const QList<QStringList> &attributeSets =
221 engine.filterAttributeSets(namespaceName);
222
223 for (const QStringList &attributes : attributeSets) {
224 if (attributesMatchFilter(attributes, filter: currentFilter)) {
225 m_reader.addNamespaceAttributes(namespaceName, attributes);
226 }
227 }
228 }
229 }
230
231 lock.relock();
232 if (m_cancel) {
233 emit searchingFinished(searchResultCount: 0); // TODO: check this, speed issue while locking???
234 return;
235 }
236 lock.unlock();
237
238 m_searchResults.clear();
239 m_reader.searchInDB(searchInput); // TODO: should this be interruptible as well ???
240
241 lock.relock();
242 m_searchResults = m_reader.searchResults();
243 lock.unlock();
244
245 emit searchingFinished(searchResultCount: m_searchResults.size());
246}
247
248} // namespace std
249} // namespace fulltextsearch
250
251QT_END_NAMESPACE
252

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