1<?php
2
3namespace Elastica;
4
5use Elastica\Exception\InvalidException;
6
7/**
8 * Elastica result set.
9 *
10 * List of all hits that are returned for a search on elasticsearch
11 * Result set implements iterator
12 *
13 * @author Nicolas Ruflin <spam@ruflin.com>
14 */
15class ResultSet implements \Iterator, \Countable, \ArrayAccess
16{
17    /**
18     * Current position.
19     *
20     * @var int Current position
21     */
22    private $_position = 0;
23
24    /**
25     * Query.
26     *
27     * @var Query Query object
28     */
29    private $_query;
30
31    /**
32     * Response.
33     *
34     * @var Response Response object
35     */
36    private $_response;
37
38    /**
39     * Results.
40     *
41     * @var Result[] Results
42     */
43    private $_results;
44
45    /**
46     * @param Result[] $results
47     */
48    public function __construct(Response $response, Query $query, $results)
49    {
50        $this->_query = $query;
51        $this->_response = $response;
52        $this->_results = $results;
53    }
54
55    /**
56     * Returns all results.
57     *
58     * @return Result[]
59     */
60    public function getResults()
61    {
62        return $this->_results;
63    }
64
65    /**
66     * Returns all Documents.
67     *
68     * @return Document[]
69     */
70    public function getDocuments()
71    {
72        $documents = [];
73        foreach ($this->_results as $doc) {
74            $documents[] = $doc->getDocument();
75        }
76
77        return $documents;
78    }
79
80    /**
81     * Returns true if the response contains suggestion results; false otherwise.
82     */
83    public function hasSuggests(): bool
84    {
85        $data = $this->_response->getData();
86
87        return isset($data['suggest']);
88    }
89
90    /**
91     * Return all suggests.
92     *
93     * @return array suggest results
94     */
95    public function getSuggests(): array
96    {
97        $data = $this->_response->getData();
98
99        return $data['suggest'] ?? [];
100    }
101
102    /**
103     * Returns whether aggregations exist.
104     */
105    public function hasAggregations(): bool
106    {
107        $data = $this->_response->getData();
108
109        return isset($data['aggregations']);
110    }
111
112    /**
113     * Returns all aggregation results.
114     */
115    public function getAggregations(): array
116    {
117        $data = $this->_response->getData();
118
119        return $data['aggregations'] ?? [];
120    }
121
122    /**
123     * Retrieve a specific aggregation from this result set.
124     *
125     * @param string $name the name of the desired aggregation
126     *
127     * @throws Exception\InvalidException if an aggregation by the given name cannot be found
128     */
129    public function getAggregation(string $name): array
130    {
131        $data = $this->_response->getData();
132
133        if (isset($data['aggregations'][$name])) {
134            return $data['aggregations'][$name];
135        }
136
137        throw new InvalidException("This result set does not contain an aggregation named {$name}.");
138    }
139
140    /**
141     * Returns the total number of found hits.
142     */
143    public function getTotalHits(): int
144    {
145        $data = $this->_response->getData();
146
147        return (int) ($data['hits']['total']['value'] ?? 0);
148    }
149
150    /**
151     * Returns the total number relation of found hits.
152     */
153    public function getTotalHitsRelation(): string
154    {
155        $data = $this->_response->getData();
156
157        return $data['hits']['total']['relation'] ?? 'eq';
158    }
159
160    /**
161     * Returns the max score of the results found.
162     */
163    public function getMaxScore(): float
164    {
165        $data = $this->_response->getData();
166
167        return (float) ($data['hits']['max_score'] ?? 0);
168    }
169
170    /**
171     * Returns the total number of ms for this search to complete.
172     */
173    public function getTotalTime(): int
174    {
175        $data = $this->_response->getData();
176
177        return $data['took'] ?? 0;
178    }
179
180    /**
181     * Returns the Point-In-Time ID, if available.
182     *
183     * @See: https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after
184     */
185    public function getPointInTimeId(): ?string
186    {
187        $data = $this->_response->getData();
188
189        return $data['pit_id'] ?? null;
190    }
191
192    /**
193     * Returns true if the query has timed out.
194     */
195    public function hasTimedOut(): bool
196    {
197        $data = $this->_response->getData();
198
199        return !empty($data['timed_out']);
200    }
201
202    /**
203     * Returns response object.
204     */
205    public function getResponse(): Response
206    {
207        return $this->_response;
208    }
209
210    public function getQuery(): Query
211    {
212        return $this->_query;
213    }
214
215    /**
216     * Returns size of current set.
217     */
218    public function count(): int
219    {
220        return \count($this->_results);
221    }
222
223    /**
224     * Returns size of current suggests.
225     */
226    public function countSuggests(): int
227    {
228        return \count($this->getSuggests());
229    }
230
231    /**
232     * Returns the current object of the set.
233     *
234     * @return Result Set object
235     */
236    public function current(): Result
237    {
238        return $this->_results[$this->key()];
239    }
240
241    /**
242     * Sets pointer (current) to the next item of the set.
243     */
244    public function next(): void
245    {
246        ++$this->_position;
247    }
248
249    /**
250     * Returns the position of the current entry.
251     *
252     * @return int Current position
253     */
254    public function key(): int
255    {
256        return $this->_position;
257    }
258
259    /**
260     * Check if an object exists at the current position.
261     *
262     * @return bool True if object exists
263     */
264    public function valid(): bool
265    {
266        return isset($this->_results[$this->key()]);
267    }
268
269    /**
270     * Resets position to 0, restarts iterator.
271     */
272    public function rewind(): void
273    {
274        $this->_position = 0;
275    }
276
277    /**
278     * Whether a offset exists.
279     *
280     * @see http://php.net/manual/en/arrayaccess.offsetexists.php
281     *
282     * @param int $offset
283     */
284    public function offsetExists($offset): bool
285    {
286        return isset($this->_results[$offset]);
287    }
288
289    /**
290     * Offset to retrieve.
291     *
292     * @see http://php.net/manual/en/arrayaccess.offsetget.php
293     *
294     * @param int $offset
295     *
296     * @throws Exception\InvalidException If offset doesn't exist
297     */
298    public function offsetGet($offset): Result
299    {
300        if ($this->offsetExists($offset)) {
301            return $this->_results[$offset];
302        }
303
304        throw new InvalidException('Offset does not exist.');
305    }
306
307    /**
308     * Offset to set.
309     *
310     * @see http://php.net/manual/en/arrayaccess.offsetset.php
311     *
312     * @param int    $offset
313     * @param Result $value
314     *
315     * @throws Exception\InvalidException
316     */
317    public function offsetSet($offset, $value): void
318    {
319        if (!($value instanceof Result)) {
320            throw new InvalidException('ResultSet is a collection of Result only.');
321        }
322
323        if (!isset($this->_results[$offset])) {
324            throw new InvalidException('Offset does not exist.');
325        }
326
327        $this->_results[$offset] = $value;
328    }
329
330    /**
331     * Offset to unset.
332     *
333     * @see http://php.net/manual/en/arrayaccess.offsetunset.php
334     *
335     * @param int $offset
336     */
337    public function offsetUnset($offset): void
338    {
339        unset($this->_results[$offset]);
340    }
341}
342