Passed
Push — sheepy/introspection ( 851cdd...69e16c )
by Marco
06: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 0
CRAP Score 20

Importance

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