xref: /plugin/elasticsearch/vendor/ruflin/elastica/src/Scroll.php (revision d832d53af2a0f84c34c8d5f17c93ffaa85869e5d)
1<?php
2
3namespace Elastica;
4
5use Elastica\Exception\InvalidException;
6
7/**
8 * Scroll Iterator.
9 *
10 * @author Manuel Andreo Garcia <andreo.garcia@gmail.com>
11 *
12 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
13 */
14class Scroll implements \Iterator
15{
16    /**
17     * @var string
18     */
19    public $expiryTime;
20
21    /**
22     * @var Search
23     */
24    protected $_search;
25
26    /**
27     * @var string|null
28     */
29    protected $_nextScrollId;
30
31    /**
32     * @var ResultSet|null
33     */
34    protected $_currentResultSet;
35
36    /**
37     * 0: scroll<br>
38     * 1: scroll id.
39     * 2: ignore_unavailable.
40     *
41     * @var array
42     */
43    protected $_options = [null, null, null];
44
45    private $totalPages = 0;
46    private $currentPage = 0;
47
48    public function __construct(Search $search, string $expiryTime = '1m')
49    {
50        $this->_search = $search;
51        $this->expiryTime = $expiryTime;
52    }
53
54    /**
55     * Returns current result set.
56     *
57     * @see http://php.net/manual/en/iterator.current.php
58     */
59    public function current(): ResultSet
60    {
61        if (!$this->_currentResultSet) {
62            throw new InvalidException('Could not fetch the current ResultSet from an invalid iterator. Did you forget to call "valid()"?');
63        }
64
65        return $this->_currentResultSet;
66    }
67
68    /**
69     * Next scroll search.
70     *
71     * @see http://php.net/manual/en/iterator.next.php
72     */
73    public function next(): void
74    {
75        $this->_currentResultSet = null;
76        if ($this->currentPage < $this->totalPages) {
77            $this->_saveOptions();
78
79            $this->_search->setOption(Search::OPTION_SCROLL, $this->expiryTime);
80            $this->_search->setOption(Search::OPTION_SCROLL_ID, $this->_nextScrollId);
81
82            $this->_setScrollId($this->_search->search());
83
84            $this->_revertOptions();
85        } else {
86            // If there are no pages left, we do not need to query ES.
87            $this->clear();
88        }
89    }
90
91    /**
92     * Returns scroll id.
93     *
94     * @see http://php.net/manual/en/iterator.key.php
95     */
96    public function key(): ?string
97    {
98        return $this->_nextScrollId;
99    }
100
101    /**
102     * Returns true if current result set contains at least one hit.
103     *
104     * @see http://php.net/manual/en/iterator.valid.php
105     */
106    public function valid(): bool
107    {
108        return null !== $this->_nextScrollId;
109    }
110
111    /**
112     * Initial scroll search.
113     *
114     * @see http://php.net/manual/en/iterator.rewind.php
115     */
116    public function rewind(): void
117    {
118        // reset state
119        $this->_options = [null, null, null];
120        $this->currentPage = 0;
121
122        // initial search
123        $this->_saveOptions();
124
125        $this->_search->setOption(Search::OPTION_SCROLL, $this->expiryTime);
126        $this->_search->setOption(Search::OPTION_SCROLL_ID, null);
127        $this->_currentResultSet = null;
128        $this->_setScrollId($this->_search->search());
129
130        $this->_revertOptions();
131    }
132
133    /**
134     * Cleares the search context on ES and marks this Scroll instance as finished.
135     */
136    public function clear(): void
137    {
138        if (null !== $this->_nextScrollId) {
139            $this->_search->getClient()->request(
140                '_search/scroll',
141                Request::DELETE,
142                [Search::OPTION_SCROLL_ID => [$this->_nextScrollId]]
143            );
144
145            // Reset scroll ID so valid() returns false.
146            $this->_nextScrollId = null;
147        }
148    }
149
150    /**
151     * Prepares Scroll for next request.
152     */
153    protected function _setScrollId(ResultSet $resultSet): void
154    {
155        if (0 === $this->currentPage) {
156            $this->totalPages = $resultSet->count() > 0 ? \ceil($resultSet->getTotalHits() / $resultSet->count()) : 0;
157        }
158
159        $this->_currentResultSet = $resultSet;
160        ++$this->currentPage;
161        $this->_nextScrollId = null;
162        if ($resultSet->getResponse()->isOk()) {
163            $this->_nextScrollId = $resultSet->getResponse()->getScrollId();
164            if (0 === $resultSet->count()) {
165                $this->clear();
166            }
167        }
168    }
169
170    /**
171     * Save all search options manipulated by Scroll.
172     */
173    protected function _saveOptions(): void
174    {
175        if ($this->_search->hasOption(Search::OPTION_SCROLL)) {
176            $this->_options[0] = $this->_search->getOption(Search::OPTION_SCROLL);
177        }
178
179        if ($this->_search->hasOption(Search::OPTION_SCROLL_ID)) {
180            $this->_options[1] = $this->_search->getOption(Search::OPTION_SCROLL_ID);
181        }
182
183        if ($this->_search->hasOption(Search::OPTION_SEARCH_IGNORE_UNAVAILABLE)) {
184            $isNotInitial = (null !== $this->_options[2]);
185            $this->_options[2] = $this->_search->getOption(Search::OPTION_SEARCH_IGNORE_UNAVAILABLE);
186
187            // remove ignore_unavailable from options if not initial search
188            if ($isNotInitial) {
189                $searchOptions = $this->_search->getOptions();
190                unset($searchOptions[Search::OPTION_SEARCH_IGNORE_UNAVAILABLE]);
191                $this->_search->setOptions($searchOptions);
192            }
193        }
194    }
195
196    /**
197     * Revert search options to previously saved state.
198     */
199    protected function _revertOptions(): void
200    {
201        $this->_search->setOption(Search::OPTION_SCROLL, $this->_options[0]);
202        $this->_search->setOption(Search::OPTION_SCROLL_ID, $this->_options[1]);
203        if (null !== $this->_options[2]) {
204            $this->_search->setOption(Search::OPTION_SEARCH_IGNORE_UNAVAILABLE, $this->_options[2]);
205        }
206    }
207}
208