Completed
Branch master (33af51)
by Timo
04:12
created

Query::setQueryElevation()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.0072

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 12
cts 13
cp 0.9231
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 17
nc 5
nop 3
crap 4.0072
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\System\Configuration\TypoScriptConfiguration;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30
/**
31
 * A Solr search query
32
 *
33
 * @author Ingo Renner <[email protected]>
34
 */
35
class Query
36
{
37
38
    // FIXME extract link building from the query, it's not the query's domain
39
40
    const SORT_ASC = 'ASC';
41
    const SORT_DESC = 'DESC';
42
43
    const OPERATOR_AND = 'AND';
44
    const OPERATOR_OR = 'OR';
45
46
    /**
47
     * Used to identify the queries.
48
     *
49
     * @var int
50
     */
51
    protected static $idCount = 0;
52
53
    /**
54
     * @var int
55
     */
56
    protected $id;
57
58
    /**
59
     * @var TypoScriptConfiguration
60
     */
61
    protected $solrConfiguration;
62
63
    /**
64
     * @var string
65
     */
66
    protected $keywords;
67
68
    /**
69
     * @var string
70
     */
71
    protected $keywordsRaw;
72
73
    /**
74
     * @var array
75
     */
76
    protected $filters = [];
77
78
    /**
79
     * @var string
80
     */
81
    protected $sorting;
82
83
    // TODO check usage of these two variants, especially the check for $rawQueryString in getQueryString()
84
    /**
85
     * @var
86
     */
87
    protected $queryString;
88
89
    /**
90
     * @var array
91
     */
92
    protected $queryParameters = [];
93
94
    /**
95
     * @var int
96
     */
97
    protected $resultsPerPage;
98
99
    /**
100
     * @var int
101
     */
102
    protected $page;
103
104
    /**
105
     * @var int
106
     */
107
    protected $linkTargetPageId;
108
109
    /**
110
     * Holds the query fields with their associated boosts. The key represents
111
     * the field name, value represents the field's boost. These are the fields
112
     * that will actually be searched.
113
     *
114
     * Used in Solr's qf parameter
115
     *
116
     * @var array
117
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#qf_.28Query_Fields.29
118
     */
119
    protected $queryFields = [];
120
121
    /**
122
     * List of fields that will be returned in the result documents.
123
     *
124
     * used in Solr's fl parameter
125
     *
126
     * @var array
127
     * @see http://wiki.apache.org/solr/CommonQueryParameters#fl
128
     */
129
    protected $fieldList = [];
130
131
    /**
132
     * @var array
133
     */
134
    protected $filterFields;
135
136
    /**
137
     * @var array
138
     */
139
    protected $sortingFields;
140
141
    /**
142
     * @var bool
143
     */
144
    private $rawQueryString = false;
145
146
    /**
147
     * The field by which the result will be collapsed
148
     * @var string
149
     */
150
    protected $variantField = 'variantId';
151
152
    /**
153
     * @param string $keywords
154
     * @param TypoScriptConfiguration $solrConfiguration
155
     */
156 97
    public function __construct($keywords, $solrConfiguration = null)
157
    {
158 97
        $keywords = (string) $keywords;
159 97
        if ($solrConfiguration == null) {
160 53
            $this->solrConfiguration = Util::getSolrConfiguration();
161
        } else {
162 44
            $this->solrConfiguration = $solrConfiguration;
163
        }
164
165 97
        $this->setKeywords($keywords);
166 97
        $this->sorting = '';
167
168
        // What fields to search
169 97
        $queryFields = $this->solrConfiguration->getSearchQueryQueryFields();
170 97
        if ($queryFields != '') {
171 25
            $this->setQueryFieldsFromString($queryFields);
172
        }
173
174
        // What fields to return from Solr
175 97
        $this->fieldList = $this->solrConfiguration->getSearchQueryReturnFieldsAsArray(array('*', 'score'));
176 97
        $this->linkTargetPageId = $this->solrConfiguration->getSearchTargetPage();
177
178 97
        $this->initializeQuery();
179
180 97
        $this->id = ++self::$idCount;
181 97
    }
182
183
    /**
184
     * @return void
185
     */
186 96
    protected function initializeQuery()
187
    {
188 96
        $this->initializeCollapsingFromConfiguration();
189 96
    }
190
191
    /**
192
     * Writes a message to the devLog.
193
     *
194
     * @param string $msg
195
     * @param int $severity
196
     * @param bool $dataVar
197
     */
198
    protected function writeDevLog($msg, $severity = 0, $dataVar = false)
199
    {
200
        GeneralUtility::devLog($msg, 'solr', $severity, $dataVar);
201
    }
202
203
    /**
204
     * Takes a string of comma separated query fields and _overwrites_ the
205
     * currently set query fields. Boost can also be specified in through the
206
     * given string.
207
     *
208
     * Example: "title^5, subtitle^2, content, author^0.5"
209
     * This sets the query fields to title with  a boost of 5.0, subtitle with
210
     * a boost of 2.0, content with a default boost of 1.0 and the author field
211
     * with a boost of 0.5
212
     *
213
     * @param string $queryFields A string defining which fields to query and their associated boosts
214
     * @return void
215
     */
216 26
    public function setQueryFieldsFromString($queryFields)
217
    {
218 26
        $fields = GeneralUtility::trimExplode(',', $queryFields, true);
219
220 26
        foreach ($fields as $field) {
221 26
            $fieldNameAndBoost = explode('^', $field);
222
223 26
            $boost = 1.0;
224 26
            if (isset($fieldNameAndBoost[1])) {
225 26
                $boost = floatval($fieldNameAndBoost[1]);
226
            }
227
228 26
            $this->setQueryField($fieldNameAndBoost[0], $boost);
229
        }
230 26
    }
231
232
    /**
233
     * Sets a query field and its boost. If the field does not exist yet, it
234
     * gets added. Boost is optional, if left out a default boost of 1.0 is
235
     * applied.
236
     *
237
     * @param string $fieldName The field's name
238
     * @param float $boost Optional field boost, defaults to 1.0
239
     * @return void
240
     */
241 26
    public function setQueryField($fieldName, $boost = 1.0)
242
    {
243 26
        $this->queryFields[$fieldName] = (float)$boost;
244 26
    }
245
246
    /**
247
     * magic implementation for clone(), makes sure that the id counter is
248
     * incremented
249
     *
250
     * @return void
251
     */
252 5
    public function __clone()
253
    {
254 5
        $this->id = ++self::$idCount;
255 5
    }
256
257
    /**
258
     * returns a string representation of the query
259
     *
260
     * @return string the string representation of the query
261
     */
262 7
    public function __toString()
263
    {
264 7
        return $this->getQueryString();
265
    }
266
267
    /**
268
     * Builds the query string which is then used for Solr's q parameters
269
     *
270
     * @return string Solr query string
271
     */
272 32
    public function getQueryString()
273
    {
274 32
        if (!$this->rawQueryString) {
275 31
            $this->buildQueryString();
276
        }
277
278 32
        return $this->queryString;
279
    }
280
281
    /**
282
     * Sets the query string without any escaping.
283
     *
284
     * Be cautious with this function!
285
     * TODO remove this method as it basically just sets the q parameter / keywords
286
     *
287
     * @param string $queryString The raw query string.
288
     */
289 1
    public function setQueryString($queryString)
290
    {
291 1
        $this->queryString = $queryString;
292 1
    }
293
294
    /**
295
     * Creates the string that is later used as the q parameter in the solr query
296
     *
297
     * @return void
298
     */
299 31
    protected function buildQueryString()
300
    {
301
        // very simple for now
302 31
        $this->queryString = $this->keywords;
303 31
    }
304
305
    /**
306
     * Sets whether a raw query sting should be used, that is, whether the query
307
     * string should be escaped or not.
308
     *
309
     * @param bool $useRawQueryString TRUE to use raw queries (like Lucene Query Language) or FALSE for regular, escaped queries
310
     */
311 1
    public function useRawQueryString($useRawQueryString)
312
    {
313 1
        $this->rawQueryString = (boolean)$useRawQueryString;
314 1
    }
315
316
    /**
317
     * Returns the query's ID.
318
     *
319
     * @return int The query's ID.
320
     */
321
    public function getId()
322
    {
323
        return $this->id;
324
    }
325
326
    /**
327
     * Quote and escape search strings
328
     *
329
     * @param string $string String to escape
330
     * @return string The escaped/quoted string
331
     */
332 97
    public function escape($string)
333
    {
334
        // when we have a numeric string only, nothing needs to be done
335 97
        if (is_numeric($string)) {
336 1
            return $string;
337
        }
338
339
        // when no whitespaces are in the query we can also just escape the special characters
340 97
        if (preg_match('/\W/', $string) != 1) {
341 82
            return $this->escapeSpecialCharacters($string);
342
        }
343
344
        // when there are no quotes inside the query string we can also just escape the whole string
345 31
        $hasQuotes = strrpos($string, '"') !== false;
346 31
        if (!$hasQuotes) {
347 24
            return $this->escapeSpecialCharacters($string);
348
        }
349
350 7
        $result = $this->tokenizeByQuotesAndEscapeDependingOnContext($string);
351
352 7
        return $result;
353
    }
354
355
    /**
356
     * This method is used to escape the content in the query string surrounded by quotes
357
     * different then when it is not in a quoted context.
358
     *
359
     * @param string $string
360
     * @return string
361
     */
362 7
    protected function tokenizeByQuotesAndEscapeDependingOnContext($string)
363
    {
364 7
        $result = '';
365 7
        $quotesCount = substr_count($string, '"');
366 7
        $isEvenAmountOfQuotes = $quotesCount % 2 === 0;
367
368
        // go over all quote segments and apply escapePhrase inside a quoted
369
        // context and escapeSpecialCharacters outside the quoted context.
370 7
        $segments = explode('"', $string);
371 7
        $segmentsIndex = 0;
372 7
        foreach ($segments as $segment) {
373 7
            $isInQuote = $segmentsIndex % 2 !== 0;
374 7
            $isLastQuote = $segmentsIndex === $quotesCount;
375
376 7
            if ($isLastQuote && !$isEvenAmountOfQuotes) {
377 1
                $result .= '\"';
378
            }
379
380 7
            if ($isInQuote && !$isLastQuote) {
381 6
                $result .= $this->escapePhrase($segment);
382
            } else {
383 7
                $result .= $this->escapeSpecialCharacters($segment);
384
            }
385
386 7
            $segmentsIndex++;
387
        }
388
389 7
        return $result;
390
    }
391
392
    // pagination
393
394
    /**
395
     * Escapes a value meant to be contained in a phrase with characters with
396
     * special meanings in Lucene query syntax.
397
     *
398
     * @param string $value Unescaped - "dirty" - string
399
     * @return string Escaped - "clean" - string
400
     */
401 6
    protected function escapePhrase($value)
402
    {
403 6
        $pattern = '/("|\\\)/';
404 6
        $replace = '\\\$1';
405
406 6
        return '"' . preg_replace($pattern, $replace, $value) . '"';
407
    }
408
409
    /**
410
     * Escapes characters with special meanings in Lucene query syntax.
411
     *
412
     * @param string $value Unescaped - "dirty" - string
413
     * @return string Escaped - "clean" - string
414
     */
415 97
    protected function escapeSpecialCharacters($value)
416
    {
417
        // list taken from http://lucene.apache.org/core/4_4_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
418
        // which mentions: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
419
        // of which we escape: ( ) { } [ ] ^ " ~ : \ /
420
        // and explicitly don't escape: + - && || ! * ?
421 97
        $pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
422 97
        $replace = '\\\$1';
423
424 97
        return preg_replace($pattern, $replace, $value);
425
    }
426
427
    /**
428
     * Gets the currently showing page's number
429
     *
430
     * @return int page number currently showing
431
     */
432
    public function getPage()
433
    {
434
        return $this->page;
435
    }
436
437
    /**
438
     * Sets the page that should be shown
439
     *
440
     * @param int $page page number to show
441
     * @return void
442
     */
443
    public function setPage($page)
444
    {
445
        $this->page = max(intval($page), 0);
446
    }
447
448
    /**
449
     * Gets the index of the first result document we're showing
450
     *
451
     * @return int index of the currently first document showing
452
     */
453
    public function getStartIndex()
454
    {
455
        return ($this->page - 1) * $this->resultsPerPage;
456
    }
457
458
    /**
459
     * Gets the index of the last result document we're showing
460
     *
461
     * @return int index of the currently last document showing
462
     */
463
    public function getEndIndex()
464
    {
465
        return $this->page * $this->resultsPerPage;
466
    }
467
468
    // query elevation
469
470
    /**
471
     * Activates and deactivates query elevation for the current query.
472
     *
473
     * @param bool $elevation True to enable query elevation (default), FALSE to disable query elevation.
474
     * @param bool $forceElevation Optionally force elevation so that the elevated documents are always on top regardless of sorting, default to TRUE.
475
     * @param bool $markElevatedResults Mark elevated results
476
     * @return void
477
     */
478 23
    public function setQueryElevation(
479
        $elevation = true,
480
        $forceElevation = true,
481
        $markElevatedResults = true
482
    ) {
483 23
        if ($elevation) {
484 20
            $this->queryParameters['enableElevation'] = 'true';
485
486 20
            if ($forceElevation) {
487 20
                $this->queryParameters['forceElevation'] = 'true';
488
            } else {
489
                $this->queryParameters['forceElevation'] = 'false';
490
            }
491
492 20
            if ($markElevatedResults) {
493 20
                $this->addReturnField('isElevated:[elevated]');
494
            }
495
        } else {
496 3
            $this->queryParameters['enableElevation'] = 'false';
497 3
            unset($this->queryParameters['forceElevation']);
498 3
            $this->removeReturnField('isElevated:[elevated]');
499 3
            $this->removeReturnField('[elevated]'); // fallback
500
        }
501 23
    }
502
503
    // collapsing
504
505
    /**
506
     * Check whether collapsing is active
507
     *
508
     * @return bool
509
     */
510 3
    public function getIsCollapsing()
511
    {
512 3
        return array_key_exists('collapsing', $this->filters);
513
    }
514
515
    /**
516
     * @param string $fieldName
517
     */
518 2
    public function setVariantField($fieldName)
519
    {
520 2
        $this->variantField = $fieldName;
521 2
    }
522
523
    /**
524
     * @return string
525
     */
526 1
    public function getVariantField()
527
    {
528 1
        return $this->variantField;
529
    }
530
531
    /**
532
     * @param bool $collapsing
533
     */
534 2
    public function setCollapsing($collapsing = true)
535
    {
536 2
        if ($collapsing) {
537 2
            $this->filters['collapsing'] = '{!collapse field=' . $this->variantField . '}';
538 2
            if ($this->solrConfiguration->getSearchVariantsExpand()) {
539 1
                $this->queryParameters['expand'] = 'true';
540 2
                $this->queryParameters['expand.rows'] = $this->solrConfiguration->getSearchVariantsLimit();
541
            }
542
        } else {
543
            unset($this->filters['collapsing']);
544
            unset($this->queryParameters['expand']);
545
            unset($this->queryParameters['expand.rows']);
546
        }
547 2
    }
548
549
    // grouping
550
551
    /**
552
     * Adds a field to the list of fields to return. Also checks whether * is
553
     * set for the fields, if so it's removed from the field list.
554
     *
555
     * @param string $fieldName Name of a field to return in the result documents
556
     */
557 20
    public function addReturnField($fieldName)
558
    {
559 20
        if (strpos($fieldName, '[') === false
560 20
            && strpos($fieldName, ']') === false
561 20
            && in_array('*', $this->fieldList)
562
        ) {
563
            $this->fieldList = array_diff($this->fieldList, array('*'));
564
        }
565
566 20
        $this->fieldList[] = $fieldName;
567 20
    }
568
569
    /**
570
     * Removes a field from the list of fields to return (fl parameter).
571
     *
572
     * @param string $fieldName Field to remove from the list of fields to return
573
     */
574 3
    public function removeReturnField($fieldName)
575
    {
576 3
        $key = array_search($fieldName, $this->fieldList);
577
578 3
        if ($key !== false) {
579
            unset($this->fieldList[$key]);
580
        }
581 3
    }
582
583
    /**
584
     * Activates and deactivates grouping for the current query.
585
     *
586
     * @param bool $grouping TRUE to enable grouping, FALSE to disable grouping
587
     * @return void
588
     */
589 2
    public function setGrouping($grouping = true)
590
    {
591 2
        if ($grouping) {
592 1
            $this->queryParameters['group'] = 'true';
593 1
            $this->queryParameters['group.format'] = 'grouped';
594 1
            $this->queryParameters['group.ngroups'] = 'true';
595
        } else {
596 1
            foreach ($this->queryParameters as $key => $value) {
597
                // remove all group.* settings
598 1
                if (GeneralUtility::isFirstPartOfStr($key, 'group')) {
599 1
                    unset($this->queryParameters[$key]);
600
                }
601
            }
602
        }
603 2
    }
604
605
    /**
606
     * Sets the number of groups to return per group field or group query
607
     *
608
     * Internally uses the rows parameter.
609
     *
610
     * @param int $numberOfGroups Number of groups per group.field or group.query
611
     */
612
    public function setNumberOfGroups($numberOfGroups)
613
    {
614
        $this->setResultsPerPage($numberOfGroups);
615
    }
616
617
    /**
618
     * Gets the number of groups to return per group field or group query
619
     *
620
     * Internally uses the rows parameter.
621
     *
622
     * @return int Number of groups per group.field or group.query
623
     */
624
    public function getNumberOfGroups()
625
    {
626
        return $this->getResultsPerPage();
627
    }
628
629
    /**
630
     * Returns the number of results that should be shown per page
631
     *
632
     * @return int number of results to show per page
633
     */
634 23
    public function getResultsPerPage()
635
    {
636 23
        return $this->resultsPerPage;
637
    }
638
639
    /**
640
     * Sets the number of results that should be shown per page
641
     *
642
     * @param int $resultsPerPage Number of results to show per page
643
     * @return void
644
     */
645 30
    public function setResultsPerPage($resultsPerPage)
646
    {
647 30
        $this->resultsPerPage = max(intval($resultsPerPage), 0);
648 30
    }
649
650
    /**
651
     * Adds a field that should be used for grouping.
652
     *
653
     * @param string $fieldName Name of a field for grouping
654
     */
655
    public function addGroupField($fieldName)
656
    {
657
        if (!isset($this->queryParameters['group.field'])) {
658
            $this->queryParameters['group.field'] = array();
659
        }
660
661
        $this->queryParameters['group.field'][] = $fieldName;
662
    }
663
664
    /**
665
     * Gets the fields set for grouping.
666
     *
667
     * @return array An array of fields set for grouping.
668
     */
669
    public function getGroupFields()
670
    {
671
        $groupFields = array();
672
673
        if (isset($this->queryParameters['group.field'])) {
674
            $groupFields = $this->queryParameters['group.field'];
675
        }
676
677
        return $groupFields;
678
    }
679
680
    /**
681
     * Adds sorting configuration for grouping.
682
     *
683
     * @param string $sorting value of sorting configuration
684
     */
685
    public function addGroupSorting($sorting)
686
    {
687
        if (!isset($this->queryParameters['group.sort'])) {
688
            $this->queryParameters['group.sort'] = array();
689
        }
690
        $this->queryParameters['group.sort'][] = $sorting;
691
    }
692
693
    /**
694
     * Gets the sorting set for grouping.
695
     *
696
     * @return array An array of sorting configurations for grouping.
697
     */
698
    public function getGroupSortings()
699
    {
700
        $groupSortings = array();
701
        if (isset($this->queryParameters['group.sort'])) {
702
            $groupSortings = $this->queryParameters['group.sort'];
703
        }
704
        return $groupSortings;
705
    }
706
707
    // faceting
708
709
    /**
710
     * Adds a query that should be used for grouping.
711
     *
712
     * @param string $query Lucene query for grouping
713
     */
714
    public function addGroupQuery($query)
715
    {
716
        if (!isset($this->queryParameters['group.query'])) {
717
            $this->queryParameters['group.query'] = array();
718
        }
719
720
        $this->queryParameters['group.query'][] = $query;
721
    }
722
723
    /**
724
     * Gets the queries set for grouping.
725
     *
726
     * @return array An array of queries set for grouping.
727
     */
728
    public function getGroupQueries()
729
    {
730
        $groupQueries = array();
731
732
        if (isset($this->queryParameters['group.query'])) {
733
            $groupQueries = $this->queryParameters['group.query'];
734
        }
735
736
        return $groupQueries;
737
    }
738
739
    /**
740
     * Sets the maximum number of results to be returned per group.
741
     *
742
     * @param int $numberOfResults Maximum number of results per group to return
743
     */
744
    public function setNumberOfResultsPerGroup($numberOfResults)
745
    {
746
        $numberOfResults = max(intval($numberOfResults), 0);
747
748
        $this->queryParameters['group.limit'] = $numberOfResults;
749
    }
750
751
    // filter
752
753
    /**
754
     * Gets the maximum number of results to be returned per group.
755
     *
756
     * @return int Maximum number of results per group to return
757
     */
758
    public function getNumberOfResultsPerGroup()
759
    {
760
        // default if nothing else set is 1, @see http://wiki.apache.org/solr/FieldCollapsing
761
        $numberOfResultsPerGroup = 1;
762
763
        if (!empty($this->queryParameters['group.limit'])) {
764
            $numberOfResultsPerGroup = $this->queryParameters['group.limit'];
765
        }
766
767
        return $numberOfResultsPerGroup;
768
    }
769
770
    /**
771
     * Activates and deactivates faceting for the current query.
772
     *
773
     * @param bool $faceting TRUE to enable faceting, FALSE to disable faceting
774
     * @return void
775
     */
776 30
    public function setFaceting($faceting = true)
777
    {
778 30
        if ($faceting) {
779 30
            $this->queryParameters['facet'] = 'true';
780 30
            $this->queryParameters['facet.mincount'] = $this->solrConfiguration->getSearchFacetingMinimumCount();
781
782 30
            $this->applyConfiguredFacetSorting();
783
        } else {
784
            foreach ($this->queryParameters as $key => $value) {
785
                // remove all facet.* settings
786
                if (GeneralUtility::isFirstPartOfStr($key, 'facet')) {
787
                    unset($this->queryParameters[$key]);
788
                }
789
790
                // remove all f.*.facet.* settings (overrides for individual fields)
791
                if (GeneralUtility::isFirstPartOfStr($key, 'f.') && strpos($key,
792
                        '.facet.') !== false
793
                ) {
794
                    unset($this->queryParameters[$key]);
795
                }
796
            }
797
        }
798 30
    }
799
800
    /**
801
     * Reads the facet sorting configuration and applies it to the queryParameters.
802
     *
803
     * @return void
804
     */
805 30
    protected function applyConfiguredFacetSorting()
806
    {
807 30
        $sorting = $this->solrConfiguration->getSearchFacetingSortBy();
808 30
        if (!GeneralUtility::inList('count,index,alpha,lex,1,0,true,false', $sorting)) {
809
            // when the sorting is not in the list of valid values we do not apply it.
810 9
            return;
811
        }
812
813
        // alpha and lex alias for index
814 21
        if ($sorting == 'alpha' || $sorting == 'lex') {
815 1
            $sorting = 'index';
816
        }
817
818 21
        $this->queryParameters['facet.sort'] = $sorting;
819 21
    }
820
821
    /**
822
     * Sets facet fields for a query.
823
     *
824
     * @param array $facetFields Array of field names
825
     */
826
    public function setFacetFields(array $facetFields)
827
    {
828
        $this->queryParameters['facet.field'] = array();
829
830
        foreach ($facetFields as $facetField) {
831
            $this->addFacetField($facetField);
832
        }
833
    }
834
835
    /**
836
     * Adds a single facet field.
837
     *
838
     * @param string $facetField field name
839
     */
840
    public function addFacetField($facetField)
841
    {
842
        $this->queryParameters['facet.field'][] = $facetField;
843
    }
844
845
    /**
846
     * Removes a filter on a field
847
     *
848
     * @param string $filterFieldName The field name the filter should be removed for
849
     * @return void
850
     */
851
    public function removeFilter($filterFieldName)
852
    {
853
        foreach ($this->filters as $key => $filterString) {
854
            if (GeneralUtility::isFirstPartOfStr($filterString,
855
                $filterFieldName . ':')
856
            ) {
857
                unset($this->filters[$key]);
858
            }
859
        }
860
    }
861
862
    /**
863
     * Removes a filter based on key of filter array
864
     *
865
     * @param string $key array key
866
     */
867
    public function removeFilterByKey($key)
868
    {
869
        unset($this->filters[$key]);
870
    }
871
872
    /**
873
     * Gets all currently applied filters.
874
     *
875
     * @return array Array of filters
876
     */
877 25
    public function getFilters()
878
    {
879 25
        return $this->filters;
880
    }
881
882
    // sorting
883
884
    /**
885
     * Sets access restrictions for a frontend user.
886
     *
887
     * @param array $groups Array of groups a user has been assigned to
888
     */
889 28
    public function setUserAccessGroups(array $groups)
890
    {
891 28
        $groups = array_map('intval', $groups);
892 28
        $groups[] = 0; // always grant access to public documents
893 28
        $groups = array_unique($groups);
894 28
        sort($groups, SORT_NUMERIC);
895
896 28
        $accessFilter = '{!typo3access}' . implode(',', $groups);
897
898 28
        foreach ($this->filters as $key => $filter) {
899 24
            if (GeneralUtility::isFirstPartOfStr($filter, '{!typo3access}')) {
900 24
                unset($this->filters[$key]);
901
            }
902
        }
903
904 28
        $this->addFilter($accessFilter);
905 28
    }
906
907
    /**
908
     * Adds a filter parameter.
909
     *
910
     * @param string $filterString The filter to add, in the form of field:value
911
     * @return void
912
     */
913 33
    public function addFilter($filterString)
914
    {
915
        // TODO refactor to split filter field and filter value, @see Drupal
916 33
        if ($this->solrConfiguration->getLoggingQueryFilters()) {
917 1
            $this->writeDevLog('adding filter', 0, array($filterString));
0 ignored issues
show
Documentation introduced by
array($filterString) is of type array<integer,string,{"0":"string"}>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
918
        }
919
920 33
        $this->filters[] = $filterString;
921 33
    }
922
923
    // query parameters
924
925
    /**
926
     * Limits the query to certain sites
927
     *
928
     * @param string $allowedSites Comma-separated list of domains
929
     */
930 23
    public function setSiteHashFilter($allowedSites)
931
    {
932 23
        $allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
933 23
        $filters = array();
934
935 23
        foreach ($allowedSites as $site) {
936 23
            $siteHash = Util::getSiteHashForDomain($site);
937
938 23
            $filters[] = 'siteHash:"' . $siteHash . '"';
939
        }
940
941 23
        $this->addFilter(implode(' OR ', $filters));
942 23
    }
943
944
    /**
945
     * Limits the query to certain page tree branches
946
     *
947
     * @param string $pageIds Comma-separated list of page IDs
948
     */
949
    public function setRootlineFilter($pageIds)
950
    {
951
        $pageIds = GeneralUtility::trimExplode(',', $pageIds);
952
        $filters = array();
953
954
        $processor = GeneralUtility::makeInstance('ApacheSolrForTypo3\\Solr\\FieldProcessor\\PageUidToHierarchy');
955
        $hierarchies = $processor->process($pageIds);
956
957
        foreach ($hierarchies as $hierarchy) {
958
            $lastLevel = array_pop($hierarchy);
959
            $filters[] = 'rootline:"' . $lastLevel . '"';
960
        }
961
962
        $this->addFilter(implode(' OR ', $filters));
963
    }
964
965
    /**
966
     * Adds a sort field and the sorting direction for that field
967
     *
968
     * @param string $fieldName The field name to sort by
969
     * @param string $direction Either ApacheSolrForTypo3\Solr\Query::SORT_ASC to sort the field ascending or ApacheSolrForTypo3\Solr\Query::SORT_DESC to sort descending
970
     * @return void
971
     * @throws \InvalidArgumentException if the $direction parameter given is neither ApacheSolrForTypo3\Solr\Query::SORT_ASC nor ApacheSolrForTypo3\Solr\Query::SORT_DESC
972
     */
973
    public function addSortField($fieldName, $direction)
974
    {
975
        switch ($direction) {
976
            case self::SORT_ASC:
977
            case self::SORT_DESC:
978
                $this->sortingFields[$fieldName] = $direction;
979
                break;
980
            default:
981
                throw new \InvalidArgumentException(
982
                    'Invalid sort direction "' . $direction . '"',
983
                    1235051723
984
                );
985
        }
986
    }
987
988
    /**
989
     * Gets the currently set sorting fields and their sorting directions
990
     *
991
     * @return array An associative array with the field names as key and their sorting direction as value
992
     */
993
    public function getSortingFields()
994
    {
995
        return $this->sortingFields;
996
    }
997
998
    /**
999
     * Gets the list of fields a query will return.
1000
     *
1001
     * @return array Array of field names the query will return
1002
     */
1003 2
    public function getFieldList()
1004
    {
1005 2
        return $this->fieldList;
1006
    }
1007
1008
    /**
1009
     * Sets the fields to return by a query.
1010
     *
1011
     * @param array|string $fieldList an array or comma-separated list of field names
1012
     * @throws \UnexpectedValueException on parameters other than comma-separated lists and arrays
1013
     */
1014
    public function setFieldList($fieldList = array('*', 'score'))
1015
    {
1016
        if (is_string($fieldList)) {
1017
            $fieldList = GeneralUtility::trimExplode(',', $fieldList);
1018
        }
1019
1020
        if (!is_array($fieldList) || empty($fieldList)) {
1021
            throw new \UnexpectedValueException(
1022
                'Field list must be a comma-separated list or array and must not be empty.',
1023
                1310740308
1024
            );
1025
        }
1026
1027
        $this->fieldList = $fieldList;
1028
    }
1029
1030
    /**
1031
     * Gets the query type, Solr's qt parameter.
1032
     *
1033
     * @return string Query type, qt parameter.
1034
     */
1035
    public function getQueryType()
1036
    {
1037
        return $this->queryParameters['qt'];
1038
    }
1039
1040
    /**
1041
     * Sets the query type, Solr's qt parameter.
1042
     *
1043
     * @param string|bool $queryType String query type or boolean FALSE to disable / reset the qt parameter.
1044
     * @see http://wiki.apache.org/solr/CoreQueryParameters#qt
1045
     */
1046
    public function setQueryType($queryType)
1047
    {
1048
        if ($queryType) {
1049
            $this->queryParameters['qt'] = $queryType;
1050
        } else {
1051
            unset($this->queryParameters['qt']);
1052
        }
1053
    }
1054
1055
    /**
1056
     * Sets the query operator to AND or OR. Unsets the query operator (actually
1057
     * sets it back to default) for FALSE.
1058
     *
1059
     * @param string|bool $operator AND or OR, FALSE to unset
1060
     */
1061
    public function setOperator($operator)
1062
    {
1063
        if (in_array($operator, array(self::OPERATOR_AND, self::OPERATOR_OR))) {
1064
            $this->queryParameters['q.op'] = $operator;
1065
        }
1066
1067
        if ($operator === false) {
1068
            unset($this->queryParameters['q.op']);
1069
        }
1070
    }
1071
1072
    /**
1073
     * Gets the alternative query, Solr's q.alt parameter.
1074
     *
1075
     * @return string Alternative query, q.alt parameter.
1076
     */
1077
    public function getAlternativeQuery()
1078
    {
1079
        return $this->queryParameters['q.alt'];
1080
    }
1081
1082
    /**
1083
     * Sets an alternative query, Solr's q.alt parameter.
1084
     *
1085
     * This query supports the complete Lucene Query Language.
1086
     *
1087
     * @param mixed $alternativeQuery String alternative query or boolean FALSE to disable / reset the q.alt parameter.
1088
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#q.alt
1089
     */
1090 24
    public function setAlternativeQuery($alternativeQuery)
1091
    {
1092 24
        if ($alternativeQuery) {
1093 24
            $this->queryParameters['q.alt'] = $alternativeQuery;
1094
        } else {
1095
            unset($this->queryParameters['q.alt']);
1096
        }
1097 24
    }
1098
1099
    // keywords
1100
1101
    /**
1102
     * Set the query to omit the response header
1103
     *
1104
     * @param bool $omitHeader TRUE (default) to omit response headers, FALSE to re-enable
1105
     */
1106
    public function setOmitHeader($omitHeader = true)
1107
    {
1108
        if ($omitHeader) {
1109
            $this->queryParameters['omitHeader'] = 'true';
1110
        } else {
1111
            unset($this->queryParameters['omitHeader']);
1112
        }
1113
    }
1114
1115
    /**
1116
     * Get the query keywords, keywords are escaped.
1117
     *
1118
     * @return string query keywords
1119
     */
1120 20
    public function getKeywords()
1121
    {
1122 20
        return $this->keywords;
1123
    }
1124
1125
    /**
1126
     * Sets the query keywords, escapes them as needed for Solr/Lucene.
1127
     *
1128
     * @param string $keywords user search terms/keywords
1129
     */
1130 97
    public function setKeywords($keywords)
1131
    {
1132 97
        $this->keywords = $this->escape($keywords);
1133 97
        $this->keywordsRaw = $keywords;
1134 97
    }
1135
1136
    /**
1137
     * Gets the cleaned keywords so that it can be used in templates f.e.
1138
     *
1139
     * @return string The cleaned keywords.
1140
     */
1141 18
    public function getKeywordsCleaned()
1142
    {
1143 18
        return $this->cleanKeywords($this->keywordsRaw);
1144
    }
1145
1146
    /**
1147
     * Helper method to escape/encode keywords for use in HTML
1148
     *
1149
     * @param string $keywords Keywords to prepare for use in HTML
1150
     * @return string Encoded keywords
1151
     */
1152 22
    public static function cleanKeywords($keywords)
1153
    {
1154 22
        $keywords = trim($keywords);
1155 22
        $keywords = GeneralUtility::removeXSS($keywords);
1156 22
        $keywords = htmlentities($keywords, ENT_QUOTES,
1157 22
            $GLOBALS['TSFE']->metaCharset);
1158
1159
        // escape triple hashes as they are used in the template engine
1160
        // TODO remove after switching to fluid templates
1161 22
        $keywords = Template::escapeMarkers($keywords);
1162
1163 22
        return $keywords;
1164
    }
1165
1166
    // relevance, matching
1167
1168
    /**
1169
     * Gets the raw, unescaped, unencoded keywords.
1170
     *
1171
     * USE WITH CAUTION!
1172
     *
1173
     * @return string raw keywords
1174
     */
1175 19
    public function getKeywordsRaw()
1176
    {
1177 19
        return $this->keywordsRaw;
1178
    }
1179
1180
    /**
1181
     * Sets the minimum match (mm) parameter
1182
     *
1183
     * @param mixed $minimumMatch Minimum match parameter as string or boolean FALSE to disable / reset the mm parameter
1184
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#mm_.28Minimum_.27Should.27_Match.29
1185
     */
1186
    public function setMinimumMatch($minimumMatch)
1187
    {
1188
        if (is_string($minimumMatch) && !empty($minimumMatch)) {
1189
            $this->queryParameters['mm'] = $minimumMatch;
1190
        } else {
1191
            unset($this->queryParameters['mm']);
1192
        }
1193
    }
1194
1195
    /**
1196
     * Sets the boost function (bf) parameter
1197
     *
1198
     * @param mixed $boostFunction boost function parameter as string or boolean FALSE to disable / reset the bf parameter
1199
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#bf_.28Boost_Functions.29
1200
     */
1201
    public function setBoostFunction($boostFunction)
1202
    {
1203
        if (is_string($boostFunction) && !empty($boostFunction)) {
1204
            $this->queryParameters['bf'] = $boostFunction;
1205
        } else {
1206
            unset($this->queryParameters['bf']);
1207
        }
1208
    }
1209
1210
    // query fields
1211
    // TODO move up to field list methods
1212
1213
    /**
1214
     * Sets the boost query (bq) parameter
1215
     *
1216
     * @param mixed $boostQuery boost query parameter as string or array to set a boost query or boolean FALSE to disable / reset the bq parameter
1217
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#bq_.28Boost_Query.29
1218
     */
1219
    public function setBoostQuery($boostQuery)
1220
    {
1221
        if ((is_string($boostQuery) || is_array($boostQuery)) && !empty($boostQuery)) {
1222
            $this->queryParameters['bq'] = $boostQuery;
1223
        } else {
1224
            unset($this->queryParameters['bq']);
1225
        }
1226
    }
1227
1228
    /**
1229
     * Gets a specific query parameter by its name.
1230
     *
1231
     * @param string $parameterName The parameter to return
1232
     * @return string The parameter's value or NULL if not set
1233
     */
1234
    public function getQueryParameter($parameterName)
1235
    {
1236
        $requestedParameter = null;
1237
        $parameters = $this->getQueryParameters();
1238
1239
        if (isset($parameters[$parameterName])) {
1240
            $requestedParameter = $parameters[$parameterName];
1241
        }
1242
1243
        return $requestedParameter;
1244
    }
1245
1246
    /**
1247
     * Builds an array of query parameters to use for the search query.
1248
     *
1249
     * @return array An array ready to use with query parameters
1250
     */
1251 46
    public function getQueryParameters()
1252
    {
1253 46
        $queryParameters = array_merge(
1254
            array(
1255 46
                'fl' => implode(',', $this->fieldList),
1256 46
                'fq' => array_values($this->filters)
1257
            ),
1258 46
            $this->queryParameters
1259
        );
1260
1261 46
        $queryFieldString = $this->getQueryFieldsAsString();
1262 46
        if (!empty($queryFieldString)) {
1263 25
            $queryParameters['qf'] = $queryFieldString;
1264
        }
1265
1266 46
        return $queryParameters;
1267
    }
1268
1269
    // general query parameters
1270
1271
    /**
1272
     * Compiles the query fields into a string to be used in Solr's qf parameter.
1273
     *
1274
     * @return string A string of query fields with their associated boosts
1275
     */
1276 48
    public function getQueryFieldsAsString()
1277
    {
1278 48
        $queryFieldString = '';
1279
1280 48
        foreach ($this->queryFields as $fieldName => $fieldBoost) {
1281 26
            $queryFieldString .= $fieldName;
1282
1283 26
            if ($fieldBoost != 1.0) {
1284 26
                $queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
1285
            }
1286
1287 26
            $queryFieldString .= ' ';
1288
        }
1289
1290 48
        return trim($queryFieldString);
1291
    }
1292
1293
    /**
1294
     * Enables or disables highlighting of search terms in result teasers.
1295
     *
1296
     * @param bool $highlighting Enables highlighting when set to TRUE, deactivates highlighting when set to FALSE, defaults to TRUE.
1297
     * @param int $fragmentSize Size, in characters, of fragments to consider for highlighting.
1298
     * @see http://wiki.apache.org/solr/HighlightingParameters
1299
     * @return void
1300
     */
1301 29
    public function setHighlighting($highlighting = true, $fragmentSize = 200)
1302
    {
1303 29
        if ($highlighting) {
1304 29
            $this->queryParameters['hl'] = 'true';
1305 29
            $this->queryParameters['hl.fragsize'] = (int)$fragmentSize;
1306
1307 29
            $highlightingFields = $this->solrConfiguration->getSearchResultsHighlightingFields();
1308 29
            if ($highlightingFields != '') {
1309 24
                $this->queryParameters['hl.fl'] = $highlightingFields;
1310
            }
1311
1312
            // the fast vector highlighter can only be used, when the fragmentSize is
1313
            // higher then 17 otherwise solr throws an exception
1314 29
            $useFastVectorHighlighter = ($fragmentSize >= 18);
1315 29
            $wrap = explode('|', $this->solrConfiguration->getSearchResultsHighlightingWrap());
1316
1317 29
            if ($useFastVectorHighlighter) {
1318 27
                $this->queryParameters['hl.useFastVectorHighlighter'] = 'true';
1319 27
                $this->queryParameters['hl.tag.pre'] = $wrap[0];
1320 27
                $this->queryParameters['hl.tag.post'] = $wrap[1];
1321
            }
1322
1323 29
            if (isset($wrap[0]) && isset($wrap[1])) {
1324 25
                $this->queryParameters['hl.simple.pre'] = $wrap[0];
1325 29
                $this->queryParameters['hl.simple.post'] = $wrap[1];
1326
            }
1327
        } else {
1328
            // remove all hl.* settings
1329
            foreach ($this->queryParameters as $key => $value) {
1330
                if (GeneralUtility::isFirstPartOfStr($key, 'hl')) {
1331
                    unset($this->queryParameters[$key]);
1332
                }
1333
            }
1334
        }
1335 29
    }
1336
1337
    // misc
1338
1339
    /**
1340
     * Enables or disables spellchecking for the query.
1341
     *
1342
     * @param bool $spellchecking Enables spellchecking when set to TRUE, deactivates spellchecking when set to FALSE, defaults to TRUE.
1343
     */
1344 22
    public function setSpellchecking($spellchecking = true)
1345
    {
1346 22
        if ($spellchecking) {
1347 22
            $this->queryParameters['spellcheck'] = 'true';
1348 22
            $this->queryParameters['spellcheck.collate'] = 'true';
1349 22
            $maxCollationTries = $this->solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
1350 22
            $this->addQueryParameter('spellcheck.maxCollationTries', $maxCollationTries);
1351
        } else {
1352
            unset($this->queryParameters['spellcheck']);
1353
            unset($this->queryParameters['spellcheck.collate']);
1354
            unset($this->queryParameters['spellcheck.maxCollationTries']);
1355
        }
1356 22
    }
1357
1358
    /**
1359
     * Adds any generic query parameter.
1360
     *
1361
     * @param string $parameterName Query parameter name
1362
     * @param string $parameterValue Parameter value
1363
     */
1364 29
    public function addQueryParameter($parameterName, $parameterValue)
1365
    {
1366 29
        $this->queryParameters[$parameterName] = $parameterValue;
1367 29
    }
1368
1369
    /**
1370
     * Sets the sort parameter.
1371
     *
1372
     * $sorting must include a field name (or the pseudo-field score),
1373
     * followed by a space,
1374
     * followed by a sort direction (asc or desc).
1375
     *
1376
     * Multiple fallback sortings can be separated by comma,
1377
     * ie: <field name> <direction>[,<field name> <direction>]...
1378
     *
1379
     * @param string|bool $sorting Either a comma-separated list of sort fields and directions or FALSE to reset sorting to the default behavior (sort by score / relevance)
1380
     * @see http://wiki.apache.org/solr/CommonQueryParameters#sort
1381
     */
1382 1
    public function setSorting($sorting)
1383
    {
1384 1
        if ($sorting) {
1385 1
            $sortParameter = $sorting;
1386
1387 1
            list($sortField) = explode(' ', $sorting);
1388 1
            if ($sortField == 'relevance') {
1389
                $sortParameter = '';
1390
            }
1391
1392 1
            $this->queryParameters['sort'] = $sortParameter;
1393
        } else {
1394
            unset($this->queryParameters['sort']);
1395
        }
1396 1
    }
1397
1398
    /**
1399
     * Enables or disables the debug parameter for the query.
1400
     *
1401
     * @param bool $debugMode Enables debugging when set to TRUE, deactivates debugging when set to FALSE, defaults to TRUE.
1402
     */
1403 20
    public function setDebugMode($debugMode = true)
1404
    {
1405 20
        if ($debugMode) {
1406 20
            $this->queryParameters['debugQuery'] = 'true';
1407 20
            $this->queryParameters['echoParams'] = 'all';
1408
        } else {
1409
            unset($this->queryParameters['debugQuery']);
1410
            unset($this->queryParameters['echoParams']);
1411
        }
1412 20
    }
1413
1414
    /**
1415
     * Returns the link target page id.
1416
     *
1417
     * @return int
1418
     */
1419 2
    public function getLinkTargetPageId()
1420
    {
1421 2
        return $this->linkTargetPageId;
1422
    }
1423
1424
    /**
1425
     * Activates the collapsing on the configured field, if collapsing was enabled.
1426
     *
1427
     * @return bool
1428
     */
1429 96
    protected function initializeCollapsingFromConfiguration()
1430
    {
1431
        // check collapsing
1432 96
        if ($this->solrConfiguration->getSearchVariants()) {
1433 2
            $collapseField = $this->solrConfiguration->getSearchVariantsField();
1434 2
            $this->setVariantField($collapseField);
1435 2
            $this->setCollapsing(true);
1436
1437 2
            return true;
1438
        }
1439
1440 94
        return false;
1441
    }
1442
}
1443