Passed
Push — Cleanup-attempt ( d96adf...4706b3 )
by Simon
01:57
created

QueryComponentFactory::getQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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