1<?php
2
3namespace Elastica\Multi;
4
5use Elastica\Client;
6use Elastica\JSON;
7use Elastica\Request;
8use Elastica\Search as BaseSearch;
9
10/**
11 * Elastica multi search.
12 *
13 * @author munkie
14 *
15 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html
16 */
17class Search
18{
19    /**
20     * @const string[] valid header options
21     */
22    private static $HEADER_OPTIONS = [
23        'index',
24        'types',
25        'search_type',
26        'routing',
27        'preference',
28    ];
29
30    /**
31     * @var MultiBuilderInterface
32     */
33    private $_builder;
34
35    /**
36     * @var Client
37     */
38    protected $_client;
39
40    /**
41     * @var array
42     */
43    protected $_options = [];
44
45    /**
46     * @var BaseSearch[]
47     */
48    protected $_searches = [];
49
50    /**
51     * Constructs search object.
52     *
53     * @param Client                $client
54     * @param MultiBuilderInterface $builder
55     */
56    public function __construct(Client $client, MultiBuilderInterface $builder = null)
57    {
58        $this->_builder = $builder ?? new MultiBuilder();
59        $this->_client = $client;
60    }
61
62    /**
63     * @return Client
64     */
65    public function getClient(): Client
66    {
67        return $this->_client;
68    }
69
70    /**
71     * @return $this
72     */
73    public function clearSearches(): self
74    {
75        $this->_searches = [];
76
77        return $this;
78    }
79
80    /**
81     * @param BaseSearch $search
82     * @param string     $key
83     *
84     * @return $this
85     */
86    public function addSearch(BaseSearch $search, string $key = null): self
87    {
88        if ($key) {
89            $this->_searches[$key] = $search;
90        } else {
91            $this->_searches[] = $search;
92        }
93
94        return $this;
95    }
96
97    /**
98     * @param BaseSearch[] $searches
99     *
100     * @return $this
101     */
102    public function addSearches(array $searches): self
103    {
104        foreach ($searches as $key => $search) {
105            $this->addSearch($search, $key);
106        }
107
108        return $this;
109    }
110
111    /**
112     * @param BaseSearch[] $searches
113     *
114     * @return $this
115     */
116    public function setSearches(array $searches): self
117    {
118        $this->clearSearches();
119        $this->addSearches($searches);
120
121        return $this;
122    }
123
124    /**
125     * @return BaseSearch[]
126     */
127    public function getSearches(): array
128    {
129        return $this->_searches;
130    }
131
132    /**
133     * @param string $searchType
134     *
135     * @return $this
136     */
137    public function setSearchType(string $searchType): self
138    {
139        $this->_options[BaseSearch::OPTION_SEARCH_TYPE] = $searchType;
140
141        return $this;
142    }
143
144    /**
145     * @return ResultSet
146     */
147    public function search(): ResultSet
148    {
149        $data = $this->_getData();
150
151        $response = $this->getClient()->request(
152            '_msearch',
153            Request::POST,
154            $data,
155            $this->_options,
156        Request::NDJSON_CONTENT_TYPE
157        );
158
159        return $this->_builder->buildMultiResultSet($response, $this->getSearches());
160    }
161
162    /**
163     * @return string
164     */
165    protected function _getData(): string
166    {
167        $data = '';
168        foreach ($this->getSearches() as $search) {
169            $data .= $this->_getSearchData($search);
170        }
171
172        return $data;
173    }
174
175    /**
176     * @param BaseSearch $search
177     *
178     * @return string
179     */
180    protected function _getSearchData(BaseSearch $search): string
181    {
182        $header = $this->_getSearchDataHeader($search);
183
184        $header = (empty($header)) ? new \stdClass() : $header;
185        $query = $search->getQuery();
186
187        // Keep other query options as part of the search body
188        $queryOptions = \array_diff_key($search->getOptions(), \array_flip(self::$HEADER_OPTIONS));
189
190        $data = JSON::stringify($header)."\n";
191        $data .= JSON::stringify($query->toArray() + $queryOptions)."\n";
192
193        return $data;
194    }
195
196    /**
197     * @param BaseSearch $search
198     *
199     * @return array
200     */
201    protected function _getSearchDataHeader(BaseSearch $search): array
202    {
203        $header = $search->getOptions();
204
205        if ($search->hasIndices()) {
206            $header['index'] = $search->getIndices();
207        }
208
209        if ($search->hasTypes()) {
210            $header['types'] = $search->getTypes();
211        }
212
213        // Filter options accepted in the "header"
214        return \array_intersect_key($header, \array_flip(self::$HEADER_OPTIONS));
215    }
216}
217