SearchResult   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 271
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 94
dl 0
loc 271
rs 9.84
c 4
b 0
f 1
wmc 32

12 Methods

Rating   Name   Duplication   Size   Complexity  
A createExcerpt() 0 8 1
A addHighlight() 0 5 1
A getHighlightByID() 0 11 4
A setCustomisedMatches() 0 5 1
A asDataobject() 0 12 6
A setSpellcheck() 0 20 5
A getMatches() 0 17 3
A setMatches() 0 14 3
A getPaginatedMatches() 0 17 1
A getClassFacet() 0 13 3
A __construct() 0 14 2
A createFacet() 0 13 2
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
        $resultObject = $result->asObject();
75
76
        $this->setMatches($resultObject->hits->hits)
77
            ->setSpellcheck($resultArray['suggest'] ?? [])
78
            ->setTotalItems($resultObject->hits->total->value);
79
        if (property_exists($resultObject, 'aggregations')) {
80
            $this->setFacets($resultObject->aggregations);
81
        }
82
    }
83
84
    /**
85
     * Set the spellcheck list as an ArrayList
86
     *
87
     * @param array $spellcheck
88
     * @return SearchResult
89
     */
90
    protected function setSpellcheck($spellcheck): self
91
    {
92
        $spellcheckList = [];
93
94
        if (count($spellcheck)) {
95
            foreach ($spellcheck as $suggestion) {
96
                foreach ($suggestion as $suggest) {
97
                    foreach ($suggest['options'] as $option) {
98
                        $spellcheckList[] = ArrayData::create([
99
                            'original'   => $suggest['text'],
100
                            'suggestion' => $option['text'],
101
                        ]);
102
                    }
103
                }
104
            }
105
        }
106
107
        $this->spellcheck = ArrayList::create($spellcheckList);
108
109
        return $this;
110
    }
111
112
    /**
113
     * Get the matches as a Paginated List
114
     *
115
     * @return PaginatedList
116
     */
117
    public function getPaginatedMatches(): PaginatedList
118
    {
119
        $request = Controller::curr()->getRequest();
120
        // Get all the items in the set and push them in to the list
121
        $items = $this->getMatches();
122
        /** @var PaginatedList $paginated */
123
        $paginated = PaginatedList::create($items, $request);
124
        // Do not limit the pagination, it's done at Elastic level
125
        $paginated->setLimitItems(false)
126
            // Override the count that's set from the item count
127
            ->setTotalItems($this->getTotalItems())
128
            // Set the start to the current page from start.
129
            ->setPageStart($this->query->getStart())
130
            // The amount of items per page to display
131
            ->setPageLength($this->query->getRows());
132
133
        return $paginated;
134
    }
135
136
    /**
137
     * Get the matches as an ArrayList and add an excerpt if possible.
138
     * {@link static::createExcerpt()}
139
     *
140
     * @return ArrayList
141
     */
142
    public function getMatches(): ArrayList
143
    {
144
        $matches = $this->matches;
145
        $items = [];
146
        $idField = BaseService::ID_FIELD;
147
        $classIDField = BaseService::CLASS_ID_FIELD;
148
        foreach ($matches as $match) {
149
            $item = $this->asDataobject($match, $classIDField);
150
            if ($item !== false) {
151
                $this->createExcerpt($idField, $match, $item);
152
                $items[] = $item;
153
                $item->destroy();
154
            }
155
            unset($match);
156
        }
157
158
        return ArrayList::create($items)->limit($this->query->getRows());
159
    }
160
161
    /**
162
     * Set the matches from Solarium as an ArrayList
163
     *
164
     * @param array|stdClass $result
165
     * @return $this
166
     */
167
    protected function setMatches($result): self
168
    {
169
        $data = [];
170
        /** @var stdClass $item */
171
        foreach ($result as $item) {
172
            $data[] = ArrayData::create($item);
173
            if (!empty($item->highlight)) {
174
                $this->addHighlight($item->highlight, $item->_id);
175
            }
176
        }
177
178
        $this->matches = ArrayList::create($data);
179
180
        return $this;
181
    }
182
183
    /**
184
     * Check if the match is a DataObject and exists
185
     * And, if so, return the found DO.
186
     *
187
     * @param $match
188
     * @param string $classIDField
189
     * @return DataObject|bool
190
     */
191
    protected function asDataobject($match, string $classIDField)
192
    {
193
        if (!$match instanceof DataObject) {
194
            $class = $match->_source->ClassName;
195
            /** @var DataObject $match */
196
            $match = $class::get()->byID($match->_source->{$classIDField});
197
            if ($match && $match->exists()) {
198
                $match->__set('elasticId', $match->_id);
199
            }
200
        }
201
202
        return ($match && $match->exists()) ? $match : false;
203
    }
204
205
    /**
206
     * Generate an excerpt for a DataObject
207
     *
208
     * @param string $idField
209
     * @param $match
210
     * @param DataObject $item
211
     */
212
    protected function createExcerpt(string $idField, $match, DataObject $item): void
213
    {
214
        $item->Excerpt = DBField::create_field(
215
            'HTMLText',
216
            str_replace(
217
                '&#65533;',
218
                '',
219
                $this->getHighlightByID($match->{$idField})
220
            )
221
        );
222
    }
223
224
    /**
225
     * Get the highlight for a specific document
226
     *
227
     * @param $docID
228
     * @return string
229
     */
230
    public function getHighlightByID($docID): string
231
    {
232
        $highlights = [];
233
        if ($this->highlight && $docID) {
234
            $highlight = (array)$this->highlight[$docID];
235
            foreach ($highlight as $field => $fieldHighlight) {
236
                $highlights[] = implode(' (...) ', $fieldHighlight);
237
            }
238
        }
239
240
        return implode(' (...) ', $highlights);
241
    }
242
243
    /**
244
     * Allow overriding of matches with a custom result. Accepts anything you like, mostly
245
     *
246
     * @param stdClass|ArrayList|DataList|DataObject $matches
247
     * @return mixed
248
     */
249
    public function setCustomisedMatches($matches)
250
    {
251
        $this->matches = $matches;
252
253
        return $matches;
254
    }
255
256
    /**
257
     * {@inheritDoc}
258
     */
259
    public function createFacet($facets, $options, $class, array $facetArray): array
260
    {
261
        $facet = $options['Title'];
262
        if (property_exists($facets, $facet)) {
263
            $buckets = $facets->$facet;
264
            $field = explode('.', $options['Field']);
265
            array_shift($field);
266
            $field = implode('.', $field);
267
            $result = $this->getClassFacet($field, $buckets, $class);
268
            $facetArray[$facet] = $result;
269
        }
270
271
        return $facetArray;
272
    }
273
274
    /**
275
     * @param string $field
276
     * @param stdClass $buckets
277
     * @param string $class
278
     * @return ArrayList
279
     */
280
    private function getClassFacet($field, $buckets, $class): ArrayList
281
    {
282
        $result = ArrayList::create();
283
        foreach ($buckets->buckets as $bucket) {
284
            $q = [$field => $bucket->key];
285
            $facetItem = $class::get()->filter($q)->first();
286
            if ($facetItem) {
287
                $facetItem->FacetCount = $bucket->doc_count;
288
                $result->push($facetItem);
289
            }
290
        }
291
292
        return $result->sort(['FacetCount' => 'DESC', 'Title' => 'ASC',]);
293
    }
294
295
    /**
296
     * Elastic is better off using the add method, as the highlights don't come in a
297
     * single bulk
298
     *
299
     * @param stdClass $highlight The highlights
300
     * @param string $docId The *Elastic* returned document ID
301
     * @return SearchResultInterface
302
     */
303
    protected function addHighlight($highlight, $docId): SearchResultInterface
304
    {
305
        $this->highlight[$docId][] = (array)$highlight;
306
307
        return $this;
308
    }
309
}
310