Passed
Push — hans/code-cleanup ( ab8c06...ca2456 )
by Simon
11:20 queued 08:45
created

BaseIndex   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Test Coverage

Coverage 90.18%

Importance

Changes 16
Bugs 2 Features 0
Metric Value
wmc 27
eloc 113
c 16
b 2
f 0
dl 0
loc 318
ccs 101
cts 112
cp 0.9018
rs 10

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getConfig() 0 7 2
A initFromConfig() 0 16 4
A init() 0 18 3
A __construct() 0 18 1
A getFieldsForIndexing() 0 23 2
A getQueryTerms() 0 3 1
A buildSolrQuery() 0 12 1
A doRetry() 0 6 4
A buildFactory() 0 12 1
A spellcheckRetry() 0 8 1
A getSynonyms() 0 6 1
A getQueryFactory() 0 3 1
A uploadConfig() 0 26 3
A doSearch() 0 22 2
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 60
    public function __construct()
76
    {
77
        // Set up the client
78 60
        $config = Config::inst()->get(SolrCoreService::class, 'config');
79 60
        $config['endpoint'] = $this->getConfig($config['endpoint']);
80 60
        $this->client = new Client($config);
81 60
        $this->client->setAdapter(new Guzzle());
82
83
        // Set up the schema service, only used in the generation of the schema
84 60
        $schemaService = Injector::inst()->get(SchemaService::class, false);
85 60
        $schemaService->setIndex($this);
86 60
        $schemaService->setStore(Director::isDev());
87 60
        $this->schemaService = $schemaService;
88 60
        $this->queryFactory = Injector::inst()->get(QueryComponentFactory::class, false);
89
90 60
        $this->extend('onBeforeInit');
91 60
        $this->init();
92 60
        $this->extend('onAfterInit');
93 60
    }
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 60
    public function getConfig($endpoints): array
102
    {
103 60
        foreach ($endpoints as $host => $endpoint) {
104 60
            $endpoints[$host]['core'] = $this->getIndexName();
105
        }
106
107 60
        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 59
    public function init()
122
    {
123 59
        if (!self::config()->get($this->getIndexName())) {
124 59
            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 59
            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 59
            return;
135
        }
136
137
138 59
        $this->initFromConfig();
139 59
    }
140
141
    /**
142
     * Generate the config from yml if possible
143
     */
144 59
    protected function initFromConfig(): void
145
    {
146 59
        $config = self::config()->get($this->getIndexName());
147
148 59
        if (!array_key_exists('Classes', $config)) {
149
            throw new LogicException('No classes to index found!');
150
        }
151
152 59
        $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 59
        foreach (self::$fieldTypes as $type) {
157 59
            if (array_key_exists($type, $config)) {
158 59
                $method = 'set' . $type;
159 59
                $this->$method($config[$type]);
160
            }
161
        }
162 59
    }
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
        // Build the actual query parameters
173 4
        $clientQuery = $this->buildSolrQuery($query);
174
175 4
        $this->extend('onBeforeSearch', $query, $clientQuery);
176
177 4
        $result = $this->client->select($clientQuery);
178
179 4
        $this->rawQuery = $result;
180
181
        // Handle the after search first. This gets a raw search result
182 4
        $this->extend('onAfterSearch', $result);
183 4
        $searchResult = new SearchResult($result, $query, $this);
184 4
        if ($this->doRetry($query, $result, $searchResult)) {
185
            return $this->spellcheckRetry($query, $searchResult);
186
        }
187
188
        // And then handle the search results, which is a useable object for SilverStripe
189 4
        $this->extend('updateSearchResults', $searchResult);
190
191 4
        return $searchResult;
192
    }
193
194
    /**
195
     * @param BaseQuery $query
196
     * @return Query
197
     */
198 4
    public function buildSolrQuery(BaseQuery $query): Query
199
    {
200 4
        $clientQuery = $this->client->createSelect();
201 4
        $factory = $this->buildFactory($query, $clientQuery);
202
203 4
        $clientQuery = $factory->buildQuery();
204 4
        $this->queryTerms = $factory->getQueryArray();
205
206 4
        $queryData = implode(' ', $this->queryTerms);
207 4
        $clientQuery->setQuery($queryData);
208
209 4
        return $clientQuery;
210
    }
211
212
    /**
213
     * @param BaseQuery $query
214
     * @param Query $clientQuery
215
     * @return QueryComponentFactory|mixed
216
     */
217 4
    protected function buildFactory(BaseQuery $query, Query $clientQuery)
218
    {
219 4
        $factory = $this->queryFactory;
220
221 4
        $helper = $clientQuery->getHelper();
222
223 4
        $factory->setQuery($query);
224 4
        $factory->setClientQuery($clientQuery);
225 4
        $factory->setHelper($helper);
226 4
        $factory->setIndex($this);
227
228 4
        return $factory;
229
    }
230
231
    /**
232
     * Check if the query should be retried with spellchecking
233
     * @param BaseQuery $query
234
     * @param Result $result
235
     * @param SearchResult $searchResult
236
     * @return bool
237
     */
238 4
    protected function doRetry(BaseQuery $query, Result $result, SearchResult $searchResult): bool
239
    {
240 4
        return !$this->retry &&
241 4
            $query->shouldFollowSpellcheck() &&
242 4
            $result->getNumFound() === 0 &&
243 4
            $searchResult->getCollatedSpellcheck();
244
    }
245
246
    /**
247
     * @param BaseQuery $query
248
     * @param SearchResult $searchResult
249
     * @return SearchResult|mixed|ArrayData
250
     */
251
    protected function spellcheckRetry(BaseQuery $query, SearchResult $searchResult)
252
    {
253
        $terms = $query->getTerms();
254
        $terms[0]['text'] = $searchResult->getCollatedSpellcheck();
255
        $query->setTerms($terms);
256
        $this->retry = true;
257
258
        return $this->doSearch($query);
259
    }
260
261
    /**
262
     * @return array
263
     */
264 6
    public function getFieldsForIndexing(): array
265
    {
266 6
        $facets = [];
267 6
        foreach ($this->getFacetFields() as $field) {
268 5
            $facets[] = $field['Field'];
269
        }
270
        // Return values to make the key reset
271
        // Only return unique values
272
        // And make it all a single array
273 6
        $fields = array_values(
274 6
            array_unique(
275 6
                array_merge(
276 6
                    $this->getFulltextFields(),
277 6
                    $this->getSortFields(),
278 6
                    $facets,
279 6
                    $this->getFilterFields()
280
                )
281
            )
282
        );
283
284 6
        $this->extend('updateFieldsForIndexing', $fields);
285
286 6
        return $fields;
287
    }
288
289
    /**
290
     * Upload config for this index to the given store
291
     *
292
     * @param ConfigStore $store
293
     */
294 25
    public function uploadConfig(ConfigStore $store): void
295
    {
296
        // @todo use types/schema/elevate rendering
297
        // Upload the config files for this index
298
        // Create a default schema which we can manage later
299 25
        $schema = (string)$this->schemaService->generateSchema();
300 25
        $store->uploadString(
301 25
            $this->getIndexName(),
302 25
            'schema.xml',
303 25
            $schema
304
        );
305
306
307 25
        $synonyms = $this->getSynonyms();
308
309
        // Upload synonyms
310 25
        $store->uploadString(
311 25
            $this->getIndexName(),
312 25
            'synonyms.txt',
313 25
            $synonyms
314
        );
315
316
        // Upload additional files
317 25
        foreach (glob($this->schemaService->getExtrasPath() . '/*') as $file) {
318 25
            if (is_file($file)) {
319 25
                $store->uploadFile($this->getIndexName(), $file);
320
            }
321
        }
322 25
    }
323
324
    /**
325
     * Add synonyms. Public to be extendable
326
     * @param bool $defaults Include UK to US synonyms
327
     * @return string
328
     */
329 25
    public function getSynonyms($defaults = true): string
330
    {
331 25
        $synonyms = Synonyms::getSynonymsAsString($defaults);
332 25
        $siteConfigSynonyms = SiteConfig::current_site_config()->getField('SearchSynonyms');
333
334 25
        return sprintf('%s%s', $synonyms, $siteConfigSynonyms);
335
    }
336
337
    /**
338
     * @return array
339
     */
340 1
    public function getQueryTerms(): array
341
    {
342 1
        return $this->queryTerms;
343
    }
344
345
    /**
346
     * @return QueryComponentFactory
347
     */
348 1
    public function getQueryFactory(): QueryComponentFactory
349
    {
350 1
        return $this->queryFactory;
351
    }
352
}
353