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