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