QueryBuilder   A
last analyzed

Complexity

Total Complexity 40

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 111
c 1
b 0
f 0
dl 0
loc 286
rs 9.2
wmc 40

14 Methods

Rating   Name   Duplication   Size   Complexity  
B buildQuery() 0 34 7
A getFilters() 0 7 1
A init() 0 7 1
A setQuery() 0 3 1
A getOrFilters() 0 11 4
A setIndex() 0 3 1
A getAndFilters() 0 18 4
A addShould() 0 12 1
A getHighlighter() 0 12 3
A getSort() 0 3 1
A getAggregates() 0 18 2
A getSuggestTermList() 0 20 4
A getFieldBoosting() 0 17 6
A getUserQuery() 0 23 4

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * class QueryBuilder|Firesphere\ElasticSearch\Queries\Builders\QueryBuilder Build the Elastic query array
4
 *
5
 * @package Firesphere\Elastic\Search
6
 * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo
7
 * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy
8
 */
9
10
namespace Firesphere\ElasticSearch\Queries\Builders;
11
12
use Firesphere\ElasticSearch\Indexes\ElasticIndex;
13
use Firesphere\ElasticSearch\Queries\ElasticQuery;
14
use Firesphere\SearchBackend\Indexes\CoreIndex;
15
use Firesphere\SearchBackend\Interfaces\QueryBuilderInterface;
16
use Firesphere\SearchBackend\Queries\BaseQuery;
17
use SilverStripe\Core\ClassInfo;
18
19
/**
20
 * Class QueryBuilder
21
 *
22
 * Build/construct an array to send to Elastic to query the results.
23
 *
24
 * @package Firesphere\Elastic\Search
25
 */
26
class QueryBuilder implements QueryBuilderInterface
27
{
28
    /**
29
     * @var ElasticQuery
30
     */
31
    protected $query;
32
33
    /**
34
     * @var ElasticIndex
35
     */
36
    protected $index;
37
38
    /**
39
     * @param BaseQuery $query
40
     * @param ElasticIndex $index
41
     * @return array
42
     */
43
    public static function buildQuery(BaseQuery $query, CoreIndex $index): array
44
    {
45
        $self = self::init($query, $index);
46
        $filters = $self->getFilters($index, $query);
47
        $terms = $self->getUserQuery($query);
48
        $highlights = $self->getHighlighter();
49
        $suggests = $self->getSuggestTermList();
50
        $aggregates = $self->getAggregates();
51
        $sort = $self->getSort();
52
        $body = [];
53
        if (count($terms)) {
54
            $body['query']['bool'] = $terms;
55
        }
56
        if (count($filters)) {
57
            $body['query']['bool'] += $filters;
58
        }
59
        if (count($highlights)) {
60
            $body['highlight'] = $highlights;
61
        }
62
        if (count($suggests)) {
63
            $body['suggest'] = $suggests;
64
        }
65
        if (count($aggregates)) {
66
            $body['aggs'] = $aggregates;
67
        }
68
        if (count($sort)) {
69
            $body['sort'] = $sort;
70
        }
71
72
        return [
73
            'index' => $index->getIndexName(),
74
            'from'  => $query->getStart(),
75
            'size'  => $query->getRows() * 2, // To be on the safe side
76
            'body'  => $body
77
        ];
78
    }
79
80
    /**
81
     * @param ElasticQuery $query
82
     * @param ElasticIndex $index
83
     * @return self
84
     */
85
    protected static function init(ElasticQuery $query, ElasticIndex $index): self
86
    {
87
        $self = new self();
88
        $self->setIndex($index);
89
        $self->setQuery($query);
90
91
        return $self;
92
    }
93
94
    /**
95
     * @param mixed $index
96
     */
97
    public function setIndex($index): void
98
    {
99
        $this->index = $index;
100
    }
101
102
    /**
103
     * @param mixed $query
104
     */
105
    public function setQuery($query): void
106
    {
107
        $this->query = $query;
108
    }
109
110
    /**
111
     * Build the `OR` and `AND` filters
112
     * @param ElasticIndex $index
113
     * @param ElasticQuery $query
114
     * @return array[]
115
     */
116
    private function getFilters(ElasticIndex $index, ElasticQuery $query): array
117
    {
118
        return [
119
            'filter' => [
120
                'bool' => [
121
                    'must'   => $this->getAndFilters($index, $query),
122
                    'should' => $this->getOrFilters($query)
123
                ],
124
            ]
125
        ];
126
    }
127
128
    /**
129
     * Required must-be filters if they're here.
130
     * @param ElasticIndex $index
131
     * @param ElasticQuery $query
132
     * @return array[]
133
     */
134
    private function getAndFilters(ElasticIndex $index, ElasticQuery $query): array
135
    {
136
        // Default,
137
        $filters = [
138
            [
139
                'terms' => [
140
                    'ViewStatus' => $index->getViewStatusFilter(),
141
                ]
142
            ]
143
        ];
144
        if (count($query->getFilters())) {
145
            foreach ($query->getFilters() as $key => $value) {
146
                $value = is_array($value) ?: [$value];
147
                $filters[] = ['terms' => [$key => $value]];
148
            }
149
        }
150
151
        return $filters;
152
    }
153
154
    /**
155
     * Create the "should" filter, that is OR instead of AND
156
     * @param ElasticQuery $query
157
     * @return array
158
     */
159
    private function getOrFilters(ElasticQuery $query): array
160
    {
161
        $filters = [];
162
        if (count($query->getOrFilters())) {
163
            foreach ($query->getOrFilters() as $key => $value) {
164
                $value = is_array($value) ?: [$value];
165
                $filters[] = ['terms' => [$key => $value]];
166
            }
167
        }
168
169
        return $filters;
170
    }
171
172
    /**
173
     * this allows for multiple search terms to be entered
174
     * @param ElasticQuery|BaseQuery $query
175
     * @return array
176
     */
177
    private function getUserQuery(ElasticQuery|BaseQuery $query): array
178
    {
179
        $q = [];
180
        $terms = $query->getTerms();
181
        // Until wildcard||fuzziness works, just set it to match
182
        $type = 'match';
183
        foreach ($terms as $term) {
184
            if ($term['fuzzy'] !== null) {
185
                $type = 'fuzzy';
186
            }
187
            $q['must'][] = [
188
                $type => [
189
                    '_text' => $term['text']
190
                ]
191
            ];
192
            if ($type === 'match') {
193
                $q = $this->getFieldBoosting($term, $type, $q);
194
            }
195
            // reset to default of "must match"
196
            $type = 'match';
197
        }
198
199
        return $q;
200
    }
201
202
    /**
203
     * @param mixed $term
204
     * @param string $type
205
     * @param array $q
206
     * @return array
207
     */
208
    private function getFieldBoosting(mixed $term, string $type, array $q): array
209
    {
210
        $shoulds = [];
211
        $queryBoosts = $this->query->getBoostedFields();
212
        if ($term['boost'] > 1 && count($term['fields'])) {
213
            foreach ($term['fields'] as $field) {
214
                $shoulds[] = $this->addShould($type, $field, $term['text'], $term['boost']);
215
            }
216
        }
217
        foreach ($queryBoosts as $field => $boost) {
218
            $shoulds[] = $this->addShould($type, $field, $term['text'], $boost);
219
        }
220
        if (count($shoulds)) {
221
            $q['should'] = $shoulds;
222
        }
223
224
        return $q;
225
    }
226
227
    /**
228
     * @param string $type
229
     * @param string $field
230
     * @param $text
231
     * @param int $boost
232
     * @return array
233
     */
234
    private function addShould(string $type, string $field, $text, int $boost): array
235
    {
236
        $should = [
237
            $type => [
238
                $field => [
239
                    'query' => $text,
240
                    'boost' => $boost
241
                ]
242
            ]
243
        ];
244
245
        return $should;
246
    }
247
248
    private function getHighlighter(): array
249
    {
250
        if ($this->query->isHighlight()) {
251
            $highlights = [];
252
            foreach ($this->index->getFulltextFields() as $field) {
253
                $highlights[$field] = ['type' => 'unified'];
254
            }
255
256
            return ['fields' => $highlights];
257
        }
258
259
        return [];
260
    }
261
262
    private function getSuggestTermList()
263
    {
264
        $terms = $this->query->getTerms();
265
        $suggest = [];
266
        $base = [
267
            'term' => ['field' => '_text']
268
        ];
269
        foreach ($terms as $j => $term) {
270
            $base['text'] = $term['text'];
271
            $suggest[$j . '-fullterm'] = $base;
272
            if (str_contains($term['text'], ' ')) {
273
                $termArray = explode(' ', $term['text']);
274
                foreach ($termArray as $i => $word) {
275
                    $base['text'] = $word;
276
                    $suggest[$i . '-partterm'] = $base;
277
                }
278
            }
279
        }
280
281
        return $suggest;
282
    }
283
284
    /**
285
     * Build the query part for aggregation/faceting
286
     *
287
     * @return array
288
     */
289
    private function getAggregates()
290
    {
291
        $aggregates = [];
292
293
        $facets = $this->index->getFacetFields();
294
295
        foreach ($facets as $class => $facet) {
296
            $shortClass = ClassInfo::shortName($facet['BaseClass']);
297
            $field = sprintf('%s.%s', $shortClass, $facet['Field']);
298
            $aggregates[$facet['Title']] = [
299
                'terms' => [
300
                    'field' => $field
301
                ]
302
303
            ];
304
        }
305
306
        return $aggregates;
307
    }
308
309
    private function getSort()
310
    {
311
        return $this->query->getSort();
312
    }
313
}
314