Passed
Push — main ( 9b88d9...7cf5ec )
by Simon
01:25
created

QueryBuilder::getAggregates()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 9
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 18
rs 9.9666
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
        $body = [];
52
        if (count($terms)) {
53
            $body['query']['bool'] = $terms;
54
        }
55
        if (count($filters)) {
56
            $body['query']['bool'] += $filters;
57
        }
58
        if (count($highlights)) {
59
            $body['highlight'] = $highlights;
60
        }
61
        if (count($suggests)) {
62
            $body['suggest'] = $suggests;
63
        }
64
        if (count($aggregates)) {
65
            $body['aggs'] = $aggregates;
66
        }
67
68
        return [
69
            'index' => $index->getIndexName(),
70
            'from'  => $query->getStart(),
71
            'size'  => $query->getRows() * 2, // To be on the safe side
72
            'body'  => $body
73
        ];
74
    }
75
76
    /**
77
     * @param ElasticQuery $query
78
     * @param ElasticIndex $index
79
     * @return self
80
     */
81
    protected static function init(ElasticQuery $query, ElasticIndex $index): self
82
    {
83
        $self = new self();
84
        $self->setIndex($index);
85
        $self->setQuery($query);
86
87
        return $self;
88
    }
89
90
    /**
91
     * @param mixed $index
92
     */
93
    public function setIndex($index): void
94
    {
95
        $this->index = $index;
96
    }
97
98
    /**
99
     * @param mixed $query
100
     */
101
    public function setQuery($query): void
102
    {
103
        $this->query = $query;
104
    }
105
106
    /**
107
     * Build the `OR` and `AND` filters
108
     * @param ElasticIndex $index
109
     * @param ElasticQuery $query
110
     * @return array[]
111
     */
112
    private function getFilters(ElasticIndex $index, ElasticQuery $query): array
113
    {
114
        return [
115
            'filter' => [
116
                'bool' => [
117
                    'must'   => $this->getAndFilters($index, $query),
118
                    'should' => $this->getOrFilters($query)
119
                ],
120
            ]
121
        ];
122
    }
123
124
    /**
125
     * Required must-be filters if they're here.
126
     * @param ElasticIndex $index
127
     * @param ElasticQuery $query
128
     * @return array[]
129
     */
130
    private function getAndFilters(ElasticIndex $index, ElasticQuery $query): array
131
    {
132
        // Default,
133
        $filters = [
134
            [
135
                'terms' => [
136
                    'ViewStatus' => $index->getViewStatusFilter(),
137
                ]
138
            ]
139
        ];
140
        if (count($query->getFilters())) {
141
            foreach ($query->getFilters() as $key => $value) {
142
                $value = is_array($value) ?: [$value];
143
                $filters[] = ['terms' => [$key => $value]];
144
            }
145
        }
146
147
        return $filters;
148
    }
149
150
    /**
151
     * Create the "should" filter, that is OR instead of AND
152
     * @param ElasticQuery $query
153
     * @return array
154
     */
155
    private function getOrFilters(ElasticQuery $query): array
156
    {
157
        $filters = [];
158
        if (count($query->getOrFilters())) {
159
            foreach ($query->getOrFilters() as $key => $value) {
160
                $value = is_array($value) ?: [$value];
161
                $filters[] = ['terms' => [$key => $value]];
162
            }
163
        }
164
165
        return $filters;
166
    }
167
168
    /**
169
     * this allows for multiple search terms to be entered
170
     * @param ElasticQuery|BaseQuery $query
171
     * @return array
172
     */
173
    private function getUserQuery(ElasticQuery|BaseQuery $query): array
174
    {
175
        $q = [];
176
        $terms = $query->getTerms();
177
        // Until wildcards work, just set it to match
178
        $type = 'match';
179
        if (!count($terms)) {
180
            $terms = ['text' => '*'];
181
        }
182
        foreach ($terms as $term) {
183
            $q['must'][] = [
184
                $type => [
185
                    '_text' => $term['text']
186
                ]
187
            ];
188
            $q = $this->getFieldBoosting($term, $type, $q);
189
        }
190
191
        return $q;
192
    }
193
194
    /**
195
     * @param mixed $term
196
     * @param string $type
197
     * @param array $q
198
     * @return array
199
     */
200
    private function getFieldBoosting(mixed $term, string $type, array $q): array
201
    {
202
        $shoulds = [];
203
        $queryBoosts = $this->query->getBoostedFields();
204
        if ($term['boost'] > 1 && count($term['fields'])) {
205
            foreach ($term['fields'] as $field) {
206
                $shoulds[] = $this->addShould($type, $field, $term['text'], $term['boost']);
207
            }
208
        }
209
        foreach ($queryBoosts as $field => $boost) {
210
            $shoulds[] = $this->addShould($type, $field, $term['text'], $boost);
211
        }
212
        if (count($shoulds)) {
213
            $q['should'] = $shoulds;
214
        }
215
216
        return $q;
217
    }
218
219
    /**
220
     * @param string $type
221
     * @param string $field
222
     * @param $text
223
     * @param int $boost
224
     * @return array
225
     */
226
    private function addShould(string $type, string $field, $text, int $boost): array
227
    {
228
        $should = [
229
            $type => [
230
                $field => [
231
                    'query' => $text,
232
                    'boost' => $boost
233
                ]
234
            ]
235
        ];
236
237
        return $should;
238
    }
239
240
    private function getHighlighter(): array
241
    {
242
        if ($this->query->isHighlight()) {
243
            $highlights = [];
244
            foreach ($this->index->getFulltextFields() as $field) {
245
                $highlights[$field] = ['type' => 'unified'];
246
            }
247
248
            return ['fields' => $highlights];
249
        }
250
251
        return [];
252
    }
253
254
    private function getSuggestTermList()
255
    {
256
        $terms = $this->query->getTerms();
257
        $suggest = [];
258
        $base = [
259
            'term' => ['field' => '_text']
260
        ];
261
        foreach ($terms as $j => $term) {
262
            $base['text'] = $term['text'];
263
            $suggest[$j . '-fullterm'] = $base;
264
            if (str_contains($term['text'], ' ')) {
265
                $termArray = explode(' ', $term['text']);
266
                foreach ($termArray as $i => $word) {
267
                    $base['text'] = $word;
268
                    $suggest[$i . '-partterm'] = $base;
269
                }
270
            }
271
        }
272
273
        return $suggest;
274
    }
275
276
    public function getAggregates()
277
    {
278
        $aggregates = [];
279
280
        $facets = $this->index->getFacetFields();
281
282
        foreach ($facets as $class => $facet) {
283
            $shortClass = ClassInfo::shortName($facet['BaseClass']);
284
            $field = sprintf('%s.%s.keyword', $shortClass, $facet['Field']);
285
            $aggregates[$facet['Title']] = [
286
                'terms' => [
287
                    'field' => $field
288
                ]
289
290
            ];
291
        }
292
293
        return $aggregates;
294
    }
295
}
296