Passed
Push — Cleanup-attempt ( b2d59e...e3e217 )
by Simon
01:59
created

QueryComponentFactory::setQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Factories;
5
6
use Firesphere\SolrSearch\Indexes\BaseIndex;
7
use Firesphere\SolrSearch\Queries\BaseQuery;
8
use Minimalcode\Search\Criteria;
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Security\Security;
11
use Solarium\Core\Query\Helper;
12
use Solarium\QueryType\Select\Query\Query;
13
14
/**
15
 * Class QueryComponentFactory
16
 * @package Firesphere\SolrSearch\Factories
17
 */
18
class QueryComponentFactory
19
{
20
    /**
21
     * @var BaseQuery
22
     */
23
    protected $query;
24
25
    /**
26
     * @var Query
27
     */
28
    protected $clientQuery;
29
30
    /**
31
     * @var Helper
32
     */
33
    protected $helper;
34
35
    /**
36
     * @var array
37
     */
38
    protected $boostTerms = [];
39
40
    /**
41
     * @var array
42
     */
43
    protected $queryArray = [];
44
45
    /**
46
     * @var BaseIndex
47
     */
48
    protected $index;
49
50
    /**
51
     * Build the full query
52
     * @return Query
53
     */
54 4
    public function buildQuery()
55
    {
56 4
        $this->buildTerms();
57 4
        $this->buildViewFilter();
58
        // Build class filtering
59 4
        $this->buildClassFilter();
60
        // Add filters
61 4
        $this->buildFilters();
62
        // And excludes
63 4
        $this->buildExcludes();
64
        // Setup the facets
65 4
        $this->buildFacets();
66
        // Build the facet filters
67 4
        $this->buildFacetQuery();
68
        // Add spellchecking
69 4
        $this->buildSpellcheck();
70
        // Set the start
71 4
        $this->clientQuery->setStart($this->query->getStart());
72
        // Double the rows in case something has been deleted, but not from Solr
73 4
        $this->clientQuery->setRows($this->query->getRows() * 2);
74
        // Add highlighting before adding boosting
75 4
        $this->clientQuery->getHighlighting()->setFields($this->query->getHighlight());
76
        // Add boosting
77 4
        $this->buildBoosts();
78
79
        // Filter out the fields we want to see if they're set
80 4
        if (count($this->query->getFields())) {
81 1
            $this->clientQuery->setFields($this->query->getFields());
82
        }
83
84 4
        return $this->clientQuery;
85
    }
86
87
88
    /**
89
     * @return void
90
     */
91 4
    protected function buildTerms(): void
92
    {
93 4
        $terms = $this->query->getTerms();
94
95 4
        $boostTerms = $this->getBoostTerms();
96
97 4
        foreach ($terms as $search) {
98 3
            $term = $search['text'];
99 3
            $term = $this->escapeSearch($term, $this->helper);
100 3
            $postfix = $this->isFuzzy($search);
101
            // We can add the same term multiple times with different boosts
102
            // Not ideal, but it might happen, so let's add the term itself only once
103 3
            if (!in_array($term, $this->queryArray, true)) {
104 2
                $this->queryArray[] = $term . $postfix;
105
            }
106
            // If boosting is set, add the fields to boost
107 3
            if ($search['boost'] > 1) {
108 1
                $boost = $this->buildQueryBoost($search, $term, $boostTerms);
109 3
                $this->boostTerms = array_merge($boostTerms, $boost);
110
            }
111
        }
112 4
    }
113
114
    /**
115
     * @param string $searchTerm
116
     * @param Helper $helper
117
     * @return string
118
     */
119 4
    public function escapeSearch($searchTerm, Helper $helper): string
120
    {
121 4
        $term = [];
122
        // Escape special characters where needed. Except for quoted parts, those should be phrased
123 4
        preg_match_all('/"[^"]*"|\S+/', $searchTerm, $parts);
124 4
        foreach ($parts[0] as $part) {
125
            // As we split the parts, everything with two quotes is a phrase
126 4
            if (substr_count($part, '"') === 2) {
127 1
                $term[] = $helper->escapePhrase($part);
128
            } else {
129 4
                $term[] = $helper->escapeTerm($part);
130
            }
131
        }
132
133 4
        return implode(' ', $term);
134
    }
135
136
    /**
137
     * @param $search
138
     * @return string
139
     */
140 3
    protected function isFuzzy($search): string
141
    {
142 3
        $postfix = ''; // When doing fuzzy search, postfix, otherwise, don't
143 3
        if ($search['fuzzy']) {
144 1
            $postfix = '~';
145 1
            if (is_numeric($search['fuzzy'])) {
146 1
                $postfix .= $search['fuzzy'];
147
            }
148
        }
149
150 3
        return $postfix;
151
    }
152
153
    /**
154
     * Set boosting at Query time
155
     *
156
     * @param array $search
157
     * @param string $term
158
     * @param array $searchQuery
159
     * @return array
160
     */
161 1
    protected function buildQueryBoost($search, string $term, array $searchQuery): array
162
    {
163 1
        foreach ($search['fields'] as $boostField) {
164 1
            $boostField = str_replace('.', '_', $boostField);
165 1
            $criteria = Criteria::where($boostField)
166 1
                ->is($term)
167 1
                ->boost($search['boost']);
168 1
            $searchQuery[] = $criteria->getQuery();
169
        }
170
171 1
        return $searchQuery;
172
    }
173
174
    /**
175
     *
176
     */
177 4
    protected function buildViewFilter(): void
178
    {
179
        // Filter by what the user is allowed to see
180 4
        $viewIDs = ['1-null']; // null is always an option as that means publicly visible
181 4
        $currentUser = Security::getCurrentUser();
182 4
        if ($currentUser && $currentUser->exists()) {
183 4
            $viewIDs[] = '1-' . $currentUser->ID;
184
        }
185
        /** Add canView criteria. These are based on {@link DataObjectExtension::ViewStatus()} */
186 4
        $query = Criteria::where('ViewStatus')->in($viewIDs);
187
188 4
        $this->clientQuery->createFilterQuery('ViewStatus')
189 4
            ->setQuery($query->getQuery());
190 4
    }
191
192
    /**
193
     * Add filtered queries based on class hierarchy
194
     * We only need the class itself, since the hierarchy will take care of the rest
195
     */
196 4
    protected function buildClassFilter(): void
197
    {
198 4
        if (count($this->query->getClasses())) {
199 1
            $classes = $this->query->getClasses();
200 1
            $criteria = Criteria::where('ClassHierarchy')->in($classes);
201 1
            $this->clientQuery->createFilterQuery('classes')
202 1
                ->setQuery($criteria->getQuery());
203
        }
204 4
    }
205
206
    /**
207
     *
208
     */
209 4
    protected function buildFilters(): void
210
    {
211 4
        $filters = $this->query->getFilter();
212 4
        foreach ($filters as $field => $value) {
213 1
            $value = is_array($value) ? $value : [$value];
214 1
            $criteria = Criteria::where($field)->in($value);
215 1
            $this->clientQuery->createFilterQuery('filter-' . $field)
216 1
                ->setQuery($criteria->getQuery());
217
        }
218 4
    }
219
220
    /**
221
     *
222
     */
223 4
    protected function buildExcludes(): void
224
    {
225 4
        $filters = $this->query->getExclude();
226 4
        foreach ($filters as $field => $value) {
227 1
            $value = is_array($value) ? $value : [$value];
228 1
            $criteria = Criteria::where($field)
229 1
                ->in($value)
230 1
                ->not();
231 1
            $this->clientQuery->createFilterQuery('exclude-' . $field)
232 1
                ->setQuery($criteria->getQuery());
233
        }
234 4
    }
235
236
    /**
237
     *
238
     */
239 4
    protected function buildFacets(): void
240
    {
241 4
        $facets = $this->clientQuery->getFacetSet();
242
        // Facets should be set from the index configuration
243 4
        foreach ($this->index->getFacetFields() as $class => $config) {
244
            $facets->createFacetField('facet-' . $config['Title'])->setField($config['Field']);
245
        }
246
        // Count however, comes from the query
247 4
        $facets->setMinCount($this->query->getFacetsMinCount());
248 4
    }
249
250
    /**
251
     *
252
     */
253 4
    protected function buildFacetQuery()
254
    {
255 4
        $filterFacets = [];
256 4
        if (Controller::has_curr()) {
257 4
            $filterFacets = Controller::curr()->getRequest()->requestVars();
258
        }
259 4
        foreach ($this->index->getFacetFields() as $class => $config) {
260
            if (array_key_exists($config['Title'], $filterFacets)) {
261
                $filter = array_filter($filterFacets[$config['Title']], 'strlen');
262
                if (count($filter)) {
263
                    $criteria = Criteria::where($config['Field'])->in($filter);
264
                    $this->clientQuery
265
                        ->createFilterQuery('facet-' . $config['Title'])
266
                        ->setQuery($criteria->getQuery());
267
                }
268
            }
269
        }
270 4
    }
271
272
    /**
273
     *
274
     */
275 4
    protected function buildSpellcheck(): void
276
    {
277
        // Assuming the first term is the term entered
278 4
        $queryString = implode(' ', $this->queryArray);
279
        // Arbitrarily limit to 5 if the config isn't set
280 4
        $count = BaseIndex::config()->get('spellcheckCount') ?: 5;
281 4
        $spellcheck = $this->clientQuery->getSpellcheck();
282 4
        $spellcheck->setQuery($queryString);
283 4
        $spellcheck->setCount($count);
284 4
        $spellcheck->setBuild(true);
285 4
        $spellcheck->setCollate(true);
286 4
        $spellcheck->setExtendedResults(true);
287 4
        $spellcheck->setCollateExtendedResults('true');
288 4
    }
289
290
    /**
291
     * Add the index-time boosting to the query
292
     */
293 4
    protected function buildBoosts(): void
294
    {
295 4
        $boosts = $this->query->getBoostedFields();
296 4
        $queries = $this->getQueryArray();
297 4
        foreach ($boosts as $field => $boost) {
298 1
            foreach ($queries as $term) {
299 1
                $booster = Criteria::where($field)
300 1
                    ->is($term)
301 1
                    ->boost($boost);
302 1
                $this->queryArray[] = $booster->getQuery();
303
            }
304
        }
305 4
    }
306
307
    /**
308
     * @return array
309
     */
310 4
    public function getQueryArray(): array
311
    {
312 4
        return array_merge($this->queryArray, $this->boostTerms);
313
    }
314
315
    /**
316
     * @param array $queryArray
317
     * @return QueryComponentFactory
318
     */
319 1
    public function setQueryArray(array $queryArray): QueryComponentFactory
320
    {
321 1
        $this->queryArray = $queryArray;
322
323 1
        return $this;
324
    }
325
326
    /**
327
     * @return BaseQuery
328
     */
329 1
    public function getQuery(): BaseQuery
330
    {
331 1
        return $this->query;
332
    }
333
334
    /**
335
     * @param BaseQuery $query
336
     * @return QueryComponentFactory
337
     */
338 4
    public function setQuery(BaseQuery $query): QueryComponentFactory
339
    {
340 4
        $this->query = $query;
341
342 4
        return $this;
343
    }
344
345
    /**
346
     * @return Query
347
     */
348 1
    public function getClientQuery(): Query
349
    {
350 1
        return $this->clientQuery;
351
    }
352
353
    /**
354
     * @param Query $clientQuery
355
     * @return QueryComponentFactory
356
     */
357 4
    public function setClientQuery(Query $clientQuery): QueryComponentFactory
358
    {
359 4
        $this->clientQuery = $clientQuery;
360
361 4
        return $this;
362
    }
363
364
    /**
365
     * @return Helper
366
     */
367 1
    public function getHelper(): Helper
368
    {
369 1
        return $this->helper;
370
    }
371
372
    /**
373
     * @param Helper $helper
374
     * @return QueryComponentFactory
375
     */
376 4
    public function setHelper(Helper $helper): QueryComponentFactory
377
    {
378 4
        $this->helper = $helper;
379
380 4
        return $this;
381
    }
382
383
    /**
384
     * @return BaseIndex
385
     */
386 2
    public function getIndex(): BaseIndex
387
    {
388 2
        return $this->index;
389
    }
390
391
    /**
392
     * @param BaseIndex $index
393
     * @return QueryComponentFactory
394
     */
395 5
    public function setIndex(BaseIndex $index): QueryComponentFactory
396
    {
397 5
        $this->index = $index;
398
399 5
        return $this;
400
    }
401
402
    /**
403
     * @return array
404
     */
405 4
    public function getBoostTerms(): array
406
    {
407 4
        return $this->boostTerms;
408
    }
409
410
    /**
411
     * @param array $boostTerms
412
     * @return QueryComponentFactory
413
     */
414
    public function setBoostTerms(array $boostTerms): self
415
    {
416
        $this->boostTerms = $boostTerms;
417
418
        return $this;
419
    }
420
}
421