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