Passed
Pull Request — main (#18)
by
unknown
12:40
created

SearchIndex::indexExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 10
c 0
b 0
f 0
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 indexExists(): bool
90
    {
91
        return $this->getConnection()
92
            ->getIndex($this->getName())
93
            ->exists();
94
    }
95
96
    /**
97
     * @inheritDoc
98
     */
99
    public function create(array $arguments = [], array $options = []): bool
100
    {
101
        if (empty(Hash::get($arguments, 'mappings.properties')) && !empty(static::$_properties)) {
102
            $arguments = Hash::insert($arguments, 'mappings.properties', static::$_properties);
103
        }
104
        if (empty(Hash::get($arguments, 'settings.analysis')) && !empty(static::$_analysis)) {
105
            $arguments = Hash::insert($arguments, 'settings.analysis', static::$_analysis);
106
        }
107
108
        $esIndex = $this->getConnection()->getIndex($this->getName());
109
        $response = $esIndex->create($arguments, $options);
110
        if (!$response->isOk()) {
111
            Log::error(sprintf(
112
                'Error creating index "%s": %s',
113
                $this->getName(),
114
                $response->getErrorMessage(),
115
            ));
116
117
            return false;
118
        }
119
120
        return true;
121
    }
122
123
    /**
124
     * @inheritDoc
125
     */
126
    public function updateProperties(array $properties = []): bool
127
    {
128
        if (empty($properties)) {
129
            $properties = static::$_properties;
130
        }
131
132
        $endpoint = new PutMapping();
133
        $endpoint->setBody(compact('properties'));
134
        $esIndex = $this->getConnection()->getIndex($this->getName());
135
        $response = $esIndex->requestEndpoint($endpoint);
136
        if (!$response->isOk()) {
137
            Log::error(sprintf(
138
                'Error updating index "%s" mappings: %s',
139
                $this->getName(),
140
                $response->getErrorMessage(),
141
            ));
142
143
            return false;
144
        }
145
146
        return true;
147
    }
148
149
    /**
150
     * @inheritDoc
151
     */
152
    public function updateAnalysis(array $analysis = []): bool
153
    {
154
        if (empty($analysis)) {
155
            $analysis = static::$_analysis;
156
        }
157
158
        // Adding new analyzers requires temporarily closing the index
159
        $esIndex = $this->getConnection()->getIndex($this->getName());
160
        $response = $esIndex->close();
161
        if (!$response->isOk()) {
162
            Log::error(sprintf(
163
                'Error closing the index "%s" before updating analysis settings: %s',
164
                $this->getName(),
165
                $response->getErrorMessage(),
166
            ));
167
168
            return false;
169
        }
170
171
        $endpoint = new PutSettings();
172
        $endpoint->setBody(compact('analysis'));
173
        $response = $esIndex->requestEndpoint($endpoint);
174
        if (!$response->isOk()) {
175
            Log::error(sprintf(
176
                'Error updating index "%s" settings: %s',
177
                $this->getName(),
178
                $response->getErrorMessage(),
179
            ));
180
181
            return false;
182
        }
183
184
        $response = $esIndex->open();
185
        if (!$response->isOk()) {
186
            Log::error(sprintf(
187
                'Error opening the index "%s" after updating analysis settings: %s',
188
                $this->getName(),
189
                $response->getErrorMessage(),
190
            ));
191
192
            return false;
193
        }
194
195
        return true;
196
    }
197
198
    /**
199
     * Delete a document from the index knowing its ID, or throw an exception upon failure.
200
     *
201
     * @param string $id Document ID.
202
     * @param array $options Options to be passed to {@see \Cake\ElasticSearch\Index::delete()} method.
203
     * @return void
204
     */
205
    public function deleteByIdOrFail(string $id, array $options = []): void
206
    {
207
        $document = $this->getIfExists($id);
208
        if ($document === null) {
209
            return;
210
        }
211
212
        $this->deleteOrFail($document, $options);
213
    }
214
215
    /**
216
     * Prepare data for indexing. This method may be overridden by implementations to customize indexed fields.
217
     *
218
     * If `null` is returned, entity indexing is skipped (and an entity with such ID is removed
219
     * from index if already present).
220
     *
221
     * @param \Cake\Datasource\EntityInterface $entity Entity to be indexed.
222
     * @return array<string, mixed>|null
223
     */
224
    protected function prepareData(EntityInterface $entity): array|null
225
    {
226
        return ['id' => (string)$entity->id] + $entity->toArray();
227
    }
228
229
    /**
230
     * @inheritDoc
231
     */
232
    public function reindex(EntityInterface $entity, string $operation): void
233
    {
234
        $id = (string)$entity->id;
235
        switch ($operation) {
236
            case 'edit':
237
                $data = $this->prepareData($entity);
238
                if ($data === null) {
239
                    $this->deleteByIdOrFail($id);
240
                } else {
241
                    $document = $this->patchEntity($this->getIfExists($id) ?: $this->newEmptyEntity(), $data);
242
                    $this->saveOrFail($document);
243
                }
244
                break;
245
246
            case 'delete':
247
                $this->deleteByIdOrFail($id);
248
                break;
249
250
            default:
251
                Log::warning(sprintf('Unknown operation on ElasticSearch reindex: %s', $operation));
252
        }
253
    }
254
255
    /**
256
     * @inheritDoc
257
     */
258
    public function findQuery(Query $query, array $options): Query
259
    {
260
        return $query->queryMust(
261
            fn (QueryBuilder $builder): AbstractQuery => $builder->simpleQueryString('title', $options['query']),
262
        );
263
    }
264
}
265