* @phpstan-import-type TCreateQueryArgsMatching from Query */ class Index implements SearchableInterface { /** * Index name. * * @var string Index name */ protected $_name; /** * Client object. * * @var Client Client object */ protected $_client; /** * Creates a new index object. * * All the communication to and from an index goes of this object * * @param Client $client Client object * @param string $name Index name */ public function __construct(Client $client, string $name) { $this->_client = $client; $this->_name = $name; } /** * Return Index Stats. * * @return IndexStats */ public function getStats() { return new IndexStats($this); } /** * Return Index Recovery. * * @return IndexRecovery */ public function getRecovery() { return new IndexRecovery($this); } /** * Sets the mappings for the current index. * * @param Mapping $mapping MappingType object * @param array $query querystring when put mapping (for example update_all_types) */ public function setMapping(Mapping $mapping, array $query = []): Response { return $mapping->send($this, $query); } /** * Gets all mappings for the current index. * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function getMapping(): array { // TODO: Use only GetMapping when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(GetMapping::class) ? new GetMapping() : new MappingGet(); $response = $this->requestEndpoint($endpoint); $data = $response->getData(); // Get first entry as if index is an Alias, the name of the mapping is the real name and not alias name $mapping = \array_shift($data); return $mapping['mappings'] ?? []; } /** * Returns the index settings object. * * @return IndexSettings */ public function getSettings() { return new IndexSettings($this); } /** * @param array|string $data * * @return Document */ public function createDocument(string $id = '', $data = []) { return new Document($id, $data, $this); } /** * Uses _bulk to send documents to the server. * * @param Document[] $docs Array of Elastica\Document * @param array $options Array of query params to use for query. For possible options check es api * * @throws ClientException * @throws ConnectionException * @throws ResponseException * @throws BulkResponseException * @throws InvalidException * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html */ public function updateDocuments(array $docs, array $options = []): ResponseSet { foreach ($docs as $doc) { $doc->setIndex($this->getName()); } return $this->getClient()->updateDocuments($docs, $options); } /** * Update entries in the db based on a query. * * @param AbstractQuery|array|Query|string|null $query Query object or array * @phpstan-param TCreateQueryArgsMatching $query * * @param AbstractScript $script Script * @param array $options Optional params * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function updateByQuery($query, AbstractScript $script, array $options = []): Response { $endpoint = new UpdateByQuery(); $q = Query::create($query)->getQuery(); $body = [ 'query' => \is_array($q) ? $q : $q->toArray(), 'script' => $script->toArray()['script'], ]; $endpoint->setBody($body); $endpoint->setParams($options); return $this->requestEndpoint($endpoint); } /** * Adds the given document to the search index. * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function addDocument(Document $doc): Response { $endpoint = new IndexEndpoint(); if (null !== $doc->getId() && '' !== $doc->getId()) { $endpoint->setId($doc->getId()); } $options = $doc->getOptions( [ 'consistency', 'op_type', 'parent', 'percolate', 'pipeline', 'refresh', 'replication', 'retry_on_conflict', 'routing', 'timeout', ] ); $endpoint->setBody($doc->getData()); $endpoint->setParams($options); $response = $this->requestEndpoint($endpoint); $data = $response->getData(); // set autogenerated id to document if ($response->isOk() && ( $doc->isAutoPopulate() || $this->getClient()->getConfigValue(['document', 'autoPopulate'], false) )) { if (isset($data['_id']) && !$doc->hasId()) { $doc->setId($data['_id']); } $doc->setVersionParams($data); } return $response; } /** * Uses _bulk to send documents to the server. * * @param array|Document[] $docs Array of Elastica\Document * @param array $options Array of query params to use for query. For possible options check es api * * @throws ClientException * @throws ConnectionException * @throws ResponseException * @throws BulkResponseException * @throws InvalidException * * @return ResponseSet * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html */ public function addDocuments(array $docs, array $options = []) { foreach ($docs as $doc) { $doc->setIndex($this->getName()); } return $this->getClient()->addDocuments($docs, $options); } /** * Get the document from search index. * * @param int|string $id Document id * @param array $options options for the get request * * @throws NotFoundException * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function getDocument($id, array $options = []): Document { $endpoint = new DocumentGet(); $endpoint->setId($id); $endpoint->setParams($options); $response = $this->requestEndpoint($endpoint); $result = $response->getData(); if (!isset($result['found']) || false === $result['found']) { throw new NotFoundException('doc id '.$id.' not found'); } if (isset($result['fields'])) { $data = $result['fields']; } elseif (isset($result['_source'])) { $data = $result['_source']; } else { $data = []; } $doc = new Document($id, $data, $this->getName()); $doc->setVersionParams($result); return $doc; } /** * Deletes a document by its unique identifier. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function deleteById(string $id, array $options = []): Response { if (!\trim($id)) { throw new NotFoundException('Doc id "'.$id.'" not found and can not be deleted'); } $endpoint = new \Elasticsearch\Endpoints\Delete(); $endpoint->setId(\trim($id)); $endpoint->setParams($options); return $this->requestEndpoint($endpoint); } /** * Deletes documents matching the given query. * * @param AbstractQuery|array|Query|string|null $query Query object or array * @phpstan-param TCreateQueryArgsMatching $query * * @param array $options Optional params * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete-by-query.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function deleteByQuery($query, array $options = []): Response { $query = Query::create($query)->getQuery(); $endpoint = new DeleteByQuery(); $endpoint->setBody(['query' => \is_array($query) ? $query : $query->toArray()]); $endpoint->setParams($options); return $this->requestEndpoint($endpoint); } /** * Opens a Point-in-Time on the index. * * @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function openPointInTime(string $keepAlive): Response { $endpoint = new OpenPointInTime(); $endpoint->setParams(['keep_alive' => $keepAlive]); return $this->requestEndpoint($endpoint); } /** * Deletes the index. * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function delete(): Response { return $this->requestEndpoint(new Delete()); } /** * Uses the "_bulk" endpoint to delete documents from the server. * * @param Document[] $docs Array of documents * * @throws ClientException * @throws ConnectionException * @throws ResponseException * @throws BulkResponseException * @throws InvalidException * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html */ public function deleteDocuments(array $docs): ResponseSet { foreach ($docs as $doc) { $doc->setIndex($this->getName()); } return $this->getClient()->deleteDocuments($docs); } /** * Force merges index. * * Detailed arguments can be found here in the ES documentation. * * @param array $args Additional arguments * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function forcemerge($args = []): Response { $endpoint = new ForceMerge(); $endpoint->setParams($args); return $this->requestEndpoint($endpoint); } /** * Refreshes the index. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function refresh(): Response { return $this->requestEndpoint(new Refresh()); } /** * Creates a new index with the given arguments. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html * * @param array $args Additional arguments to pass to the Create endpoint * @param array|bool $options OPTIONAL * bool=> Deletes index first if already exists (default = false). * array => Associative array of options (option=>value) * * @throws InvalidException * @throws ClientException * @throws ConnectionException * @throws ResponseException * * @return Response Server response */ public function create(array $args = [], $options = null): Response { if (null === $options) { if (\func_num_args() >= 2) { \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__); } $options = []; } elseif (\is_bool($options)) { \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__); $options = ['recreate' => $options]; } elseif (!\is_array($options)) { 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))); } $endpoint = new Create(); $invalidOptions = \array_diff(\array_keys($options), $allowedOptions = \array_merge($endpoint->getParamWhitelist(), [ 'recreate', ])); if (1 === $invalidOptionCount = \count($invalidOptions)) { throw new InvalidException(\sprintf('"%s" is not a valid option. Allowed options are "%s".', \implode('", "', $invalidOptions), \implode('", "', $allowedOptions))); } if ($invalidOptionCount > 1) { throw new InvalidException(\sprintf('"%s" are not valid options. Allowed options are "%s".', \implode('", "', $invalidOptions), \implode('", "', $allowedOptions))); } if ($options['recreate'] ?? false) { try { $this->delete(); } catch (ResponseException $e) { // Index can't be deleted, because it doesn't exist } } unset($options['recreate']); $endpoint->setParams($options); $endpoint->setBody($args); return $this->requestEndpoint($endpoint); } /** * Checks if the given index exists ans is created. * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function exists(): bool { $response = $this->requestEndpoint(new Exists()); return 200 === $response->getStatus(); } /** * {@inheritdoc} */ public function createSearch($query = '', $options = null, ?BuilderInterface $builder = null): Search { $search = new Search($this->getClient(), $builder); $search->addIndex($this); $search->setOptionsAndQuery($options, $query); return $search; } /** * {@inheritdoc} */ public function search($query = '', $options = null, string $method = Request::POST): ResultSet { $search = $this->createSearch($query, $options); return $search->search('', null, $method); } /** * {@inheritdoc} */ public function count($query = '', string $method = Request::POST): int { $search = $this->createSearch($query); return $search->count('', false, $method); } /** * Opens an index. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function open(): Response { return $this->requestEndpoint(new Open()); } /** * Closes the index. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function close(): Response { return $this->requestEndpoint(new Close()); } /** * Returns the index name. */ public function getName(): string { return $this->_name; } /** * Returns index client. */ public function getClient(): Client { return $this->_client; } /** * Adds an alias to the current index. * * @param bool $replace If set, an existing alias will be replaced * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function addAlias(string $name, bool $replace = false): Response { $data = ['actions' => []]; if ($replace) { $status = new Status($this->getClient()); foreach ($status->getIndicesWithAlias($name) as $index) { $data['actions'][] = ['remove' => ['index' => $index->getName(), 'alias' => $name]]; } } $data['actions'][] = ['add' => ['index' => $this->getName(), 'alias' => $name]]; // TODO: Use only UpdateAliases when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(UpdateAliases::class) ? new UpdateAliases() : new Update(); $endpoint->setBody($data); return $this->getClient()->requestEndpoint($endpoint); } /** * Removes an alias pointing to the current index. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function removeAlias(string $name): Response { // TODO: Use only DeleteAlias when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(DeleteAlias::class) ? new DeleteAlias() : new Alias\Delete(); $endpoint->setName($name); return $this->requestEndpoint($endpoint); } /** * Returns all index aliases. * * @throws ClientException * @throws ConnectionException * @throws ResponseException * * @return string[] */ public function getAliases(): array { // TODO: Use only GetAlias when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(GetAlias::class) ? new GetAlias() : new Alias\Get(); $endpoint->setName('*'); $responseData = $this->requestEndpoint($endpoint)->getData(); if (!isset($responseData[$this->getName()])) { return []; } $data = $responseData[$this->getName()]; if (!empty($data['aliases'])) { return \array_keys($data['aliases']); } return []; } /** * Checks if the index has the given alias. */ public function hasAlias(string $name): bool { return \in_array($name, $this->getAliases(), true); } /** * Clears the cache of an index. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function clearCache(): Response { // TODO: Use only ClearCache when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(ClearCache::class) ? new ClearCache() : new Clear(); // TODO: add additional cache clean arguments return $this->requestEndpoint($endpoint); } /** * Flushes the index to storage. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function flush(array $options = []): Response { $endpoint = new Flush(); $endpoint->setParams($options); return $this->requestEndpoint($endpoint); } /** * Can be used to change settings during runtime. One example is to use it for bulk updating. * * @param array $data Data array * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function setSettings(array $data): Response { // TODO: Use only PutSettings when dropping support for elasticsearch/elasticsearch 7.x $endpoint = \class_exists(PutSettings::class) ? new PutSettings() : new Put(); $endpoint->setBody($data); return $this->requestEndpoint($endpoint); } /** * Makes calls to the elasticsearch server based on this index. * * @param string $path Path to call * @param string $method Rest method to use (GET, POST, DELETE, PUT) * @param array|string $data Arguments as array or encoded string * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function request(string $path, string $method, $data = [], array $queryParameters = []): Response { $path = $this->getName().'/'.$path; return $this->getClient()->request($path, $method, $data, $queryParameters); } /** * Makes calls to the elasticsearch server with usage official client Endpoint based on this index. * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function requestEndpoint(AbstractEndpoint $endpoint): Response { $cloned = clone $endpoint; $cloned->setIndex($this->getName()); return $this->getClient()->requestEndpoint($cloned); } /** * Run the analysis on the index. * * @param array $body request body for the `_analyze` API, see API documentation for the required properties * @param array $args Additional arguments * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html * * @throws ClientException * @throws ConnectionException * @throws ResponseException */ public function analyze(array $body, $args = []): array { $endpoint = new Analyze(); $endpoint->setBody($body); $endpoint->setParams($args); $data = $this->requestEndpoint($endpoint)->getData(); // Support for "Explain" parameter, that returns a different response structure from Elastic // @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/_explain_analyze.html if (isset($body['explain']) && $body['explain']) { return $data['detail']; } return $data['tokens']; } /** * Update document, using update script. * * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html * * @param AbstractScript|Document $data Document or Script with update data * @param array $options array of query params to use for query */ public function updateDocument($data, array $options = []): Response { if (!($data instanceof Document) && !($data instanceof AbstractScript)) { throw new \InvalidArgumentException('Data should be a Document or Script'); } if (!$data->hasId()) { throw new InvalidException('Document or Script id is not set'); } return $this->getClient()->updateDocument($data->getId(), $data, $this->getName(), $options); } }