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