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