1<?php
2
3namespace Elastica;
4
5/**
6 * Scroll Iterator.
7 *
8 * @author Manuel Andreo Garcia <andreo.garcia@gmail.com>
9 *
10 * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
11 */
12class Scroll implements \Iterator
13{
14    /**
15     * @var string
16     */
17    public $expiryTime;
18
19    /**
20     * @var Search
21     */
22    protected $_search;
23
24    /**
25     * @var string|null
26     */
27    protected $_nextScrollId;
28
29    /**
30     * @var ResultSet|null
31     */
32    protected $_currentResultSet;
33
34    /**
35     * 0: scroll<br>
36     * 1: scroll id.
37     *
38     * @var array
39     */
40    protected $_options = [null, null];
41
42    private $totalPages = 0;
43    private $currentPage = 0;
44
45    /**
46     * Constructor.
47     *
48     * @param Search $search
49     * @param string $expiryTime
50     */
51    public function __construct(Search $search, $expiryTime = '1m')
52    {
53        $this->_search = $search;
54        $this->expiryTime = $expiryTime;
55    }
56
57    /**
58     * Returns current result set.
59     *
60     * @see http://php.net/manual/en/iterator.current.php
61     *
62     * @return ResultSet
63     */
64    public function current()
65    {
66        return $this->_currentResultSet;
67    }
68
69    /**
70     * Next scroll search.
71     *
72     * @see http://php.net/manual/en/iterator.next.php
73     */
74    public function next()
75    {
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     * @return string
97     */
98    public function key()
99    {
100        return $this->_nextScrollId;
101    }
102
103    /**
104     * Returns true if current result set contains at least one hit.
105     *
106     * @see http://php.net/manual/en/iterator.valid.php
107     *
108     * @return bool
109     */
110    public function valid()
111    {
112        return null !== $this->_nextScrollId;
113    }
114
115    /**
116     * Initial scroll search.
117     *
118     * @see http://php.net/manual/en/iterator.rewind.php
119     */
120    public function rewind()
121    {
122        // reset state
123        $this->_options = [null, null];
124        $this->currentPage = 0;
125
126        // initial search
127        $this->_saveOptions();
128
129        $this->_search->setOption(Search::OPTION_SCROLL, $this->expiryTime);
130        $this->_search->setOption(Search::OPTION_SCROLL_ID, null);
131        $this->_setScrollId($this->_search->search());
132
133        $this->_revertOptions();
134    }
135
136    /**
137     * Cleares the search context on ES and marks this Scroll instance as finished.
138     */
139    public function clear()
140    {
141        if (null !== $this->_nextScrollId) {
142            $this->_search->getClient()->request(
143                '_search/scroll',
144                Request::DELETE,
145                [Search::OPTION_SCROLL_ID => [$this->_nextScrollId]]
146            );
147
148            // Reset scroll ID so valid() returns false.
149            $this->_nextScrollId = null;
150            $this->_currentResultSet = null;
151        }
152    }
153
154    /**
155     * Prepares Scroll for next request.
156     *
157     * @param ResultSet $resultSet
158     */
159    protected function _setScrollId(ResultSet $resultSet)
160    {
161        if (0 === $this->currentPage) {
162            $this->totalPages = $resultSet->count() > 0 ? \ceil($resultSet->getTotalHits() / $resultSet->count()) : 0;
163        }
164
165        $this->_currentResultSet = $resultSet;
166        ++$this->currentPage;
167        $this->_nextScrollId = $resultSet->getResponse()->isOk() && $resultSet->count() > 0 ? $resultSet->getResponse()->getScrollId() : null;
168    }
169
170    /**
171     * Save all search options manipulated by Scroll.
172     */
173    protected function _saveOptions()
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
184    /**
185     * Revert search options to previously saved state.
186     */
187    protected function _revertOptions()
188    {
189        $this->_search->setOption(Search::OPTION_SCROLL, $this->_options[0]);
190        $this->_search->setOption(Search::OPTION_SCROLL_ID, $this->_options[1]);
191    }
192}
193