Passed
Push — sheepy/introspection ( 17b395...901708 )
by Simon
08:33 queued 05:46
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\BaseIndexTrait;
14
use Firesphere\SolrSearch\Traits\GetterSetterTrait;
15
use LogicException;
16
use SilverStripe\Control\Director;
17
use SilverStripe\Core\Config\Config;
18
use SilverStripe\Core\Config\Configurable;
19
use SilverStripe\Core\Extensible;
20
use SilverStripe\Core\Injector\Injector;
21
use SilverStripe\Dev\Deprecation;
22
use SilverStripe\SiteConfig\SiteConfig;
23
use SilverStripe\View\ArrayData;
24
use Solarium\Core\Client\Adapter\Guzzle;
25
use Solarium\Core\Client\Client;
26
use Solarium\QueryType\Select\Query\Query;
27
use Solarium\QueryType\Select\Result\Result;
28
29
/**
30
 * Class BaseIndex
31
 * @package Firesphere\SolrSearch\Indexes
32
 */
33
abstract class BaseIndex
34
{
35
    use Extensible;
36
    use Configurable;
37
    use GetterSetterTrait;
38
    use BaseIndexTrait;
39
40
    private static $fieldTypes = [
41
        'FulltextFields',
42
        'SortFields',
43
        'FilterFields',
44
        'BoostedFields',
45
        'CopyFields',
46
        'DefaultField',
47
        'FacetFields',
48
        'StoredFields'
49
    ];
50
    /**
51
     * @var \Solarium\Core\Query\Result\Result
52
     */
53
    protected $rawQuery;
54
    /**
55
     * @var SchemaService
56
     */
57
    protected $schemaService;
58
    /**
59
     * @var QueryComponentFactory
60
     */
61
    protected $queryFactory;
62
    /**
63
     * The query terms as an array
64
     * @var array
65
     */
66
    protected $queryTerms = [];
67
    /**
68
     * @var bool
69
     */
70
    private $retry = false;
71
72
    /**
73
     * BaseIndex constructor.
74
     */
75 59
    public function __construct()
76
    {
77
        // Set up the client
78 59
        $config = Config::inst()->get(SolrCoreService::class, 'config');
79 59
        $config['endpoint'] = $this->getConfig($config['endpoint']);
80 59
        $this->client = new Client($config);
81 59
        $this->client->setAdapter(new Guzzle());
82
83
        // Set up the schema service, only used in the generation of the schema
84 59
        $schemaService = Injector::inst()->get(SchemaService::class, false);
85 59
        $schemaService->setIndex($this);
86 59
        $schemaService->setStore(Director::isDev());
87 59
        $this->schemaService = $schemaService;
88 59
        $this->queryFactory = Injector::inst()->get(QueryComponentFactory::class, false);
89
90 59
        $this->extend('onBeforeInit');
91 59
        $this->init();
92 59
        $this->extend('onAfterInit');
93 59
    }
94
95
    /**
96
     * Build a full config for all given endpoints
97
     * This is to add the current index to e.g. an index or select
98
     * @param array $endpoints
99
     * @return array
100
     */
101 59
    public function getConfig($endpoints): array
102
    {
103 59
        foreach ($endpoints as $host => $endpoint) {
104 59
            $endpoints[$host]['core'] = $this->getIndexName();
105
        }
106
107 59
        return $endpoints;
108
    }
109
110
    /**
111
     * @return string
112
     */
113
    abstract public function getIndexName();
114
115
    /**
116
     * Required to initialise the fields.
117
     * It's loaded in to the non-static properties for backward compatibility with FTS
118
     * Also, it's a tad easier to use this way, loading the other way around would be very
119
     * memory intensive, as updating the config for each item is not efficient
120
     */
121 58
    public function init()
122
    {
123 58
        if (!self::config()->get($this->getIndexName())) {
124 58
            Deprecation::notice('5', 'Please set an index name and use a config yml');
125
126
            // If the old init method is found, skip the config based init
127 58
            if (!count($this->getClasses())) {
128
                Deprecation::notice(
129
                    '5',
130
                    'You are running init at the top of your method. The new API requires it to be at the bottom'
131
                );
132
            }
133
134 58
            return;
135
        }
136
137
138 58
        $this->initFromConfig();
139 58
    }
140
141
    /**
142
     * Generate the config from yml if possible
143
     */
144 58
    protected function initFromConfig(): void
145
    {
146 58
        $config = self::config()->get($this->getIndexName());
147
148 58
        if (!array_key_exists('Classes', $config)) {
149
            throw new LogicException('No classes to index found!');
150
        }
151
152 58
        $this->setClasses($config['Classes']);
153
154
        // For backward compatibility, copy the config to the protected values
155
        // Saves doubling up further down the line
156 58
        foreach (self::$fieldTypes as $type) {
157 58
            if (array_key_exists($type, $config)) {
158 58
                $method = 'set' . $type;
159 58
                $this->$method($config[$type]);
160
            }
161
        }
162 58
    }
163
164
    /**
165
     * Default returns a SearchResult. It can return an ArrayData if FTS Compat is enabled
166
     *
167
     * @param BaseQuery $query
168
     * @return SearchResult|ArrayData|mixed
169
     */
170 4
    public function doSearch(BaseQuery $query)
171
    {
172 4
        $this->extend('onBeforeSearch', $query);
173
        // Build the actual query parameters
174 4
        $clientQuery = $this->buildSolrQuery($query);
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 20
    public function getFieldsForIndexing(): array
264
    {
265 20
        $facets = [];
266 20
        foreach ($this->getFacetFields() as $field) {
267 19
            $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 20
        return array_values(
273 20
            array_unique(
274 20
                array_merge(
275 20
                    $this->getFulltextFields(),
276 20
                    $this->getSortFields(),
277 20
                    $facets,
278 20
                    $this->getFilterFields()
279
                )
280
            )
281
        );
282
    }
283
284
    /**
285
     * Upload config for this index to the given store
286
     *
287
     * @param ConfigStore $store
288
     */
289 25
    public function uploadConfig(ConfigStore $store): void
290
    {
291
        // @todo use types/schema/elevate rendering
292
        // Upload the config files for this index
293
        // Create a default schema which we can manage later
294 25
        $schema = (string)$this->schemaService->generateSchema();
295 25
        $store->uploadString(
296 25
            $this->getIndexName(),
297 25
            'schema.xml',
298 25
            $schema
299
        );
300
301
302 25
        $synonyms = $this->getSynonyms();
303
304
        // Upload synonyms
305 25
        $store->uploadString(
306 25
            $this->getIndexName(),
307 25
            'synonyms.txt',
308 25
            $synonyms
309
        );
310
311
        // Upload additional files
312 25
        foreach (glob($this->schemaService->getExtrasPath() . '/*') as $file) {
313 25
            if (is_file($file)) {
314 25
                $store->uploadFile($this->getIndexName(), $file);
315
            }
316
        }
317 25
    }
318
319
    /**
320
     * Add synonyms. Public to be extendable
321
     * @param bool $defaults Include UK to US synonyms
322
     * @return string
323
     */
324 25
    public function getSynonyms($defaults = true): string
325
    {
326 25
        $synonyms = Synonyms::getSynonymsAsString($defaults);
327 25
        $siteConfigSynonyms = SiteConfig::current_site_config()->getField('SearchSynonyms');
328
329 25
        return sprintf('%s%s', $synonyms, $siteConfigSynonyms);
330
    }
331
332
    /**
333
     * @return array
334
     */
335 1
    public function getQueryTerms(): array
336
    {
337 1
        return $this->queryTerms;
338
    }
339
340
    /**
341
     * @return QueryComponentFactory
342
     */
343 1
    public function getQueryFactory(): QueryComponentFactory
344
    {
345 1
        return $this->queryFactory;
346
    }
347
}
348