BatchQueryResult.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\elasticsearch;
  8. use yii\base\BaseObject;
  9. /**
  10. * BatchQueryResult represents a batch query from which you can retrieve data in batches.
  11. *
  12. * You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by
  13. * calling [[Query::batch()]] or [[Query::each()]]. Because BatchQueryResult implements the [[\Iterator]] interface,
  14. * you can iterate it to obtain a batch of data in each iteration.
  15. *
  16. * Batch size is determined by the [[Query::$limit]] setting. [[Query::$offset]] setting is ignored.
  17. * New batches will be obtained until the server runs out of results.
  18. *
  19. * If [[Query::$orderBy]] parameter is not set, batches will be processed using the highly efficient "scan" mode.
  20. * In this case, [[Query::$limit]] setting determines batch size per shard.
  21. * See [Elasticsearch guide](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html)
  22. * for more information.
  23. *
  24. * Example:
  25. * ```php
  26. * $query = (new Query)->from('user');
  27. * foreach ($query->batch() as $i => $users) {
  28. * // $users represents the rows in the $i-th batch
  29. * }
  30. * foreach ($query->each() as $user) {
  31. * }
  32. * ```
  33. *
  34. * @author Konstantin Sirotkin <beowulfenator@gmail.com>
  35. * @since 2.0.4
  36. */
  37. class BatchQueryResult extends BaseObject implements \Iterator
  38. {
  39. /**
  40. * @var Connection the DB connection to be used when performing batch query.
  41. * If null, the `elasticsearch` application component will be used.
  42. */
  43. public $db;
  44. /**
  45. * @var Query the query object associated with this batch query.
  46. * Do not modify this property directly unless after [[reset()]] is called explicitly.
  47. */
  48. public $query;
  49. /**
  50. * @var boolean whether to return a single row during each iteration.
  51. * If false, a whole batch of rows will be returned in each iteration.
  52. */
  53. public $each = false;
  54. /**
  55. * @var DataReader the data reader associated with this batch query.
  56. */
  57. private $_dataReader;
  58. /**
  59. * @var array the data retrieved in the current batch
  60. */
  61. private $_batch;
  62. /**
  63. * @var mixed the value for the current iteration
  64. */
  65. private $_value;
  66. /**
  67. * @var string|integer the key for the current iteration
  68. */
  69. private $_key;
  70. /**
  71. * @var string the amount of time to keep the scroll window open
  72. * (in Elasticsearch [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units).
  73. */
  74. public $scrollWindow = '1m';
  75. /*
  76. * @var string internal Elasticsearch scroll id
  77. */
  78. private $_lastScrollId = null;
  79. /**
  80. * Destructor.
  81. */
  82. public function __destruct()
  83. {
  84. // make sure cursor is closed
  85. $this->reset();
  86. }
  87. /**
  88. * Resets the batch query.
  89. * This method will clean up the existing batch query so that a new batch query can be performed.
  90. */
  91. public function reset()
  92. {
  93. if(isset($this->_lastScrollId)) {
  94. $this->query->createCommand($this->db)->clearScroll(['scroll_id' => $this->_lastScrollId]);
  95. }
  96. $this->_batch = null;
  97. $this->_value = null;
  98. $this->_key = null;
  99. $this->_lastScrollId = null;
  100. }
  101. /**
  102. * Resets the iterator to the initial state.
  103. * This method is required by the interface [[\Iterator]].
  104. */
  105. public function rewind()
  106. {
  107. $this->reset();
  108. $this->next();
  109. }
  110. /**
  111. * Moves the internal pointer to the next dataset.
  112. * This method is required by the interface [[\Iterator]].
  113. */
  114. public function next()
  115. {
  116. if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
  117. $this->_batch = $this->fetchData();
  118. reset($this->_batch);
  119. }
  120. if ($this->each) {
  121. $this->_value = current($this->_batch);
  122. if ($this->query->indexBy !== null) {
  123. $this->_key = key($this->_batch);
  124. } elseif (key($this->_batch) !== null) {
  125. $this->_key++;
  126. } else {
  127. $this->_key = null;
  128. }
  129. } else {
  130. $this->_value = $this->_batch;
  131. $this->_key = $this->_key === null ? 0 : $this->_key + 1;
  132. }
  133. }
  134. /**
  135. * Fetches the next batch of data.
  136. * @return array the data fetched
  137. */
  138. protected function fetchData()
  139. {
  140. if (null === $this->_lastScrollId) {
  141. //first query - do search
  142. $options = ['scroll' => $this->scrollWindow];
  143. if(!$this->query->orderBy) {
  144. $query = clone $this->query;
  145. $query->orderBy('_doc');
  146. $cmd = $this->query->createCommand($this->db);
  147. } else {
  148. $cmd = $this->query->createCommand($this->db);
  149. }
  150. $result = $cmd->search($options);
  151. if ($result === false) {
  152. throw new Exception('Elasticsearch search query failed.');
  153. }
  154. } else {
  155. //subsequent queries - do scroll
  156. $result = $this->query->createCommand($this->db)->scroll([
  157. 'scroll_id' => $this->_lastScrollId,
  158. 'scroll' => $this->scrollWindow,
  159. ]);
  160. }
  161. //get last scroll id
  162. $this->_lastScrollId = $result['_scroll_id'];
  163. //get data
  164. return $this->query->populate($result['hits']['hits']);
  165. }
  166. /**
  167. * Returns the index of the current dataset.
  168. * This method is required by the interface [[\Iterator]].
  169. * @return int the index of the current row.
  170. */
  171. public function key()
  172. {
  173. return $this->_key;
  174. }
  175. /**
  176. * Returns the current dataset.
  177. * This method is required by the interface [[\Iterator]].
  178. * @return mixed the current dataset.
  179. */
  180. public function current()
  181. {
  182. return $this->_value;
  183. }
  184. /**
  185. * Returns whether there is a valid dataset at the current position.
  186. * This method is required by the interface [[\Iterator]].
  187. * @return bool whether there is a valid dataset at the current position.
  188. */
  189. public function valid()
  190. {
  191. return !empty($this->_batch);
  192. }
  193. }