ElasticConfigureTask::convertForJSON()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 6
eloc 14
c 1
b 1
f 0
nc 7
nop 1
dl 0
loc 21
rs 9.2222
1
<?php
2
/**
3
 * class ElasticConfigureTask|Firesphere\ElasticSearch\Tasks\ElasticConfigureTask Configure custom mappings in Elastic
4
 *
5
 * @package Firesphere\Elastic\Search
6
 * @author Simon `Firesphere` Erkelens; Marco `Sheepy` Hermo
7
 * @copyright Copyright (c) 2018 - now() Firesphere & Sheepy
8
 */
9
10
namespace Firesphere\ElasticSearch\Tasks;
11
12
use Elastic\Elasticsearch\Exception\ClientResponseException;
13
use Elastic\Elasticsearch\Exception\MissingParameterException;
14
use Elastic\Elasticsearch\Exception\ServerResponseException;
15
use Elastic\Elasticsearch\Response\Elasticsearch;
16
use Exception;
17
use Firesphere\ElasticSearch\Helpers\Statics;
18
use Firesphere\ElasticSearch\Indexes\ElasticIndex;
19
use Firesphere\ElasticSearch\Services\ElasticCoreService;
20
use Firesphere\SearchBackend\Helpers\FieldResolver;
21
use Firesphere\SearchBackend\Indexes\CoreIndex;
22
use Firesphere\SearchBackend\Traits\LoggerTrait;
23
use Psr\Container\NotFoundExceptionInterface;
24
use SilverStripe\Control\HTTPRequest;
25
use SilverStripe\Core\ClassInfo;
26
use SilverStripe\Core\Injector\Injector;
27
use SilverStripe\Dev\BuildTask;
28
use SilverStripe\ORM\DB;
29
30
/**
31
 * Class ElasticConfigureTask
32
 *
33
 * Used to create field-specific mappings in Elastic. Not explicitly needed for a basic search
34
 * functionality, but does add filter/sort/aggregate features without the performance drawback
35
 */
36
class ElasticConfigureTask extends BuildTask
37
{
38
    use LoggerTrait;
39
40
    /**
41
     * @var string URLSegment
42
     */
43
    private static $segment = 'ElasticConfigureTask';
0 ignored issues
show
introduced by
The private property $segment is not used, and could be removed.
Loading history...
44
    /**
45
     * DBHTML and DBText etc. should never be made sortable
46
     * It doesn't make sense for large text objects
47
     * @var string[]
48
     */
49
    private static $unSsortables = [
50
        'HTML',
51
        'Text'
52
    ];
53
    /**
54
     * @var bool[]
55
     */
56
    public $result;
57
    /**
58
     * @var string Title
59
     */
60
    protected $title = 'Configure Elastic cores';
61
    /**
62
     * @var string Description
63
     */
64
    protected $description = 'Create or reload a Elastic Core by adding or reloading a configuration.';
65
    /**
66
     * @var ElasticCoreService $service
67
     */
68
    protected $service;
69
70
    /**
71
     * @throws NotFoundExceptionInterface
72
     */
73
    public function __construct()
74
    {
75
        parent::__construct();
76
        $this->service = Injector::inst()->get(ElasticCoreService::class);
77
    }
78
79
    /**
80
     * Run the config
81
     *
82
     * @param HTTPRequest $request
83
     * @return void
84
     * @throws NotFoundExceptionInterface
85
     */
86
    public function run($request)
87
    {
88
        $this->extend('onBeforeElasticConfigureTask', $request);
89
90
        $indexes = $this->service->getValidIndexes();
91
92
        $result = [];
93
94
        foreach ($indexes as $index) {
95
            try {
96
                /** @var ElasticIndex $instance */
97
                $instance = Injector::inst()->get($index, false);
98
                // If delete in advance, do so
99
                $instance->deleteIndex($request);
100
                $configResult = $this->configureIndex($instance);
101
                $result[] = $configResult->asBool();
102
            } catch (Exception $error) {
103
                // @codeCoverageIgnoreStart
104
                $this->getLogger()->error(sprintf('Core loading failed for %s', $index));
105
                $this->getLogger()->error($error->getMessage()); // in browser mode, it might not always show
106
                // Continue to the next index
107
                continue;
108
                // @codeCoverageIgnoreEnd
109
            }
110
            $this->extend('onAfterConfigureIndex', $index);
111
        }
112
113
        $this->extend('onAfterElasticConfigureTask');
114
115
        if ($request->getVar('istest')) {
116
            $this->result = $result;
117
        }
118
    }
119
120
    /**
121
     * Update/create a single index.
122
     * @param ElasticIndex $index
123
     * @return Elasticsearch
124
     * @throws ClientResponseException
125
     * @throws MissingParameterException
126
     * @throws NotFoundExceptionInterface
127
     * @throws ServerResponseException
128
     */
129
    public function configureIndex(CoreIndex $index): Elasticsearch
130
    {
131
        $indexName = $index->getIndexName();
132
133
        $instanceConfig = $this->createConfigForIndex($index);
134
135
        $mappings = $this->convertForJSON($instanceConfig);
136
137
        $body = ['index' => $indexName];
138
        $client = $this->service->getClient();
139
140
        $method = $this->getMethod($index);
141
        $msg = "%s index %s";
142
        $msgType = 'Updating';
143
        if ($method === 'create') {
144
            $mappings = ['mappings' => $mappings];
145
            $msgType = 'Creating';
146
        }
147
        $body['body'] = $mappings;
148
        $msg = sprintf($msg, $msgType, $indexName);
149
        DB::alteration_message($msg);
150
        $this->getLogger()->info($msg);
151
152
        return $client->indices()->$method($body);
153
    }
154
155
    /**
156
     * @param ElasticIndex $instance
157
     * @return array
158
     * @throws NotFoundExceptionInterface
159
     */
160
    protected function createConfigForIndex(ElasticIndex $instance)
161
    {
162
        /** @var FieldResolver $resolver */
163
        $resolver = Injector::inst()->get(FieldResolver::class);
164
        $resolver->setIndex($instance);
165
        $result = [];
166
167
        foreach ($instance->getFulltextFields() as $field) {
168
            $field = $resolver->resolveField($field);
169
            $result = array_merge($result, $field);
170
        }
171
172
        return $result;
173
    }
174
175
    /**
176
     * Take the config from the resolver and build an array that's
177
     * ready to be converted to JSON for Elastic.
178
     *
179
     * @param $config
180
     * @return array[]
181
     */
182
    protected function convertForJSON($config)
183
    {
184
        $base = [];
185
        $typeMap = Statics::getTypeMap();
186
        foreach ($config as $key => &$conf) {
187
            $shortClass = ClassInfo::shortName($conf['origin']);
188
            $dotField = str_replace('_', '.', $conf['fullfield']);
189
            $conf['name'] = sprintf('%s.%s', $shortClass, $dotField);
190
            $base[$conf['name']] = [
191
                'type' => $typeMap[$conf['type'] ?? '*']
192
            ];
193
            $shouldHold = true;
194
            foreach (self::$unSsortables as $unSortable) {
195
                $shouldHold = !str_contains($conf['type'], $unSortable) && $shouldHold;
196
            }
197
            if ($shouldHold && $typeMap[$conf['type']] === 'text') {
198
                $base[$conf['name']]['fielddata'] = true;
199
            }
200
        }
201
202
        return ['properties' => $base];
203
    }
204
205
    /**
206
     * Get the method to use. Create or Update
207
     *
208
     * WARNING: Update often fails because Elastic does not allow changing
209
     * of mappings on-the-fly, it will commonly require a delete-and-recreate!
210
     *
211
     * @param ElasticIndex $index
212
     * @return string
213
     * @throws ClientResponseException
214
     * @throws MissingParameterException
215
     * @throws ServerResponseException
216
     */
217
    protected function getMethod(ElasticIndex $index): string
218
    {
219
        $check = $index->indexExists();
220
221
        if ($check) {
222
            return 'putMapping';
223
        }
224
225
        return 'create';
226
    }
227
228
    /**
229
     * @return ElasticCoreService|mixed|object|Injector
230
     */
231
    public function getService(): mixed
232
    {
233
        return $this->service;
234
    }
235
}
236