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     * @var Client
21     */
22    protected $_client;
23
24    /**
25     * @var array
26     */
27    protected $_options = [];
28
29    /**
30     * @var BaseSearch[]
31     */
32    protected $_searches = [];
33    /**
34     * @const string[] valid header options
35     */
36    private static $HEADER_OPTIONS = [
37        'index',
38        'types',
39        'search_type',
40        'routing',
41        'preference',
42    ];
43
44    /**
45     * @var MultiBuilderInterface
46     */
47    private $_builder;
48
49    public function __construct(Client $client, ?MultiBuilderInterface $builder = null)
50    {
51        $this->_builder = $builder ?? new MultiBuilder();
52        $this->_client = $client;
53    }
54
55    public function getClient(): Client
56    {
57        return $this->_client;
58    }
59
60    /**
61     * @return $this
62     */
63    public function clearSearches(): self
64    {
65        $this->_searches = [];
66
67        return $this;
68    }
69
70    /**
71     * @return $this
72     */
73    public function addSearch(BaseSearch $search, ?string $key = null): self
74    {
75        if ($key) {
76            $this->_searches[$key] = $search;
77        } else {
78            $this->_searches[] = $search;
79        }
80
81        return $this;
82    }
83
84    /**
85     * @param BaseSearch[] $searches
86     *
87     * @return $this
88     */
89    public function addSearches(array $searches): self
90    {
91        foreach ($searches as $key => $search) {
92            $this->addSearch($search, $key);
93        }
94
95        return $this;
96    }
97
98    /**
99     * @param BaseSearch[] $searches
100     *
101     * @return $this
102     */
103    public function setSearches(array $searches): self
104    {
105        $this->clearSearches();
106        $this->addSearches($searches);
107
108        return $this;
109    }
110
111    /**
112     * @return BaseSearch[]
113     */
114    public function getSearches(): array
115    {
116        return $this->_searches;
117    }
118
119    /**
120     * @return $this
121     */
122    public function setSearchType(string $searchType): self
123    {
124        $this->_options[BaseSearch::OPTION_SEARCH_TYPE] = $searchType;
125
126        return $this;
127    }
128
129    public function search(): ResultSet
130    {
131        $data = $this->_getData();
132
133        $response = $this->getClient()->request(
134            '_msearch',
135            Request::POST,
136            $data,
137            $this->_options,
138            Request::NDJSON_CONTENT_TYPE
139        );
140
141        return $this->_builder->buildMultiResultSet($response, $this->getSearches());
142    }
143
144    protected function _getData(): string
145    {
146        $data = '';
147        foreach ($this->getSearches() as $search) {
148            $data .= $this->_getSearchData($search);
149        }
150
151        return $data;
152    }
153
154    protected function _getSearchData(BaseSearch $search): string
155    {
156        $header = $this->_getSearchDataHeader($search);
157
158        $header = (empty($header)) ? new \stdClass() : $header;
159        $query = $search->getQuery();
160
161        // Keep other query options as part of the search body
162        $queryOptions = \array_diff_key($search->getOptions(), \array_flip(self::$HEADER_OPTIONS));
163
164        $data = JSON::stringify($header)."\n";
165        $data .= JSON::stringify($query->toArray() + $queryOptions)."\n";
166
167        return $data;
168    }
169
170    protected function _getSearchDataHeader(BaseSearch $search): array
171    {
172        $header = $search->getOptions();
173
174        if ($search->hasIndices()) {
175            $header['index'] = $search->getIndices();
176        }
177
178        // Filter options accepted in the "header"
179        return \array_intersect_key($header, \array_flip(self::$HEADER_OPTIONS));
180    }
181}
182