Passed
Push — hans/spellchecktest ( b5d68c...3163c4 )
by Simon
06:42
created

BaseIndex::init()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.3332

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 8
c 4
b 0
f 0
dl 0
loc 18
rs 10
ccs 6
cts 9
cp 0.6667
cc 3
nc 3
nop 0
crap 3.3332
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
            // If the old init method is found, skip the config based init
136 67
            if (!count($this->getClasses())) {
137
                Deprecation::notice(
138
                    '5',
139
                    'No classes to add to index found, did you maybe call parent::init() too early?'
140
                );
141
            }
142
143 67
            return;
144
        }
145
146
147 67
        $this->initFromConfig();
148 67
    }
149
150
    /**
151
     * Generate the config from yml if possible
152
     */
153 67
    protected function initFromConfig(): void
154
    {
155 67
        $config = self::config()->get($this->getIndexName());
156
157 67
        if (!array_key_exists('Classes', $config)) {
158
            throw new LogicException('No classes to index found!');
159
        }
160
161 67
        $this->setClasses($config['Classes']);
162
163
        // For backward compatibility, copy the config to the protected values
164
        // Saves doubling up further down the line
165 67
        foreach (self::$fieldTypes as $type) {
166 67
            if (array_key_exists($type, $config)) {
167 67
                $method = 'set' . $type;
168 67
                $this->$method($config[$type]);
169
            }
170
        }
171 67
    }
172
173
    /**
174
     * Default returns a SearchResult. It can return an ArrayData if FTS Compat is enabled
175
     *
176
     * @param BaseQuery $query
177
     * @return SearchResult|ArrayData|mixed
178
     * @throws GuzzleException
179
     * @throws ValidationException
180
     */
181 5
    public function doSearch(BaseQuery $query)
182
    {
183
        // Build the actual query parameters
184 5
        $clientQuery = $this->buildSolrQuery($query);
185
186 5
        $this->extend('onBeforeSearch', $query, $clientQuery);
187
188
        try {
189 5
            $result = $this->client->select($clientQuery);
190
        } catch (Exception $e) {
191
            $logger = new SolrLogger();
192
            $logger->saveSolrLog('Query');
193
        }
194
195 5
        $this->rawQuery = $result;
196
197
        // Handle the after search first. This gets a raw search result
198 5
        $this->extend('onAfterSearch', $result);
199 5
        $searchResult = new SearchResult($result, $query, $this);
200 5
        if ($this->doRetry($query, $result, $searchResult)) {
201 1
            return $this->spellcheckRetry($query, $searchResult);
202
        }
203
204
        // And then handle the search results, which is a useable object for SilverStripe
205 5
        $this->extend('updateSearchResults', $searchResult);
206
207 5
        return $searchResult;
208
    }
209
210
    /**
211
     * @param BaseQuery $query
212
     * @return Query
213
     */
214 5
    public function buildSolrQuery(BaseQuery $query): Query
215
    {
216 5
        $clientQuery = $this->client->createSelect();
217 5
        $factory = $this->buildFactory($query, $clientQuery);
218
219 5
        $clientQuery = $factory->buildQuery();
220 5
        $this->queryTerms = $factory->getQueryArray();
221
222 5
        $queryData = implode(' ', $this->queryTerms);
223 5
        $clientQuery->setQuery($queryData);
224
225 5
        return $clientQuery;
226
    }
227
228
    /**
229
     * @param BaseQuery $query
230
     * @param Query $clientQuery
231
     * @return QueryComponentFactory|mixed
232
     */
233 5
    protected function buildFactory(BaseQuery $query, Query $clientQuery)
234
    {
235 5
        $factory = $this->queryFactory;
236
237 5
        $helper = $clientQuery->getHelper();
238
239 5
        $factory->setQuery($query);
240 5
        $factory->setClientQuery($clientQuery);
241 5
        $factory->setHelper($helper);
242 5
        $factory->setIndex($this);
243
244 5
        return $factory;
245
    }
246
247
    /**
248
     * Check if the query should be retried with spellchecking
249
     * @param BaseQuery $query
250
     * @param Result $result
251
     * @param SearchResult $searchResult
252
     * @return bool
253
     */
254 5
    protected function doRetry(BaseQuery $query, Result $result, SearchResult $searchResult): bool
255
    {
256 5
        return !$this->retry &&
257 5
            $query->shouldFollowSpellcheck() &&
258 5
            $searchResult->getCollatedSpellcheck();
259
    }
260
261
    /**
262
     * Retry the query with the first collated spellcheck found.
263
     *
264
     * @param BaseQuery $query
265
     * @param SearchResult $searchResult
266
     * @return SearchResult|mixed|ArrayData
267
     * @throws GuzzleException
268
     * @throws ValidationException
269
     */
270 1
    protected function spellcheckRetry(BaseQuery $query, SearchResult $searchResult)
271
    {
272 1
        $terms = $query->getTerms();
273 1
        $spellChecked = $searchResult->getCollatedSpellcheck();
274
        // Remove the fuzzyness from the collated check
275 1
        $term = preg_replace('/~\d+/', '', $spellChecked);
276 1
        $terms[0]['text'] = $term;
277 1
        $query->setTerms($terms);
278 1
        $this->retry = true;
279
280 1
        return $this->doSearch($query);
281
    }
282
283
    /**
284
     * @return array
285
     */
286 6
    public function getFieldsForIndexing(): array
287
    {
288 6
        $facets = [];
289 6
        foreach ($this->getFacetFields() as $field) {
290 5
            $facets[] = $field['Field'];
291
        }
292
        // Return values to make the key reset
293
        // Only return unique values
294
        // And make it all a single array
295 6
        $fields = array_values(
296 6
            array_unique(
297 6
                array_merge(
298 6
                    $this->getFulltextFields(),
299 6
                    $this->getSortFields(),
300 6
                    $facets,
301 6
                    $this->getFilterFields()
302
                )
303
            )
304
        );
305
306 6
        $this->extend('updateFieldsForIndexing', $fields);
307
308 6
        return $fields;
309
    }
310
311
    /**
312
     * Upload config for this index to the given store
313
     *
314
     * @param ConfigStore $store
315
     */
316 30
    public function uploadConfig(ConfigStore $store): void
317
    {
318
        // @todo use types/schema/elevate rendering
319
        // Upload the config files for this index
320
        // Create a default schema which we can manage later
321 30
        $schema = (string)$this->schemaService->generateSchema();
322 30
        $store->uploadString(
323 30
            $this->getIndexName(),
324 30
            'schema.xml',
325 30
            $schema
326
        );
327
328
329 30
        $synonyms = $this->getSynonyms();
330
331
        // Upload synonyms
332 30
        $store->uploadString(
333 30
            $this->getIndexName(),
334 30
            'synonyms.txt',
335 30
            $synonyms
336
        );
337
338
        // Upload additional files
339 30
        foreach (glob($this->schemaService->getExtrasPath() . '/*') as $file) {
340 30
            if (is_file($file)) {
341 30
                $store->uploadFile($this->getIndexName(), $file);
342
            }
343
        }
344 30
    }
345
346
    /**
347
     * Add synonyms. Public to be extendable
348
     * @param bool $defaults Include UK to US synonyms
349
     * @return string
350
     */
351 30
    public function getSynonyms($defaults = true): string
352
    {
353 30
        $synonyms = Synonyms::getSynonymsAsString($defaults);
354 30
        $siteConfigSynonyms = SiteConfig::current_site_config()->getField('SearchSynonyms');
355
356 30
        return sprintf('%s%s', $synonyms, $siteConfigSynonyms);
357
    }
358
359
    /**
360
     * @return array
361
     */
362 2
    public function getQueryTerms(): array
363
    {
364 2
        return $this->queryTerms;
365
    }
366
367
    /**
368
     * @return QueryComponentFactory
369
     */
370 1
    public function getQueryFactory(): QueryComponentFactory
371
    {
372 1
        return $this->queryFactory;
373
    }
374
}
375