Passed
Push — hans/logtests ( 76c086...71695d )
by Simon
06:24 queued 02:27
created

BaseIndex::uploadConfig()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

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