1<?php 2 3namespace Elastica; 4 5use Elastica\Exception\ClientException; 6use Elastica\Exception\ConnectionException; 7use Elastica\Exception\InvalidException; 8use Elastica\Exception\ResponseException; 9use Elastica\Query\AbstractQuery; 10use Elastica\Query\MatchAll; 11use Elastica\ResultSet\BuilderInterface; 12use Elastica\ResultSet\DefaultBuilder; 13use Elastica\Suggest\AbstractSuggest; 14 15/** 16 * Elastica search object. 17 * 18 * @author Nicolas Ruflin <spam@ruflin.com> 19 * @phpstan-import-type TCreateQueryArgs from Query 20 */ 21class Search 22{ 23 /* 24 * Options 25 */ 26 public const OPTION_SEARCH_TYPE = 'search_type'; 27 public const OPTION_ROUTING = 'routing'; 28 public const OPTION_PREFERENCE = 'preference'; 29 public const OPTION_VERSION = 'version'; 30 public const OPTION_TIMEOUT = 'timeout'; 31 public const OPTION_FROM = 'from'; 32 public const OPTION_SIZE = 'size'; 33 public const OPTION_SCROLL = 'scroll'; 34 public const OPTION_SCROLL_ID = 'scroll_id'; 35 public const OPTION_QUERY_CACHE = 'query_cache'; 36 public const OPTION_TERMINATE_AFTER = 'terminate_after'; 37 public const OPTION_SHARD_REQUEST_CACHE = 'request_cache'; 38 public const OPTION_FILTER_PATH = 'filter_path'; 39 public const OPTION_TYPED_KEYS = 'typed_keys'; 40 41 /* 42 * Search types 43 */ 44 public const OPTION_SEARCH_TYPE_DFS_QUERY_THEN_FETCH = 'dfs_query_then_fetch'; 45 public const OPTION_SEARCH_TYPE_QUERY_THEN_FETCH = 'query_then_fetch'; 46 public const OPTION_SEARCH_TYPE_SUGGEST = 'suggest'; 47 public const OPTION_SEARCH_IGNORE_UNAVAILABLE = 'ignore_unavailable'; 48 49 /** 50 * Array of indices names. 51 * 52 * @var string[] 53 */ 54 protected $_indices = []; 55 56 /** 57 * @var Query 58 */ 59 protected $_query; 60 61 /** 62 * @var array 63 */ 64 protected $_options = []; 65 66 /** 67 * Client object. 68 * 69 * @var Client 70 */ 71 protected $_client; 72 73 /** 74 * @var BuilderInterface|null 75 */ 76 private $builder; 77 78 public function __construct(Client $client, ?BuilderInterface $builder = null) 79 { 80 $this->_client = $client; 81 $this->builder = $builder ?: new DefaultBuilder(); 82 } 83 84 /** 85 * Adds a index to the list. 86 * 87 * @param Index $index Index object or string 88 * 89 * @throws InvalidException 90 */ 91 public function addIndex($index): self 92 { 93 if ($index instanceof Index) { 94 $index = $index->getName(); 95 } else { 96 \trigger_deprecation( 97 'ruflin/elastica', 98 '7.2.0', 99 'Passing a string as 1st argument to "%s()" is deprecated, pass an Index instance or use "addIndexByName" instead. It will throw a %s in 8.0.', 100 __METHOD__, 101 \TypeError::class 102 ); 103 } 104 105 if (!\is_scalar($index)) { 106 throw new InvalidException('Invalid param type'); 107 } 108 109 return $this->addIndexByName((string) $index); 110 } 111 112 /** 113 * Adds an index to the list. 114 */ 115 public function addIndexByName(string $index): self 116 { 117 $this->_indices[] = $index; 118 119 return $this; 120 } 121 122 /** 123 * Add array of indices at once. 124 * 125 * @param Index[] $indices 126 */ 127 public function addIndices(array $indices = []): self 128 { 129 foreach ($indices as $index) { 130 if (\is_string($index)) { 131 \trigger_deprecation( 132 'ruflin/elastica', 133 '7.2.0', 134 'Passing a array of strings as 1st argument to "%s()" is deprecated, pass an array of Indexes or use "addIndicesByName" instead. It will throw a %s in 8.0.', 135 __METHOD__, 136 \TypeError::class 137 ); 138 $this->addIndexByName($index); 139 140 continue; 141 } 142 143 if (!$index instanceof Index) { 144 throw new InvalidException('Invalid param type for addIndices(), expected Index[]'); 145 } 146 147 $this->addIndex($index); 148 } 149 150 return $this; 151 } 152 153 /** 154 * @param string[] $indices 155 */ 156 public function addIndicesByName(array $indices = []): self 157 { 158 foreach ($indices as $index) { 159 if (!\is_string($index)) { 160 throw new InvalidException('Invalid param type for addIndicesByName(), expected string[]'); 161 } 162 $this->addIndexByName($index); 163 } 164 165 return $this; 166 } 167 168 /** 169 * @param AbstractQuery|AbstractSuggest|array|Collapse|Query|string|Suggest|null $query 170 * @phpstan-param TCreateQueryArgs $query 171 */ 172 public function setQuery($query): self 173 { 174 $this->_query = Query::create($query); 175 176 return $this; 177 } 178 179 /** 180 * @param mixed $value 181 */ 182 public function setOption(string $key, $value): self 183 { 184 $this->validateOption($key); 185 186 $this->_options[$key] = $value; 187 188 return $this; 189 } 190 191 public function setOptions(array $options): self 192 { 193 $this->clearOptions(); 194 195 foreach ($options as $key => $value) { 196 $this->setOption($key, $value); 197 } 198 199 return $this; 200 } 201 202 public function clearOptions(): self 203 { 204 $this->_options = []; 205 206 return $this; 207 } 208 209 /** 210 * @param mixed $value 211 */ 212 public function addOption(string $key, $value): self 213 { 214 $this->validateOption($key); 215 216 $this->_options[$key][] = $value; 217 218 return $this; 219 } 220 221 public function hasOption(string $key): bool 222 { 223 return isset($this->_options[$key]); 224 } 225 226 /** 227 * @throws InvalidException if the given key does not exists as an option 228 * 229 * @return mixed 230 */ 231 public function getOption(string $key) 232 { 233 if (!$this->hasOption($key)) { 234 throw new InvalidException('Option '.$key.' does not exist'); 235 } 236 237 return $this->_options[$key]; 238 } 239 240 public function getOptions(): array 241 { 242 return $this->_options; 243 } 244 245 /** 246 * Return client object. 247 */ 248 public function getClient(): Client 249 { 250 return $this->_client; 251 } 252 253 /** 254 * Return array of indices names. 255 * 256 * @return string[] 257 */ 258 public function getIndices(): array 259 { 260 return $this->_indices; 261 } 262 263 public function hasIndices(): bool 264 { 265 return \count($this->_indices) > 0; 266 } 267 268 /** 269 * @param Index $index 270 */ 271 public function hasIndex($index): bool 272 { 273 if ($index instanceof Index) { 274 $index = $index->getName(); 275 } else { 276 \trigger_deprecation( 277 'ruflin/elastica', 278 '7.2.0', 279 'Passing a string as 1st argument to "%s()" is deprecated, pass an Index instance or use "hasIndexByName" instead. It will throw a %s in 8.0.', 280 __METHOD__, 281 \TypeError::class 282 ); 283 } 284 285 return $this->hasIndexByName($index); 286 } 287 288 public function hasIndexByName(string $index): bool 289 { 290 return \in_array($index, $this->_indices, true); 291 } 292 293 public function getQuery(): Query 294 { 295 if (null === $this->_query) { 296 $this->_query = new Query(new MatchAll()); 297 } 298 299 return $this->_query; 300 } 301 302 /** 303 * Creates new search object. 304 */ 305 public static function create(SearchableInterface $searchObject): Search 306 { 307 return $searchObject->createSearch(); 308 } 309 310 /** 311 * Combines indices to the search request path. 312 */ 313 public function getPath(): string 314 { 315 if (isset($this->_options[self::OPTION_SCROLL_ID])) { 316 return '_search/scroll'; 317 } 318 319 return \implode(',', $this->getIndices()).'/_search'; 320 } 321 322 /** 323 * Search in the set indices. 324 * 325 * @param AbstractQuery|AbstractSuggest|array|Collapse|Query|string|Suggest|null $query 326 * @phpstan-param TCreateQueryArgs $query 327 * 328 * @param array|int $options Limit or associative array of options (option=>value) 329 * 330 * @throws InvalidException 331 * @throws ClientException 332 * @throws ConnectionException 333 * @throws ResponseException 334 */ 335 public function search($query = '', $options = null, string $method = Request::POST): ResultSet 336 { 337 $this->setOptionsAndQuery($options, $query); 338 339 $query = $this->getQuery(); 340 $path = $this->getPath(); 341 342 $params = $this->getOptions(); 343 344 // Send scroll_id via raw HTTP body to handle cases of very large (> 4kb) ids. 345 if ('_search/scroll' === $path) { 346 $data = [self::OPTION_SCROLL_ID => $params[self::OPTION_SCROLL_ID]]; 347 unset($params[self::OPTION_SCROLL_ID]); 348 } else { 349 $data = $query->toArray(); 350 } 351 352 $response = $this->getClient()->request($path, $method, $data, $params); 353 354 return $this->builder->buildResultSet($response, $query); 355 } 356 357 /** 358 * @param array|Query|Query\AbstractQuery|string $query 359 * @param bool $fullResult By default only the total hit count is returned. If set to true, the full ResultSet including aggregations is returned 360 * 361 * @throws ClientException 362 * @throws ConnectionException 363 * @throws ResponseException 364 * 365 * @return int|ResultSet 366 */ 367 public function count($query = '', bool $fullResult = false, string $method = Request::POST) 368 { 369 $this->setOptionsAndQuery(null, $query); 370 371 // Clone the object as we do not want to modify the original query. 372 $query = clone $this->getQuery(); 373 $query->setSize(0); 374 $query->setTrackTotalHits(true); 375 376 $path = $this->getPath(); 377 378 $response = $this->getClient()->request( 379 $path, 380 $method, 381 $query->toArray(), 382 [self::OPTION_SEARCH_TYPE => self::OPTION_SEARCH_TYPE_QUERY_THEN_FETCH] 383 ); 384 $resultSet = $this->builder->buildResultSet($response, $query); 385 386 return $fullResult ? $resultSet : $resultSet->getTotalHits(); 387 } 388 389 /** 390 * @param array|int $options 391 * @param AbstractQuery|AbstractSuggest|array|Collapse|Query|string|Suggest|null $query 392 * @phpstan-param TCreateQueryArgs $query 393 */ 394 public function setOptionsAndQuery($options = null, $query = ''): self 395 { 396 if ('' !== $query) { 397 $this->setQuery($query); 398 } 399 400 if (\is_int($options)) { 401 \trigger_deprecation('ruflin/elastica', '7.1.3', 'Passing an int as 1st argument to "%s()" is deprecated, pass an array with the key "size" instead. It will be removed in 8.0.', __METHOD__); 402 $this->getQuery()->setSize($options); 403 } elseif (\is_array($options)) { 404 if (isset($options['limit'])) { 405 $this->getQuery()->setSize($options['limit']); 406 unset($options['limit']); 407 } 408 if (isset($options['explain'])) { 409 $this->getQuery()->setExplain($options['explain']); 410 unset($options['explain']); 411 } 412 $this->setOptions($options); 413 } 414 415 return $this; 416 } 417 418 public function setSuggest(Suggest $suggest): self 419 { 420 return $this->setOptionsAndQuery([self::OPTION_SEARCH_TYPE_SUGGEST => 'suggest'], $suggest); 421 } 422 423 /** 424 * Returns the Scroll Iterator. 425 * 426 * @see Scroll 427 */ 428 public function scroll(string $expiryTime = '1m'): Scroll 429 { 430 return new Scroll($this, $expiryTime); 431 } 432 433 public function getResultSetBuilder(): BuilderInterface 434 { 435 return $this->builder; 436 } 437 438 /** 439 * @throws InvalidException If the given key is not a valid option 440 */ 441 protected function validateOption(string $key): void 442 { 443 switch ($key) { 444 case self::OPTION_SEARCH_TYPE: 445 case self::OPTION_ROUTING: 446 case self::OPTION_PREFERENCE: 447 case self::OPTION_VERSION: 448 case self::OPTION_TIMEOUT: 449 case self::OPTION_FROM: 450 case self::OPTION_SIZE: 451 case self::OPTION_SCROLL: 452 case self::OPTION_SCROLL_ID: 453 case self::OPTION_SEARCH_TYPE_SUGGEST: 454 case self::OPTION_SEARCH_IGNORE_UNAVAILABLE: 455 case self::OPTION_QUERY_CACHE: 456 case self::OPTION_TERMINATE_AFTER: 457 case self::OPTION_SHARD_REQUEST_CACHE: 458 case self::OPTION_FILTER_PATH: 459 case self::OPTION_TYPED_KEYS: 460 return; 461 } 462 463 throw new InvalidException('Invalid option '.$key); 464 } 465} 466