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