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 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | namespace fulltextsearch { |
15 | namespace qt { |
16 | |
17 | void Reader::setIndexPath(const QString &path) |
18 | { |
19 | m_indexPath = path; |
20 | m_namespaceAttributes.clear(); |
21 | m_filterEngineNamespaceList.clear(); |
22 | m_useFilterEngine = false; |
23 | } |
24 | |
25 | void Reader::addNamespaceAttributes(const QString &namespaceName, const QStringList &attributes) |
26 | { |
27 | m_namespaceAttributes.insert(key: namespaceName, value: attributes); |
28 | } |
29 | |
30 | void Reader::setFilterEngineNamespaceList(const QStringList &namespaceList) |
31 | { |
32 | m_useFilterEngine = true; |
33 | m_filterEngineNamespaceList = namespaceList; |
34 | } |
35 | |
36 | static 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 | |
68 | static 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 | |
82 | static 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 | |
96 | static void bindNamespacesAndAttributes(QSqlQuery *query, const QStringList &namespaceList) |
97 | { |
98 | for (const QString &ns : namespaceList) |
99 | query->addBindValue(val: ns); |
100 | } |
101 | |
102 | QList<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 | |
133 | void 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 | |
172 | QList<QHelpSearchResult> Reader::searchResults() const |
173 | { |
174 | return m_searchResults; |
175 | } |
176 | |
177 | static 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 | |
188 | void 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 ®isteredDocs = engine.registeredDocumentations(); |
217 | const QStringList ¤tFilter = 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 | |
251 | QT_END_NAMESPACE |
252 | |