xref: /plugin/elasticsearch/vendor/ruflin/elastica/src/Index.php (revision d832d53af2a0f84c34c8d5f17c93ffaa85869e5d)
1<?php
2
3namespace Elastica;
4
5use Elastica\Bulk\ResponseSet;
6use Elastica\Exception\InvalidException;
7use Elastica\Exception\NotFoundException;
8use Elastica\Exception\ResponseException;
9use Elastica\Index\Recovery as IndexRecovery;
10use Elastica\Index\Settings as IndexSettings;
11use Elastica\Index\Stats as IndexStats;
12use Elastica\Query\AbstractQuery;
13use Elastica\ResultSet\BuilderInterface;
14use Elastica\Script\AbstractScript;
15use Elasticsearch\Endpoints\AbstractEndpoint;
16use Elasticsearch\Endpoints\DeleteByQuery;
17use Elasticsearch\Endpoints\Get as DocumentGet;
18use Elasticsearch\Endpoints\Index as IndexEndpoint;
19use Elasticsearch\Endpoints\Indices\Alias;
20use Elasticsearch\Endpoints\Indices\Aliases\Update;
21use Elasticsearch\Endpoints\Indices\Analyze;
22use Elasticsearch\Endpoints\Indices\Cache\Clear;
23use Elasticsearch\Endpoints\Indices\ClearCache;
24use Elasticsearch\Endpoints\Indices\Close;
25use Elasticsearch\Endpoints\Indices\Create;
26use Elasticsearch\Endpoints\Indices\Delete;
27use Elasticsearch\Endpoints\Indices\DeleteAlias;
28use Elasticsearch\Endpoints\Indices\Exists;
29use Elasticsearch\Endpoints\Indices\Flush;
30use Elasticsearch\Endpoints\Indices\ForceMerge;
31use Elasticsearch\Endpoints\Indices\GetAlias;
32use Elasticsearch\Endpoints\Indices\GetMapping;
33use Elasticsearch\Endpoints\Indices\Mapping\Get as MappingGet;
34use Elasticsearch\Endpoints\Indices\Open;
35use Elasticsearch\Endpoints\Indices\PutSettings;
36use Elasticsearch\Endpoints\Indices\Refresh;
37use Elasticsearch\Endpoints\Indices\Settings\Put;
38use Elasticsearch\Endpoints\Indices\UpdateAliases;
39use Elasticsearch\Endpoints\OpenPointInTime;
40use Elasticsearch\Endpoints\UpdateByQuery;
41
42/**
43 * Elastica index object.
44 *
45 * Handles reads, deletes and configurations of an index
46 *
47 * @author   Nicolas Ruflin <spam@ruflin.com>
48 * @phpstan-import-type TCreateQueryArgsMatching from Query
49 */
50class Index implements SearchableInterface
51{
52    /**
53     * Index name.
54     *
55     * @var string Index name
56     */
57    protected $_name;
58
59    /**
60     * Client object.
61     *
62     * @var Client Client object
63     */
64    protected $_client;
65
66    /**
67     * Creates a new index object.
68     *
69     * All the communication to and from an index goes of this object
70     *
71     * @param Client $client Client object
72     * @param string $name   Index name
73     */
74    public function __construct(Client $client, string $name)
75    {
76        $this->_client = $client;
77        $this->_name = $name;
78    }
79
80    /**
81     * Return Index Stats.
82     *
83     * @return IndexStats
84     */
85    public function getStats()
86    {
87        return new IndexStats($this);
88    }
89
90    /**
91     * Return Index Recovery.
92     *
93     * @return IndexRecovery
94     */
95    public function getRecovery()
96    {
97        return new IndexRecovery($this);
98    }
99
100    /**
101     * Sets the mappings for the current index.
102     *
103     * @param Mapping $mapping MappingType object
104     * @param array   $query   querystring when put mapping (for example update_all_types)
105     */
106    public function setMapping(Mapping $mapping, array $query = []): Response
107    {
108        return $mapping->send($this, $query);
109    }
110
111    /**
112     * Gets all mappings for the current index.
113     */
114    public function getMapping(): array
115    {
116        // TODO: Use only GetMapping when dropping support for elasticsearch/elasticsearch 7.x
117        $endpoint = \class_exists(GetMapping::class) ? new GetMapping() : new MappingGet();
118
119        $response = $this->requestEndpoint($endpoint);
120        $data = $response->getData();
121
122        // Get first entry as if index is an Alias, the name of the mapping is the real name and not alias name
123        $mapping = \array_shift($data);
124
125        return $mapping['mappings'] ?? [];
126    }
127
128    /**
129     * Returns the index settings object.
130     *
131     * @return IndexSettings
132     */
133    public function getSettings()
134    {
135        return new IndexSettings($this);
136    }
137
138    /**
139     * @param array|string $data
140     *
141     * @return Document
142     */
143    public function createDocument(string $id = '', $data = [])
144    {
145        return new Document($id, $data, $this);
146    }
147
148    /**
149     * Uses _bulk to send documents to the server.
150     *
151     * @param Document[] $docs    Array of Elastica\Document
152     * @param array      $options Array of query params to use for query. For possible options check es api
153     *
154     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
155     */
156    public function updateDocuments(array $docs, array $options = []): ResponseSet
157    {
158        foreach ($docs as $doc) {
159            $doc->setIndex($this->getName());
160        }
161
162        return $this->getClient()->updateDocuments($docs, $options);
163    }
164
165    /**
166     * Update entries in the db based on a query.
167     *
168     * @param AbstractQuery|array|Query|string|null $query Query object or array
169     * @phpstan-param TCreateQueryArgsMatching $query
170     *
171     * @param AbstractScript $script  Script
172     * @param array          $options Optional params
173     *
174     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
175     */
176    public function updateByQuery($query, AbstractScript $script, array $options = []): Response
177    {
178        $endpoint = new UpdateByQuery();
179        $q = Query::create($query)->getQuery();
180        $body = [
181            'query' => \is_array($q) ? $q : $q->toArray(),
182            'script' => $script->toArray()['script'],
183        ];
184
185        $endpoint->setBody($body);
186        $endpoint->setParams($options);
187
188        return $this->requestEndpoint($endpoint);
189    }
190
191    /**
192     * Adds the given document to the search index.
193     */
194    public function addDocument(Document $doc): Response
195    {
196        $endpoint = new IndexEndpoint();
197
198        if (null !== $doc->getId() && '' !== $doc->getId()) {
199            $endpoint->setId($doc->getId());
200        }
201
202        $options = $doc->getOptions(
203            [
204                'consistency',
205                'op_type',
206                'parent',
207                'percolate',
208                'pipeline',
209                'refresh',
210                'replication',
211                'retry_on_conflict',
212                'routing',
213                'timeout',
214            ]
215        );
216
217        $endpoint->setBody($doc->getData());
218        $endpoint->setParams($options);
219
220        $response = $this->requestEndpoint($endpoint);
221
222        $data = $response->getData();
223        // set autogenerated id to document
224        if ($response->isOk() && (
225            $doc->isAutoPopulate() || $this->getClient()->getConfigValue(['document', 'autoPopulate'], false)
226        )) {
227            if (isset($data['_id']) && !$doc->hasId()) {
228                $doc->setId($data['_id']);
229            }
230            $doc->setVersionParams($data);
231        }
232
233        return $response;
234    }
235
236    /**
237     * Uses _bulk to send documents to the server.
238     *
239     * @param array|Document[] $docs    Array of Elastica\Document
240     * @param array            $options Array of query params to use for query. For possible options check es api
241     *
242     * @return ResponseSet
243     *
244     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
245     */
246    public function addDocuments(array $docs, array $options = [])
247    {
248        foreach ($docs as $doc) {
249            $doc->setIndex($this->getName());
250        }
251
252        return $this->getClient()->addDocuments($docs, $options);
253    }
254
255    /**
256     * Get the document from search index.
257     *
258     * @param int|string $id      Document id
259     * @param array      $options options for the get request
260     *
261     * @throws ResponseException
262     * @throws NotFoundException
263     */
264    public function getDocument($id, array $options = []): Document
265    {
266        $endpoint = new DocumentGet();
267        $endpoint->setId($id);
268        $endpoint->setParams($options);
269
270        $response = $this->requestEndpoint($endpoint);
271        $result = $response->getData();
272
273        if (!isset($result['found']) || false === $result['found']) {
274            throw new NotFoundException('doc id '.$id.' not found');
275        }
276
277        if (isset($result['fields'])) {
278            $data = $result['fields'];
279        } elseif (isset($result['_source'])) {
280            $data = $result['_source'];
281        } else {
282            $data = [];
283        }
284
285        $doc = new Document($id, $data, $this->getName());
286        $doc->setVersionParams($result);
287
288        return $doc;
289    }
290
291    /**
292     * Deletes a document by its unique identifier.
293     *
294     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html
295     */
296    public function deleteById(string $id, array $options = []): Response
297    {
298        if (!\trim($id)) {
299            throw new NotFoundException('Doc id "'.$id.'" not found and can not be deleted');
300        }
301
302        $endpoint = new \Elasticsearch\Endpoints\Delete();
303        $endpoint->setId(\trim($id));
304        $endpoint->setParams($options);
305
306        return $this->requestEndpoint($endpoint);
307    }
308
309    /**
310     * Deletes documents matching the given query.
311     *
312     * @param AbstractQuery|array|Query|string|null $query Query object or array
313     * @phpstan-param TCreateQueryArgsMatching $query
314     *
315     * @param array $options Optional params
316     *
317     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html
318     */
319    public function deleteByQuery($query, array $options = []): Response
320    {
321        $query = Query::create($query)->getQuery();
322
323        $endpoint = new DeleteByQuery();
324        $endpoint->setBody(['query' => \is_array($query) ? $query : $query->toArray()]);
325        $endpoint->setParams($options);
326
327        return $this->requestEndpoint($endpoint);
328    }
329
330    /**
331     * Opens a Point-in-Time on the index.
332     *
333     * @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html
334     */
335    public function openPointInTime(string $keepAlive): Response
336    {
337        $endpoint = new OpenPointInTime();
338        $endpoint->setParams(['keep_alive' => $keepAlive]);
339
340        return $this->requestEndpoint($endpoint);
341    }
342
343    /**
344     * Deletes the index.
345     */
346    public function delete(): Response
347    {
348        return $this->requestEndpoint(new Delete());
349    }
350
351    /**
352     * Uses the "_bulk" endpoint to delete documents from the server.
353     *
354     * @param Document[] $docs Array of documents
355     *
356     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
357     */
358    public function deleteDocuments(array $docs): ResponseSet
359    {
360        foreach ($docs as $doc) {
361            $doc->setIndex($this->getName());
362        }
363
364        return $this->getClient()->deleteDocuments($docs);
365    }
366
367    /**
368     * Force merges index.
369     *
370     * Detailed arguments can be found here in the ES documentation.
371     *
372     * @param array $args Additional arguments
373     *
374     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html
375     */
376    public function forcemerge($args = []): Response
377    {
378        $endpoint = new ForceMerge();
379        $endpoint->setParams($args);
380
381        return $this->requestEndpoint($endpoint);
382    }
383
384    /**
385     * Refreshes the index.
386     *
387     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
388     */
389    public function refresh(): Response
390    {
391        return $this->requestEndpoint(new Refresh());
392    }
393
394    /**
395     * Creates a new index with the given arguments.
396     *
397     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
398     *
399     * @param array      $args    Additional arguments to pass to the Create endpoint
400     * @param array|bool $options OPTIONAL
401     *                            bool=> Deletes index first if already exists (default = false).
402     *                            array => Associative array of options (option=>value)
403     *
404     * @throws InvalidException
405     * @throws ResponseException
406     *
407     * @return Response Server response
408     */
409    public function create(array $args = [], $options = null): Response
410    {
411        if (null === $options) {
412            if (\func_num_args() >= 2) {
413                \trigger_deprecation('ruflin/elastica', '7.1.0', 'Passing null as 2nd argument to "%s()" is deprecated, avoid passing this argument or pass an array instead. It will be removed in 8.0.', __METHOD__);
414            }
415            $options = [];
416        } elseif (\is_bool($options)) {
417            \trigger_deprecation('ruflin/elastica', '7.1.0', 'Passing a bool as 2nd argument to "%s()" is deprecated, pass an array with the key "recreate" instead. It will be removed in 8.0.', __METHOD__);
418            $options = ['recreate' => $options];
419        } elseif (!\is_array($options)) {
420            throw new \TypeError(\sprintf('Argument 2 passed to "%s()" must be of type array|bool|null, %s given.', __METHOD__, \is_object($options) ? \get_class($options) : \gettype($options)));
421        }
422
423        $endpoint = new Create();
424        $invalidOptions = \array_diff(\array_keys($options), $allowedOptions = \array_merge($endpoint->getParamWhitelist(), [
425            'recreate',
426        ]));
427
428        if (1 === $invalidOptionCount = \count($invalidOptions)) {
429            throw new InvalidException(\sprintf('"%s" is not a valid option. Allowed options are "%s".', \implode('", "', $invalidOptions), \implode('", "', $allowedOptions)));
430        }
431
432        if ($invalidOptionCount > 1) {
433            throw new InvalidException(\sprintf('"%s" are not valid options. Allowed options are "%s".', \implode('", "', $invalidOptions), \implode('", "', $allowedOptions)));
434        }
435
436        if ($options['recreate'] ?? false) {
437            try {
438                $this->delete();
439            } catch (ResponseException $e) {
440                // Index can't be deleted, because it doesn't exist
441            }
442        }
443
444        unset($options['recreate']);
445
446        $endpoint->setParams($options);
447        $endpoint->setBody($args);
448
449        return $this->requestEndpoint($endpoint);
450    }
451
452    /**
453     * Checks if the given index exists ans is created.
454     */
455    public function exists(): bool
456    {
457        $response = $this->requestEndpoint(new Exists());
458
459        return 200 === $response->getStatus();
460    }
461
462    /**
463     * {@inheritdoc}
464     */
465    public function createSearch($query = '', $options = null, ?BuilderInterface $builder = null): Search
466    {
467        $search = new Search($this->getClient(), $builder);
468        $search->addIndex($this);
469        $search->setOptionsAndQuery($options, $query);
470
471        return $search;
472    }
473
474    /**
475     * {@inheritdoc}
476     */
477    public function search($query = '', $options = null, string $method = Request::POST): ResultSet
478    {
479        $search = $this->createSearch($query, $options);
480
481        return $search->search('', null, $method);
482    }
483
484    /**
485     * {@inheritdoc}
486     */
487    public function count($query = '', string $method = Request::POST): int
488    {
489        $search = $this->createSearch($query);
490
491        return $search->count('', false, $method);
492    }
493
494    /**
495     * Opens an index.
496     *
497     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
498     */
499    public function open(): Response
500    {
501        return $this->requestEndpoint(new Open());
502    }
503
504    /**
505     * Closes the index.
506     *
507     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
508     */
509    public function close(): Response
510    {
511        return $this->requestEndpoint(new Close());
512    }
513
514    /**
515     * Returns the index name.
516     */
517    public function getName(): string
518    {
519        return $this->_name;
520    }
521
522    /**
523     * Returns index client.
524     */
525    public function getClient(): Client
526    {
527        return $this->_client;
528    }
529
530    /**
531     * Adds an alias to the current index.
532     *
533     * @param bool $replace If set, an existing alias will be replaced
534     *
535     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
536     */
537    public function addAlias(string $name, bool $replace = false): Response
538    {
539        $data = ['actions' => []];
540
541        if ($replace) {
542            $status = new Status($this->getClient());
543            foreach ($status->getIndicesWithAlias($name) as $index) {
544                $data['actions'][] = ['remove' => ['index' => $index->getName(), 'alias' => $name]];
545            }
546        }
547
548        $data['actions'][] = ['add' => ['index' => $this->getName(), 'alias' => $name]];
549
550        // TODO: Use only UpdateAliases when dropping support for elasticsearch/elasticsearch 7.x
551        $endpoint = \class_exists(UpdateAliases::class) ? new UpdateAliases() : new Update();
552        $endpoint->setBody($data);
553
554        return $this->getClient()->requestEndpoint($endpoint);
555    }
556
557    /**
558     * Removes an alias pointing to the current index.
559     *
560     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
561     */
562    public function removeAlias(string $name): Response
563    {
564        // TODO: Use only DeleteAlias when dropping support for elasticsearch/elasticsearch 7.x
565        $endpoint = \class_exists(DeleteAlias::class) ? new DeleteAlias() : new Alias\Delete();
566        $endpoint->setName($name);
567
568        return $this->requestEndpoint($endpoint);
569    }
570
571    /**
572     * Returns all index aliases.
573     *
574     * @return string[]
575     */
576    public function getAliases(): array
577    {
578        // TODO: Use only GetAlias when dropping support for elasticsearch/elasticsearch 7.x
579        $endpoint = \class_exists(GetAlias::class) ? new GetAlias() : new Alias\Get();
580        $endpoint->setName('*');
581
582        $responseData = $this->requestEndpoint($endpoint)->getData();
583
584        if (!isset($responseData[$this->getName()])) {
585            return [];
586        }
587
588        $data = $responseData[$this->getName()];
589        if (!empty($data['aliases'])) {
590            return \array_keys($data['aliases']);
591        }
592
593        return [];
594    }
595
596    /**
597     * Checks if the index has the given alias.
598     */
599    public function hasAlias(string $name): bool
600    {
601        return \in_array($name, $this->getAliases(), true);
602    }
603
604    /**
605     * Clears the cache of an index.
606     *
607     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html
608     */
609    public function clearCache(): Response
610    {
611        // TODO: Use only ClearCache when dropping support for elasticsearch/elasticsearch 7.x
612        $endpoint = \class_exists(ClearCache::class) ? new ClearCache() : new Clear();
613
614        // TODO: add additional cache clean arguments
615        return $this->requestEndpoint($endpoint);
616    }
617
618    /**
619     * Flushes the index to storage.
620     *
621     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html
622     */
623    public function flush(array $options = []): Response
624    {
625        $endpoint = new Flush();
626        $endpoint->setParams($options);
627
628        return $this->requestEndpoint($endpoint);
629    }
630
631    /**
632     * Can be used to change settings during runtime. One example is to use it for bulk updating.
633     *
634     * @param array $data Data array
635     *
636     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
637     */
638    public function setSettings(array $data): Response
639    {
640        // TODO: Use only PutSettings when dropping support for elasticsearch/elasticsearch 7.x
641        $endpoint = \class_exists(PutSettings::class) ? new PutSettings() : new Put();
642        $endpoint->setBody($data);
643
644        return $this->requestEndpoint($endpoint);
645    }
646
647    /**
648     * Makes calls to the elasticsearch server based on this index.
649     *
650     * @param string       $path   Path to call
651     * @param string       $method Rest method to use (GET, POST, DELETE, PUT)
652     * @param array|string $data   Arguments as array or encoded string
653     */
654    public function request(string $path, string $method, $data = [], array $queryParameters = []): Response
655    {
656        $path = $this->getName().'/'.$path;
657
658        return $this->getClient()->request($path, $method, $data, $queryParameters);
659    }
660
661    /**
662     * Makes calls to the elasticsearch server with usage official client Endpoint based on this index.
663     */
664    public function requestEndpoint(AbstractEndpoint $endpoint): Response
665    {
666        $cloned = clone $endpoint;
667        $cloned->setIndex($this->getName());
668
669        return $this->getClient()->requestEndpoint($cloned);
670    }
671
672    /**
673     * Run the analysis on the index.
674     *
675     * @param array $body request body for the `_analyze` API, see API documentation for the required properties
676     * @param array $args Additional arguments
677     *
678     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html
679     */
680    public function analyze(array $body, $args = []): array
681    {
682        $endpoint = new Analyze();
683        $endpoint->setBody($body);
684        $endpoint->setParams($args);
685
686        $data = $this->requestEndpoint($endpoint)->getData();
687
688        // Support for "Explain" parameter, that returns a different response structure from Elastic
689        // @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/_explain_analyze.html
690        if (isset($body['explain']) && $body['explain']) {
691            return $data['detail'];
692        }
693
694        return $data['tokens'];
695    }
696
697    /**
698     * Update document, using update script.
699     *
700     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html
701     *
702     * @param AbstractScript|Document $data    Document or Script with update data
703     * @param array                   $options array of query params to use for query
704     */
705    public function updateDocument($data, array $options = []): Response
706    {
707        if (!($data instanceof Document) && !($data instanceof AbstractScript)) {
708            throw new \InvalidArgumentException('Data should be a Document or Script');
709        }
710
711        if (!$data->hasId()) {
712            throw new InvalidException('Document or Script id is not set');
713        }
714
715        return $this->getClient()->updateDocument($data->getId(), $data, $this->getName(), $options);
716    }
717}
718