vendor/ruflin/elastica/src/Search.php line 352

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