SearchResult::setSpellcheck()   A
last analyzed

Complexity

Conditions 4
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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