xref: /plugin/elasticsearch/vendor/ruflin/elastica/src/Search.php (revision d832d53af2a0f84c34c8d5f17c93ffaa85869e5d)
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