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