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