Passed
Push — hans/code-cleanup ( 3f21ea...2d381d )
by Simon
09:28 queued 06:39
created

BaseIndex::initFromConfig()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

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