Passed
Pull Request — main (#56)
by Simon
01:17
created

SearchResult::getClassFacet()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 8
c 1
b 0
f 0
nc 3
nop 3
dl 0
loc 13
rs 10
1
<?php
2
/**
3
 * class SearchResult|Firesphere\ElasticSearch\Results\SearchResult Result of a query
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\Results;
11
12
use Elastic\Elasticsearch\Response\Elasticsearch;
13
use Firesphere\ElasticSearch\Indexes\ElasticIndex;
14
use Firesphere\ElasticSearch\Queries\ElasticQuery;
15
use Firesphere\SearchBackend\Interfaces\SearchResultInterface;
16
use Firesphere\SearchBackend\Services\BaseService;
17
use Firesphere\SearchBackend\Traits\SearchResultGetTrait;
18
use Firesphere\SearchBackend\Traits\SearchResultSetTrait;
19
use SilverStripe\Control\Controller;
20
use SilverStripe\ORM\ArrayList;
21
use SilverStripe\ORM\DataList;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\FieldType\DBField;
24
use SilverStripe\ORM\PaginatedList;
25
use SilverStripe\View\ArrayData;
26
use SilverStripe\View\ViewableData;
27
use stdClass;
28
29
/**
30
 * Class SearchResult is the combined result in a SilverStripe readable way
31
 *
32
 * Each of the requested features of a ElasticQuery are generated to be easily accessible in the controller.
33
 * In the controller, each required item can be accessed through the resulting method in this class.
34
 *
35
 * @package Firesphere\Elastic\Search
36
 */
37
class SearchResult extends ViewableData implements SearchResultInterface
38
{
39
    use SearchResultGetTrait;
40
    use SearchResultSetTrait;
41
42
    /**
43
     * @var ElasticQuery Query that has been executed
44
     */
45
    protected $query;
46
    /**
47
     * @var ElasticIndex Index the query has run on
48
     */
49
    protected $index;
50
    /**
51
     * @var stdClass|ArrayList|DataList|DataObject Resulting matches from the query on the index
52
     */
53
    protected $matches;
54
55
    /**
56
     * @var Elasticsearch
57
     */
58
    protected $elasticResult;
59
60
    /**
61
     * SearchResult constructor.
62
     *
63
     * @param Elasticsearch $result
64
     * @param ElasticQuery $query
65
     * @param ElasticIndex $index
66
     */
67
    public function __construct(Elasticsearch $result, ElasticQuery $query, ElasticIndex $index)
68
    {
69
        parent::__construct();
70
        $this->index = $index;
71
        $this->query = $query;
72
        $this->elasticResult = $result;
73
        $resultArray = $result->asArray();
74
        $result = $result->asObject();
75
76
        $this->setMatches($result->hits->hits)
77
            ->setSpellcheck($resultArray['suggest'] ?? [])
78
            ->setFacets($result->aggregations)
79
            ->setTotalItems($result->hits->total->value);
80
    }
81
82
    /**
83
     * Set the spellcheck list as an ArrayList
84
     *
85
     * @param array $spellcheck
86
     * @return SearchResult
87
     */
88
    private function setSpellcheck($spellcheck): self
89
    {
90
        $spellcheckList = [];
91
92
        if (count($spellcheck)) {
93
            foreach ($spellcheck as $suggestion) {
94
                foreach ($suggestion as $suggest) {
95
                    foreach ($suggest['options'] as $option) {
96
                        $spellcheckList[] = ArrayData::create([
97
                            'original'   => $suggest['text'],
98
                            'suggestion' => $option['text'],
99
                        ]);
100
                    }
101
                }
102
            }
103
        }
104
105
        $this->spellcheck = ArrayList::create($spellcheckList);
106
107
        return $this;
108
    }
109
110
    /**
111
     * Get the matches as a Paginated List
112
     *
113
     * @return PaginatedList
114
     */
115
    public function getPaginatedMatches(): PaginatedList
116
    {
117
        $request = Controller::curr()->getRequest();
118
        // Get all the items in the set and push them in to the list
119
        $items = $this->getMatches();
120
        /** @var PaginatedList $paginated */
121
        $paginated = PaginatedList::create($items, $request);
122
        // Do not limit the pagination, it's done at Elastic level
123
        $paginated->setLimitItems(false)
124
            // Override the count that's set from the item count
125
            ->setTotalItems($this->getTotalItems())
126
            // Set the start to the current page from start.
127
            ->setPageStart($this->query->getStart())
128
            // The amount of items per page to display
129
            ->setPageLength($this->query->getRows());
130
131
        return $paginated;
132
    }
133
134
    /**
135
     * Get the matches as an ArrayList and add an excerpt if possible.
136
     * {@link static::createExcerpt()}
137
     *
138
     * @return ArrayList
139
     */
140
    public function getMatches(): ArrayList
141
    {
142
        $matches = $this->matches;
143
        $items = [];
144
        $idField = BaseService::ID_FIELD;
145
        $classIDField = BaseService::CLASS_ID_FIELD;
146
        foreach ($matches as $match) {
147
            $item = $this->asDataobject($match, $classIDField);
148
            if ($item !== false) {
149
                $this->createExcerpt($idField, $match, $item);
150
                $items[] = $item;
151
                $item->destroy();
152
            }
153
            unset($match);
154
        }
155
156
        return ArrayList::create($items)->limit($this->query->getRows());
157
    }
158
159
    /**
160
     * Set the matches from Solarium as an ArrayList
161
     *
162
     * @param array|stdClass $result
163
     * @return $this
164
     */
165
    protected function setMatches($result): self
166
    {
167
        $data = [];
168
        /** @var stdClass $item */
169
        foreach ($result as $item) {
170
            $data[] = ArrayData::create($item);
171
            if (!empty($item->highlight)) {
172
                $this->addHighlight($item->highlight, $item->_id);
173
            }
174
        }
175
176
        $this->matches = ArrayList::create($data);
177
178
        return $this;
179
    }
180
181
    /**
182
     * Check if the match is a DataObject and exists
183
     * And, if so, return the found DO.
184
     *
185
     * @param $match
186
     * @param string $classIDField
187
     * @return DataObject|bool
188
     */
189
    protected function asDataobject($match, string $classIDField)
190
    {
191
        if (!$match instanceof DataObject) {
192
            $class = $match->_source->ClassName;
193
            /** @var DataObject $match */
194
            $match = $class::get()->byID($match->_source->{$classIDField});
195
            if ($match && $match->exists()) {
196
                $match->__set('elasticId', $match->_id);
197
            }
198
        }
199
200
        return ($match && $match->exists()) ? $match : false;
201
    }
202
203
    /**
204
     * Generate an excerpt for a DataObject
205
     *
206
     * @param string $idField
207
     * @param $match
208
     * @param DataObject $item
209
     */
210
    protected function createExcerpt(string $idField, $match, DataObject $item): void
211
    {
212
        $item->Excerpt = DBField::create_field(
213
            'HTMLText',
214
            str_replace(
215
                '&#65533;',
216
                '',
217
                $this->getHighlightByID($match->{$idField})
218
            )
219
        );
220
    }
221
222
    /**
223
     * Get the highlight for a specific document
224
     *
225
     * @param $docID
226
     * @return string
227
     */
228
    public function getHighlightByID($docID): string
229
    {
230
        $highlights = [];
231
        if ($this->highlight && $docID) {
232
            $highlight = (array)$this->highlight[$docID];
233
            foreach ($highlight as $field => $fieldHighlight) {
234
                $highlights[] = implode(' (...) ', $fieldHighlight);
235
            }
236
        }
237
238
        return implode(' (...) ', $highlights);
239
    }
240
241
    /**
242
     * Allow overriding of matches with a custom result. Accepts anything you like, mostly
243
     *
244
     * @param stdClass|ArrayList|DataList|DataObject $matches
245
     * @return mixed
246
     */
247
    public function setCustomisedMatches($matches)
248
    {
249
        $this->matches = $matches;
250
251
        return $matches;
252
    }
253
254
    /**
255
     * {@inheritDoc}
256
     */
257
    public function createFacet($facets, $options, $class, array $facetArray): array
258
    {
259
        $facet = $options['Title'];
260
        if (property_exists($facets, $facet)) {
261
            $buckets = $facets->$facet;
262
            $field = explode('.', $options['Field']);
263
            array_shift($field);
264
            $field = implode('.', $field);
265
            $result = $this->getClassFacet($field, $buckets, $class);
266
            $facetArray[$facet] = $result;
267
        }
268
269
        return $facetArray;
270
    }
271
272
    /**
273
     * @param string $field
274
     * @param stdClass $buckets
275
     * @param string $class
276
     * @return ArrayList
277
     */
278
    private function getClassFacet($field, $buckets, $class): ArrayList
279
    {
280
        $result = ArrayList::create();
281
        foreach ($buckets->buckets as $bucket) {
282
            $q = [$field => $bucket->key];
283
            $facetItem = $class::get()->filter($q)->first();
284
            if ($facetItem) {
285
                $facetItem->FacetCount = $bucket->doc_count;
286
                $result->push($facetItem);
287
            }
288
        }
289
290
        return $result->sort(['FacetCount' => 'DESC', 'Title' => 'ASC',]);
291
    }
292
293
    /**
294
     * Elastic is better off using the add method, as the highlights don't come in a
295
     * single bulk
296
     *
297
     * @param stdClass $highlight The highlights
298
     * @param string $docId The *Elastic* returned document ID
299
     * @return SearchResultInterface
300
     */
301
    protected function addHighlight($highlight, $docId): SearchResultInterface
302
    {
303
        $this->highlight[$docId][] = (array)$highlight;
304
305
        return $this;
306
    }
307
}
308