1<?php 2 3namespace Elastica; 4 5use Elastica\Aggregation\AbstractAggregation; 6use Elastica\Exception\InvalidException; 7use Elastica\Query\AbstractQuery; 8use Elastica\Query\MatchAll; 9use Elastica\Query\QueryString; 10use Elastica\Rescore\Query as QueryRescore; 11use Elastica\Script\AbstractScript; 12use Elastica\Script\ScriptFields; 13use Elastica\Suggest\AbstractSuggest; 14 15/** 16 * Elastica query object. 17 * 18 * Creates different types of queries 19 * 20 * @author Nicolas Ruflin <spam@ruflin.com> 21 * 22 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html 23 * @todo: improve THighlightArgs https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html 24 * @phpstan-type THighlightArgs = array<mixed> 25 * @phpstan-type TStoredFields = list<string> 26 * @todo: improve TDocValueFields https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-docvalue-fields 27 * @phpstan-type TDocValueFields = array<mixed> 28 * @phpstan-type TRescoreArgs = QueryRescore|list<QueryRescore> 29 * @phpstan-type TSourceArgs = non-empty-string|list<non-empty-string>|array{includes?: list<non-empty-string>, excludes?: list<non-empty-string>}|false 30 * @phpstan-type TSortArrayArg = array<string, string>|array<string, array{ 31 * order?: non-empty-string, 32 * mode?: non-empty-string, 33 * numeric_type?: non-empty-string, 34 * nested?: array{path: non-empty-string, filter?: array<mixed>, max_children?: int, nested?: array<mixed>}, 35 * missing?: non-empty-string, 36 * unmapped_type?: non-empty-string, 37 * }>|array{_geo_distance: array<string, mixed>} 38 * @phpstan-type TSortArg = non-empty-string|TSortArrayArg 39 * @phpstan-type TSortArgs = list<TSortArg>|TSortArrayArg 40 * @phpstan-type TRawQuery = array{ 41 * _source?: TSourceArgs, 42 * aggs?: list<AbstractAggregation>|array<string, array<string, array<string, mixed>>>, 43 * collapse?: Collapse, 44 * docvalue_fields?: TDocValueFields, 45 * explain?: bool, 46 * from?: int, 47 * highlight?: THighlightArgs, 48 * indices_boost?: array<string, float>, 49 * min_score?: float, 50 * pit?: PointInTime, 51 * post_filter?: AbstractQuery, 52 * query?: AbstractQuery|array<string, array<string, mixed>>, 53 * rescore?: TRescoreArgs, 54 * script_fields?: ScriptFields, 55 * size?: int, 56 * sort?: TSortArgs, 57 * stored_fields?: TStoredFields, 58 * suggest?: Suggest, 59 * track_scores?: bool, 60 * track_total_hits?: bool|int, 61 * version?: bool, 62 * } 63 * @phpstan-type TCreateQueryArgsMatching = AbstractQuery|TRawQuery|self|string|null 64 * @phpstan-type TCreateQueryArgs = TCreateQueryArgsMatching|AbstractSuggest|Collapse|Suggest 65 */ 66class Query extends Param 67{ 68 /** 69 * If the current query has a suggest in it. 70 * 71 * @var bool 72 */ 73 private $hasSuggest = false; 74 75 /** 76 * Creates a query object. 77 * 78 * @param AbstractQuery|array|Collapse|Suggest $query Query object (default = null) 79 * @phpstan-param AbstractQuery|Suggest|Collapse|TRawQuery $query 80 */ 81 public function __construct($query = null) 82 { 83 if (\is_array($query)) { 84 $this->setRawQuery($query); 85 } elseif ($query instanceof AbstractQuery) { 86 $this->setQuery($query); 87 } elseif ($query instanceof Suggest) { 88 $this->setSuggest($query); 89 } elseif ($query instanceof Collapse) { 90 $this->setCollapse($query); 91 } 92 } 93 94 /** 95 * Transforms the argument to a query object. 96 * 97 * For example, an empty argument will return a \Elastica\Query with a \Elastica\Query\MatchAll. 98 * 99 * @param AbstractQuery|AbstractSuggest|array|Collapse|Query|string|Suggest|null $query 100 * @phpstan-param TCreateQueryArgs $query 101 * 102 * @throws InvalidException For an invalid argument 103 */ 104 public static function create($query): self 105 { 106 switch (true) { 107 case empty($query): 108 return new static(new MatchAll()); 109 case $query instanceof self: 110 return $query; 111 case $query instanceof AbstractSuggest: 112 return new static(new Suggest($query)); 113 case $query instanceof AbstractQuery: 114 case $query instanceof Suggest: 115 case $query instanceof Collapse: 116 case \is_array($query): 117 return new static($query); 118 case \is_string($query): 119 return new static(new QueryString($query)); 120 } 121 122 throw new InvalidException('Unexpected argument to create a query for.'); 123 } 124 125 /** 126 * Sets query as raw array. Will overwrite all already set arguments. 127 * 128 * @param array $query Query array 129 * @phpstan-param TRawQuery $query 130 */ 131 public function setRawQuery(array $query): self 132 { 133 $this->_params = $query; 134 135 return $this; 136 } 137 138 public function setQuery(AbstractQuery $query): self 139 { 140 return $this->setParam('query', $query); 141 } 142 143 /** 144 * Gets the query object. 145 * 146 * @return AbstractQuery|array<string, array<string, mixed>> 147 */ 148 public function getQuery() 149 { 150 return $this->getParam('query'); 151 } 152 153 /** 154 * Sets the start from which the search results should be returned. 155 * 156 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-from-size 157 */ 158 public function setFrom(int $from): self 159 { 160 return $this->setParam('from', $from); 161 } 162 163 /** 164 * Sets sort arguments for the query 165 * Replaces existing values. 166 * 167 * @param array $sortArgs Sorting arguments 168 * @phpstan-param TSortArgs $sortArgs 169 * 170 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html 171 */ 172 public function setSort(array $sortArgs): self 173 { 174 return $this->setParam('sort', $sortArgs); 175 } 176 177 /** 178 * Adds a sort param to the query. 179 * 180 * @param mixed $sort Sort parameter 181 * @phpstan-param TSortArg $sort 182 * 183 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html 184 */ 185 public function addSort($sort): self 186 { 187 return $this->addParam('sort', $sort); 188 } 189 190 /** 191 * Keep track of the scores when sorting results. 192 * 193 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_track_scores 194 */ 195 public function setTrackScores(bool $trackScores = true): self 196 { 197 return $this->setParam('track_scores', $trackScores); 198 } 199 200 /** 201 * Sets highlight arguments for the query. 202 * 203 * @param array $highlightArgs Set all highlight arguments 204 * @phpstan-param THighlightArgs $highlightArgs 205 * 206 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html 207 */ 208 public function setHighlight(array $highlightArgs): self 209 { 210 return $this->setParam('highlight', $highlightArgs); 211 } 212 213 /** 214 * Adds a highlight argument. 215 * 216 * @param mixed $highlight Add highlight argument 217 * 218 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html 219 */ 220 public function addHighlight($highlight): self 221 { 222 return $this->addParam('highlight', $highlight); 223 } 224 225 /** 226 * Sets maximum number of results for this query. 227 * 228 * @param int $size Maximal number of results for query (default = 10) 229 * 230 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-from-size 231 */ 232 public function setSize(int $size = 10): self 233 { 234 return $this->setParam('size', $size); 235 } 236 237 /** 238 * Enables explain on the query. 239 * 240 * @param bool $explain Enabled or disable explain (default = true) 241 * 242 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-explain 243 */ 244 public function setExplain($explain = true): self 245 { 246 return $this->setParam('explain', $explain); 247 } 248 249 /** 250 * Enables version on the query. 251 * 252 * @param bool $version Enabled or disable version (default = true) 253 * 254 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-version 255 */ 256 public function setVersion($version = true): self 257 { 258 return $this->setParam('version', $version); 259 } 260 261 /** 262 * Sets the fields to be returned by the search 263 * NOTICE php will encode modified(or named keys) array into object format in json format request 264 * so the fields array must a sequence(list) type of array. 265 * 266 * @param array $fields Fields to be returned 267 * @phpstan-param TStoredFields $fields 268 * 269 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-stored-fields 270 */ 271 public function setStoredFields(array $fields): self 272 { 273 return $this->setParam('stored_fields', $fields); 274 } 275 276 /** 277 * Set the doc value representation of a fields to return for each hit. 278 * 279 * @param array $fieldDataFields Fields not stored to be returned 280 * @phpstan-param TDocValueFields $fieldDataFields 281 * 282 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-docvalue-fields 283 */ 284 public function setFieldDataFields(array $fieldDataFields): self 285 { 286 return $this->setParam('docvalue_fields', $fieldDataFields); 287 } 288 289 /** 290 * Set script fields. 291 * 292 * @param array<string, AbstractScript>|ScriptFields $scriptFields Script fields 293 * 294 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-script-fields 295 */ 296 public function setScriptFields($scriptFields): self 297 { 298 if (\is_array($scriptFields)) { 299 $scriptFields = new ScriptFields($scriptFields); 300 } 301 302 return $this->setParam('script_fields', $scriptFields); 303 } 304 305 /** 306 * Adds a Script to the query. 307 */ 308 public function addScriptField(string $name, AbstractScript $script): self 309 { 310 if (isset($this->_params['script_fields'])) { 311 $this->_params['script_fields']->addScript($name, $script); 312 } else { 313 $this->setScriptFields([$name => $script]); 314 } 315 316 return $this; 317 } 318 319 /** 320 * Adds an Aggregation to the query. 321 */ 322 public function addAggregation(AbstractAggregation $agg): self 323 { 324 $this->_params['aggs'][] = $agg; 325 326 return $this; 327 } 328 329 /** 330 * Converts all query params to an array. 331 */ 332 public function toArray(): array 333 { 334 if (!$this->hasSuggest && !isset($this->_params['query'])) { 335 $this->setQuery(new MatchAll()); 336 } 337 338 if (isset($this->_params['post_filter']) && 0 === \count($this->_params['post_filter'])) { 339 unset($this->_params['post_filter']); 340 } 341 342 $array = $this->_convertArrayable($this->_params); 343 344 if (isset($array['suggest'])) { 345 $array['suggest'] = $array['suggest']['suggest']; 346 } 347 348 return $array; 349 } 350 351 /** 352 * Allows filtering of documents based on a minimum score. 353 * 354 * @param float $minScore Minimum score to filter documents by 355 * 356 * @throws InvalidException 357 * 358 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-min-score 359 */ 360 public function setMinScore(float $minScore): self 361 { 362 return $this->setParam('min_score', $minScore); 363 } 364 365 /** 366 * Add a suggest term. 367 * 368 * @param Suggest $suggest suggestion object 369 * 370 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html 371 */ 372 public function setSuggest(Suggest $suggest): self 373 { 374 $this->setParam('suggest', $suggest); 375 $this->hasSuggest = true; 376 377 return $this; 378 } 379 380 /** 381 * Add a Rescore. 382 * 383 * @param mixed $rescore suggestion object 384 * @phpstan-param TRescoreArgs $rescore 385 * 386 * @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-rescore 387 */ 388 public function setRescore($rescore): self 389 { 390 if (\is_array($rescore)) { 391 $buffer = []; 392 393 foreach ($rescore as $rescoreQuery) { 394 $buffer[] = $rescoreQuery; 395 } 396 } else { 397 $buffer = $rescore; 398 } 399 400 return $this->setParam('rescore', $buffer); 401 } 402 403 /** 404 * Sets the _source field to be returned with every hit. 405 * 406 * @param array|bool $params Fields to be returned or false to disable source 407 * @phpstan-param TSourceArgs $params 408 * 409 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-source-filtering 410 */ 411 public function setSource($params): self 412 { 413 return $this->setParam('_source', $params); 414 } 415 416 /** 417 * Sets a post_filter to the current query. 418 * 419 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-post-filter 420 */ 421 public function setPostFilter(AbstractQuery $filter): self 422 { 423 return $this->setParam('post_filter', $filter); 424 } 425 426 /** 427 * Allows to collapse search results based on field values. 428 * 429 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-collapse 430 */ 431 public function setCollapse(Collapse $collapse): self 432 { 433 return $this->setParam('collapse', $collapse); 434 } 435 436 /** 437 * Set the Point-in-Time used for the query. 438 * Use for results pagination with Search with search_after requests. 439 * 440 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after 441 */ 442 public function setPointInTime(PointInTime $pit): self 443 { 444 return $this->setParam('pit', $pit); 445 } 446 447 /** 448 * @param array<string, float> $indicesBoost 449 * 450 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multiple-indices.html#index-boost 451 */ 452 public function setIndicesBoost(array $indicesBoost): self 453 { 454 return $this->setParam('indices_boost', \array_chunk($indicesBoost, 1, true)); 455 } 456 457 /** 458 * Adds a track_total_hits argument. 459 * 460 * @param bool|int $trackTotalHits Track total hits parameter 461 * 462 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#request-body-search-track-total-hits 463 */ 464 public function setTrackTotalHits($trackTotalHits = true): self 465 { 466 if (!\is_bool($trackTotalHits) && !\is_int($trackTotalHits)) { 467 throw new InvalidException('TrackTotalHits must be either a boolean, or an integer value'); 468 } 469 470 return $this->setParam('track_total_hits', $trackTotalHits); 471 } 472} 473