1
|
|
|
<?php |
2
|
|
|
namespace ApacheSolrForTypo3\Solr; |
3
|
|
|
|
4
|
|
|
/*************************************************************** |
5
|
|
|
* Copyright notice |
6
|
|
|
* |
7
|
|
|
* (c) 2009-2015 Ingo Renner <[email protected]> |
8
|
|
|
* All rights reserved |
9
|
|
|
* |
10
|
|
|
* This script is part of the TYPO3 project. The TYPO3 project is |
11
|
|
|
* free software; you can redistribute it and/or modify |
12
|
|
|
* it under the terms of the GNU General Public License as published by |
13
|
|
|
* the Free Software Foundation; either version 2 of the License, or |
14
|
|
|
* (at your option) any later version. |
15
|
|
|
* |
16
|
|
|
* The GNU General Public License can be found at |
17
|
|
|
* http://www.gnu.org/copyleft/gpl.html. |
18
|
|
|
* |
19
|
|
|
* This script is distributed in the hope that it will be useful, |
20
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
21
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
22
|
|
|
* GNU General Public License for more details. |
23
|
|
|
* |
24
|
|
|
* This copyright notice MUST APPEAR in all copies of the script! |
25
|
|
|
***************************************************************/ |
26
|
|
|
|
27
|
|
|
use ApacheSolrForTypo3\Solr\Search\FacetsModifier; |
28
|
|
|
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration; |
29
|
|
|
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager; |
30
|
|
|
use TYPO3\CMS\Core\SingletonInterface; |
31
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Class to handle solr search requests |
35
|
|
|
* |
36
|
|
|
* @author Ingo Renner <[email protected]> |
37
|
|
|
*/ |
38
|
|
|
class Search |
39
|
|
|
{ |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* An instance of the Solr service |
43
|
|
|
* |
44
|
|
|
* @var SolrService |
45
|
|
|
*/ |
46
|
|
|
protected $solr = null; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* The search query |
50
|
|
|
* |
51
|
|
|
* @var Query |
52
|
|
|
*/ |
53
|
|
|
protected $query = null; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* The search response |
57
|
|
|
* |
58
|
|
|
* @var \Apache_Solr_Response |
59
|
|
|
*/ |
60
|
|
|
protected $response = null; |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Flag for marking a search |
64
|
|
|
* |
65
|
|
|
* @var bool |
66
|
|
|
*/ |
67
|
|
|
protected $hasSearched = false; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var TypoScriptConfiguration |
71
|
|
|
*/ |
72
|
|
|
protected $configuration; |
73
|
|
|
|
74
|
|
|
// TODO Override __clone to reset $response and $hasSearched |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager |
78
|
|
|
*/ |
79
|
|
|
protected $logger = null; |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* Constructor |
83
|
|
|
* |
84
|
|
|
* @param SolrService $solrConnection The Solr connection to use for searching |
85
|
|
|
*/ |
86
|
40 |
|
public function __construct(SolrService $solrConnection = null) |
87
|
|
|
{ |
88
|
40 |
|
$this->logger = GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__); |
89
|
|
|
|
90
|
40 |
|
$this->solr = $solrConnection; |
91
|
|
|
|
92
|
40 |
|
if (is_null($solrConnection)) { |
93
|
|
|
/** @var $connectionManager ConnectionManager */ |
94
|
1 |
|
$connectionManager = GeneralUtility::makeInstance(ConnectionManager::class); |
95
|
1 |
|
$this->solr = $connectionManager->getConnectionByPageId($GLOBALS['TSFE']->id, $GLOBALS['TSFE']->sys_language_uid); |
96
|
|
|
} |
97
|
|
|
|
98
|
40 |
|
$this->configuration = Util::getSolrConfiguration(); |
99
|
40 |
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Gets the Solr connection used by this search. |
103
|
|
|
* |
104
|
|
|
* @return SolrService Solr connection |
105
|
|
|
*/ |
106
|
|
|
public function getSolrConnection() |
107
|
|
|
{ |
108
|
|
|
return $this->solr; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Sets the Solr connection used by this search. |
113
|
|
|
* |
114
|
|
|
* Since ApacheSolrForTypo3\Solr\Search is a \TYPO3\CMS\Core\SingletonInterface, this is needed to |
115
|
|
|
* be able to switch between multiple cores/connections during |
116
|
|
|
* one request |
117
|
|
|
* |
118
|
|
|
* @param SolrService $solrConnection |
119
|
|
|
*/ |
120
|
|
|
public function setSolrConnection(SolrService $solrConnection) |
121
|
|
|
{ |
122
|
|
|
$this->solr = $solrConnection; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Executes a query against a Solr server. |
127
|
|
|
* |
128
|
|
|
* 1) Gets the query string |
129
|
|
|
* 2) Conducts the actual search |
130
|
|
|
* 3) Checks debug settings |
131
|
|
|
* |
132
|
|
|
* @param Query $query The query with keywords, filters, and so on. |
133
|
|
|
* @param int $offset Result offset for pagination. |
134
|
|
|
* @param int $limit Maximum number of results to return. If set to NULL, this value is taken from the query object. |
135
|
|
|
* @return \Apache_Solr_Response Solr response |
136
|
|
|
*/ |
137
|
34 |
|
public function search(Query $query, $offset = 0, $limit = 10) |
138
|
|
|
{ |
139
|
34 |
|
$this->query = $query; |
140
|
|
|
|
141
|
34 |
|
if (empty($limit)) { |
142
|
30 |
|
$limit = $query->getResultsPerPage(); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
try { |
146
|
34 |
|
$response = $this->solr->search( |
147
|
34 |
|
$query->getQueryString(), |
148
|
|
|
$offset, |
149
|
|
|
$limit, |
150
|
34 |
|
$query->getQueryParameters() |
151
|
|
|
); |
152
|
|
|
|
153
|
33 |
|
if ($this->configuration->getLoggingQueryQueryString()) { |
154
|
|
|
$this->logger->log( |
155
|
|
|
SolrLogManager::INFO, |
156
|
|
|
'Querying Solr, getting result', |
157
|
|
|
[ |
158
|
|
|
'query string' => $query->getQueryString(), |
159
|
|
|
'query parameters' => $query->getQueryParameters(), |
160
|
|
|
'response' => json_decode($response->getRawResponse(), |
161
|
33 |
|
true) |
162
|
|
|
] |
163
|
|
|
); |
164
|
|
|
} |
165
|
1 |
|
} catch (\RuntimeException $e) { |
166
|
1 |
|
$response = $this->solr->getResponse(); |
167
|
|
|
|
168
|
1 |
|
if ($this->configuration->getLoggingExceptions()) { |
169
|
1 |
|
$this->logger->log( |
170
|
1 |
|
SolrLogManager::ERROR, |
171
|
1 |
|
'Exception while querying Solr', |
172
|
|
|
[ |
173
|
1 |
|
'exception' => $e->__toString(), |
174
|
1 |
|
'query' => (array)$query, |
175
|
1 |
|
'offset' => $offset, |
176
|
1 |
|
'limit' => $limit |
177
|
|
|
] |
178
|
|
|
); |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
|
182
|
34 |
|
$this->response = $response; |
183
|
34 |
|
$this->hasSearched = true; |
184
|
|
|
|
185
|
34 |
|
return $this->response; |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* Sends a ping to the solr server to see whether it is available. |
190
|
|
|
* |
191
|
|
|
* @param bool $useCache Set to true if the cache should be used. |
192
|
|
|
* @return bool Returns TRUE on successful ping. |
193
|
|
|
* @throws \Exception Throws an exception in case ping was not successful. |
194
|
|
|
*/ |
195
|
29 |
|
public function ping($useCache = true) |
196
|
|
|
{ |
197
|
29 |
|
$solrAvailable = false; |
198
|
|
|
|
199
|
|
|
try { |
200
|
29 |
|
if (!$this->solr->ping(2, $useCache)) { |
201
|
|
|
throw new \Exception('Solr Server not responding.', 1237475791); |
202
|
|
|
} |
203
|
|
|
|
204
|
29 |
|
$solrAvailable = true; |
205
|
|
|
} catch (\Exception $e) { |
206
|
|
|
if ($this->configuration->getLoggingExceptions()) { |
207
|
|
|
$this->logger->log( |
208
|
|
|
SolrLogManager::ERROR, |
209
|
|
|
'Exception while trying to ping the solr server', |
210
|
|
|
[ |
211
|
|
|
$e->__toString() |
212
|
|
|
] |
213
|
|
|
); |
214
|
|
|
} |
215
|
|
|
} |
216
|
|
|
|
217
|
29 |
|
return $solrAvailable; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* checks whether a search has been executed |
222
|
|
|
* |
223
|
|
|
* @return bool TRUE if there was a search, FALSE otherwise (if the user just visited the search page f.e.) |
224
|
|
|
*/ |
225
|
29 |
|
public function hasSearched() |
226
|
|
|
{ |
227
|
29 |
|
return $this->hasSearched; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Gets the query object. |
232
|
|
|
* |
233
|
|
|
* @return Query Query |
234
|
|
|
*/ |
235
|
26 |
|
public function getQuery() |
236
|
|
|
{ |
237
|
26 |
|
return $this->query; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Gets the Solr response |
242
|
|
|
* |
243
|
|
|
* @return \Apache_Solr_Response |
244
|
|
|
*/ |
245
|
27 |
|
public function getResponse() |
246
|
|
|
{ |
247
|
27 |
|
return $this->response; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
public function getRawResponse() |
251
|
|
|
{ |
252
|
|
|
return $this->response->getRawResponse(); |
253
|
|
|
} |
254
|
|
|
|
255
|
21 |
|
public function getResponseHeader() |
256
|
|
|
{ |
257
|
21 |
|
return $this->getResponse()->responseHeader; |
258
|
|
|
} |
259
|
|
|
|
260
|
27 |
|
public function getResponseBody() |
261
|
|
|
{ |
262
|
27 |
|
return $this->getResponse()->response; |
|
|
|
|
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Returns all results documents raw. Use with caution! |
267
|
|
|
* |
268
|
|
|
* @return \Apache_Solr_Document[] |
269
|
|
|
*/ |
270
|
26 |
|
public function getResultDocumentsRaw() |
271
|
|
|
{ |
272
|
26 |
|
return $this->getResponseBody()->docs; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Returns all result documents but applies htmlspecialchars() on all fields retrieved |
277
|
|
|
* from solr except the configured fields in plugin.tx_solr.search.trustedFields |
278
|
|
|
* |
279
|
|
|
* @return \Apache_Solr_Document[] |
280
|
|
|
*/ |
281
|
3 |
|
public function getResultDocumentsEscaped() |
282
|
|
|
{ |
283
|
3 |
|
return $this->applyHtmlSpecialCharsOnAllFields($this->getResponseBody()->docs); |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* This method is used to apply htmlspecialchars on all document fields that |
288
|
|
|
* are not configured to be secure. Secure mean that we know where the content is coming from. |
289
|
|
|
* |
290
|
|
|
* @param array $documents |
291
|
|
|
* @return \Apache_Solr_Document[] |
292
|
|
|
*/ |
293
|
3 |
|
protected function applyHtmlSpecialCharsOnAllFields(array $documents) |
294
|
|
|
{ |
295
|
3 |
|
$trustedSolrFields = $this->configuration->getSearchTrustedFieldsArray(); |
296
|
|
|
|
297
|
3 |
|
foreach ($documents as $key => $document) { |
298
|
3 |
|
$fieldNames = $document->getFieldNames(); |
299
|
|
|
|
300
|
3 |
|
foreach ($fieldNames as $fieldName) { |
301
|
3 |
|
if (in_array($fieldName, $trustedSolrFields)) { |
302
|
|
|
// we skip this field, since it was marked as secure |
303
|
3 |
|
continue; |
304
|
|
|
} |
305
|
|
|
|
306
|
3 |
|
$document->{$fieldName} = $this->applyHtmlSpecialCharsOnSingleFieldValue($document->{$fieldName}); |
307
|
|
|
} |
308
|
|
|
|
309
|
3 |
|
$documents[$key] = $document; |
310
|
|
|
} |
311
|
|
|
|
312
|
3 |
|
return $documents; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Applies htmlspecialchars on all items of an array of a single value. |
317
|
|
|
* |
318
|
|
|
* @param $fieldValue |
319
|
|
|
* @return array|string |
320
|
|
|
*/ |
321
|
3 |
|
protected function applyHtmlSpecialCharsOnSingleFieldValue($fieldValue) |
322
|
|
|
{ |
323
|
3 |
|
if (is_array($fieldValue)) { |
324
|
3 |
|
foreach ($fieldValue as $key => $fieldValueItem) { |
325
|
3 |
|
$fieldValue[$key] = htmlspecialchars($fieldValueItem, null, null, false); |
326
|
|
|
} |
327
|
|
|
} else { |
328
|
3 |
|
$fieldValue = htmlspecialchars($fieldValue, null, null, false); |
329
|
|
|
} |
330
|
|
|
|
331
|
3 |
|
return $fieldValue; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
/** |
335
|
|
|
* Gets the time Solr took to execute the query and return the result. |
336
|
|
|
* |
337
|
|
|
* @return int Query time in milliseconds |
338
|
|
|
*/ |
339
|
21 |
|
public function getQueryTime() |
340
|
|
|
{ |
341
|
21 |
|
return $this->getResponseHeader()->QTime; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Gets the number of results per page. |
346
|
|
|
* |
347
|
|
|
* @return int Number of results per page |
348
|
|
|
*/ |
349
|
|
|
public function getResultsPerPage() |
350
|
|
|
{ |
351
|
|
|
return $this->getResponseHeader()->params->rows; |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
/** |
355
|
|
|
* Gets all facets with their fields, options, and counts. |
356
|
|
|
* |
357
|
|
|
* @return array |
358
|
|
|
*/ |
359
|
|
|
public function getFacetCounts() |
360
|
|
|
{ |
361
|
|
|
static $facetCountsModified = false; |
362
|
|
|
static $facetCounts = null; |
363
|
|
|
|
364
|
|
|
$unmodifiedFacetCounts = $this->response->facet_counts; |
|
|
|
|
365
|
|
|
|
366
|
|
|
if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'])) { |
367
|
|
|
if (!$facetCountsModified) { |
368
|
|
|
$facetCounts = $unmodifiedFacetCounts; |
369
|
|
|
|
370
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'] as $classReference) { |
371
|
|
|
$facetsModifier = GeneralUtility::getUserObj($classReference); |
372
|
|
|
|
373
|
|
|
if ($facetsModifier instanceof FacetsModifier) { |
374
|
|
|
$facetCounts = $facetsModifier->modifyFacets($facetCounts); |
375
|
|
|
$facetCountsModified = true; |
376
|
|
|
} else { |
377
|
|
|
throw new \UnexpectedValueException( |
378
|
|
|
get_class($facetsModifier) . ' must implement interface ' . FacetsModifier::class, |
379
|
|
|
1310387526 |
380
|
|
|
); |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
} |
384
|
|
|
} else { |
385
|
|
|
$facetCounts = $unmodifiedFacetCounts; |
386
|
|
|
} |
387
|
|
|
|
388
|
|
|
return $facetCounts; |
389
|
|
|
} |
390
|
|
|
|
391
|
|
|
public function getFacetFieldOptions($facetField) |
392
|
|
|
{ |
393
|
|
|
$facetOptions = null; |
394
|
|
|
|
395
|
|
|
if (property_exists($this->getFacetCounts()->facet_fields, |
396
|
|
|
$facetField)) { |
397
|
|
|
$facetOptions = get_object_vars($this->getFacetCounts()->facet_fields->$facetField); |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
return $facetOptions; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
public function getFacetQueryOptions($facetField) |
404
|
|
|
{ |
405
|
|
|
$options = []; |
406
|
|
|
|
407
|
|
|
$facetQueries = get_object_vars($this->getFacetCounts()->facet_queries); |
408
|
|
|
foreach ($facetQueries as $facetQuery => $numberOfResults) { |
409
|
|
|
// remove tags from the facet.query response, for facet.field |
410
|
|
|
// and facet.range Solr does that on its own automatically |
411
|
|
|
$facetQuery = preg_replace('/^\{!ex=[^\}]*\}(.*)/', '\\1', |
412
|
|
|
$facetQuery); |
413
|
|
|
|
414
|
|
|
if (GeneralUtility::isFirstPartOfStr($facetQuery, $facetField)) { |
415
|
|
|
$options[$facetQuery] = $numberOfResults; |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
// filter out queries with no results |
420
|
|
|
$options = array_filter($options); |
421
|
|
|
|
422
|
|
|
return $options; |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
public function getFacetRangeOptions($rangeFacetField) |
426
|
|
|
{ |
427
|
|
|
return get_object_vars($this->getFacetCounts()->facet_ranges->$rangeFacetField); |
428
|
|
|
} |
429
|
|
|
|
430
|
26 |
|
public function getNumberOfResults() |
431
|
|
|
{ |
432
|
26 |
|
return $this->response->response->numFound; |
|
|
|
|
433
|
|
|
} |
434
|
|
|
|
435
|
|
|
/** |
436
|
|
|
* Gets the result offset. |
437
|
|
|
* |
438
|
|
|
* @return int Result offset |
439
|
|
|
*/ |
440
|
|
|
public function getResultOffset() |
441
|
|
|
{ |
442
|
|
|
return $this->response->response->start; |
|
|
|
|
443
|
|
|
} |
444
|
|
|
|
445
|
21 |
|
public function getMaximumResultScore() |
446
|
|
|
{ |
447
|
21 |
|
return $this->response->response->maxScore; |
|
|
|
|
448
|
|
|
} |
449
|
|
|
|
450
|
2 |
|
public function getDebugResponse() |
451
|
|
|
{ |
452
|
2 |
|
return $this->response->debug; |
453
|
|
|
} |
454
|
|
|
|
455
|
21 |
|
public function getHighlightedContent() |
456
|
|
|
{ |
457
|
21 |
|
$highlightedContent = false; |
458
|
|
|
|
459
|
21 |
|
if ($this->response->highlighting) { |
460
|
21 |
|
$highlightedContent = $this->response->highlighting; |
|
|
|
|
461
|
|
|
} |
462
|
|
|
|
463
|
21 |
|
return $highlightedContent; |
464
|
|
|
} |
465
|
|
|
|
466
|
|
|
public function getSpellcheckingSuggestions() |
467
|
|
|
{ |
468
|
|
|
$spellcheckingSuggestions = false; |
469
|
|
|
|
470
|
|
|
$suggestions = (array)$this->response->spellcheck->suggestions; |
471
|
|
|
|
472
|
|
|
if (!empty($suggestions)) { |
473
|
|
|
$spellcheckingSuggestions = $suggestions; |
474
|
|
|
|
475
|
|
|
if (isset($this->response->spellcheck->collations)) { |
476
|
|
|
$collections = (array)$this->response->spellcheck->collations; |
477
|
|
|
$spellcheckingSuggestions['collation'] = $collections['collation']; |
478
|
|
|
} |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
return $spellcheckingSuggestions; |
482
|
|
|
} |
483
|
|
|
} |
484
|
|
|
|
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.
If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.