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