Passed
Push — hans/spellchecktest ( 431739...ba405c )
by Simon
06:59
created

BaseIndex::init()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.0466

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 6
c 5
b 0
f 0
dl 0
loc 14
ccs 6
cts 7
cp 0.8571
rs 10
cc 4
nc 3
nop 0
crap 4.0466
1
<?php
2
3
4
namespace Firesphere\SolrSearch\Indexes;
5
6
use Exception;
7
use Firesphere\SolrSearch\Factories\QueryComponentFactory;
8
use Firesphere\SolrSearch\Helpers\SolrLogger;
9
use Firesphere\SolrSearch\Helpers\Synonyms;
10
use Firesphere\SolrSearch\Interfaces\ConfigStore;
11
use Firesphere\SolrSearch\Queries\BaseQuery;
12
use Firesphere\SolrSearch\Results\SearchResult;
13
use Firesphere\SolrSearch\Services\SchemaService;
14
use Firesphere\SolrSearch\Services\SolrCoreService;
15
use Firesphere\SolrSearch\Traits\BaseIndexTrait;
16
use Firesphere\SolrSearch\Traits\GetterSetterTrait;
17
use GuzzleHttp\Exception\GuzzleException;
18
use LogicException;
19
use SilverStripe\Control\Director;
20
use SilverStripe\Core\Config\Config;
21
use SilverStripe\Core\Config\Configurable;
22
use SilverStripe\Core\Extensible;
23
use SilverStripe\Core\Injector\Injector;
24
use SilverStripe\Dev\Deprecation;
25
use SilverStripe\ORM\ValidationException;
26
use SilverStripe\SiteConfig\SiteConfig;
27
use SilverStripe\View\ArrayData;
28
use Solarium\Core\Client\Adapter\Guzzle;
29
use Solarium\Core\Client\Client;
30
use Solarium\QueryType\Select\Query\Query;
31
use Solarium\QueryType\Select\Result\Result;
32
33
/**
34
 * Class BaseIndex
35
 * @package Firesphere\SolrSearch\Indexes
36
 */
37
abstract class BaseIndex
38
{
39
    use Extensible;
40
    use Configurable;
41
    use GetterSetterTrait;
42
    use BaseIndexTrait;
43
44
    /**
45
     * Field types that can be added
46
     * Used in init to call build methods from configuration yml
47
     * @array
48
     */
49
    private static $fieldTypes = [
50
        'FulltextFields',
51
        'SortFields',
52
        'FilterFields',
53
        'BoostedFields',
54
        'CopyFields',
55
        'DefaultField',
56
        'FacetFields',
57
        'StoredFields'
58
    ];
59
    /**
60
     * @var \Solarium\Core\Query\Result\Result
61
     */
62
    protected $rawQuery;
63
    /**
64
     * @var SchemaService
65
     */
66
    protected $schemaService;
67
    /**
68
     * @var QueryComponentFactory
69
     */
70
    protected $queryFactory;
71
    /**
72
     * The query terms as an array
73
     * @var array
74
     */
75
    protected $queryTerms = [];
76
    /**
77
     * @var bool
78
     */
79
    private $retry = false;
80
81
    /**
82
     * BaseIndex constructor.
83
     */
84 68
    public function __construct()
85
    {
86
        // Set up the client
87 68
        $config = Config::inst()->get(SolrCoreService::class, 'config');
88 68
        $config['endpoint'] = $this->getConfig($config['endpoint']);
89 68
        $this->client = new Client($config);
90 68
        $this->client->setAdapter(new Guzzle());
91
92
        // Set up the schema service, only used in the generation of the schema
93 68
        $schemaService = Injector::inst()->get(SchemaService::class, false);
94 68
        $schemaService->setIndex($this);
95 68
        $schemaService->setStore(Director::isDev());
96 68
        $this->schemaService = $schemaService;
97 68
        $this->queryFactory = Injector::inst()->get(QueryComponentFactory::class, false);
98
99 68
        $this->extend('onBeforeInit');
100 68
        $this->init();
101 68
        $this->extend('onAfterInit');
102 68
    }
103
104
    /**
105
     * Build a full config for all given endpoints
106
     * This is to add the current index to e.g. an index or select
107
     * @param array $endpoints
108
     * @return array
109
     */
110 68
    public function getConfig($endpoints): array
111
    {
112 68
        foreach ($endpoints as $host => $endpoint) {
113 68
            $endpoints[$host]['core'] = $this->getIndexName();
114
        }
115
116 68
        return $endpoints;
117
    }
118
119
    /**
120
     * @return string
121
     */
122
    abstract public function getIndexName();
123
124
    /**
125
     * Required to initialise the fields.
126
     * It's loaded in to the non-static properties for backward compatibility with FTS
127
     * Also, it's a tad easier to use this way, loading the other way around would be very
128
     * memory intensive, as updating the config for each item is not efficient
129
     */
130 67
    public function init()
131
    {
132 67
        if (!self::config()->get($this->getIndexName())) {
133 67
            Deprecation::notice('5', 'Please set an index name and use a config yml');
134
135 67
            return;
136
        }
137
138 67
        if (!empty($this->getClasses()) && !$this->usedAllFields) {
139
            Deprecation::notice('5', 'It is adviced to use a config YML for most cases');
140
        }
141
142
143 67
        $this->initFromConfig();
144 67
    }
145
146
    /**
147
     * Generate the config from yml if possible
148
     */
149 67
    protected function initFromConfig(): void
150
    {
151 67
        $config = self::config()->get($this->getIndexName());
152
153 67
        if (!array_key_exists('Classes', $config)) {
154
            throw new LogicException('No classes to index found!');
155
        }
156
157 67
        $this->setClasses($config['Classes']);
158
159
        // For backward compatibility, copy the config to the protected values
160
        // Saves doubling up further down the line
161 67
        foreach (self::$fieldTypes as $type) {
162 67
            if (array_key_exists($type, $config)) {
163 67
                $method = 'set' . $type;
164 67
                $this->$method($config[$type]);
165
            }
166
        }
167 67
    }
168
169
    /**
170
     * Default returns a SearchResult. It can return an ArrayData if FTS Compat is enabled
171
     *
172
     * @param BaseQuery $query
173
     * @return SearchResult|ArrayData|mixed
174
     * @throws GuzzleException
175
     * @throws ValidationException
176
     */
177 5
    public function doSearch(BaseQuery $query)
178
    {
179
        // Build the actual query parameters
180 5
        $clientQuery = $this->buildSolrQuery($query);
181
182 5
        $this->extend('onBeforeSearch', $query, $clientQuery);
183
184
        try {
185 5
            $result = $this->client->select($clientQuery);
186
        } catch (Exception $e) {
187
            $logger = new SolrLogger();
188
            $logger->saveSolrLog('Query');
189
        }
190
191 5
        $this->rawQuery = $result;
192
193
        // Handle the after search first. This gets a raw search result
194 5
        $this->extend('onAfterSearch', $result);
195 5
        $searchResult = new SearchResult($result, $query, $this);
196 5
        if ($this->doRetry($query, $result, $searchResult)) {
197 1
            return $this->spellcheckRetry($query, $searchResult);
198
        }
199
200
        // And then handle the search results, which is a useable object for SilverStripe
201 5
        $this->extend('updateSearchResults', $searchResult);
202
203 5
        return $searchResult;
204
    }
205
206
    /**
207
     * @param BaseQuery $query
208
     * @return Query
209
     */
210 5
    public function buildSolrQuery(BaseQuery $query): Query
211
    {
212 5
        $clientQuery = $this->client->createSelect();
213 5
        $factory = $this->buildFactory($query, $clientQuery);
214
215 5
        $clientQuery = $factory->buildQuery();
216 5
        $this->queryTerms = $factory->getQueryArray();
217
218 5
        $queryData = implode(' ', $this->queryTerms);
219 5
        $clientQuery->setQuery($queryData);
220
221 5
        return $clientQuery;
222
    }
223
224
    /**
225
     * @param BaseQuery $query
226
     * @param Query $clientQuery
227
     * @return QueryComponentFactory|mixed
228
     */
229 5
    protected function buildFactory(BaseQuery $query, Query $clientQuery)
230
    {
231 5
        $factory = $this->queryFactory;
232
233 5
        $helper = $clientQuery->getHelper();
234
235 5
        $factory->setQuery($query);
236 5
        $factory->setClientQuery($clientQuery);
237 5
        $factory->setHelper($helper);
238 5
        $factory->setIndex($this);
239
240 5
        return $factory;
241
    }
242
243
    /**
244
     * Check if the query should be retried with spellchecking
245
     * Conditions are:
246
     * It is not already a retry with spellchecking
247
     * Spellchecking is enabled
248
     * If spellchecking is enabled and nothing is found OR it should follow spellchecking none the less
249
     * There is a spellcheck output
250
     * @param BaseQuery $query
251
     * @param Result $result
252
     * @param SearchResult $searchResult
253
     * @return bool
254
     */
255 5
    protected function doRetry(BaseQuery $query, Result $result, SearchResult $searchResult): bool
256
    {
257 5
        return !$this->retry &&
258 5
            $query->hasSpellcheck() &&
259 5
            ($query->shouldFollowSpellcheck() || $result->getNumFound() === 0) &&
260 5
            $searchResult->getCollatedSpellcheck();
261
    }
262
263
    /**
264
     * Retry the query with the first collated spellcheck found.
265
     *
266
     * @param BaseQuery $query
267
     * @param SearchResult $searchResult
268
     * @return SearchResult|mixed|ArrayData
269
     * @throws GuzzleException
270
     * @throws ValidationException
271
     */
272 1
    protected function spellcheckRetry(BaseQuery $query, SearchResult $searchResult)
273
    {
274 1
        $terms = $query->getTerms();
275 1
        $spellChecked = $searchResult->getCollatedSpellcheck();
276
        // Remove the fuzzyness from the collated check
277 1
        $term = preg_replace('/~\d+/', '', $spellChecked);
278 1
        $terms[0]['text'] = $term;
279 1
        $query->setTerms($terms);
280 1
        $this->retry = true;
281
282 1
        return $this->doSearch($query);
283
    }
284
285
    /**
286
     * @return array
287
     */
288 6
    public function getFieldsForIndexing(): array
289
    {
290 6
        $facets = [];
291 6
        foreach ($this->getFacetFields() as $field) {
292 5
            $facets[] = $field['Field'];
293
        }
294
        // Return values to make the key reset
295
        // Only return unique values
296
        // And make it all a single array
297 6
        $fields = array_values(
298 6
            array_unique(
299 6
                array_merge(
300 6
                    $this->getFulltextFields(),
301 6
                    $this->getSortFields(),
302 6
                    $facets,
303 6
                    $this->getFilterFields()
304
                )
305
            )
306
        );
307
308 6
        $this->extend('updateFieldsForIndexing', $fields);
309
310 6
        return $fields;
311
    }
312
313
    /**
314
     * Upload config for this index to the given store
315
     *
316
     * @param ConfigStore $store
317
     */
318 30
    public function uploadConfig(ConfigStore $store): void
319
    {
320
        // @todo use types/schema/elevate rendering
321
        // Upload the config files for this index
322
        // Create a default schema which we can manage later
323 30
        $schema = (string)$this->schemaService->generateSchema();
324 30
        $store->uploadString(
325 30
            $this->getIndexName(),
326 30
            'schema.xml',
327 30
            $schema
328
        );
329
330
331 30
        $synonyms = $this->getSynonyms();
332
333
        // Upload synonyms
334 30
        $store->uploadString(
335 30
            $this->getIndexName(),
336 30
            'synonyms.txt',
337 30
            $synonyms
338
        );
339
340
        // Upload additional files
341 30
        foreach (glob($this->schemaService->getExtrasPath() . '/*') as $file) {
342 30
            if (is_file($file)) {
343 30
                $store->uploadFile($this->getIndexName(), $file);
344
            }
345
        }
346 30
    }
347
348
    /**
349
     * Add synonyms. Public to be extendable
350
     * @param bool $defaults Include UK to US synonyms
351
     * @return string
352
     */
353 30
    public function getSynonyms($defaults = true): string
354
    {
355 30
        $synonyms = Synonyms::getSynonymsAsString($defaults);
356 30
        $siteConfigSynonyms = SiteConfig::current_site_config()->getField('SearchSynonyms');
357
358 30
        return sprintf('%s%s', $synonyms, $siteConfigSynonyms);
359
    }
360
361
    /**
362
     * @return array
363
     */
364 2
    public function getQueryTerms(): array
365
    {
366 2
        return $this->queryTerms;
367
    }
368
369
    /**
370
     * @return QueryComponentFactory
371
     */
372 1
    public function getQueryFactory(): QueryComponentFactory
373
    {
374 1
        return $this->queryFactory;
375
    }
376
}
377