Passed
Push — main ( 5a4aff...d40937 )
by Simon
01:12
created

SearchResult::setCollatedSpellcheck()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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