Passed
Push — hans/searchfixes ( 49661c...95bb44 )
by Simon
05:16
created

QueryComponentFactory::getBuildTerm()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 = $this->getBuildTerm($search);
233 6
            $postfix = $this->isFuzzy($search);
234
            // We can add the same term multiple times with different boosts
235
            // Not ideal, but it might happen, so let's add the term itself only once
236 6
            if (!in_array($term, $this->queryArray, true)) {
237 5
                $this->queryArray[] = $term . $postfix;
238
            }
239
            // If boosting is set, add the fields to boost
240 6
            if ($search['boost'] > 1) {
241 6
                $boostTerms = $this->buildQueryBoost($search, $term, $boostTerms);
242
            }
243
        }
244
        // Clean up the boost terms, remove doubles
245 9
        $this->setBoostTerms(array_values(array_unique($boostTerms)));
246 9
    }
247
248
    /**
249
     * Escape the search query
250
     *
251
     * @param string $searchTerm
252
     * @return string
253
     */
254 7
    public function escapeSearch($searchTerm): string
255
    {
256 7
        $term = [];
257
        // Escape special characters where needed. Except for quoted parts, those should be phrased
258 7
        preg_match_all('/"[^"]*"|\S+/', $searchTerm, $parts);
259 7
        foreach ($parts[0] as $part) {
260
            // As we split the parts, everything with two quotes is a phrase
261
            // We need however, to strip out double quoting
262 7
            if (substr_count($part, '"') === 2) {
263
                // Strip all double quotes out for the phrase.
264
                // @todo make this less clunky
265
                // @todo add useful tests for this
266 1
                $part = str_replace('"', '', $part);
267 1
                $term[] = $this->helper->escapePhrase($part);
268
            } else {
269 7
                $term[] = $this->helper->escapeTerm($part);
270
            }
271
        }
272
273 7
        return implode(' ', $term);
274
    }
275
276
    /**
277
     * If the search is fuzzy, add fuzzyness
278
     *
279
     * @param $search
280
     * @return string
281
     */
282 6
    protected function isFuzzy($search): string
283
    {
284
        // When doing fuzzy search, postfix, otherwise, don't
285 6
        if ($search['fuzzy']) {
286 1
            return '~' . (is_numeric($search['fuzzy']) ? $search['fuzzy'] : '');
287
        }
288
289 6
        return '';
290
    }
291
292
    /**
293
     * Add spellcheck elements
294
     */
295 9
    protected function buildSpellcheck(): void
296
    {
297
        // Assuming the first term is the term entered
298 9
        $queryString = implode(' ', $this->queryArray);
299
        // Arbitrarily limit to 5 if the config isn't set
300 9
        $count = BaseIndex::config()->get('spellcheckCount') ?: 5;
301 9
        $spellcheck = $this->clientQuery->getSpellcheck();
302 9
        $spellcheck->setQuery($queryString);
303 9
        $spellcheck->setCount($count);
304 9
        $spellcheck->setBuild(true);
305 9
        $spellcheck->setCollate(true);
306 9
        $spellcheck->setExtendedResults(true);
307 9
        $spellcheck->setCollateExtendedResults(true);
308 9
    }
309
310
    /**
311
     * Get the escaped search string, or, if empty, a global search
312
     *
313
     * @param array $search
314
     * @return string
315
     */
316 6
    protected function getBuildTerm($search)
317
    {
318 6
        $term = $search['text'];
319 6
        $term = $this->escapeSearch($term);
320 6
        if ($term === '') {
321 1
            $term = '*:*';
322
        }
323
324 6
        return $term;
325
    }
326
}
327