Passed
Push — hans/code-cleanup ( 7f6349...96a9e1 )
by Simon
06:38
created

SearchResult::createFacet()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 4
dl 0
loc 15
ccs 7
cts 8
cp 0.875
crap 2.0078
rs 10
c 0
b 0
f 0
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Results;
5
6
use Firesphere\SolrSearch\Indexes\BaseIndex;
7
use Firesphere\SolrSearch\Queries\BaseQuery;
8
use Firesphere\SolrSearch\Services\SolrCoreService;
9
use Firesphere\SolrSearch\Traits\SearchResultGetTrait;
10
use Firesphere\SolrSearch\Traits\SearchResultSetTrait;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\DataList;
14
use SilverStripe\ORM\DataObject;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\ORM\PaginatedList;
17
use SilverStripe\View\ArrayData;
18
use Solarium\Component\Result\Facet\Field;
19
use Solarium\Component\Result\FacetSet;
20
use Solarium\Component\Result\Spellcheck\Collation;
21
use Solarium\Component\Result\Spellcheck\Result as SpellcheckResult;
22
use Solarium\QueryType\Select\Result\Document;
23
use Solarium\QueryType\Select\Result\Result;
24
use stdClass;
25
26
/**
27
 * Class SearchResult is the combined result in a SilverStripe readable way
28
 *
29
 * Each of the requested features of a BaseQuery are generated to be easily accessible in the controller.
30
 * In the controller, each required item can be accessed through the resulting method in this class.
31
 *
32
 * @package Firesphere\SolrSearch\Results
33
 */
34
class SearchResult
35
{
36
    use SearchResultGetTrait;
37
    use SearchResultSetTrait;
38
    /**
39
     * Query that has been executed
40
     *
41
     * @var BaseQuery
42
     */
43
    protected $query;
44
45
    /**
46
     * Index the query has run on
47
     *
48
     * @var BaseIndex
49
     */
50
    protected $index;
51
52
    /**
53
     * Resulting matches from the query on the index
54
     *
55
     * @var stdClass|ArrayList|DataList|DataObject
56
     */
57
    protected $matches;
58
59
    /**
60
     * SearchResult constructor.
61
     * Funnily enough, the $result contains the actual results, and has methods for the other things.
62
     * See Solarium docs for this.
63
     *
64
     * @param Result $result
65
     * @param BaseQuery $query
66
     * @param BaseIndex $index
67
     */
68 5
    public function __construct(Result $result, BaseQuery $query, BaseIndex $index)
69
    {
70 5
        $this->index = $index;
71 5
        $this->query = $query;
72 5
        $this->setMatches($result->getDocuments())
73 5
            ->setFacets($result->getFacetSet())
74 5
            ->setHighlight($result->getHighlighting())
75 5
            ->setTotalItems($result->getNumFound());
76 5
        if ($query->hasSpellcheck()) {
77 5
            $this->setSpellcheck($result->getSpellcheck())
78 5
                ->setCollatedSpellcheck($result->getSpellcheck());
79
        }
80 5
    }
81
82
    /**
83
     * Set the facets to build
84
     *
85
     * @param FacetSet|null $facets
86
     * @return $this
87
     */
88 5
    protected function setFacets($facets): self
89
    {
90 5
        $this->facets = $this->buildFacets($facets);
91
92 5
        return $this;
93
    }
94
95
    /**
96
     * Build the given list of key-value pairs in to a SilverStripe useable array
97
     *
98
     * @param FacetSet|null $facets
99
     * @return ArrayData
100
     */
101 5
    protected function buildFacets($facets): ArrayData
102
    {
103 5
        $facetArray = [];
104 5
        if ($facets) {
105 5
            $facetTypes = $this->index->getFacetFields();
106
            // Loop all available facet fields by type
107 5
            foreach ($facetTypes as $class => $options) {
108 1
                $facetArray = $this->createFacet($facets, $options, $class, $facetArray);
109
            }
110
        }
111
112
        // Return an ArrayList of the results
113 5
        return ArrayData::create($facetArray);
114
    }
115
116
    /**
117
     * Create facets from each faceted class
118
     *
119
     * @param FacetSet $facets
120
     * @param array $options
121
     * @param string $class
122
     * @param array $facetArray
123
     * @return array
124
     */
125 1
    protected function createFacet($facets, $options, $class, array $facetArray): array
126
    {
127
        // Get the facets by its title
128
        /** @var Field $typeFacets */
129 1
        $typeFacets = $facets->getFacet('facet-' . $options['Title']);
130 1
        $values = $typeFacets->getValues();
131 1
        $results = ArrayList::create();
132
        // If there are values, get the items one by one and push them in to the list
133 1
        if (count($values)) {
134
            $this->getClassFacets($class, $values, $results);
135
        }
136
        // Put the results in to the array
137 1
        $facetArray[$options['Title']] = $results;
138
139 1
        return $facetArray;
140
    }
141
142
    /**
143
     * Get the facets for each class and their count
144
     *
145
     * @param $class
146
     * @param array $values
147
     * @param ArrayList $results
148
     */
149
    protected function getClassFacets($class, array $values, &$results): void
150
    {
151
        $items = $class::get()->byIds(array_keys($values));
152
        foreach ($items as $item) {
153
            // Set the FacetCount value to be sorted on later
154
            $item->FacetCount = $values[$item->ID];
155
            $results->push($item);
156
        }
157
        // Sort the results by FacetCount
158
        $results = $results->sort(['FacetCount' => 'DESC', 'Title' => 'ASC',]);
159
    }
160
161
    /**
162
     * Set the collated spellcheck string
163
     *
164
     * @param mixed $collatedSpellcheck
165
     * @return $this
166
     */
167 5
    public function setCollatedSpellcheck($collatedSpellcheck): self
168
    {
169
        /** @var Collation $collated */
170 5
        if ($collatedSpellcheck && ($collated = $collatedSpellcheck->getCollations())) {
171 2
            $this->collatedSpellcheck = $collated[0]->getQuery();
172
        }
173
174 5
        return $this;
175
    }
176
177
    /**
178
     * Set the spellcheck list as an ArrayList
179
     *
180
     * @param SpellcheckResult|null $spellcheck
181
     * @return SearchResult
182
     */
183 5
    public function setSpellcheck($spellcheck): self
184
    {
185 5
        $spellcheckList = [];
186
187 5
        if ($spellcheck && ($suggestions = $spellcheck->getSuggestion(0))) {
188 2
            foreach ($suggestions->getWords() as $suggestion) {
189 2
                $spellcheckList[] = ArrayData::create($suggestion);
190
            }
191
        }
192
193 5
        $this->spellcheck = ArrayList::create($spellcheckList);
194
195 5
        return $this;
196
    }
197
198
    /**
199
     * Get the matches as a Paginated List
200
     *
201
     * @param HTTPRequest $request
202
     * @return PaginatedList
203
     */
204 1
    public function getPaginatedMatches(HTTPRequest $request): PaginatedList
205
    {
206
        // Get all the items in the set and push them in to the list
207 1
        $items = $this->getMatches();
208
        /** @var PaginatedList $paginated */
209 1
        $paginated = PaginatedList::create($items, $request);
210
        // Do not limit the pagination, it's done at Solr level
211 1
        $paginated->setLimitItems(false)
212
            // Override the count that's set from the item count
213 1
            ->setTotalItems($this->getTotalItems())
214
            // Set the start to the current page from start.
215 1
            ->setPageStart($this->query->getStart())
216
            // The amount of items per page to display
217 1
            ->setPageLength($this->query->getRows());
218
219 1
        return $paginated;
220
    }
221
222
    /**
223
     * Get the matches as an ArrayList and add an excerpt if possible.
224
     * {@link static::createExcerpt()}
225
     *
226
     * @return ArrayList
227
     */
228 1
    public function getMatches(): ArrayList
229
    {
230 1
        $matches = $this->matches;
231 1
        $items = [];
232 1
        $idField = SolrCoreService::ID_FIELD;
233 1
        $classIDField = SolrCoreService::CLASS_ID_FIELD;
234 1
        foreach ($matches as $match) {
235 1
            $item = $this->isDataObject($match, $classIDField);
236 1
            if ($item !== false) {
237 1
                $this->createExcerpt($idField, $match, $item);
238 1
                $items[] = $item;
239 1
                $item->destroy();
240
            }
241 1
            unset($match);
242
        }
243
244 1
        return ArrayList::create($items)->limit($this->query->getRows());
245
    }
246
247
    /**
248
     * Set the matches from Solarium as an ArrayList
249
     *
250
     * @param array $result
251
     * @return $this
252
     */
253 5
    protected function setMatches($result): self
254
    {
255 5
        $data = [];
256
        /** @var Document $item */
257 5
        foreach ($result as $item) {
258 2
            $data[] = ArrayData::create($item->getFields());
259
        }
260
261 5
        $docs = ArrayList::create($data);
262 5
        $this->matches = $docs;
263
264 5
        return $this;
265
    }
266
267
    /**
268
     * Check if the match is a DataObject and exists
269
     *
270
     * @param $match
271
     * @param string $classIDField
272
     * @return DataObject|bool
273
     */
274 1
    protected function isDataObject($match, string $classIDField)
275
    {
276 1
        if (!$match instanceof DataObject) {
277 1
            $class = $match->ClassName;
278
            /** @var DataObject $match */
279 1
            $match = $class::get()->byID($match->{$classIDField});
280
        }
281
282 1
        return ($match && $match->exists()) ? $match : false;
283
    }
284
285
    /**
286
     * Generate an excerpt for a DataObject
287
     *
288
     * @param string $idField
289
     * @param $match
290
     * @param DataObject $item
291
     */
292 1
    protected function createExcerpt(string $idField, $match, DataObject $item): void
293
    {
294 1
        $item->Excerpt = DBField::create_field(
295 1
            'HTMLText',
296 1
            str_replace(
297 1
                '&#65533;',
298 1
                '',
299 1
                $this->getHighlightByID($match->{$idField})
300
            )
301
        );
302 1
    }
303
304
    /**
305
     * Get the highlight for a specific document
306
     *
307
     * @param $docID
308
     * @return string
309
     */
310 1
    public function getHighlightByID($docID): string
311
    {
312 1
        $highlights = [];
313 1
        if ($this->highlight && $docID) {
314 1
            $highlights = [];
315 1
            foreach ($this->highlight->getResult($docID) as $field => $highlight) {
316 1
                $highlights[] = implode(' (...) ', $highlight);
317
            }
318
        }
319
320 1
        return implode(' (...) ', $highlights);
321
    }
322
323
    /**
324
     * Allow overriding of matches with a custom result. Accepts anything you like, mostly
325
     *
326
     * @param stdClass|ArrayList|DataList|DataObject $matches
327
     * @return mixed
328
     */
329 1
    public function setCustomisedMatches($matches)
330
    {
331 1
        $this->matches = $matches;
332
333 1
        return $matches;
334
    }
335
}
336