1<?php
2
3namespace Elastica\Index;
4
5use Elastica\Exception\ClientException;
6use Elastica\Exception\ConnectionException;
7use Elastica\Exception\NotFoundException;
8use Elastica\Exception\ResponseException;
9use Elastica\Index as BaseIndex;
10use Elastica\Request;
11use Elastica\Response;
12
13/**
14 * Elastica index settings object.
15 *
16 * All settings listed in the update settings API (https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html)
17 * can be changed on a running indices. To make changes like the merge policy (https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html)
18 * the index has to be closed first and reopened after the call
19 *
20 * @author Nicolas Ruflin <spam@ruflin.com>
21 *
22 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
23 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
24 */
25class Settings
26{
27    public const DEFAULT_REFRESH_INTERVAL = '1s';
28
29    public const DEFAULT_NUMBER_OF_REPLICAS = 1;
30
31    public const DEFAULT_NUMBER_OF_SHARDS = 1;
32
33    /**
34     * Response.
35     *
36     * @var Response Response object
37     */
38    protected $_response;
39
40    /**
41     * Stats info.
42     *
43     * @var array Stats info
44     */
45    protected $_data = [];
46
47    /**
48     * Index.
49     *
50     * @var BaseIndex Index object
51     */
52    protected $_index;
53
54    /**
55     * Construct.
56     *
57     * @param BaseIndex $index Index object
58     */
59    public function __construct(BaseIndex $index)
60    {
61        $this->_index = $index;
62    }
63
64    /**
65     * Returns the current settings of the index.
66     *
67     * If param is set, only specified setting is return.
68     * 'index.' is added in front of $setting.
69     *
70     * @param string $setting OPTIONAL Setting name to return
71     *
72     * @throws ClientException
73     * @throws ConnectionException
74     * @throws ResponseException
75     *
76     * @return array|int|string|null Settings data
77     *
78     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
79     */
80    public function get(string $setting = '', bool $includeDefaults = false)
81    {
82        $queryParameters = [
83            'include_defaults' => $includeDefaults,
84        ];
85
86        $requestData = $this->request([], Request::GET, $queryParameters)->getData();
87        $data = \reset($requestData);
88
89        if (empty($data['settings']) || empty($data['settings']['index'])) {
90            // should not append, the request should throw a ResponseException
91            throw new NotFoundException('Index '.$this->getIndex()->getName().' not found');
92        }
93
94        $settings = $data['settings']['index'];
95        $defaults = $data['defaults']['index'] ?? [];
96
97        $settings = \array_merge($defaults, $settings);
98
99        if (!$setting) {
100            // return all array
101            return $settings;
102        }
103
104        if (isset($settings[$setting])) {
105            return $settings[$setting];
106        }
107
108        if (false !== \strpos($setting, '.')) {
109            // translate old dot-notation settings to nested arrays
110            $keys = \explode('.', $setting);
111            foreach ($keys as $key) {
112                if (isset($settings[$key])) {
113                    $settings = $settings[$key];
114                } else {
115                    return null;
116                }
117            }
118
119            return $settings;
120        }
121
122        return null;
123    }
124
125    /**
126     * Returns a setting interpreted as a bool.
127     *
128     * One can use a real bool, int(0), int(1) to set bool settings.
129     * But Elasticsearch stores and returns all settings as strings and does
130     * not normalize bool values. This method ensures a bool is returned for
131     * whichever string representation is used like 'true', '1', 'on', 'yes'.
132     *
133     * @param string $setting Setting name to return
134     *
135     * @throws ClientException
136     * @throws ConnectionException
137     * @throws ResponseException
138     */
139    public function getBool(string $setting): bool
140    {
141        $data = $this->get($setting);
142
143        return 'true' === $data || '1' === $data || 'on' === $data || 'yes' === $data;
144    }
145
146    /**
147     * Sets the number of replicas.
148     *
149     * @param int $replicas Number of replicas
150     *
151     * @throws ClientException
152     * @throws ConnectionException
153     * @throws ResponseException
154     *
155     * @return Response Response object
156     */
157    public function setNumberOfReplicas(int $replicas): Response
158    {
159        return $this->set(['number_of_replicas' => $replicas]);
160    }
161
162    /**
163     * Returns the number of replicas.
164     *
165     * If no number of replicas is set, the default number is returned
166     *
167     * @throws ClientException
168     * @throws ConnectionException
169     * @throws ResponseException
170     *
171     * @return int The number of replicas
172     */
173    public function getNumberOfReplicas(): int
174    {
175        return $this->get('number_of_replicas') ?? self::DEFAULT_NUMBER_OF_REPLICAS;
176    }
177
178    /**
179     * Returns the number of shards.
180     *
181     * If no number of shards is set, the default number is returned
182     *
183     * @throws ClientException
184     * @throws ConnectionException
185     * @throws ResponseException
186     *
187     * @return int The number of shards
188     */
189    public function getNumberOfShards(): int
190    {
191        return $this->get('number_of_shards') ?? self::DEFAULT_NUMBER_OF_SHARDS;
192    }
193
194    /**
195     * Sets the index to read only.
196     *
197     * @param bool $readOnly (default = true)
198     *
199     * @throws ClientException
200     * @throws ConnectionException
201     * @throws ResponseException
202     */
203    public function setReadOnly(bool $readOnly = true): Response
204    {
205        return $this->set(['blocks.read_only' => $readOnly]);
206    }
207
208    /**
209     * @throws ClientException
210     * @throws ConnectionException
211     * @throws ResponseException
212     */
213    public function getReadOnly(): bool
214    {
215        return $this->getBool('blocks.read_only');
216    }
217
218    /**
219     * @throws ClientException
220     * @throws ConnectionException
221     * @throws ResponseException
222     */
223    public function getBlocksRead(): bool
224    {
225        return $this->getBool('blocks.read');
226    }
227
228    /**
229     * @param bool $state OPTIONAL (default = true)
230     *
231     * @throws ClientException
232     * @throws ConnectionException
233     * @throws ResponseException
234     */
235    public function setBlocksRead(bool $state = true): Response
236    {
237        return $this->set(['blocks.read' => $state]);
238    }
239
240    /**
241     * @throws ClientException
242     * @throws ConnectionException
243     * @throws ResponseException
244     */
245    public function getBlocksWrite(): bool
246    {
247        return $this->getBool('blocks.write');
248    }
249
250    /**
251     * @param bool $state OPTIONAL (default = true)
252     *
253     * @throws ClientException
254     * @throws ConnectionException
255     * @throws ResponseException
256     */
257    public function setBlocksWrite(bool $state = true): Response
258    {
259        return $this->set(['blocks.write' => $state]);
260    }
261
262    /**
263     * @throws ClientException
264     * @throws ConnectionException
265     * @throws ResponseException
266     */
267    public function getBlocksMetadata(): bool
268    {
269        // When blocks.metadata is enabled, reading the settings is not possible anymore.
270        // So when a cluster_block_exception happened it must be enabled.
271        try {
272            return $this->getBool('blocks.metadata');
273        } catch (ResponseException $e) {
274            if ('cluster_block_exception' === $e->getResponse()->getFullError()['type']) {
275                return true;
276            }
277
278            throw $e;
279        }
280    }
281
282    /**
283     * Set to true to disable index metadata reads and writes.
284     *
285     * @param bool $state OPTIONAL (default = true)
286     *
287     * @throws ClientException
288     * @throws ConnectionException
289     * @throws ResponseException
290     */
291    public function setBlocksMetadata(bool $state = true): Response
292    {
293        return $this->set(['blocks.metadata' => $state]);
294    }
295
296    /**
297     * Sets the index refresh interval.
298     *
299     * Value can be for example 3s for 3 seconds or
300     * 5m for 5 minutes. -1 to disabled refresh.
301     *
302     * @param string $interval Duration of the refresh interval
303     *
304     * @throws ClientException
305     * @throws ConnectionException
306     * @throws ResponseException
307     *
308     * @return Response Response object
309     */
310    public function setRefreshInterval(string $interval): Response
311    {
312        return $this->set(['refresh_interval' => $interval]);
313    }
314
315    /**
316     * Returns the refresh interval.
317     *
318     * If no interval is set, the default interval is returned
319     *
320     * @throws ClientException
321     * @throws ConnectionException
322     * @throws ResponseException
323     *
324     * @return string Refresh interval
325     */
326    public function getRefreshInterval(): string
327    {
328        return $this->get('refresh_interval') ?? self::DEFAULT_REFRESH_INTERVAL;
329    }
330
331    /**
332     * Sets the specific merge policies.
333     *
334     * To have this changes made the index has to be closed and reopened
335     *
336     * @param string     $key   Merge policy key (for ex. expunge_deletes_allowed)
337     * @param int|string $value
338     *
339     * @throws ClientException
340     * @throws ConnectionException
341     * @throws ResponseException
342     *
343     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
344     */
345    public function setMergePolicy(string $key, $value): Response
346    {
347        $this->_index->close();
348        $response = $this->set(['merge.policy.'.$key => $value]);
349        $this->_index->open();
350
351        return $response;
352    }
353
354    /**
355     * Returns the specific merge policy value.
356     *
357     * @param string $key Merge policy key (for ex. expunge_deletes_allowed)
358     *
359     * @throws ClientException
360     * @throws ConnectionException
361     * @throws ResponseException
362     *
363     * @return int|string
364     *
365     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
366     */
367    public function getMergePolicy(string $key)
368    {
369        $settings = $this->get();
370
371        return $settings['merge']['policy'][$key] ?? null;
372    }
373
374    /**
375     * Can be used to set/update settings.
376     *
377     * @param array $data Arguments
378     *
379     * @throws ClientException
380     * @throws ConnectionException
381     * @throws ResponseException
382     *
383     * @return Response Response object
384     */
385    public function set(array $data): Response
386    {
387        return $this->request($data, Request::PUT);
388    }
389
390    /**
391     * Returns the index object.
392     *
393     * @return BaseIndex Index object
394     */
395    public function getIndex(): BaseIndex
396    {
397        return $this->_index;
398    }
399
400    /**
401     * Updates the given settings for the index.
402     *
403     * With elasticsearch 0.16 the following settings are supported
404     * - index.term_index_interval
405     * - index.term_index_divisor
406     * - index.translog.flush_threshold_ops
407     * - index.translog.flush_threshold_size
408     * - index.translog.flush_threshold_period
409     * - index.refresh_interval
410     * - index.merge.policy
411     * - index.auto_expand_replicas
412     *
413     * @param array  $data   OPTIONAL Data array
414     * @param string $method OPTIONAL Transfer method (default = \Elastica\Request::GET)
415     *
416     * @throws ClientException
417     * @throws ConnectionException
418     * @throws ResponseException
419     *
420     * @return Response Response object
421     */
422    public function request(array $data = [], string $method = Request::GET, array $queryParameters = []): Response
423    {
424        $path = '_settings';
425
426        if ($data) {
427            $data = ['index' => $data];
428        }
429
430        return $this->getIndex()->request($path, $method, $data, $queryParameters);
431    }
432}
433