Passed
Push — hans/searchfixes ( 0ac753 )
by Simon
05:11
created

QueryComponentFactory   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 284
Duplicated Lines 0 %

Test Coverage

Coverage 98.68%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 82
c 3
b 0
f 0
dl 0
loc 284
ccs 75
cts 76
cp 0.9868
rs 10
wmc 26

15 Methods

Rating   Name   Duplication   Size   Complexity  
A isFuzzy() 0 8 3
A buildSpellcheck() 0 13 2
A getHelper() 0 3 1
A setClientQuery() 0 5 1
A getClientQuery() 0 3 1
A setQuery() 0 5 1
A setIndex() 0 5 1
A getQueryArray() 0 3 1
A getIndex() 0 3 1
A setQueryArray() 0 5 1
A getQuery() 0 3 1
A setHelper() 0 5 1
A buildQuery() 0 24 3
A buildTerms() 0 24 5
A escapeSearch() 0 20 3
1
<?php
2
/**
3
 * class QueryComponentFactory|Firesphere\SolrSearch\Factories\QueryComponentFactory Build a Query component
4
 *
5
 * @package Firesphere\SolrSearch\Factories
6
 * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo
7
 * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy
8
 */
9
10
namespace Firesphere\SolrSearch\Factories;
11
12
use Firesphere\SolrSearch\Indexes\BaseIndex;
13
use Firesphere\SolrSearch\Queries\BaseQuery;
14
use Firesphere\SolrSearch\Services\SolrCoreService;
15
use Firesphere\SolrSearch\Traits\QueryComponentBoostTrait;
16
use Firesphere\SolrSearch\Traits\QueryComponentFacetTrait;
17
use Firesphere\SolrSearch\Traits\QueryComponentFilterTrait;
18
use Solarium\Core\Query\Helper;
19
use Solarium\QueryType\Select\Query\Query;
20
21
/**
22
 * Class QueryComponentFactory
23
 *
24
 * Build a query component for each available build part
25
 *
26
 * @package Firesphere\SolrSearch\Factories
27
 */
28
class QueryComponentFactory
29
{
30
    use QueryComponentFilterTrait;
31
    use QueryComponentBoostTrait;
32
    use QueryComponentFacetTrait;
33
34
    /**
35
     * Default fields that should always be added
36
     *
37
     * @var array
38
     */
39
    const DEFAULT_FIELDS = [
40
        SolrCoreService::ID_FIELD,
41
        SolrCoreService::CLASS_ID_FIELD,
42
        SolrCoreService::CLASSNAME,
43
    ];
44
45
    /**
46
     * @var array Build methods to run
47
     */
48
    protected static $builds = [
49
        'Terms',
50
        'ViewFilter',
51
        'ClassFilter',
52
        'Filters',
53
        'Excludes',
54
        'Facets',
55
        'FacetQuery',
56
        'Spellcheck',
57
    ];
58
    /**
59
     * @var BaseQuery BaseQuery that needs to be executed
60
     */
61
    protected $query;
62
    /**
63
     * @var Helper Helper to escape the query terms properly
64
     */
65
    protected $helper;
66
    /**
67
     * @var array Resulting query parts as an array
68
     */
69
    protected $queryArray = [];
70
    /**
71
     * @var BaseIndex Index to query
72
     */
73
    protected $index;
74
75
    /**
76
     * Build the full query
77
     *
78
     * @return Query
79
     */
80 9
    public function buildQuery(): Query
81
    {
82 9
        foreach (static::$builds as $build) {
83 9
            $method = sprintf('build%s', $build);
84 9
            $this->$method();
85
        }
86
        // Set the start
87 9
        $this->clientQuery->setStart($this->query->getStart());
88
        // Double the rows in case something has been deleted, but not from Solr
89 9
        $this->clientQuery->setRows($this->query->getRows() * 2);
90
        // Add highlighting before adding boosting
91 9
        $this->clientQuery->getHighlighting()->setFields($this->query->getHighlight());
92
        // Add boosting
93 9
        $this->buildBoosts();
94
95
        // Filter out the fields we want to see if they're set
96 9
        $fields = $this->query->getFields();
97 9
        if (count($fields)) {
98
            // We _ALWAYS_ need the ClassName for getting the DataObjects back
99 1
            $fields = array_merge(static::DEFAULT_FIELDS, $fields);
100 1
            $this->clientQuery->setFields($fields);
101
        }
102
103 9
        return $this->clientQuery;
104
    }
105
106
    /**
107
     * Get the base query
108
     *
109
     * @return BaseQuery
110
     */
111 1
    public function getQuery(): BaseQuery
112
    {
113 1
        return $this->query;
114
    }
115
116
    /**
117
     * Set the base query
118
     *
119
     * @param BaseQuery $query
120
     * @return self
121
     */
122 9
    public function setQuery(BaseQuery $query): self
123
    {
124 9
        $this->query = $query;
125
126 9
        return $this;
127
    }
128
129
    /**
130
     * Get the array of terms used to query Solr
131
     *
132
     * @return array
133
     */
134 9
    public function getQueryArray(): array
135
    {
136 9
        return array_merge($this->queryArray, $this->boostTerms);
137
    }
138
139
    /**
140
     * Set the array of queries that are sent to Solr
141
     *
142
     * @param array $queryArray
143
     * @return self
144
     */
145 1
    public function setQueryArray(array $queryArray): self
146
    {
147 1
        $this->queryArray = $queryArray;
148
149 1
        return $this;
150
    }
151
152
    /**
153
     * Get the client Query components
154
     *
155
     * @return Query
156
     */
157 1
    public function getClientQuery(): Query
158
    {
159 1
        return $this->clientQuery;
160
    }
161
162
    /**
163
     * Set a custom Client Query object
164
     *
165
     * @param Query $clientQuery
166
     * @return self
167
     */
168 9
    public function setClientQuery(Query $clientQuery): self
169
    {
170 9
        $this->clientQuery = $clientQuery;
171
172 9
        return $this;
173
    }
174
175
    /**
176
     * Get the query helper
177
     *
178
     * @return Helper
179
     */
180 1
    public function getHelper(): Helper
181
    {
182 1
        return $this->helper;
183
    }
184
185
    /**
186
     * Set the Helper
187
     *
188
     * @param Helper $helper
189
     * @return self
190
     */
191 8
    public function setHelper(Helper $helper): self
192
    {
193 8
        $this->helper = $helper;
194
195 8
        return $this;
196
    }
197
198
    /**
199
     * Get the BaseIndex
200
     *
201
     * @return BaseIndex
202
     */
203 2
    public function getIndex(): BaseIndex
204
    {
205 2
        return $this->index;
206
    }
207
208
    /**
209
     * Set a BaseIndex
210
     *
211
     * @param BaseIndex $index
212
     * @return self
213
     */
214 11
    public function setIndex(BaseIndex $index): self
215
    {
216 11
        $this->index = $index;
217
218 11
        return $this;
219
    }
220
221
    /**
222
     * Build the terms and boost terms
223
     *
224
     * @return void
225
     */
226 9
    protected function buildTerms(): void
227
    {
228 9
        $terms = $this->query->getTerms();
229 9
        $boostTerms = $this->getBoostTerms();
230
231 9
        foreach ($terms as $search) {
232 6
            $term = $search['text'];
233 6
            $term = $this->escapeSearch($term);
234 6
            if ($term === '') {
235
                $term = '*:*';
236
            }
237 6
            $postfix = $this->isFuzzy($search);
238
            // We can add the same term multiple times with different boosts
239
            // Not ideal, but it might happen, so let's add the term itself only once
240 6
            if (!in_array($term, $this->queryArray, true)) {
241 5
                $this->queryArray[] = $term . $postfix;
242
            }
243
            // If boosting is set, add the fields to boost
244 6
            if ($search['boost'] > 1) {
245 6
                $boostTerms = $this->buildQueryBoost($search, $term, $boostTerms);
246
            }
247
        }
248
        // Clean up the boost terms, remove doubles
249 9
        $this->setBoostTerms(array_values(array_unique($boostTerms)));
250 9
    }
251
252
    /**
253
     * Escape the search query
254
     *
255
     * @param string $searchTerm
256
     * @return string
257
     */
258 7
    public function escapeSearch($searchTerm): string
259
    {
260 7
        $term = [];
261
        // Escape special characters where needed. Except for quoted parts, those should be phrased
262 7
        preg_match_all('/"[^"]*"|\S+/', $searchTerm, $parts);
263 7
        foreach ($parts[0] as $part) {
264
            // As we split the parts, everything with two quotes is a phrase
265
            // We need however, to strip out double quoting
266 7
            if (substr_count($part, '"') === 2) {
267
                // Strip all double quotes out for the phrase.
268
                // @todo make this less clunky
269
                // @todo add useful tests for this
270 1
                $part = str_replace('"', '', $part);
271 1
                $term[] = $this->helper->escapePhrase($part);
272
            } else {
273 7
                $term[] = $this->helper->escapeTerm($part);
274
            }
275
        }
276
277 7
        return implode(' ', $term);
278
    }
279
280
    /**
281
     * If the search is fuzzy, add fuzzyness
282
     *
283
     * @param $search
284
     * @return string
285
     */
286 6
    protected function isFuzzy($search): string
287
    {
288
        // When doing fuzzy search, postfix, otherwise, don't
289 6
        if ($search['fuzzy']) {
290 1
            return '~' . (is_numeric($search['fuzzy']) ? $search['fuzzy'] : '');
291
        }
292
293 6
        return '';
294
    }
295
296
    /**
297
     * Add spellcheck elements
298
     */
299 9
    protected function buildSpellcheck(): void
300
    {
301
        // Assuming the first term is the term entered
302 9
        $queryString = implode(' ', $this->queryArray);
303
        // Arbitrarily limit to 5 if the config isn't set
304 9
        $count = BaseIndex::config()->get('spellcheckCount') ?: 5;
305 9
        $spellcheck = $this->clientQuery->getSpellcheck();
306 9
        $spellcheck->setQuery($queryString);
307 9
        $spellcheck->setCount($count);
308 9
        $spellcheck->setBuild(true);
309 9
        $spellcheck->setCollate(true);
310 9
        $spellcheck->setExtendedResults(true);
311 9
        $spellcheck->setCollateExtendedResults(true);
312 9
    }
313
}
314