Completed
Push — master ( 33af51...d819b9 )
by Timo
01:27
created

Query::getGroupQueries()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 0
crap 6
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 114
    public function __construct($keywords, $solrConfiguration = null)
157
    {
158 114
        $keywords = (string) $keywords;
159 114
        if ($solrConfiguration == null) {
160 66
            $this->solrConfiguration = Util::getSolrConfiguration();
161 66
        } else {
162 48
            $this->solrConfiguration = $solrConfiguration;
163
        }
164
165 114
        $this->setKeywords($keywords);
166 114
        $this->sorting = '';
167
168
        // What fields to search
169 114
        $queryFields = $this->solrConfiguration->getSearchQueryQueryFields();
170 114
        if ($queryFields != '') {
171 25
            $this->setQueryFieldsFromString($queryFields);
172 25
        }
173
174
        // What fields to return from Solr
175 114
        $this->fieldList = $this->solrConfiguration->getSearchQueryReturnFieldsAsArray(array('*', 'score'));
176 114
        $this->linkTargetPageId = $this->solrConfiguration->getSearchTargetPage();
177
178 114
        $this->initializeQuery();
179
180 114
        $this->id = ++self::$idCount;
181 114
    }
182
183
    /**
184
     * @return void
185
     */
186 113
    protected function initializeQuery()
187
    {
188 113
        $this->initializeCollapsingFromConfiguration();
189 113
    }
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 26
            }
227
228 26
            $this->setQueryField($fieldNameAndBoost[0], $boost);
229 26
        }
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 22
    public function __clone()
253 22
    {
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 8
    public function __toString()
263
    {
264 8
        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 33
    public function getQueryString()
273
    {
274 33
        if (!$this->rawQueryString) {
275 32
            $this->buildQueryString();
276 32
        }
277
278 33
        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 32
    protected function buildQueryString()
300
    {
301
        // very simple for now
302 32
        $this->queryString = $this->keywords;
303 32
    }
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 114
    public function escape($string)
333
    {
334
        // when we have a numeric string only, nothing needs to be done
335 114
        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 114
        if (preg_match('/\W/', $string) != 1) {
341 99
            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 1
            }
379
380 7
            if ($isInQuote && !$isLastQuote) {
381 6
                $result .= $this->escapePhrase($segment);
382 6
            } else {
383 7
                $result .= $this->escapeSpecialCharacters($segment);
384
            }
385
386 7
            $segmentsIndex++;
387 7
        }
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 114
    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 114
        $pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
422 114
        $replace = '\\\$1';
423
424 114
        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 24
    public function setQueryElevation(
479
        $elevation = true,
480
        $forceElevation = true,
481
        $markElevatedResults = true
482
    ) {
483 24
        if ($elevation) {
484 21
            $this->queryParameters['enableElevation'] = 'true';
485
486
            // @todo can be extracted to an own method
487 21
            if ($forceElevation) {
488 21
                $this->queryParameters['forceElevation'] = 'true';
489 21
            } else {
490
                $this->queryParameters['forceElevation'] = 'false';
491
            }
492
493 21
            if ($markElevatedResults) {
494 21
                $this->addReturnField('isElevated:[elevated]');
495 21
            }
496 21
        } else {
497 4
            $this->queryParameters['enableElevation'] = 'false';
498 4
            unset($this->queryParameters['forceElevation']);
499 4
            $this->removeReturnField('isElevated:[elevated]');
500 4
            $this->removeReturnField('[elevated]'); // fallback
501
        }
502 24
    }
503
504
    // collapsing
505
506
    /**
507
     * Check whether collapsing is active
508
     *
509
     * @return bool
510
     */
511 3
    public function getIsCollapsing()
512
    {
513 3
        return array_key_exists('collapsing', $this->filters);
514
    }
515
516
    /**
517
     * @param string $fieldName
518
     */
519 2
    public function setVariantField($fieldName)
520
    {
521 2
        $this->variantField = $fieldName;
522 2
    }
523
524
    /**
525
     * @return string
526
     */
527 1
    public function getVariantField()
528
    {
529 1
        return $this->variantField;
530
    }
531
532
    /**
533
     * @param bool $collapsing
534
     */
535 2
    public function setCollapsing($collapsing = true)
536
    {
537 2
        if ($collapsing) {
538 2
            $this->filters['collapsing'] = '{!collapse field=' . $this->variantField . '}';
539 2
            if ($this->solrConfiguration->getSearchVariantsExpand()) {
540 1
                $this->queryParameters['expand'] = 'true';
541 1
                $this->queryParameters['expand.rows'] = $this->solrConfiguration->getSearchVariantsLimit();
542 1
            }
543 2
        } else {
544
            unset($this->filters['collapsing']);
545
            unset($this->queryParameters['expand']);
546
            unset($this->queryParameters['expand.rows']);
547
        }
548 2
    }
549
550
    // grouping
551
552
    /**
553
     * Adds a field to the list of fields to return. Also checks whether * is
554
     * set for the fields, if so it's removed from the field list.
555
     *
556
     * @param string $fieldName Name of a field to return in the result documents
557
     */
558 22
    public function addReturnField($fieldName)
559
    {
560 22
        if (strpos($fieldName, '[') === false
561 22
            && strpos($fieldName, ']') === false
562 22
            && in_array('*', $this->fieldList)
563 22
        ) {
564 1
            $this->fieldList = array_diff($this->fieldList, array('*'));
565 1
        }
566
567 22
        $this->fieldList[] = $fieldName;
568 22
    }
569
570
    /**
571
     * Removes a field from the list of fields to return (fl parameter).
572
     *
573
     * @param string $fieldName Field to remove from the list of fields to return
574
     */
575 5
    public function removeReturnField($fieldName)
576
    {
577 5
        $key = array_search($fieldName, $this->fieldList);
578
579 5
        if ($key !== false) {
580 2
            unset($this->fieldList[$key]);
581 2
        }
582 5
    }
583
584
    /**
585
     * Activates and deactivates grouping for the current query.
586
     *
587
     * @param bool $grouping TRUE to enable grouping, FALSE to disable grouping
588
     * @return void
589
     */
590 2
    public function setGrouping($grouping = true)
591
    {
592 2
        if ($grouping) {
593 1
            $this->queryParameters['group'] = 'true';
594 1
            $this->queryParameters['group.format'] = 'grouped';
595 1
            $this->queryParameters['group.ngroups'] = 'true';
596 1
        } else {
597 1
            foreach ($this->queryParameters as $key => $value) {
598
                // remove all group.* settings
599 1
                if (GeneralUtility::isFirstPartOfStr($key, 'group')) {
600 1
                    unset($this->queryParameters[$key]);
601 1
                }
602 1
            }
603
        }
604 2
    }
605
606
    /**
607
     * Sets the number of groups to return per group field or group query
608
     *
609
     * Internally uses the rows parameter.
610
     *
611
     * @param int $numberOfGroups Number of groups per group.field or group.query
612
     */
613
    public function setNumberOfGroups($numberOfGroups)
614
    {
615
        $this->setResultsPerPage($numberOfGroups);
616
    }
617
618
    /**
619
     * Gets the number of groups to return per group field or group query
620
     *
621
     * Internally uses the rows parameter.
622
     *
623
     * @return int Number of groups per group.field or group.query
624
     */
625
    public function getNumberOfGroups()
626
    {
627
        return $this->getResultsPerPage();
628
    }
629
630
    /**
631
     * Returns the number of results that should be shown per page
632
     *
633
     * @return int number of results to show per page
634
     */
635 23
    public function getResultsPerPage()
636
    {
637 23
        return $this->resultsPerPage;
638
    }
639
640
    /**
641
     * Sets the number of results that should be shown per page
642
     *
643
     * @param int $resultsPerPage Number of results to show per page
644
     * @return void
645
     */
646 30
    public function setResultsPerPage($resultsPerPage)
647
    {
648 30
        $this->resultsPerPage = max(intval($resultsPerPage), 0);
649 30
    }
650
651
    /**
652
     * Adds a field that should be used for grouping.
653
     *
654
     * @param string $fieldName Name of a field for grouping
655
     */
656
    public function addGroupField($fieldName)
657
    {
658
        if (!isset($this->queryParameters['group.field'])) {
659
            $this->queryParameters['group.field'] = array();
660
        }
661
662
        $this->queryParameters['group.field'][] = $fieldName;
663
    }
664
665
    /**
666
     * Gets the fields set for grouping.
667
     *
668
     * @return array An array of fields set for grouping.
669
     */
670
    public function getGroupFields()
671
    {
672
        $groupFields = array();
673
674
        if (isset($this->queryParameters['group.field'])) {
675
            $groupFields = $this->queryParameters['group.field'];
676
        }
677
678
        return $groupFields;
679
    }
680
681
    /**
682
     * Adds sorting configuration for grouping.
683
     *
684
     * @param string $sorting value of sorting configuration
685
     */
686
    public function addGroupSorting($sorting)
687
    {
688
        if (!isset($this->queryParameters['group.sort'])) {
689
            $this->queryParameters['group.sort'] = array();
690
        }
691
        $this->queryParameters['group.sort'][] = $sorting;
692
    }
693
694
    /**
695
     * Gets the sorting set for grouping.
696
     *
697
     * @return array An array of sorting configurations for grouping.
698
     */
699
    public function getGroupSortings()
700
    {
701
        $groupSortings = array();
702
        if (isset($this->queryParameters['group.sort'])) {
703
            $groupSortings = $this->queryParameters['group.sort'];
704
        }
705
        return $groupSortings;
706
    }
707
708
    // faceting
709
710
    /**
711
     * Adds a query that should be used for grouping.
712
     *
713
     * @param string $query Lucene query for grouping
714
     */
715
    public function addGroupQuery($query)
716
    {
717
        if (!isset($this->queryParameters['group.query'])) {
718
            $this->queryParameters['group.query'] = array();
719
        }
720
721
        $this->queryParameters['group.query'][] = $query;
722
    }
723
724
    /**
725
     * Gets the queries set for grouping.
726
     *
727
     * @return array An array of queries set for grouping.
728
     */
729
    public function getGroupQueries()
730
    {
731
        $groupQueries = array();
732
733
        if (isset($this->queryParameters['group.query'])) {
734
            $groupQueries = $this->queryParameters['group.query'];
735
        }
736
737
        return $groupQueries;
738
    }
739
740
    /**
741
     * Sets the maximum number of results to be returned per group.
742
     *
743
     * @param int $numberOfResults Maximum number of results per group to return
744
     */
745
    public function setNumberOfResultsPerGroup($numberOfResults)
746
    {
747
        $numberOfResults = max(intval($numberOfResults), 0);
748
749
        $this->queryParameters['group.limit'] = $numberOfResults;
750
    }
751
752
    // filter
753
754
    /**
755
     * Gets the maximum number of results to be returned per group.
756
     *
757
     * @return int Maximum number of results per group to return
758
     */
759
    public function getNumberOfResultsPerGroup()
760
    {
761
        // default if nothing else set is 1, @see http://wiki.apache.org/solr/FieldCollapsing
762
        $numberOfResultsPerGroup = 1;
763
764
        if (!empty($this->queryParameters['group.limit'])) {
765
            $numberOfResultsPerGroup = $this->queryParameters['group.limit'];
766
        }
767
768
        return $numberOfResultsPerGroup;
769
    }
770
771
    /**
772
     * Activates and deactivates faceting for the current query.
773
     *
774
     * @param bool $faceting TRUE to enable faceting, FALSE to disable faceting
775
     * @return void
776
     */
777 31
    public function setFaceting($faceting = true)
778
    {
779 31
        if ($faceting) {
780 31
            $this->queryParameters['facet'] = 'true';
781 31
            $this->queryParameters['facet.mincount'] = $this->solrConfiguration->getSearchFacetingMinimumCount();
782
783 31
            $this->applyConfiguredFacetSorting();
784 31
        } else {
785
            // @todo can be moved to method removeFacetingArgumentsFromQueryParameters
786 1
            foreach ($this->queryParameters as $key => $value) {
787
                // remove all facet.* settings
788 1
                if (GeneralUtility::isFirstPartOfStr($key, 'facet')) {
789 1
                    unset($this->queryParameters[$key]);
790 1
                }
791
792
                // remove all f.*.facet.* settings (overrides for individual fields)
793 1
                if (GeneralUtility::isFirstPartOfStr($key, 'f.') && strpos($key,
794 1
                        '.facet.') !== false
795 1
                ) {
796 1
                    unset($this->queryParameters[$key]);
797 1
                }
798 1
            }
799
        }
800 31
    }
801
802
    /**
803
     * Reads the facet sorting configuration and applies it to the queryParameters.
804
     *
805
     * @return void
806
     */
807 31
    protected function applyConfiguredFacetSorting()
808
    {
809 31
        $sorting = $this->solrConfiguration->getSearchFacetingSortBy();
810 31
        if (!GeneralUtility::inList('count,index,alpha,lex,1,0,true,false', $sorting)) {
811
            // when the sorting is not in the list of valid values we do not apply it.
812 10
            return;
813
        }
814
815
        // alpha and lex alias for index
816 21
        if ($sorting == 'alpha' || $sorting == 'lex') {
817 1
            $sorting = 'index';
818 1
        }
819
820 21
        $this->queryParameters['facet.sort'] = $sorting;
821 21
    }
822
823
    /**
824
     * Sets facet fields for a query.
825
     *
826
     * @param array $facetFields Array of field names
827
     */
828
    public function setFacetFields(array $facetFields)
829
    {
830
        $this->queryParameters['facet.field'] = array();
831
832
        foreach ($facetFields as $facetField) {
833
            $this->addFacetField($facetField);
834
        }
835
    }
836
837
    /**
838
     * Adds a single facet field.
839
     *
840
     * @param string $facetField field name
841
     */
842
    public function addFacetField($facetField)
843
    {
844
        $this->queryParameters['facet.field'][] = $facetField;
845
    }
846
847
    /**
848
     * Removes a filter on a field
849
     *
850
     * @param string $filterFieldName The field name the filter should be removed for
851
     * @return void
852
     */
853 1
    public function removeFilter($filterFieldName)
854
    {
855 1
        foreach ($this->filters as $key => $filterString) {
856 1
            if (GeneralUtility::isFirstPartOfStr($filterString,
857 1
                $filterFieldName . ':')
858 1
            ) {
859 1
                unset($this->filters[$key]);
860 1
            }
861 1
        }
862 1
    }
863
864
    /**
865
     * Removes a filter based on key of filter array
866
     *
867
     * @param string $key array key
868
     */
869 1
    public function removeFilterByKey($key)
870
    {
871 1
        unset($this->filters[$key]);
872 1
    }
873
874
    /**
875
     * Gets all currently applied filters.
876
     *
877
     * @return array Array of filters
878
     */
879 26
    public function getFilters()
880
    {
881 26
        return $this->filters;
882
    }
883
884
    // sorting
885
886
    /**
887
     * Sets access restrictions for a frontend user.
888
     *
889
     * @param array $groups Array of groups a user has been assigned to
890
     */
891 28
    public function setUserAccessGroups(array $groups)
892
    {
893 28
        $groups = array_map('intval', $groups);
894 28
        $groups[] = 0; // always grant access to public documents
895 28
        $groups = array_unique($groups);
896 28
        sort($groups, SORT_NUMERIC);
897
898 28
        $accessFilter = '{!typo3access}' . implode(',', $groups);
899
900 28
        foreach ($this->filters as $key => $filter) {
901 24
            if (GeneralUtility::isFirstPartOfStr($filter, '{!typo3access}')) {
902 1
                unset($this->filters[$key]);
903 1
            }
904 28
        }
905
906 28
        $this->addFilter($accessFilter);
907 28
    }
908
909
    /**
910
     * Adds a filter parameter.
911
     *
912
     * @param string $filterString The filter to add, in the form of field:value
913
     * @return void
914
     */
915 34
    public function addFilter($filterString)
916
    {
917
        // TODO refactor to split filter field and filter value, @see Drupal
918 34
        if ($this->solrConfiguration->getLoggingQueryFilters()) {
919 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...
920 1
        }
921
922 34
        $this->filters[] = $filterString;
923 34
    }
924
925
    // query parameters
926
927
    /**
928
     * Limits the query to certain sites
929
     *
930
     * @param string $allowedSites Comma-separated list of domains
931
     */
932 23
    public function setSiteHashFilter($allowedSites)
933
    {
934 23
        $allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
935 23
        $filters = array();
936
937 23
        foreach ($allowedSites as $site) {
938 23
            $siteHash = Util::getSiteHashForDomain($site);
939
940 23
            $filters[] = 'siteHash:"' . $siteHash . '"';
941 23
        }
942
943 23
        $this->addFilter(implode(' OR ', $filters));
944 23
    }
945
946
    /**
947
     * Limits the query to certain page tree branches
948
     *
949
     * @param string $pageIds Comma-separated list of page IDs
950
     */
951
    public function setRootlineFilter($pageIds)
952
    {
953
        $pageIds = GeneralUtility::trimExplode(',', $pageIds);
954
        $filters = array();
955
956
        $processor = GeneralUtility::makeInstance('ApacheSolrForTypo3\\Solr\\FieldProcessor\\PageUidToHierarchy');
957
        $hierarchies = $processor->process($pageIds);
958
959
        foreach ($hierarchies as $hierarchy) {
960
            $lastLevel = array_pop($hierarchy);
961
            $filters[] = 'rootline:"' . $lastLevel . '"';
962
        }
963
964
        $this->addFilter(implode(' OR ', $filters));
965
    }
966
967
    /**
968
     * Adds a sort field and the sorting direction for that field
969
     *
970
     * @param string $fieldName The field name to sort by
971
     * @param string $direction Either ApacheSolrForTypo3\Solr\Query::SORT_ASC to sort the field ascending or ApacheSolrForTypo3\Solr\Query::SORT_DESC to sort descending
972
     * @return void
973
     * @throws \InvalidArgumentException if the $direction parameter given is neither ApacheSolrForTypo3\Solr\Query::SORT_ASC nor ApacheSolrForTypo3\Solr\Query::SORT_DESC
974
     */
975 1
    public function addSortField($fieldName, $direction)
976
    {
977
        // @todo can be refactored with a guard clause
978
        switch ($direction) {
979 1
            case self::SORT_ASC:
980 1
            case self::SORT_DESC:
981 1
                $this->sortingFields[$fieldName] = $direction;
982 1
                break;
983 1
            default:
984 1
                throw new \InvalidArgumentException(
985 1
                    'Invalid sort direction "' . $direction . '"',
986
                    1235051723
987 1
                );
988 1
        }
989 1
    }
990
991
    /**
992
     * Gets the currently set sorting fields and their sorting directions
993
     *
994
     * @return array An associative array with the field names as key and their sorting direction as value
995
     */
996 1
    public function getSortingFields()
997
    {
998 1
        return $this->sortingFields;
999
    }
1000
1001
    /**
1002
     * Gets the list of fields a query will return.
1003
     *
1004
     * @return array Array of field names the query will return
1005
     */
1006 6
    public function getFieldList()
1007
    {
1008 6
        return $this->fieldList;
1009
    }
1010
1011
    /**
1012
     * Sets the fields to return by a query.
1013
     *
1014
     * @param array|string $fieldList an array or comma-separated list of field names
1015
     * @throws \UnexpectedValueException on parameters other than comma-separated lists and arrays
1016
     */
1017 2
    public function setFieldList($fieldList = array('*', 'score'))
1018
    {
1019 2
        if (is_string($fieldList)) {
1020 1
            $fieldList = GeneralUtility::trimExplode(',', $fieldList);
1021 1
        }
1022
1023 2
        if (!is_array($fieldList) || empty($fieldList)) {
1024 1
            throw new \UnexpectedValueException(
1025 1
                'Field list must be a comma-separated list or array and must not be empty.',
1026
                1310740308
1027 1
            );
1028
        }
1029
1030 2
        $this->fieldList = $fieldList;
1031 2
    }
1032
1033
    /**
1034
     * Gets the query type, Solr's qt parameter.
1035
     *
1036
     * @return string Query type, qt parameter.
1037
     */
1038 1
    public function getQueryType()
1039
    {
1040 1
        return $this->queryParameters['qt'];
1041
    }
1042
1043
    /**
1044
     * Sets the query type, Solr's qt parameter.
1045
     *
1046
     * @param string|bool $queryType String query type or boolean FALSE to disable / reset the qt parameter.
1047
     * @see http://wiki.apache.org/solr/CoreQueryParameters#qt
1048
     */
1049 1
    public function setQueryType($queryType)
1050
    {
1051 1
        if ($queryType) {
1052 1
            $this->queryParameters['qt'] = $queryType;
1053 1
        } else {
1054 1
            unset($this->queryParameters['qt']);
1055
        }
1056 1
    }
1057
1058
    /**
1059
     * Sets the query operator to AND or OR. Unsets the query operator (actually
1060
     * sets it back to default) for FALSE.
1061
     *
1062
     * @param string|bool $operator AND or OR, FALSE to unset
1063
     */
1064 1
    public function setOperator($operator)
1065
    {
1066 1
        if (in_array($operator, array(self::OPERATOR_AND, self::OPERATOR_OR))) {
1067 1
            $this->queryParameters['q.op'] = $operator;
1068 1
        }
1069
1070 1
        if ($operator === false) {
1071 1
            unset($this->queryParameters['q.op']);
1072 1
        }
1073 1
    }
1074
1075
    /**
1076
     * Gets the alternative query, Solr's q.alt parameter.
1077
     *
1078
     * @return string Alternative query, q.alt parameter.
1079
     */
1080 1
    public function getAlternativeQuery()
1081
    {
1082 1
        return $this->queryParameters['q.alt'];
1083
    }
1084
1085
    /**
1086
     * Sets an alternative query, Solr's q.alt parameter.
1087
     *
1088
     * This query supports the complete Lucene Query Language.
1089
     *
1090
     * @param mixed $alternativeQuery String alternative query or boolean FALSE to disable / reset the q.alt parameter.
1091
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#q.alt
1092
     */
1093 25
    public function setAlternativeQuery($alternativeQuery)
1094
    {
1095 25
        if ($alternativeQuery) {
1096 25
            $this->queryParameters['q.alt'] = $alternativeQuery;
1097 25
        } else {
1098 1
            unset($this->queryParameters['q.alt']);
1099
        }
1100 25
    }
1101
1102
    // keywords
1103
1104
    /**
1105
     * Set the query to omit the response header
1106
     *
1107
     * @param bool $omitHeader TRUE (default) to omit response headers, FALSE to re-enable
1108
     */
1109 1
    public function setOmitHeader($omitHeader = true)
1110
    {
1111 1
        if ($omitHeader) {
1112 1
            $this->queryParameters['omitHeader'] = 'true';
1113 1
        } else {
1114 1
            unset($this->queryParameters['omitHeader']);
1115
        }
1116 1
    }
1117
1118
    /**
1119
     * Get the query keywords, keywords are escaped.
1120
     *
1121
     * @return string query keywords
1122
     */
1123 20
    public function getKeywords()
1124
    {
1125 20
        return $this->keywords;
1126
    }
1127
1128
    /**
1129
     * Sets the query keywords, escapes them as needed for Solr/Lucene.
1130
     *
1131
     * @param string $keywords user search terms/keywords
1132
     */
1133 114
    public function setKeywords($keywords)
1134
    {
1135 114
        $this->keywords = $this->escape($keywords);
1136 114
        $this->keywordsRaw = $keywords;
1137 114
    }
1138
1139
    /**
1140
     * Gets the cleaned keywords so that it can be used in templates f.e.
1141
     *
1142
     * @return string The cleaned keywords.
1143
     */
1144 18
    public function getKeywordsCleaned()
1145
    {
1146 18
        return $this->cleanKeywords($this->keywordsRaw);
1147
    }
1148
1149
    /**
1150
     * Helper method to escape/encode keywords for use in HTML
1151
     *
1152
     * @param string $keywords Keywords to prepare for use in HTML
1153
     * @return string Encoded keywords
1154
     */
1155 22
    public static function cleanKeywords($keywords)
1156
    {
1157 22
        $keywords = trim($keywords);
1158 22
        $keywords = GeneralUtility::removeXSS($keywords);
1159 22
        $keywords = htmlentities($keywords, ENT_QUOTES,
1160 22
            $GLOBALS['TSFE']->metaCharset);
1161
1162
        // escape triple hashes as they are used in the template engine
1163
        // TODO remove after switching to fluid templates
1164 22
        $keywords = Template::escapeMarkers($keywords);
1165
1166 22
        return $keywords;
1167
    }
1168
1169
    // relevance, matching
1170
1171
    /**
1172
     * Gets the raw, unescaped, unencoded keywords.
1173
     *
1174
     * USE WITH CAUTION!
1175
     *
1176
     * @return string raw keywords
1177
     */
1178 19
    public function getKeywordsRaw()
1179
    {
1180 19
        return $this->keywordsRaw;
1181
    }
1182
1183
    /**
1184
     * Sets the minimum match (mm) parameter
1185
     *
1186
     * @param mixed $minimumMatch Minimum match parameter as string or boolean FALSE to disable / reset the mm parameter
1187
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#mm_.28Minimum_.27Should.27_Match.29
1188
     */
1189 1
    public function setMinimumMatch($minimumMatch)
1190
    {
1191 1
        if (is_string($minimumMatch) && !empty($minimumMatch)) {
1192 1
            $this->queryParameters['mm'] = $minimumMatch;
1193 1
        } else {
1194 1
            unset($this->queryParameters['mm']);
1195
        }
1196 1
    }
1197
1198
    /**
1199
     * Sets the boost function (bf) parameter
1200
     *
1201
     * @param mixed $boostFunction boost function parameter as string or boolean FALSE to disable / reset the bf parameter
1202
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#bf_.28Boost_Functions.29
1203
     */
1204 1
    public function setBoostFunction($boostFunction)
1205
    {
1206 1
        if (is_string($boostFunction) && !empty($boostFunction)) {
1207 1
            $this->queryParameters['bf'] = $boostFunction;
1208 1
        } else {
1209 1
            unset($this->queryParameters['bf']);
1210
        }
1211 1
    }
1212
1213
    // query fields
1214
    // TODO move up to field list methods
1215
1216
    /**
1217
     * Sets the boost query (bq) parameter
1218
     *
1219
     * @param mixed $boostQuery boost query parameter as string or array to set a boost query or boolean FALSE to disable / reset the bq parameter
1220
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#bq_.28Boost_Query.29
1221
     */
1222 1
    public function setBoostQuery($boostQuery)
1223
    {
1224 1
        if ((is_string($boostQuery) || is_array($boostQuery)) && !empty($boostQuery)) {
1225 1
            $this->queryParameters['bq'] = $boostQuery;
1226 1
        } else {
1227 1
            unset($this->queryParameters['bq']);
1228
        }
1229 1
    }
1230
1231
    /**
1232
     * Gets a specific query parameter by its name.
1233
     *
1234
     * @param string $parameterName The parameter to return
1235
     * @return string The parameter's value or NULL if not set
1236
     */
1237 4
    public function getQueryParameter($parameterName)
1238
    {
1239 4
        $requestedParameter = null;
1240 4
        $parameters = $this->getQueryParameters();
1241
1242 4
        if (isset($parameters[$parameterName])) {
1243 4
            $requestedParameter = $parameters[$parameterName];
1244 4
        }
1245
1246 4
        return $requestedParameter;
1247
    }
1248
1249
    /**
1250
     * Builds an array of query parameters to use for the search query.
1251
     *
1252
     * @return array An array ready to use with query parameters
1253
     */
1254 56
    public function getQueryParameters()
1255
    {
1256 56
        $queryParameters = array_merge(
1257
            array(
1258 56
                'fl' => implode(',', $this->fieldList),
1259 56
                'fq' => array_values($this->filters)
1260 56
            ),
1261 56
            $this->queryParameters
1262 56
        );
1263
1264 56
        $queryFieldString = $this->getQueryFieldsAsString();
1265 56
        if (!empty($queryFieldString)) {
1266 25
            $queryParameters['qf'] = $queryFieldString;
1267 25
        }
1268
1269 56
        return $queryParameters;
1270
    }
1271
1272
    // general query parameters
1273
1274
    /**
1275
     * Compiles the query fields into a string to be used in Solr's qf parameter.
1276
     *
1277
     * @return string A string of query fields with their associated boosts
1278
     */
1279 58
    public function getQueryFieldsAsString()
1280
    {
1281 58
        $queryFieldString = '';
1282
1283 58
        foreach ($this->queryFields as $fieldName => $fieldBoost) {
1284 26
            $queryFieldString .= $fieldName;
1285
1286 26
            if ($fieldBoost != 1.0) {
1287 26
                $queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
1288 26
            }
1289
1290 26
            $queryFieldString .= ' ';
1291 58
        }
1292
1293 58
        return trim($queryFieldString);
1294
    }
1295
1296
    /**
1297
     * Enables or disables highlighting of search terms in result teasers.
1298
     *
1299
     * @param bool $highlighting Enables highlighting when set to TRUE, deactivates highlighting when set to FALSE, defaults to TRUE.
1300
     * @param int $fragmentSize Size, in characters, of fragments to consider for highlighting.
1301
     * @see http://wiki.apache.org/solr/HighlightingParameters
1302
     * @return void
1303
     */
1304 30
    public function setHighlighting($highlighting = true, $fragmentSize = 200)
1305
    {
1306 30
        if ($highlighting) {
1307 30
            $this->queryParameters['hl'] = 'true';
1308 30
            $this->queryParameters['hl.fragsize'] = (int)$fragmentSize;
1309
1310 30
            $highlightingFields = $this->solrConfiguration->getSearchResultsHighlightingFields();
1311 30
            if ($highlightingFields != '') {
1312 24
                $this->queryParameters['hl.fl'] = $highlightingFields;
1313 24
            }
1314
1315
            // the fast vector highlighter can only be used, when the fragmentSize is
1316
            // higher then 17 otherwise solr throws an exception
1317 30
            $useFastVectorHighlighter = ($fragmentSize >= 18);
1318 30
            $wrap = explode('|', $this->solrConfiguration->getSearchResultsHighlightingWrap());
1319
1320 30
            if ($useFastVectorHighlighter) {
1321 28
                $this->queryParameters['hl.useFastVectorHighlighter'] = 'true';
1322 28
                $this->queryParameters['hl.tag.pre'] = $wrap[0];
1323 28
                $this->queryParameters['hl.tag.post'] = $wrap[1];
1324 28
            }
1325
1326 30
            if (isset($wrap[0]) && isset($wrap[1])) {
1327 25
                $this->queryParameters['hl.simple.pre'] = $wrap[0];
1328 25
                $this->queryParameters['hl.simple.post'] = $wrap[1];
1329 25
            }
1330 30
        } else {
1331
            // remove all hl.* settings
1332 1
            foreach ($this->queryParameters as $key => $value) {
1333 1
                if (GeneralUtility::isFirstPartOfStr($key, 'hl')) {
1334 1
                    unset($this->queryParameters[$key]);
1335 1
                }
1336 1
            }
1337
        }
1338 30
    }
1339
1340
    // misc
1341
1342
    /**
1343
     * Enables or disables spellchecking for the query.
1344
     *
1345
     * @param bool $spellchecking Enables spellchecking when set to TRUE, deactivates spellchecking when set to FALSE, defaults to TRUE.
1346
     */
1347 22
    public function setSpellchecking($spellchecking = true)
1348
    {
1349 22
        if ($spellchecking) {
1350 22
            $this->queryParameters['spellcheck'] = 'true';
1351 22
            $this->queryParameters['spellcheck.collate'] = 'true';
1352 22
            $maxCollationTries = $this->solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
1353 22
            $this->addQueryParameter('spellcheck.maxCollationTries', $maxCollationTries);
1354 22
        } else {
1355 1
            unset($this->queryParameters['spellcheck']);
1356 1
            unset($this->queryParameters['spellcheck.collate']);
1357 1
            unset($this->queryParameters['spellcheck.maxCollationTries']);
1358
        }
1359 22
    }
1360
1361
    /**
1362
     * Adds any generic query parameter.
1363
     *
1364
     * @param string $parameterName Query parameter name
1365
     * @param string $parameterValue Parameter value
1366
     */
1367 30
    public function addQueryParameter($parameterName, $parameterValue)
1368
    {
1369 30
        $this->queryParameters[$parameterName] = $parameterValue;
1370 30
    }
1371
1372
    /**
1373
     * Sets the sort parameter.
1374
     *
1375
     * $sorting must include a field name (or the pseudo-field score),
1376
     * followed by a space,
1377
     * followed by a sort direction (asc or desc).
1378
     *
1379
     * Multiple fallback sortings can be separated by comma,
1380
     * ie: <field name> <direction>[,<field name> <direction>]...
1381
     *
1382
     * @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)
1383
     * @see http://wiki.apache.org/solr/CommonQueryParameters#sort
1384
     */
1385 2
    public function setSorting($sorting)
1386
    {
1387 2
        if ($sorting) {
1388
            // @todo can be extracted to method "removeRelevanceSortField"
1389 2
            $sortParameter = $sorting;
1390 2
            list($sortField) = explode(' ', $sorting);
1391 2
            if ($sortField == 'relevance') {
1392 1
                $sortParameter = '';
1393 1
            }
1394
1395 2
            $this->queryParameters['sort'] = $sortParameter;
1396 2
        } else {
1397 1
            unset($this->queryParameters['sort']);
1398
        }
1399 2
    }
1400
1401
    /**
1402
     * Enables or disables the debug parameter for the query.
1403
     *
1404
     * @param bool $debugMode Enables debugging when set to TRUE, deactivates debugging when set to FALSE, defaults to TRUE.
1405
     */
1406 20
    public function setDebugMode($debugMode = true)
1407
    {
1408 20
        if ($debugMode) {
1409 20
            $this->queryParameters['debugQuery'] = 'true';
1410 20
            $this->queryParameters['echoParams'] = 'all';
1411 20
        } else {
1412
            unset($this->queryParameters['debugQuery']);
1413
            unset($this->queryParameters['echoParams']);
1414
        }
1415 20
    }
1416
1417
    /**
1418
     * Returns the link target page id.
1419
     *
1420
     * @return int
1421
     */
1422 2
    public function getLinkTargetPageId()
1423
    {
1424 2
        return $this->linkTargetPageId;
1425
    }
1426
1427
    /**
1428
     * Activates the collapsing on the configured field, if collapsing was enabled.
1429
     *
1430
     * @return bool
1431
     */
1432 113
    protected function initializeCollapsingFromConfiguration()
1433
    {
1434
        // check collapsing
1435 113
        if ($this->solrConfiguration->getSearchVariants()) {
1436 2
            $collapseField = $this->solrConfiguration->getSearchVariantsField();
1437 2
            $this->setVariantField($collapseField);
1438 2
            $this->setCollapsing(true);
1439
1440 2
            return true;
1441
        }
1442
1443 111
        return false;
1444
    }
1445
}
1446