1<?php
2
3namespace Elastica;
4
5use Elastica\Exception\InvalidException;
6use Elastica\Exception\ResponseException;
7use Elastica\Index\Recovery as IndexRecovery;
8use Elastica\Index\Settings as IndexSettings;
9use Elastica\Index\Stats as IndexStats;
10use Elastica\ResultSet\BuilderInterface;
11use Elastica\Script\AbstractScript;
12use Elasticsearch\Endpoints\AbstractEndpoint;
13use Elasticsearch\Endpoints\DeleteByQuery;
14use Elasticsearch\Endpoints\Indices\Aliases\Update;
15use Elasticsearch\Endpoints\Indices\Analyze;
16use Elasticsearch\Endpoints\Indices\Cache\Clear;
17use Elasticsearch\Endpoints\Indices\Close;
18use Elasticsearch\Endpoints\Indices\Create;
19use Elasticsearch\Endpoints\Indices\Delete;
20use Elasticsearch\Endpoints\Indices\Exists;
21use Elasticsearch\Endpoints\Indices\Flush;
22use Elasticsearch\Endpoints\Indices\ForceMerge;
23use Elasticsearch\Endpoints\Indices\Mapping\Get;
24use Elasticsearch\Endpoints\Indices\Open;
25use Elasticsearch\Endpoints\Indices\Refresh;
26use Elasticsearch\Endpoints\Indices\Settings\Put;
27use Elasticsearch\Endpoints\UpdateByQuery;
28
29/**
30 * Elastica index object.
31 *
32 * Handles reads, deletes and configurations of an index
33 *
34 * @author   Nicolas Ruflin <spam@ruflin.com>
35 */
36class Index implements SearchableInterface
37{
38    /**
39     * Index name.
40     *
41     * @var string Index name
42     */
43    protected $_name;
44
45    /**
46     * Client object.
47     *
48     * @var \Elastica\Client Client object
49     */
50    protected $_client;
51
52    /**
53     * Creates a new index object.
54     *
55     * All the communication to and from an index goes of this object
56     *
57     * @param \Elastica\Client $client Client object
58     * @param string           $name   Index name
59     */
60    public function __construct(Client $client, $name)
61    {
62        $this->_client = $client;
63
64        if (!\is_scalar($name)) {
65            throw new InvalidException('Index name should be a scalar type');
66        }
67        $this->_name = (string) $name;
68    }
69
70    /**
71     * Returns a type object for the current index with the given name.
72     *
73     * @param string $type Type name
74     *
75     * @return \Elastica\Type Type object
76     */
77    public function getType($type)
78    {
79        return new Type($this, $type);
80    }
81
82    /**
83     * Return Index Stats.
84     *
85     * @return \Elastica\Index\Stats
86     */
87    public function getStats()
88    {
89        return new IndexStats($this);
90    }
91
92    /**
93     * Return Index Recovery.
94     *
95     * @return \Elastica\Index\Recovery
96     */
97    public function getRecovery()
98    {
99        return new IndexRecovery($this);
100    }
101
102    /**
103     * Gets all the type mappings for an index.
104     *
105     * @return array
106     */
107    public function getMapping()
108    {
109        $response = $this->requestEndpoint(new Get());
110        $data = $response->getData();
111
112        // Get first entry as if index is an Alias, the name of the mapping is the real name and not alias name
113        $mapping = \array_shift($data);
114
115        if (isset($mapping['mappings'])) {
116            return $mapping['mappings'];
117        }
118
119        return [];
120    }
121
122    /**
123     * Returns the index settings object.
124     *
125     * @return \Elastica\Index\Settings Settings object
126     */
127    public function getSettings()
128    {
129        return new IndexSettings($this);
130    }
131
132    /**
133     * Uses _bulk to send documents to the server.
134     *
135     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
136     * @param array                      $options Array of query params to use for query. For possible options check es api
137     *
138     * @return \Elastica\Bulk\ResponseSet
139     *
140     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
141     */
142    public function updateDocuments(array $docs, array $options = [])
143    {
144        foreach ($docs as $doc) {
145            $doc->setIndex($this->getName());
146        }
147
148        return $this->getClient()->updateDocuments($docs, $options);
149    }
150
151    /**
152     * Update entries in the db based on a query.
153     *
154     * @param \Elastica\Query|string|array $query   Query object or array
155     * @param AbstractScript               $script  Script
156     * @param array                        $options Optional params
157     *
158     * @return \Elastica\Response
159     *
160     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
161     */
162    public function updateByQuery($query, AbstractScript $script, array $options = [])
163    {
164        $query = Query::create($query)->getQuery();
165
166        $endpoint = new UpdateByQuery();
167        $body = ['query' => \is_array($query)
168            ? $query
169            : $query->toArray(), ];
170
171        $body['script'] = $script->toArray()['script'];
172        $endpoint->setBody($body);
173        $endpoint->setParams($options);
174
175        return $this->requestEndpoint($endpoint);
176    }
177
178    /**
179     * Uses _bulk to send documents to the server.
180     *
181     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
182     * @param array                      $options Array of query params to use for query. For possible options check es api
183     *
184     * @return \Elastica\Bulk\ResponseSet
185     *
186     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
187     */
188    public function addDocuments(array $docs, array $options = [])
189    {
190        foreach ($docs as $doc) {
191            $doc->setIndex($this->getName());
192        }
193
194        return $this->getClient()->addDocuments($docs, $options);
195    }
196
197    /**
198     * Deletes entries in the db based on a query.
199     *
200     * @param \Elastica\Query|\Elastica\Query\AbstractQuery|string|array $query   Query object or array
201     * @param array                                                      $options Optional params
202     *
203     * @return \Elastica\Response
204     *
205     * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/docs-delete-by-query.html
206     */
207    public function deleteByQuery($query, array $options = [])
208    {
209        $query = Query::create($query)->getQuery();
210
211        $endpoint = new DeleteByQuery();
212        $endpoint->setBody(['query' => \is_array($query) ? $query : $query->toArray()]);
213        $endpoint->setParams($options);
214
215        return $this->requestEndpoint($endpoint);
216    }
217
218    /**
219     * Deletes the index.
220     *
221     * @return \Elastica\Response Response object
222     */
223    public function delete()
224    {
225        return $this->requestEndpoint(new Delete());
226    }
227
228    /**
229     * Uses _bulk to delete documents from the server.
230     *
231     * @param array|\Elastica\Document[] $docs Array of Elastica\Document
232     *
233     * @return \Elastica\Bulk\ResponseSet
234     *
235     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
236     */
237    public function deleteDocuments(array $docs)
238    {
239        foreach ($docs as $doc) {
240            $doc->setIndex($this->getName());
241        }
242
243        return $this->getClient()->deleteDocuments($docs);
244    }
245
246    /**
247     * Force merges index.
248     *
249     * Detailed arguments can be found here in the link
250     *
251     * @param array $args OPTIONAL Additional arguments
252     *
253     * @return Response
254     *
255     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html
256     */
257    public function forcemerge($args = [])
258    {
259        $endpoint = new ForceMerge();
260        $endpoint->setParams($args);
261
262        return $this->requestEndpoint($endpoint);
263    }
264
265    /**
266     * Refreshes the index.
267     *
268     * @return \Elastica\Response Response object
269     *
270     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
271     */
272    public function refresh()
273    {
274        return $this->requestEndpoint(new Refresh());
275    }
276
277    /**
278     * Creates a new index with the given arguments.
279     *
280     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
281     *
282     * @param array      $args    OPTIONAL Arguments to use
283     * @param bool|array $options OPTIONAL
284     *                            bool=> Deletes index first if already exists (default = false).
285     *                            array => Associative array of options (option=>value)
286     *
287     * @throws \Elastica\Exception\InvalidException
288     * @throws \Elastica\Exception\ResponseException
289     *
290     * @return \Elastica\Response Server response
291     */
292    public function create(array $args = [], $options = null)
293    {
294        if (\is_bool($options) && $options) {
295            try {
296                $this->delete();
297            } catch (ResponseException $e) {
298                // Table can't be deleted, because doesn't exist
299            }
300        } elseif (\is_array($options)) {
301            foreach ($options as $key => $value) {
302                switch ($key) {
303                    case 'recreate':
304                        try {
305                            $this->delete();
306                        } catch (ResponseException $e) {
307                            // Table can't be deleted, because doesn't exist
308                        }
309                        break;
310                    default:
311                        throw new InvalidException('Invalid option '.$key);
312                        break;
313                }
314            }
315        }
316
317        $endpoint = new Create();
318        $endpoint->setBody($args);
319
320        return $this->requestEndpoint($endpoint);
321    }
322
323    /**
324     * Checks if the given index is already created.
325     *
326     * @return bool True if index exists
327     */
328    public function exists()
329    {
330        $response = $this->requestEndpoint(new Exists());
331
332        return 200 === $response->getStatus();
333    }
334
335    /**
336     * @param string|array|\Elastica\Query $query
337     * @param int|array                    $options
338     * @param BuilderInterface             $builder
339     *
340     * @return Search
341     */
342    public function createSearch($query = '', $options = null, BuilderInterface $builder = null)
343    {
344        $search = new Search($this->getClient(), $builder);
345        $search->addIndex($this);
346        $search->setOptionsAndQuery($options, $query);
347
348        return $search;
349    }
350
351    /**
352     * Searches in this index.
353     *
354     * @param string|array|\Elastica\Query $query   Array with all query data inside or a Elastica\Query object
355     * @param int|array                    $options OPTIONAL Limit or associative array of options (option=>value)
356     *
357     * @return \Elastica\ResultSet with all results inside
358     *
359     * @see \Elastica\SearchableInterface::search
360     */
361    public function search($query = '', $options = null)
362    {
363        $search = $this->createSearch($query, $options);
364
365        return $search->search();
366    }
367
368    /**
369     * Counts results of query.
370     *
371     * @param string|array|\Elastica\Query $query Array with all query data inside or a Elastica\Query object
372     *
373     * @return int number of documents matching the query
374     *
375     * @see \Elastica\SearchableInterface::count
376     */
377    public function count($query = '')
378    {
379        $search = $this->createSearch($query);
380
381        return $search->count();
382    }
383
384    /**
385     * Opens an index.
386     *
387     * @return \Elastica\Response Response object
388     *
389     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
390     */
391    public function open()
392    {
393        return $this->requestEndpoint(new Open());
394    }
395
396    /**
397     * Closes the index.
398     *
399     * @return \Elastica\Response Response object
400     *
401     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
402     */
403    public function close()
404    {
405        return $this->requestEndpoint(new Close());
406    }
407
408    /**
409     * Returns the index name.
410     *
411     * @return string Index name
412     */
413    public function getName()
414    {
415        return $this->_name;
416    }
417
418    /**
419     * Returns index client.
420     *
421     * @return \Elastica\Client Index client object
422     */
423    public function getClient()
424    {
425        return $this->_client;
426    }
427
428    /**
429     * Adds an alias to the current index.
430     *
431     * @param string $name    Alias name
432     * @param bool   $replace OPTIONAL If set, an existing alias will be replaced
433     *
434     * @return \Elastica\Response Response
435     *
436     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
437     */
438    public function addAlias($name, $replace = false)
439    {
440        $data = ['actions' => []];
441
442        if ($replace) {
443            $status = new Status($this->getClient());
444            foreach ($status->getIndicesWithAlias($name) as $index) {
445                $data['actions'][] = ['remove' => ['index' => $index->getName(), 'alias' => $name]];
446            }
447        }
448
449        $data['actions'][] = ['add' => ['index' => $this->getName(), 'alias' => $name]];
450
451        $endpoint = new Update();
452        $endpoint->setBody($data);
453
454        return $this->getClient()->requestEndpoint($endpoint);
455    }
456
457    /**
458     * Removes an alias pointing to the current index.
459     *
460     * @param string $name Alias name
461     *
462     * @return \Elastica\Response Response
463     *
464     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
465     */
466    public function removeAlias($name)
467    {
468        $endpoint = new \Elasticsearch\Endpoints\Indices\Alias\Delete();
469        $endpoint->setName($name);
470
471        return $this->requestEndpoint($endpoint);
472    }
473
474    /**
475     * Returns all index aliases.
476     *
477     * @return array Aliases
478     */
479    public function getAliases()
480    {
481        $endpoint = new \Elasticsearch\Endpoints\Indices\Alias\Get();
482        $endpoint->setName('*');
483
484        $responseData = $this->requestEndpoint($endpoint)->getData();
485
486        if (!isset($responseData[$this->getName()])) {
487            return [];
488        }
489
490        $data = $responseData[$this->getName()];
491        if (!empty($data['aliases'])) {
492            return \array_keys($data['aliases']);
493        }
494
495        return [];
496    }
497
498    /**
499     * Checks if the index has the given alias.
500     *
501     * @param string $name Alias name
502     *
503     * @return bool
504     */
505    public function hasAlias($name)
506    {
507        return \in_array($name, $this->getAliases());
508    }
509
510    /**
511     * Clears the cache of an index.
512     *
513     * @return \Elastica\Response Response object
514     *
515     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html
516     */
517    public function clearCache()
518    {
519        // TODO: add additional cache clean arguments
520        return $this->requestEndpoint(new Clear());
521    }
522
523    /**
524     * Flushes the index to storage.
525     *
526     * @param array $options
527     *
528     * @return Response Response object
529     *
530     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html
531     */
532    public function flush(array $options = [])
533    {
534        $endpoint = new Flush();
535        $endpoint->setParams($options);
536
537        return $this->requestEndpoint($endpoint);
538    }
539
540    /**
541     * Can be used to change settings during runtime. One example is to use it for bulk updating.
542     *
543     * @param array $data Data array
544     *
545     * @return \Elastica\Response Response object
546     *
547     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
548     */
549    public function setSettings(array $data)
550    {
551        $endpoint = new Put();
552        $endpoint->setBody($data);
553
554        return $this->requestEndpoint($endpoint);
555    }
556
557    /**
558     * Makes calls to the elasticsearch server based on this index.
559     *
560     * @param string       $path   Path to call
561     * @param string       $method Rest method to use (GET, POST, DELETE, PUT)
562     * @param array|string $data   OPTIONAL Arguments as array or encoded string
563     * @param array        $query  OPTIONAL Query params
564     *
565     * @return \Elastica\Response Response object
566     */
567    public function request($path, $method, $data = [], array $query = [])
568    {
569        $path = $this->getName().'/'.$path;
570
571        return $this->getClient()->request($path, $method, $data, $query);
572    }
573
574    /**
575     * Makes calls to the elasticsearch server with usage official client Endpoint based on this index.
576     *
577     * @param AbstractEndpoint $endpoint
578     *
579     * @return Response
580     */
581    public function requestEndpoint(AbstractEndpoint $endpoint)
582    {
583        $cloned = clone $endpoint;
584
585        $cloned->setIndex($this->getName());
586        if ($endpoint instanceof Create || $endpoint instanceof \Elasticsearch\Endpoints\Indices\Mapping\Put) {
587            $cloned->setParams(['include_type_name' => true]);
588        }
589
590        return $this->getClient()->requestEndpoint($cloned);
591    }
592
593    /**
594     * Analyzes a string.
595     *
596     * Detailed arguments can be found here in the link
597     *
598     * @param array $body String to be analyzed
599     * @param array $args OPTIONAL Additional arguments
600     *
601     * @return array Server response
602     *
603     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html
604     */
605    public function analyze(array $body, $args = [])
606    {
607        $endpoint = new Analyze();
608        $endpoint->setBody($body);
609        $endpoint->setParams($args);
610
611        $data = $this->requestEndpoint($endpoint)->getData();
612
613        // Support for "Explain" parameter, that returns a different response structure from Elastic
614        // @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/_explain_analyze.html
615        if (isset($body['explain']) && $body['explain']) {
616            return $data['detail'];
617        }
618
619        return $data['tokens'];
620    }
621}
622