SearchIndex::updateProperties()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 13
c 1
b 0
f 1
nc 4
nop 1
dl 0
loc 21
rs 9.8333
1
<?php
2
declare(strict_types=1);
3
4
namespace BEdita\ElasticSearch\Model\Index;
5
6
use Cake\Database\DriverInterface;
7
use Cake\Datasource\ConnectionManager;
8
use Cake\Datasource\EntityInterface;
9
use Cake\ElasticSearch\Index;
10
use Cake\ElasticSearch\Query;
11
use Cake\ElasticSearch\QueryBuilder;
12
use Cake\Log\Log;
13
use Cake\Log\LogTrait;
14
use Cake\Utility\Hash;
15
use Cake\Utility\Inflector;
16
use Elastica\Query\AbstractQuery;
17
use Elasticsearch\Endpoints\Indices\PutMapping;
18
use Elasticsearch\Endpoints\Indices\PutSettings;
19
20
/**
21
 * Base search index for ElasticSearch.
22
 */
23
class SearchIndex extends Index implements AdapterCompatibleInterface
24
{
25
    use IndexTrait;
26
    use LogTrait;
27
28
    /**
29
     * Map of fields and their configuration (type, analyzer, normalizer, etc.).
30
     *
31
     * This property must be overridden by implementations to customize the mappings used by the index.
32
     *
33
     * @var array
34
     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-params.html
35
     */
36
    protected static array $_properties = [];
37
38
    /**
39
     * Settings for the text analysis (define/configure analyzers, tokenizers, filters, etc.).
40
     *
41
     * @var array
42
     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/configure-text-analysis.html
43
     */
44
    protected static array $_analysis = [];
45
46
    /**
47
     * Returns the index name.
48
     *
49
     * If it isn't set, it is constructed from the default collection schema and the alias for the index.
50
     *
51
     * @return string
52
     */
53
    public function getName(): string
54
    {
55
        if ($this->_name === null) {
56
            $defaultName = $this->getDefaultName();
57
            if ($defaultName !== null) {
58
                $this->_name = $defaultName;
59
            }
60
        }
61
62
        return parent::getName();
63
    }
64
65
    /**
66
     * Returns the default index name, constructed from the default collection schema and the alias for the index.
67
     *
68
     * @return string|null
69
     */
70
    protected function getDefaultName(): string|null
71
    {
72
        $driver = ConnectionManager::get('default')->getDriver();
73
        if (!$driver instanceof DriverInterface) {
74
            return null;
75
        }
76
77
        $prefix = $driver->schema();
78
        $suffix = Inflector::underscore($this->getAlias());
79
        if (empty($prefix) || empty($suffix)) {
80
            return null;
81
        }
82
83
        return $prefix . '_' . $suffix;
84
    }
85
86
    /**
87
     * @inheritDoc
88
     */
89
    public function create(array $arguments = [], array $options = []): bool
90
    {
91
        if (empty(Hash::get($arguments, 'mappings.properties')) && !empty(static::$_properties)) {
92
            $arguments = Hash::insert($arguments, 'mappings.properties', static::$_properties);
93
        }
94
        if (empty(Hash::get($arguments, 'settings.analysis')) && !empty(static::$_analysis)) {
95
            $arguments = Hash::insert($arguments, 'settings.analysis', static::$_analysis);
96
        }
97
98
        $esIndex = $this->getConnection()->getIndex($this->getName());
99
        $response = $esIndex->create($arguments, $options);
100
        if (!$response->isOk()) {
101
            Log::error(sprintf(
102
                'Error creating index "%s": %s',
103
                $this->getName(),
104
                $response->getErrorMessage(),
105
            ));
106
107
            return false;
108
        }
109
110
        return true;
111
    }
112
113
    /**
114
     * @inheritDoc
115
     */
116
    public function updateProperties(array $properties = []): bool
117
    {
118
        if (empty($properties)) {
119
            $properties = static::$_properties;
120
        }
121
122
        $endpoint = new PutMapping();
123
        $endpoint->setBody(compact('properties'));
124
        $esIndex = $this->getConnection()->getIndex($this->getName());
125
        $response = $esIndex->requestEndpoint($endpoint);
126
        if (!$response->isOk()) {
127
            Log::error(sprintf(
128
                'Error updating index "%s" mappings: %s',
129
                $this->getName(),
130
                $response->getErrorMessage(),
131
            ));
132
133
            return false;
134
        }
135
136
        return true;
137
    }
138
139
    /**
140
     * @inheritDoc
141
     */
142
    public function updateAnalysis(array $analysis = []): bool
143
    {
144
        if (empty($analysis)) {
145
            $analysis = static::$_analysis;
146
        }
147
148
        // Adding new analyzers requires temporarily closing the index
149
        $esIndex = $this->getConnection()->getIndex($this->getName());
150
        $response = $esIndex->close();
151
        if (!$response->isOk()) {
152
            Log::error(sprintf(
153
                'Error closing the index "%s" before updating analysis settings: %s',
154
                $this->getName(),
155
                $response->getErrorMessage(),
156
            ));
157
158
            return false;
159
        }
160
161
        $endpoint = new PutSettings();
162
        $endpoint->setBody(compact('analysis'));
163
        $response = $esIndex->requestEndpoint($endpoint);
164
        if (!$response->isOk()) {
165
            Log::error(sprintf(
166
                'Error updating index "%s" settings: %s',
167
                $this->getName(),
168
                $response->getErrorMessage(),
169
            ));
170
171
            return false;
172
        }
173
174
        $response = $esIndex->open();
175
        if (!$response->isOk()) {
176
            Log::error(sprintf(
177
                'Error opening the index "%s" after updating analysis settings: %s',
178
                $this->getName(),
179
                $response->getErrorMessage(),
180
            ));
181
182
            return false;
183
        }
184
185
        return true;
186
    }
187
188
    /**
189
     * Delete a document from the index knowing its ID, or throw an exception upon failure.
190
     *
191
     * @param string $id Document ID.
192
     * @param array $options Options to be passed to {@see \Cake\ElasticSearch\Index::delete()} method.
193
     * @return void
194
     */
195
    public function deleteByIdOrFail(string $id, array $options = []): void
196
    {
197
        $document = $this->getIfExists($id);
198
        if ($document === null) {
199
            return;
200
        }
201
202
        $this->deleteOrFail($document, $options);
203
    }
204
205
    /**
206
     * Prepare data for indexing. This method may be overridden by implementations to customize indexed fields.
207
     *
208
     * If `null` is returned, entity indexing is skipped (and an entity with such ID is removed
209
     * from index if already present).
210
     *
211
     * @param \Cake\Datasource\EntityInterface $entity Entity to be indexed.
212
     * @return array<string, mixed>|null
213
     */
214
    protected function prepareData(EntityInterface $entity): array|null
215
    {
216
        return ['id' => (string)$entity->id] + $entity->toArray();
217
    }
218
219
    /**
220
     * @inheritDoc
221
     */
222
    public function reindex(EntityInterface $entity, string $operation): void
223
    {
224
        $id = (string)$entity->id;
225
        switch ($operation) {
226
            case 'edit':
227
                $data = $this->prepareData($entity);
228
                if ($data === null) {
229
                    $this->deleteByIdOrFail($id);
230
                } else {
231
                    $document = $this->patchEntity($this->getIfExists($id) ?: $this->newEmptyEntity(), $data);
232
                    $this->saveOrFail($document);
233
                }
234
                break;
235
236
            case 'delete':
237
                $this->deleteByIdOrFail($id);
238
                break;
239
240
            default:
241
                Log::warning(sprintf('Unknown operation on ElasticSearch reindex: %s', $operation));
242
        }
243
    }
244
245
    /**
246
     * @inheritDoc
247
     */
248
    public function findQuery(Query $query, array $options): Query
249
    {
250
        return $query->queryMust(
251
            fn (QueryBuilder $builder): AbstractQuery => $builder->simpleQueryString('title', $options['query']),
252
        );
253
    }
254
}
255