Passed
Push — main ( 77a257...2c4181 )
by Simon
01:14
created

QueryBuilder::getSort()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
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 wildcards work, just set it to match
182
        $type = 'match';
183
        if (!count($terms)) {
184
            $terms = ['text' => '*'];
185
        }
186
        foreach ($terms as $term) {
187
            $q['must'][] = [
188
                $type => [
189
                    '_text' => $term['text']
190
                ]
191
            ];
192
            $q = $this->getFieldBoosting($term, $type, $q);
193
        }
194
195
        return $q;
196
    }
197
198
    /**
199
     * @param mixed $term
200
     * @param string $type
201
     * @param array $q
202
     * @return array
203
     */
204
    private function getFieldBoosting(mixed $term, string $type, array $q): array
205
    {
206
        $shoulds = [];
207
        $queryBoosts = $this->query->getBoostedFields();
208
        if ($term['boost'] > 1 && count($term['fields'])) {
209
            foreach ($term['fields'] as $field) {
210
                $shoulds[] = $this->addShould($type, $field, $term['text'], $term['boost']);
211
            }
212
        }
213
        foreach ($queryBoosts as $field => $boost) {
214
            $shoulds[] = $this->addShould($type, $field, $term['text'], $boost);
215
        }
216
        if (count($shoulds)) {
217
            $q['should'] = $shoulds;
218
        }
219
220
        return $q;
221
    }
222
223
    /**
224
     * @param string $type
225
     * @param string $field
226
     * @param $text
227
     * @param int $boost
228
     * @return array
229
     */
230
    private function addShould(string $type, string $field, $text, int $boost): array
231
    {
232
        $should = [
233
            $type => [
234
                $field => [
235
                    'query' => $text,
236
                    'boost' => $boost
237
                ]
238
            ]
239
        ];
240
241
        return $should;
242
    }
243
244
    private function getHighlighter(): array
245
    {
246
        if ($this->query->isHighlight()) {
247
            $highlights = [];
248
            foreach ($this->index->getFulltextFields() as $field) {
249
                $highlights[$field] = ['type' => 'unified'];
250
            }
251
252
            return ['fields' => $highlights];
253
        }
254
255
        return [];
256
    }
257
258
    private function getSuggestTermList()
259
    {
260
        $terms = $this->query->getTerms();
261
        $suggest = [];
262
        $base = [
263
            'term' => ['field' => '_text']
264
        ];
265
        foreach ($terms as $j => $term) {
266
            $base['text'] = $term['text'];
267
            $suggest[$j . '-fullterm'] = $base;
268
            if (str_contains($term['text'], ' ')) {
269
                $termArray = explode(' ', $term['text']);
270
                foreach ($termArray as $i => $word) {
271
                    $base['text'] = $word;
272
                    $suggest[$i . '-partterm'] = $base;
273
                }
274
            }
275
        }
276
277
        return $suggest;
278
    }
279
280
    /**
281
     * Build the query part for aggregation/faceting
282
     *
283
     * @return array
284
     */
285
    private function getAggregates()
286
    {
287
        $aggregates = [];
288
289
        $facets = $this->index->getFacetFields();
290
291
        foreach ($facets as $class => $facet) {
292
            $shortClass = ClassInfo::shortName($facet['BaseClass']);
293
            $field = sprintf('%s.%s', $shortClass, $facet['Field']);
294
            $aggregates[$facet['Title']] = [
295
                'terms' => [
296
                    'field' => $field
297
                ]
298
299
            ];
300
        }
301
302
        return $aggregates;
303
    }
304
305
    private function getSort()
306
    {
307
        return $this->query->getSort();
308
    }
309
}
310