diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bb70422d..5799c7b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,30 +33,17 @@ jobs: strategy: matrix: php: - - 8.0 - 8.1 - 8.2 es: - - 8.1.3 - - 7.14.0 - - services: - elasticsearch: - image: elasticsearch:${{ matrix.es }} - ports: - - 9200:9200 - env: - http.publish_host: 127.0.0.1 - transport.host: 127.0.0.1 - xpack.security.enabled: false - indices.id_field_data.enabled: true - options: >- - --health-cmd "curl http://localhost:9200/_cluster/health" - --health-interval 10s - --health-timeout 5s - --health-retries 10 + - 8.7.0 steps: + - name: Service elastisearch. + run: | + docker network create somenetwork + docker run -d --name elasticsearch --net somenetwork -p 9200:9200 -e "http.publish_host=127.0.0.1" -e "transport.host=127.0.0.1" -e "indices.id_field_data.enabled=true" -e "xpack.security.enabled=false" elasticsearch:${{ matrix.es }} + - name: Checkout. uses: actions/checkout@v3 @@ -70,5 +57,15 @@ jobs: - name: Install dependencies with composer. run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader + - name: Wait for Elasticsearch server to start. + run: wget --retry-connrefused --waitretry=3 --timeout=30 -t 10 -O /dev/null http://127.0.0.1:9200 + - name: Run tests with phpunit. - run: vendor/bin/phpunit + run: vendor/bin/phpunit --coverage-clover=coverage.xml --colors=always + + - name: Upload coverage to Codecov. + if: matrix.php == '8.1' + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index ae5893f5..0474238e 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -30,4 +30,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1'] + ['8.1', '8.2'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index fb7fc77d..301ab7c1 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -28,4 +28,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.0', '8.1', '8.2'] + ['8.1', '8.2'] diff --git a/composer.json b/composer.json index 4188f7fa..8dbb0a40 100644 --- a/composer.json +++ b/composer.json @@ -1,43 +1,52 @@ { "name": "yiisoft/db-elasticsearch", - "description": "Yii Framework Elasticsearch Query and ActiveRecord", - "keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"], + "description": "Elasticsearch integration and ActiveRecord for the Yii framework", + "keywords": [ + "yii2", + "elasticsearch", + "active-record", + "search", + "fulltext" + ], "type": "library", "license": "BSD-3-Clause", "support": { - "issues": "https://github.com/yiisoft/yii-elasticsearch/issues", + "issues": "https://github.com/yiisoft/yii2-elasticsearch/issues", "forum": "http://www.yiiframework.com/forum/", "wiki": "http://www.yiiframework.com/wiki/", "irc": "irc://irc.freenode.net/yii", - "source": "https://github.com/yiisoft/yii-elasticsearch" + "source": "https://github.com/yiisoft/yii2-elasticsearch" }, - "authors": [ - { - "name": "Carsten Brandt", - "email": "mail@cebe.cc" - } - ], - "minimum-stability": "dev", - "prefer-stable": true, "require": { - "yiisoft/arrays": "^3.0@dev", - "yiisoft/di": "^3.0@dev", - "yiisoft/strings": "^3.0@dev", - "yiisoft/yii-core": "^3.0@dev", - "ext-curl": "*" + "php": "^8.1", + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "paragonie/random_compat": ">=1", + "psr/log": "^2.0|^3.0", + "yiisoft/arrays": "^3.0", + "yiisoft/json": "^1.0" }, "require-dev": { - "yiisoft/log": "^3.0@dev", - "yiisoft/cache": "^3.0@dev", - "yiisoft/active-record": "^3.0@dev", - "phpunit/phpunit": "^9.0", - "hiqdev/composer-config-plugin": "^1.0@dev" + "maglnet/composer-require-checker": "^4.2", + "phpunit/phpunit": "^9.6|^10.0", + "rector/rector": "^0.15", + "vimeo/psalm": "^4.8|^5.8" }, "autoload": { - "psr-4": { "Yiisoft\\Db\\ElasticSearch\\": "src" } + "psr-4": { + "Yiisoft\\Elasticsearch\\": "src" + } }, "autoload-dev": { - "psr-4": {"Yiisoft\\Db\\ElasticSearch\\Tests\\": "tests"} + "psr-4": { + "Yiisoft\\Elasticsearch\\Tests\\": "tests" + } + }, + "config": { + "allow-plugins": { + "yiisoft/yii2-composer": true + } }, "extra": { "branch-alias": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3a3db9fa..ace7e7a1 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,13 +1,14 @@ - - - - ./tests - - + + + + + ./tests + + + + + ./src + + diff --git a/src/ActiveDataProvider.php b/src/ActiveDataProvider.php deleted file mode 100644 index 0471ad4c..00000000 --- a/src/ActiveDataProvider.php +++ /dev/null @@ -1,161 +0,0 @@ - - * @since 2.0.5 - */ -class ActiveDataProvider extends \yii\data\ActiveDataProvider -{ - /** - * @var array the full query results. - */ - private $_queryResults; - - /** - * @param array $results full query results - */ - public function setQueryResults($results) - { - $this->_queryResults = $results; - } - - /** - * @return array full query results - */ - public function getQueryResults() - { - if (!is_array($this->_queryResults)) { - $this->prepare(); - } - return $this->_queryResults; - } - - /** - * @return array all aggregations results - */ - public function getAggregations() - { - $results = $this->getQueryResults(); - return $results['aggregations'] ?? []; - } - - /** - * Returns results of the specified aggregation. - * @param string $name aggregation name. - * @throws InvalidCallException if requested aggregation does not present in query results. - * @return array aggregation results. - */ - public function getAggregation($name) - { - $aggregations = $this->getAggregations(); - if (!isset($aggregations[$name])) { - throw new InvalidCallException("Aggregation '{$name}' does not present."); - } - return $aggregations[$name]; - } - - /** - * @inheritdoc - */ - protected function prepareModels() - { - if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.'); - } - - $query = clone $this->query; - if (($pagination = $this->getPagination()) !== false) { - // pagination fails to validate page number, because total count is unknown at this stage - $pagination->validatePage = false; - $query->limit($pagination->getLimit())->offset($pagination->getOffset()); - } - if (($sort = $this->getSort()) !== false) { - $query->addOrderBy($sort->getOrders()); - } - - $results = $query->search($this->db); - $this->setQueryResults($results); - - if ($pagination !== false) { - $pagination->totalCount = $this->getTotalCount(); - } - - return $results['hits']['hits']; - } - - /** - * @inheritdoc - */ - protected function prepareTotalCount() - { - if (!$this->query instanceof Query) { - throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.'); - } - - $results = $this->getQueryResults(); - return (int)$results['hits']['total']; - } - - /** - * @inheritdoc - */ - protected function prepareKeys($models) - { - $keys = []; - if ($this->key !== null) { - foreach ($models as $model) { - if (is_string($this->key)) { - $keys[] = $model[$this->key]; - } else { - $keys[] = ($this->key)($model); - } - } - - return $keys; - } - if ($this->query instanceof ActiveQueryInterface) { - /* @var $class \Yiisoft\Db\ActiveRecord */ - $class = $this->query->modelClass; - $pks = $class::primaryKey(); - if (count($pks) === 1) { - foreach ($models as $model) { - $keys[] = $model->primaryKey; - } - } else { - foreach ($models as $model) { - $kk = []; - foreach ($pks as $pk) { - $kk[$pk] = $model[$pk]; - } - $keys[] = $kk; - } - } - - return $keys; - } - return array_keys($models); - } -} diff --git a/src/ActiveFixture.php b/src/ActiveFixture.php deleted file mode 100644 index 45f94cab..00000000 --- a/src/ActiveFixture.php +++ /dev/null @@ -1,157 +0,0 @@ - - * @author Qiang Xue - * @since 2.0.2 - */ -class ActiveFixture extends BaseActiveFixture -{ - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the DbFixture object is created, if you want to change this property, you should only assign it - * with a DB connection object. - */ - public $db = 'elasticsearch'; - /** - * @var string the name of the index that this fixture is about. If this property is not set, - * the name will be determined via [[modelClass]]. - * @see modelClass - */ - public $index; - /** - * @var string the name of the type that this fixture is about. If this property is not set, - * the name will be determined via [[modelClass]]. - * @see modelClass - */ - public $type; - /** - * @var bool|string the file path or path alias of the data file that contains the fixture data - * to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/Index/Type.php`, - * where `FixturePath` stands for the directory containing this fixture class, `Index` stands for the elasticsearch [[index]] name - * and `Type` stands for the [[type]] associated with this fixture. - * You can set this property to be false to prevent loading any data. - */ - public $dataFile; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - if (!isset($this->modelClass) && (!isset($this->index) || !isset($this->type))) { - throw new InvalidConfigException('Either "modelClass" or "index" and "type" must be set.'); - } - /* @var $modelClass ActiveRecord */ - $modelClass = $this->modelClass; - if ($this->index === null) { - $this->index = $modelClass::index(); - } - if ($this->type === null) { - $this->type = $modelClass::type(); - } - } - - /** - * Loads the fixture. - * - * The default implementation will first clean up the index by calling [[resetIndex()]]. - * It will then populate the index with the data returned by [[getData()]]. - * - * If you override this method, you should consider calling the parent implementation - * so that the data returned by [[getData()]] can be populated into the index. - */ - public function load() - { - $this->resetIndex(); - $this->data = []; - - $mapping = $this->db->createCommand()->getMapping($this->index, $this->type); - if (isset($mapping[$this->index]['mappings'][$this->type]['_id']['path'])) { - $idField = $mapping[$this->index]['mappings'][$this->type]['_id']['path']; - } else { - $idField = '_id'; - } - - foreach ($this->getData() as $alias => $row) { - $options = []; - $id = $row[$idField] ?? null; - if ($idField === '_id') { - unset($row[$idField]); - } - if (isset($row['_parent'])) { - $options['parent'] = $row['_parent']; - unset($row['_parent']); - } - - try { - $response = $this->db->createCommand()->insert($this->index, $this->type, $row, $id, $options); - } catch (\Yiisoft\Db\Exception $e) { - throw new \yii\base\Exception("Failed to insert fixture data \"$alias\": " . $e->getMessage() . "\n" . print_r($e->errorInfo, true), $e->getCode(), $e); - } - if ($id === null) { - $row[$idField] = $response['_id']; - } - $this->data[$alias] = $row; - } - // ensure all data is flushed and immediately available in the test - $this->db->createCommand()->flushIndex($this->index); - } - - /** - * Returns the fixture data. - * - * The default implementation will try to return the fixture data by including the external file specified by [[dataFile]]. - * The file should return an array of data rows (column name => column value), each corresponding to a row in the index. - * - * If the data file does not exist, an empty array will be returned. - * - * @return array the data rows to be inserted into the database index. - */ - protected function getData() - { - if ($this->dataFile === null) { - $class = new \ReflectionClass($this); - $dataFile = dirname($class->getFileName()) . "/data/{$this->index}/{$this->type}.php"; - return is_file($dataFile) ? require($dataFile) : []; - } - return parent::getData(); - } - - /** - * Removes all existing data from the specified index and type. - * This method is called before populating fixture data into the index associated with this fixture. - */ - protected function resetIndex() - { - $this->db->createCommand([ - 'index' => $this->index, - 'type' => $this->type, - 'queryParts' => ['query' => ['match_all' => new \stdClass()]], - ])->deleteByQuery(); - } -} diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php deleted file mode 100644 index 350fd622..00000000 --- a/src/ActiveQuery.php +++ /dev/null @@ -1,341 +0,0 @@ -with('orders')->asArray()->all(); - * ``` - * > NOTE: elasticsearch limits the number of records returned to 10 records by default. - * > If you expect to get more records you should specify limit explicitly. - * - * Relational query - * ---------------- - * - * In relational context ActiveQuery represents a relation between two Active Record classes. - * - * Relational ActiveQuery instances are usually created by calling [[ActiveRecord::hasOne()]] and - * [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining - * a getter method which calls one of the above methods and returns the created ActiveQuery object. - * - * A relation is specified by [[link]] which represents the association between columns - * of different tables; and the multiplicity of the relation is indicated by [[multiple]]. - * - * If a relation involves a junction table, it may be specified by [[via()]]. - * This methods may only be called in a relational context. Same is true for [[inverseOf()]], which - * marks a relation as inverse of another relation. - * - * > Note: elasticsearch limits the number of records returned by any query to 10 records by default. - * > If you expect to get more records you should specify limit explicitly in relation definition. - * > This is also important for relations that use [[via()]] so that if via records are limited to 10 - * > the relations records can also not be more than 10. - * - * > Note: Currently [[with]] is not supported in combination with [[asArray]]. - * - * @author Carsten Brandt - * @since 2.0 - */ -class ActiveQuery extends Query implements ActiveQueryInterface -{ - use ActiveQueryTrait; - use ActiveRelationTrait; - - /** - * @event Event an event that is triggered when the query is initialized via [[init()]]. - */ - public const EVENT_INIT = 'init'; - - /** - * Constructor. - * @param array $modelClass the model class associated with this query - * @param array $config configurations to be applied to the newly created query object - */ - public function __construct($modelClass, $config = []) - { - $this->modelClass = $modelClass; - parent::__construct($config); - } - - /** - * Initializes the object. - * This method is called at the end of the constructor. The default implementation will trigger - * an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end - * to ensure triggering of the event. - */ - public function init() - { - parent::init(); - $this->trigger(self::EVENT_INIT); - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($this->primaryModel !== null) { - // lazy loading - if (is_array($this->via)) { - // via relation - /* @var $viaQuery ActiveQuery */ - [$viaName, $viaQuery] = $this->via; - if ($viaQuery->multiple) { - $viaModels = $viaQuery->all(); - $this->primaryModel->populateRelation($viaName, $viaModels); - } else { - $model = $viaQuery->one(); - $this->primaryModel->populateRelation($viaName, $model); - $viaModels = $model === null ? [] : [$model]; - } - $this->filterByModels($viaModels); - } else { - $this->filterByModels([$this->primaryModel]); - } - } - - /* @var $modelClass ActiveRecord */ - $modelClass = $this->modelClass; - if ($db === null) { - $db = $modelClass::getDb(); - } - - if ($this->type === null) { - $this->type = $modelClass::type(); - } - if ($this->index === null) { - $this->index = $modelClass::index(); - $this->type = $modelClass::type(); - } - $commandConfig = $db->getQueryBuilder()->build($this); - - return $db->createCommand($commandConfig); - } - - /** - * Executes query and returns all results as an array. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - return parent::all($db); - } - - /** - * Converts found rows into model instances - * @param array $rows - * @return ActiveRecord[]|array - * @since 2.0.4 - */ - private function createModels($rows) - { - $models = []; - if ($this->asArray) { - if ($this->indexBy === null) { - return $rows; - } - foreach ($rows as $row) { - if (is_string($this->indexBy)) { - $key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy]; - } else { - $key = ($this->indexBy)($row); - } - $models[$key] = $row; - } - } else { - /* @var $class ActiveRecord */ - $class = $this->modelClass; - if ($this->indexBy === null) { - foreach ($rows as $row) { - $model = $class::instantiate($row); - $modelClass = get_class($model); - $modelClass::populateRecord($model, $row); - $models[] = $model; - } - } else { - foreach ($rows as $row) { - $model = $class::instantiate($row); - $modelClass = get_class($model); - $modelClass::populateRecord($model, $row); - if (is_string($this->indexBy)) { - $key = $model->{$this->indexBy}; - } else { - $key = ($this->indexBy)($model); - } - $models[$key] = $model; - } - } - } - - return $models; - } - - /** - * @inheritdoc - * @since 2.0.4 - */ - public function populate($rows) - { - if (empty($rows)) { - return []; - } - - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - if (!$this->asArray) { - foreach ($models as $model) { - $model->afterFind(); - } - } - - return $models; - } - - /** - * Executes query and returns a single row of result. - * @param Connection $db the DB connection used to create the DB command. - * If null, the DB connection returned by [[modelClass]] will be used. - * @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], - * the query result may be either an array or an ActiveRecord object. Null will be returned - * if the query results in nothing. - */ - public function one($db = null) - { - if (($result = parent::one($db)) === false) { - return null; - } - if ($this->asArray) { - // TODO implement with() -// /* @var $modelClass ActiveRecord */ -// $modelClass = $this->modelClass; -// $model = $result['_source']; -// $pk = $modelClass::primaryKey()[0]; -// if ($pk === '_id') { -// $model['_id'] = $result['_id']; -// } -// $model['_score'] = $result['_score']; -// if (!empty($this->with)) { -// $models = [$model]; -// $this->findWith($this->with, $models); -// $model = $models[0]; -// } - return $result; - } - /* @var $class ActiveRecord */ - $class = $this->modelClass; - $model = $class::instantiate($result); - $class = get_class($model); - $class::populateRecord($model, $result); - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - $model->afterFind(); - return $model; - } - - /** - * @inheritdoc - */ - public function search($db = null, $options = []) - { - $command = $this->createCommand($db); - $result = $command->search($options); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.', [ - 'index' => $command->index, - 'type' => $command->type, - 'query' => $command->queryParts, - 'options' => $command->options, - ]); - } - // TODO implement with() for asArray - if (!empty($result['hits']['hits']) && !$this->asArray) { - $models = $this->createModels($result['hits']['hits']); - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - foreach ($models as $model) { - $model->afterFind(); - } - $result['hits']['hits'] = $models; - } - - return $result; - } - - /** - * @inheritdoc - */ - public function column($field, $db = null) - { - if ($field === '_id') { - $command = $this->createCommand($db); - $command->queryParts['fields'] = []; - $command->queryParts['_source'] = false; - $result = $command->search(); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - if (empty($result['hits']['hits'])) { - return []; - } - $column = []; - foreach ($result['hits']['hits'] as $row) { - $column[] = $row['_id']; - } - - return $column; - } - - return parent::column($field, $db); - } -} diff --git a/src/ActiveRecord.php b/src/ActiveRecord.php deleted file mode 100644 index 642420ba..00000000 --- a/src/ActiveRecord.php +++ /dev/null @@ -1,865 +0,0 @@ - - * @since 2.0 - */ -class ActiveRecord extends BaseActiveRecord -{ - private $_id; - private $_score; - private $_version; - private $_highlight; - private $_explanation; - - /** - * Returns the database connection used by this AR class. - * By default, the "elasticsearch" application component is used as the database connection. - * You may override this method if you want to use a different database connection. - * @return Connection the database connection used by this AR class. - */ - public static function getDb() - { - return \Yii::$app->get('elasticsearch'); - } - - /** - * @inheritdoc - * @return ActiveQuery the newly created [[ActiveQuery]] instance. - */ - public static function find() - { - return Yii::createObject(ActiveQuery::className(), [static::class]); - } - - /** - * @inheritdoc - */ - public static function findOne($condition) - { - if (!is_array($condition)) { - return static::get($condition); - } - if (!ArrayHelper::isAssociative($condition)) { - $records = static::mget(array_values($condition)); - return empty($records) ? null : reset($records); - } - - $condition = static::filterCondition($condition); - return static::find()->andWhere($condition)->one(); - } - - /** - * @inheritdoc - */ - public static function findAll($condition) - { - if (!ArrayHelper::isAssociative($condition)) { - return static::mget(is_array($condition) ? array_values($condition) : [$condition]); - } - - $condition = static::filterCondition($condition); - return static::find()->andWhere($condition)->all(); - } - - /** - * Filter out condition parts that are array valued, to prevent building arbitrary conditions. - * @param array $condition - */ - private static function filterCondition($condition) - { - foreach ($condition as $k => $v) { - if (is_array($v)) { - $condition[$k] = array_values($v); - foreach ($v as $vv) { - if (is_array($vv)) { - throw new InvalidArgumentException('Nested arrays are not allowed in condition for findAll() and findOne().'); - } - } - } - } - return $condition; - } - - /** - * Gets a record by its primary key. - * - * @param mixed $primaryKey the primaryKey value - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. - * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) - * for more details on these options. - * @return static|null The record instance or null if it was not found. - */ - public static function get($primaryKey, $options = []) - { - if ($primaryKey === null) { - return null; - } - $command = static::getDb()->createCommand(); - $result = $command->get(static::index(), static::type(), $primaryKey, $options); - if ($result['found']) { - $model = static::instantiate($result); - static::populateRecord($model, $result); - $model->afterFind(); - - return $model; - } - - return null; - } - - /** - * Gets a list of records by its primary keys. - * - * @param array $primaryKeys an array of primaryKey values - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. - * - * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) - * for more details on these options. - * @return array The record instances, or empty array if nothing was found - */ - public static function mget(array $primaryKeys, $options = []) - { - if (empty($primaryKeys)) { - return []; - } - if (count($primaryKeys) === 1) { - $model = static::get(reset($primaryKeys)); - return $model === null ? [] : [$model]; - } - - $command = static::getDb()->createCommand(); - $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); - $models = []; - foreach ($result['docs'] as $doc) { - if ($doc['found']) { - $model = static::instantiate($doc); - static::populateRecord($model, $doc); - $model->afterFind(); - $models[] = $model; - } - } - - return $models; - } - - // TODO add more like this feature http://www.elastic.co/guide/en/elasticsearch/reference/current/search-more-like-this.html - - // TODO add percolate functionality http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html - - // TODO implement copy and move as pk change is not possible - - /** - * @return float returns the score of this record when it was retrieved via a [[find()]] query. - */ - public function getScore() - { - return $this->_score; - } - - /** - * @return array|null A list of arrays with highlighted excerpts indexed by field names. - */ - public function getHighlight() - { - return $this->_highlight; - } - - /** - * @return array|null An explanation for each hit on how its score was computed. - * @since 2.0.5 - */ - public function getExplanation() - { - return $this->_explanation; - } - - /** - * Sets the primary key - * @param mixed $value - * @throws \yii\base\InvalidCallException when record is not new - */ - public function setPrimaryKey($value) - { - $pk = static::primaryKey()[0]; - if ($this->getIsNewRecord() || $pk != '_id') { - $this->$pk = $value; - } else { - throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); - } - } - - /** - * @inheritdoc - */ - public function getPrimaryKey($asArray = false) - { - $pk = static::primaryKey()[0]; - if ($asArray) { - return [$pk => $this->$pk]; - } - return $this->$pk; - } - - /** - * @inheritdoc - */ - public function getOldPrimaryKey($asArray = false) - { - $pk = static::primaryKey()[0]; - if ($this->getIsNewRecord()) { - $id = null; - } elseif ($pk == '_id') { - $id = $this->_id; - } else { - $id = $this->getOldAttribute($pk); - } - if ($asArray) { - return [$pk => $id]; - } - return $id; - } - - /** - * This method defines the attribute that uniquely identifies a record. - * - * The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the - * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]]. - * - * You may override this method to define the primary key name when you have defined - * [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) - * for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]]. - * - * Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature - * of the [[\Yiisoft\Db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a - * single string. - * - * @return string[] array of primary key attributes. Only the first element of the array will be used. - */ - public static function primaryKey() - { - return ['_id']; - } - - /** - * Returns the list of all attribute names of the model. - * - * This method must be overridden by child classes to define available attributes. - * - * Attributes are names of fields of the corresponding elasticsearch document. - * The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes. - * You may define [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) - * for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes. - * - * @throws \yii\base\InvalidConfigException if not overridden in a child class. - * @return string[] list of attribute names. - */ - public function attributes() - { - throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); - } - - /** - * A list of attributes that should be treated as array valued when retrieved through [[ActiveQuery::fields]]. - * - * If not listed by this method, attributes retrieved through [[ActiveQuery::fields]] will converted to a scalar value - * when the result array contains only one value. - * - * @return string[] list of attribute names. Must be a subset of [[attributes()]]. - */ - public function arrayAttributes() - { - return []; - } - - /** - * @return string the name of the index this record is stored in. - */ - public static function index() - { - return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(static::class), '-')); - } - - /** - * @return string the name of the type of this record. - */ - public static function type() - { - return Inflector::camel2id(StringHelper::basename(static::class), '-'); - } - - /** - * @inheritdoc - * - * @param ActiveRecord $record the record to be populated. In most cases this will be an instance - * created by [[instantiate()]] beforehand. - * @param array $row attribute values (name => value) - */ - public static function populateRecord($record, $row) - { - $attributes = []; - if (isset($row['_source'])) { - $attributes = $row['_source']; - } - if (isset($row['fields'])) { - // reset fields in case it is scalar value - $arrayAttributes = $record->arrayAttributes(); - foreach ($row['fields'] as $key => $value) { - if (!isset($arrayAttributes[$key]) && count($value) == 1) { - $row['fields'][$key] = reset($value); - } - } - $attributes = array_merge($attributes, $row['fields']); - } - - parent::populateRecord($record, $attributes); - - $pk = static::primaryKey()[0];//TODO should always set ID in case of fields are not returned - if ($pk === '_id') { - $record->_id = $row['_id']; - } - $record->_highlight = $row['highlight'] ?? null; - $record->_score = $row['_score'] ?? null; - $record->_version = $row['_version'] ?? null; // TODO version should always be available... - $record->_explanation = $row['_explanation'] ?? null; - } - - /** - * Creates an active record instance. - * - * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. - * It is not meant to be used for creating new records directly. - * - * You may override this method if the instance being created - * depends on the row data to be populated into the record. - * For example, by creating a record based on the value of a column, - * you may implement the so-called single-table inheritance mapping. - * @param array $row row data to be populated into the record. - * This array consists of the following keys: - * - `_source`: refers to the attributes of the record. - * - `_type`: the type this record is stored in. - * - `_index`: the index this record is stored in. - * @return static the newly created active record - */ - public static function instantiate($row) - { - return new static(); - } - - /** - * Inserts a document into the associated index using the attribute values of this record. - * - * This method performs the following steps in order: - * - * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation - * fails, it will skip the rest of the steps; - * 2. call [[afterValidate()]] when `$runValidation` is true. - * 3. call [[beforeSave()]]. If the method returns false, it will skip the - * rest of the steps; - * 4. insert the record into database. If this fails, it will skip the rest of the steps; - * 5. call [[afterSave()]]; - * - * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], - * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] - * will be raised by the corresponding methods. - * - * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. - * - * If the [[primaryKey|primary key]] is not set (null) during insertion, - * it will be populated with a - * [randomly generated value](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) - * after insertion. - * - * For example, to insert a customer record: - * - * ~~~ - * $customer = new Customer; - * $customer->name = $name; - * $customer->email = $email; - * $customer->insert(); - * ~~~ - * - * @param bool $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributes list of attributes that need to be saved. Defaults to null, - * meaning all attributes will be saved. - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. These are among others: - * - * - `routing` define shard placement of this record. - * - `parent` by giving the primaryKey of another record this defines a parent-child relation - * - * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html) - * for more details on these options. - * - * By default the `op_type` is set to `create` if model primary key is present. - * @return bool whether the attributes are valid and the record is inserted successfully. - */ - public function insert($runValidation = true, $attributes = null, $options = [ ]) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - if (!$this->beforeSave(true)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - - if ($this->getPrimaryKey() !== null) { - $options['op_type'] = $options['op_type'] ?? 'create'; - } - - $response = static::getDb()->createCommand()->insert( - static::index(), - static::type(), - $values, - $this->getPrimaryKey(), - $options - ); - - $pk = static::primaryKey()[0]; - $this->$pk = $response['_id']; - if ($pk != '_id') { - $values[$pk] = $response['_id']; - } - $this->_version = $response['_version']; - $this->_score = null; - - $changedAttributes = array_fill_keys(array_keys($values), null); - $this->setOldAttributes($values); - $this->afterSave(true, $changedAttributes); - - return true; - } - - /** - * @inheritdoc - * - * @param bool $runValidation whether to perform validation before saving the record. - * If the validation fails, the record will not be inserted into the database. - * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, - * meaning all attributes that are loaded from DB will be saved. - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. These are among others: - * - * - `routing` define shard placement of this record. - * - `parent` by giving the primaryKey of another record this defines a parent-child relation - * - `timeout` timeout waiting for a shard to become available. - * - `replication` the replication type for the delete/index operation (sync or async). - * - `consistency` the write consistency of the index/delete operation. - * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately. - * - `detect_noop` this parameter will become part of the request body and will prevent the index from getting updated when nothing has changed. - * - * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html#_parameters_3) - * for more details on these options. - * - * The following parameters are Yii specific: - * - * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it - * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]]. - * See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html) for details. - * - * Make sure the record has been fetched with a [[version]] before. This is only the case - * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly. - * - * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated. - * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled. - * @throws Exception in case update failed. - * @return bool|int the number of rows affected, or false if validation fails - * or [[beforeSave()]] stops the updating process. - */ - public function update($runValidation = true, $attributeNames = null, $options = []) - { - if ($runValidation && !$this->validate($attributeNames)) { - return false; - } - return $this->updateInternal($attributeNames, $options); - } - - /** - * @see update() - * @param array $attributes attributes to update - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. See [[update()]] for details. - * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated. - * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled. - * @throws Exception in case update failed. - * @return false|int the number of rows affected, or false if [[beforeSave()]] stops the updating process. - */ - protected function updateInternal($attributes = null, $options = []) - { - if (!$this->beforeSave(false)) { - return false; - } - $values = $this->getDirtyAttributes($attributes); - if (empty($values)) { - $this->afterSave(false, $values); - return 0; - } - - if (isset($options['optimistic_locking']) && $options['optimistic_locking']) { - if ($this->_version === null) { - throw new InvalidParamException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::update() for details.'); - } - $options['version'] = $this->_version; - unset($options['optimistic_locking']); - } - - try { - $result = static::getDb()->createCommand()->update( - static::index(), - static::type(), - $this->getOldPrimaryKey(false), - $values, - $options - ); - } catch (Exception $e) { - // HTTP 409 is the response in case of failed optimistic locking - // http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html - if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) { - throw new StaleObjectException('The object being updated is outdated.', $e->errorInfo, $e->getCode(), $e); - } - throw $e; - } - - if (is_array($result) && isset($result['_version'])) { - $this->_version = $result['_version']; - } - - $changedAttributes = []; - foreach ($values as $name => $value) { - $changedAttributes[$name] = $this->getOldAttribute($name); - $this->setOldAttribute($name, $value); - } - $this->afterSave(false, $changedAttributes); - - if ($result === false) { - return 0; - } - return 1; - } - - /** - * Performs a quick and highly efficient scroll/scan query to get the list of primary keys that - * satisfy the given condition. If condition is a list of primary keys - * (e.g.: `['_id' => ['1', '2', '3']]`), the query is not performed for performance considerations. - * @param array $condition please refer to [[ActiveQuery::where()]] on how to specify this parameter - * @return array primary keys that correspond to given conditions - * @see updateAll() - * @see updateAllCounters() - * @see deleteAll() - * @since 2.0.4 - */ - protected static function primaryKeysByCondition($condition) - { - $pkName = static::primaryKey()[0]; - if (count($condition) == 1 && isset($condition[$pkName])) { - $primaryKeys = (array)$condition[$pkName]; - } else { - //fetch only document metadata (no fields), 1000 documents per shard - $query = static::find()->where($condition)->asArray()->source(false)->limit(1000); - $primaryKeys = []; - foreach ($query->each('1m') as $document) { - $primaryKeys[] = $document['_id']; - } - } - return $primaryKeys; - } - - /** - * Updates all records whos primary keys are given. - * For example, to change the status to be 1 for all customers whose status is 2: - * - * ~~~ - * Customer::updateAll(['status' => 1], ['status' => 2]); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param array $condition the conditions that will be passed to the `where()` method when building the query. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @see [[ActiveRecord::primaryKeysByCondition()]] - * @throws Exception on error. - * @return int the number of rows updated - */ - public static function updateAll($attributes, $condition = []) - { - $primaryKeys = static::primaryKeysByCondition($condition); - if (empty($primaryKeys)) { - return 0; - } - - $bulkCommand = static::getDb()->createBulkCommand([ - 'index' => static::index(), - 'type' => static::type(), - ]); - foreach ($primaryKeys as $pk) { - $bulkCommand->addAction(['update' => ['_id' => $pk]], ['doc' => $attributes]); - } - $response = $bulkCommand->execute(); - - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['update']['status']) && $item['update']['status'] == 200) { - $n++; - } else { - $errors[] = $item['update']; - } - } - if (!empty($errors) || isset($response['errors']) && $response['errors']) { - throw new Exception(__METHOD__ . ' failed updating records.', $errors); - } - - return $n; - } - - /** - * Updates all matching records using the provided counter changes and conditions. - * For example, to add 1 to age of all customers whose status is 2, - * - * ~~~ - * Customer::updateAllCounters(['age' => 1], ['status' => 2]); - * ~~~ - * - * @param array $counters the counters to be updated (attribute name => increment value). - * Use negative values if you want to decrement the counters. - * @param array $condition the conditions that will be passed to the `where()` method when building the query. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @see [[ActiveRecord::primaryKeysByCondition()]] - * @throws Exception on error. - * @return int the number of rows updated - */ - public static function updateAllCounters($counters, $condition = []) - { - $primaryKeys = static::primaryKeysByCondition($condition); - if (empty($primaryKeys) || empty($counters)) { - return 0; - } - - $bulkCommand = static::getDb()->createBulkCommand([ - 'index' => static::index(), - 'type' => static::type(), - ]); - foreach ($primaryKeys as $pk) { - $script = ''; - foreach ($counters as $counter => $value) { - $script .= "ctx._source.{$counter} += params.{$counter};\n"; - } - $bulkCommand->addAction(['update' => ['_id' => $pk]], [ - 'script' => [ - 'inline' => $script, - 'params' => $counters, - 'lang' => 'painless', - ], - ]); - } - $response = $bulkCommand->execute(); - - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['update']['status']) && $item['update']['status'] == 200) { - $n++; - } else { - $errors[] = $item['update']; - } - } - if (!empty($errors) || isset($response['errors']) && $response['errors']) { - throw new Exception(__METHOD__ . ' failed updating records counters.', $errors); - } - - return $n; - } - - /** - * @inheritdoc - * - * @param array $options options given in this parameter are passed to elasticsearch - * as request URI parameters. These are among others: - * - * - `routing` define shard placement of this record. - * - `parent` by giving the primaryKey of another record this defines a parent-child relation - * - `timeout` timeout waiting for a shard to become available. - * - `replication` the replication type for the delete/index operation (sync or async). - * - `consistency` the write consistency of the index/delete operation. - * - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately. - * - * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html) - * for more details on these options. - * - * The following parameters are Yii specific: - * - * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it - * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]]. - * See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html#delete-versioning) for details. - * - * Make sure the record has been fetched with a [[version]] before. This is only the case - * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly. - * - * @throws StaleObjectException if optimistic locking is enabled and the data being deleted is outdated. - * @throws Exception in case delete failed. - * @return bool|int the number of rows deleted, or false if the deletion is unsuccessful for some reason. - * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. - */ - public function delete($options = []) - { - if (!$this->beforeDelete()) { - return false; - } - if (isset($options['optimistic_locking']) && $options['optimistic_locking']) { - if ($this->_version === null) { - throw new InvalidParamException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::delete() for details.'); - } - $options['version'] = $this->_version; - unset($options['optimistic_locking']); - } - - try { - $result = static::getDb()->createCommand()->delete( - static::index(), - static::type(), - $this->getOldPrimaryKey(false), - $options - ); - } catch (Exception $e) { - // HTTP 409 is the response in case of failed optimistic locking - // http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html - if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) { - throw new StaleObjectException('The object being deleted is outdated.', $e->errorInfo, $e->getCode(), $e); - } - throw $e; - } - - $this->setOldAttributes(null); - - $this->afterDelete(); - - if ($result === false) { - return 0; - } - return 1; - } - - /** - * Deletes rows in the table using the provided conditions. - * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. - * - * For example, to delete all customers whose status is 3: - * - * ~~~ - * Customer::deleteAll(['status' => 3]); - * ~~~ - * - * @param array $condition the conditions that will be passed to the `where()` method when building the query. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @see [[ActiveRecord::primaryKeysByCondition()]] - * @throws Exception on error. - * @return int the number of rows deleted - */ - public static function deleteAll($condition = []) - { - $primaryKeys = static::primaryKeysByCondition($condition); - if (empty($primaryKeys)) { - return 0; - } - - $bulkCommand = static::getDb()->createBulkCommand([ - 'index' => static::index(), - 'type' => static::type(), - ]); - foreach ($primaryKeys as $pk) { - $bulkCommand->addDeleteAction($pk); - } - $response = $bulkCommand->execute(); - - $n = 0; - $errors = []; - foreach ($response['items'] as $item) { - if (isset($item['delete']['status']) && $item['delete']['status'] == 200) { - if (isset($item['delete']['found']) && $item['delete']['found']) { - $n++; - } - } else { - $errors[] = $item['delete']; - } - } - if (!empty($errors) || isset($response['errors']) && $response['errors']) { - throw new Exception(__METHOD__ . ' failed deleting records.', $errors); - } - - return $n; - } - - /** - * This method has no effect in Elasticsearch ActiveRecord. - * - * Elasticsearch ActiveRecord uses [native Optimistic locking](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html). - * See [[update()]] for more details. - */ - public function optimisticLock() - { - return null; - } - - /** - * Destroys the relationship in current model. - * - * This method is not supported by elasticsearch. - */ - public function unlinkAll($name, $delete = false) - { - throw new NotSupportedException('unlinkAll() is not supported by elasticsearch, use unlink() instead.'); - } -} diff --git a/src/BatchQueryResult.php b/src/BatchQueryResult.php deleted file mode 100644 index d2c5b949..00000000 --- a/src/BatchQueryResult.php +++ /dev/null @@ -1,211 +0,0 @@ -from('user'); - * foreach ($query->batch() as $i => $users) { - * // $users represents the rows in the $i-th batch - * } - * foreach ($query->each() as $user) { - * } - * ``` - * - * @author Konstantin Sirotkin - * @since 2.0.4 - */ -class BatchQueryResult extends BaseObject implements \Iterator -{ - /** - * @var Connection the DB connection to be used when performing batch query. - * If null, the `elasticsearch` application component will be used. - */ - public $db; - /** - * @var Query the query object associated with this batch query. - * Do not modify this property directly unless after [[reset()]] is called explicitly. - */ - public $query; - /** - * @var bool whether to return a single row during each iteration. - * If false, a whole batch of rows will be returned in each iteration. - */ - public $each = false; - /** - * @var DataReader the data reader associated with this batch query. - */ - private $_dataReader; - /** - * @var array the data retrieved in the current batch - */ - private $_batch; - /** - * @var mixed the value for the current iteration - */ - private $_value; - /** - * @var int|string the key for the current iteration - */ - private $_key; - /** - * @var string the amount of time to keep the scroll window open - * (in ElasticSearch [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units). - */ - public $scrollWindow = '1m'; - - /* - * @var string internal ElasticSearch scroll id - */ - private $_lastScrollId = null; - - /** - * Destructor. - */ - public function __destruct() - { - // make sure cursor is closed - $this->reset(); - } - - /** - * Resets the batch query. - * This method will clean up the existing batch query so that a new batch query can be performed. - */ - public function reset() - { - if (isset($this->_lastScrollId)) { - $this->query->createCommand($this->db)->clearScroll(['scroll_id' => $this->_lastScrollId]); - } - - $this->_batch = null; - $this->_value = null; - $this->_key = null; - $this->_lastScrollId = null; - } - - /** - * Resets the iterator to the initial state. - * This method is required by the interface [[\Iterator]]. - */ - public function rewind() - { - $this->reset(); - $this->next(); - } - - /** - * Moves the internal pointer to the next dataset. - * This method is required by the interface [[\Iterator]]. - */ - public function next() - { - if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) { - $this->_batch = $this->fetchData(); - reset($this->_batch); - } - - if ($this->each) { - $this->_value = current($this->_batch); - if ($this->query->indexBy !== null) { - $this->_key = key($this->_batch); - } elseif (key($this->_batch) !== null) { - $this->_key++; - } else { - $this->_key = null; - } - } else { - $this->_value = $this->_batch; - $this->_key = $this->_key === null ? 0 : $this->_key + 1; - } - } - - /** - * Fetches the next batch of data. - * @return array the data fetched - */ - protected function fetchData() - { - if (null === $this->_lastScrollId) { - //first query - do search - $options = ['scroll' => $this->scrollWindow]; - if (!$this->query->orderBy) { - $query = clone $this->query; - $query->orderBy('_doc'); - $cmd = $this->query->createCommand($this->db); - } else { - $cmd = $this->query->createCommand($this->db); - } - $result = $cmd->search($options); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - } else { - //subsequent queries - do scroll - $result = $this->query->createCommand($this->db)->scroll([ - 'scroll_id' => $this->_lastScrollId, - 'scroll' => $this->scrollWindow, - ]); - } - - //get last scroll id - $this->_lastScrollId = $result['_scroll_id']; - - //get data - return $this->query->populate($result['hits']['hits']); - } - - /** - * Returns the index of the current dataset. - * This method is required by the interface [[\Iterator]]. - * @return int the index of the current row. - */ - public function key() - { - return $this->_key; - } - - /** - * Returns the current dataset. - * This method is required by the interface [[\Iterator]]. - * @return mixed the current dataset. - */ - public function current() - { - return $this->_value; - } - - /** - * Returns whether there is a valid dataset at the current position. - * This method is required by the interface [[\Iterator]]. - * @return bool whether there is a valid dataset at the current position. - */ - public function valid() - { - return !empty($this->_batch); - } -} diff --git a/src/BulkCommand.php b/src/BulkCommand.php deleted file mode 100644 index ab871e88..00000000 --- a/src/BulkCommand.php +++ /dev/null @@ -1,120 +0,0 @@ - - * @since 2.0.5 - */ -class BulkCommand extends Component -{ - /** - * @var Connection - */ - public $db; - /** - * @var string Default index to execute the queries on. Defaults to null meaning that index needs to be specified in every action. - */ - public $index; - /** - * @var string Default type to execute the queries on. Defaults to null meaning that type needs to be specified in every action. - */ - public $type; - /** - * @var array|string Actions to be executed in this bulk command, given as either an array of arrays or as one newline-delimited string. - * All actions except delete span two lines. - */ - public $actions; - /** - * @var array Options to be appended to the query URL. - */ - public $options = []; - - /** - * Executes the bulk command. - * @throws yii\base\InvalidCallException - * @return mixed - */ - public function execute() - { - //valid endpoints are /_bulk, /{index}/_bulk, and {index}/{type}/_bulk - if ($this->index === null && $this->type === null) { - $endpoint = ['_bulk']; - } elseif ($this->index !== null && $this->type === null) { - $endpoint = [$this->index, '_bulk']; - } elseif ($this->index !== null && $this->type !== null) { - $endpoint = [$this->index, $this->type, '_bulk']; - } else { - throw new InvalidCallException('Invalid endpoint: if type is defined, index must be defined too.'); - } - - if (empty($this->actions)) { - $body = '{}'; - } elseif (is_array($this->actions)) { - $body = ''; - foreach ($this->actions as $action) { - $body .= Json::encode($action) . "\n"; - } - } else { - $body = $this->actions; - } - - return $this->db->post($endpoint, $this->options, $body); - } - - /** - * Adds an action to the command. Will overwrite existing actions if they are specified as a string. - * @param array $action Action expressed as an array (will be encoded to JSON automatically). - */ - public function addAction($line1, $line2 = null) - { - if (!is_array($this->actions)) { - $this->actions = []; - } - - $this->actions[] = $line1; - - if ($line2 !== null) { - $this->actions[] = $line2; - } - } - - /** - * Adds a delete action to the command. - * @param string $id Document ID - * @param string $index Index that the document belogs to. Can be set to null if the command has - * a default index ([[BulkCommand::$index]]) assigned. - * @param string $type Type that the document belogs to. Can be set to null if the command has - * a default type ([[BulkCommand::$type]]) assigned. - */ - public function addDeleteAction($id, $index = null, $type = null) - { - $actionData = ['_id' => $id]; - - if (!empty($index)) { - $actionData['_index'] = $index; - } - - if (!empty($type)) { - $actionData['_type'] = $type; - } - - $this->addAction(['delete' => $actionData]); - } -} diff --git a/src/Command.php b/src/Command.php index f845332c..8431096e 100644 --- a/src/Command.php +++ b/src/Command.php @@ -1,556 +1,765 @@ - * @since 2.0 + * Check the @link https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html for details on these + * commands. */ -class Command extends Component +final class Command { + public function __construct(private Connection $db) + { + } + /** - * @var Connection - */ - public $db; - /** - * @var array|string the indexes to execute the query on. Defaults to null meaning all indexes - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type + * An alias can also be added with the endpoint. + * + * @param string $index The index the alias refers to. Can be any of `*`, `_all`, `glob pattern`, `name1`, `name2`. + * @param string $name The name of the alias. + * @param array $options Additional options `routing` and `filter`. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-adding */ - public $index; + public function addAlias($index, $name, $options = []): bool + { + return (bool)$this->db->put([$index, '_alias', $name], [], json_encode((object) $options)); + } + /** - * @var array|string the types to execute the query on. Defaults to null meaning all types + * Check if alias exists. + * + * @param string $name The name of the alias. */ - public $type; + public function aliasExists(string $name): bool + { + return !empty($this->getIndexesByAlias($name)); + } + /** - * @var array list of arrays or json strings that become parts of a query + * Runs alias manipulations. + * + * If you want to add alias1 to index1 and remove alias2 from index2 you can use following commands: + * + * ```php + * $actions = [ + * ['add' => ['index' => 'index1', 'alias' => 'alias1']], + * ['remove' => ['index' => 'index2', 'alias' => 'alias2']], + * ]; + * ``` + * + * @param array $actions list of actions to manipulate aliases. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#indices-aliases */ - public $queryParts; + public function aliasActions(array $actions): bool + { + return (bool)$this->db->post(['_aliases'], [], json_encode(['actions' => $actions])); + } + /** - * @var array options to be appended to the query URL, such as "search_type" for search or "timeout" for delete + * Clear caches for all data streams and indices + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html#clear-cache-api-all-ex */ - public $options = []; + public function clearIndexCache(string $index): mixed + { + return $this->db->post([$index, '_cache', 'clear']); + } /** - * Sends a request to the _search API and returns the result - * @param array $options - * @return mixed + * Clears the search context and results for a scrolling search. + * + * @param array $options Additional options `scroll_id`. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/clear-scroll-api.html */ - public function search($options = []) + public function clearScroll(array $options = []): mixed { - $query = $this->queryParts; - if (empty($query)) { - $query = '{}'; - } - if (is_array($query)) { - $query = Json::encode($query); - } - $url = [$this->index !== null ? $this->index : '_all']; - if ($this->type !== null) { - $url[] = $this->type; + $body = array_filter( + [ + 'scroll_id' => ArrayHelper::remove($options, 'scroll_id', null), + ], + ); + + if (empty($body)) { + $body = (object) []; } - $url[] = '_search'; - return $this->db->get($url, array_merge($this->options, $options), $query); + return $this->db->delete(['_search', 'scroll'], $options, Json::encode($body)); } /** - * Sends a request to the delete by query - * @param array $options - * @return mixed + * Closes an index. + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-close.html */ - public function deleteByQuery($options = []) + public function closeIndex(string $index): mixed { - if (!isset($this->queryParts['query'])) { - throw new InvalidCallException('Can not call deleteByQuery when no query is given.'); - } - $query = [ - 'query' => $this->queryParts['query'], - ]; - if (isset($this->queryParts['filter'])) { - $query['filter'] = $this->queryParts['filter']; - } - $query = Json::encode($query); - $url = [$this->index !== null ? $this->index : '_all']; - if ($this->type !== null) { - $url[] = $this->type; - } - $url[] = '_delete_by_query'; - - return $this->db->post($url, array_merge($this->options, $options), $query); + return $this->db->post([$index, '_close']); } /** - * Sends a request to the _suggest API and returns the result - * @param array|string $suggester the suggester body - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html + * Creates a new index. + * + * @param string $index Name of the index you wish to create. + * @param array|null $configuration Index configuration. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html */ - public function suggest($suggester, $options = []) + public function createIndex(string $index, array $configuration = null): mixed { - if (empty($suggester)) { - $suggester = '{}'; - } - if (is_array($suggester)) { - $suggester = Json::encode($suggester); - } - $url = [ - $this->index !== null ? $this->index : '_all', - '_suggest', - ]; + $body = $configuration !== null ? Json::encode($configuration) : null; + + return $this->db->put([$index], [], $body); + } - return $this->db->post($url, array_merge($this->options, $options), $suggester); + /** + * Creates a index template. + * + * Index templates define settings, mappings, and aliases that can be applied automatically to new indices. + * + * @param string $name Name of the index template to create. + * @param array $pattern Array of wildcard `(*)` expressions used to match the names of data streams and indices + * during creation. + * @param array $settings Configuration options for the index. + * @param array $mappings Mapping for fields in the index. + * @param array $aliases The key is the alias name. Index alias names support date math. + * @param array $options Additional options `version`, `priority`. + * + * ```php + * [ + * 'index_patterns' : ['t*], + * 'priority' : 0, + * 'template' => [ + * 'settings' => [ + * 'number_of_shards' => 1, + * 'number_of_replicas' => 0, + * ], + * 'mappings' => [ + * '_source' => [ + * 'enabled' => false + * ], + * ], + * ]; + * ``` + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-template.html + */ + public function createIndexTemplate( + string $name, + array|string $pattern, + array $settings, + array $mappings, + array $aliases = [], + array $options = [], + ): mixed { + $body = Json::encode( + array_merge( + [ + 'index_patterns' => $pattern, + 'template' => [ + 'settings' => (object) $settings, + 'mappings' => (object) $mappings, + 'aliases' => (object) $aliases, + ], + ], + $options, + ), + ); + + return $this->db->put(['_index_template', $name], [], $body); } /** - * Inserts a document into an index - * @param string $index - * @param string $type - * @param array|string $data json string or array of data to store - * @param null $id the documents id. If not specified Id will be automatically chosen - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html + * Removes a JSON document from the specified index. + * + * @param string $index Name of the target index. + * @param string $id Unique identifier for the document. + * @param string|null $type Type that the document belongs to. + * @param array $options Additional options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html */ - public function insert($index, $type, $data, $id = null, $options = []) + public function delete(string $index, string $id, string $type = null, array $options = []): mixed { - if (empty($data)) { - $body = '{}'; - } else { - $body = is_array($data) ? Json::encode($data) : $data; + if ($this->db->getDslVersion() >= 7) { + return $this->db->delete([$index, '_doc', $id], $options); } - if ($id !== null) { - return $this->db->put([$index, $type, $id], $options, $body); - } - return $this->db->post([$index, $type], $options, $body); + return $this->db->delete([$index, $type, $id], $options); } /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html + * Deletes all indexes. + * + * To use this command set the action.destructive_requires_name cluster setting to `false`. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html */ - public function get($index, $type, $id, $options = []) + public function deleteAllIndexes(): mixed { - return $this->db->get([$index, $type, $id], $options); + return $this->db->delete(['_all']); } /** - * gets multiple documents from the index + * Deletes documents that match the specified query. + * + * @param string $index Name of the index. + * @param array $query Query to match documents. + * @param string|null $type Type that the document belongs to. + * @param array $options Additional options `conflicts`, `refresh`, `routing`, `timeout`, `wait_for_active_shards`, + * `wait_for_completion`, `requests_per_second`. * - * TODO allow specifying type and index + fields - * @param $index - * @param $type - * @param $ids - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html + * @throws InvalidArgumentException If no query is given. + * + * @todo Review this method. It is not working. */ - public function mget($index, $type, $ids, $options = []) + public function deleteByQuery(string $index, array $query, string $type = null, array $options = []): mixed { - $body = Json::encode(['ids' => array_values($ids)]); + if (!isset($query['query'])) { + throw new InvalidArgumentException('Can not call deleteByQuery when no query is given.'); + } - return $this->db->get([$index, $type, '_mget'], $options, $body); - } + $query = Json::encode($query); + $url = [$index]; - /** - * gets a documents _source from the index (>=v0.90.1) - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html#_source - */ - public function getSource($index, $type, $id) - { - return $this->db->get([$index, $type, $id]); + if ($this->db->getDslVersion() < 7 && $type !== null) { + $url[] = $type; + } + + $url[] = '_delete_by_query'; + + return $this->db->post($url, $options, $query); } /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html + * Deletes an index + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html */ - public function exists($index, $type, $id) + public function deleteIndex(string $index): mixed { - return $this->db->head([$index, $type, $id]); + return $this->db->delete([$index]); } /** - * deletes a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html + * Deletes a template. + * + * @param string $name Template name. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html */ - public function delete($index, $type, $id, $options = []) + public function deleteIndexTemplate(string $name): mixed { - return $this->db->delete([$index, $type, $id], $options); + return $this->db->delete(['_template', $name]); } /** - * updates a document - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html + * Checks if a document exists. + * + * @param string $index Index that the document belongs to. + * @param string $id The documents id. + * @param string|null $type Type that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html */ - public function update($index, $type, $id, $data, $options = []) + public function exists(string $index, string $id, string $type = null): mixed { - $body = [ - 'doc' => empty($data) ? new \stdClass() : $data, - ]; - if (isset($options['detect_noop'])) { - $body['detect_noop'] = $options['detect_noop']; - unset($options['detect_noop']); + if ($this->db->getDslVersion() >= 7) { + return $this->db->head([$index, '_doc', $id]); } - return $this->db->post([$index, $type, $id, '_update'], $options, Json::encode($body)); + return $this->db->head([$index, $type, $id]); } - // TODO bulk http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html - /** - * creates an index - * @param $index - * @param array $configuration - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html + * Flushes an index. + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html */ - public function createIndex($index, $configuration = null) + public function flushIndex($index = '_all'): mixed { - $body = $configuration !== null ? Json::encode($configuration) : null; - - return $this->db->put([$index], [], $body); + return $this->db->post([$index, '_flush']); } /** - * deletes an index - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html + * Gets a document from the index. + * + * @param string $index Index that the document belongs to. + * @param string $id The documents id. + * @param string|null $type Type that the document belongs to. + * @param array $options Additional options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html */ - public function deleteIndex($index) + public function get(string $index, string $id, string $type = null, array $options = []): mixed { - return $this->db->delete([$index]); + if ($this->db->getDslVersion() >= 7) { + return $this->db->get([$index, '_doc', $id], $options); + } + + return $this->db->get([$index, $type, $id], $options); } /** - * deletes all indexes - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html + * Gets the alias info. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving */ - public function deleteAllIndexes() + public function getAliasInfo(): array { - return $this->db->delete(['_all']); + $aliasInfo = $this->db->get(['_alias', '*']); + return $aliasInfo ?: []; } /** - * checks whether an index exists - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-exists.html + * Gets the index aliases. + * + * @param string $index Index that the document belongs to. + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving */ - public function indexExists($index) + public function getIndexAliases(string $index): array { - return $this->db->head([$index]); + $responseData = $this->db->get([$index, '_alias', '*']); + if (empty($responseData)) { + return []; + } + + return $responseData[$index]['aliases']; } /** - * @param $index - * @param $type - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-types-exists.html + * Gets the index info by alias. + * + * @param string $alias Alias name. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving */ - public function typeExists($index, $type) + public function getIndexInfoByAlias(string $alias): array { - return $this->db->head([$index, $type]); - } + $responseData = $this->db->get(['_alias', $alias]); + if (empty($responseData)) { + return []; + } - // TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html + return $responseData; + } /** - * Change specific index level settings in real time. - * Note that update analyzers required to [[close()]] the index first and [[open()]] it after the changes are made, - * use [[updateAnalyzers()]] for it. + * Gets the index recovery stats. * - * @param string $index - * @param array|string $setting - * @param array $options URL options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html - * @since 2.0.4 + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html */ - public function updateSettings($index, $setting, $options = []) + public function getIndexRecoveryStats(string $index = '_all'): mixed { - $body = $setting !== null ? (is_string($setting) ? $setting : Json::encode($setting)) : null; - return $this->db->put([$index, '_settings'], $options, $body); + return $this->db->get([$index, '_recovery']); } /** - * Define new analyzers for the index. - * For example if content analyzer hasn’t been defined on "myindex" yet - * you can use the following commands to add it: + * Gets the index stats. * - * ~~~ - * $setting = [ - * 'analysis' => [ - * 'analyzer' => [ - * 'ngram_analyzer_with_filter' => [ - * 'tokenizer' => 'ngram_tokenizer', - * 'filter' => 'lowercase, snowball' - * ], - * ], - * 'tokenizer' => [ - * 'ngram_tokenizer' => [ - * 'type' => 'nGram', - * 'min_gram' => 3, - * 'max_gram' => 10, - * 'token_chars' => ['letter', 'digit', 'whitespace', 'punctuation', 'symbol'] - * ], - * ], - * ] - * ]; - * $elasticQuery->createCommand()->updateAnalyzers('myindex', $setting); - * ~~~ + * @param string $index Index that the document belongs to. * - * @param string $index - * @param array|string $setting - * @param array $options URL options - * @return mixed - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html#update-settings-analysis - * @since 2.0.4 + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html */ - public function updateAnalyzers($index, $setting, $options = []) + public function getIndexStats($index = '_all'): mixed { - $this->closeIndex($index); - $result = $this->updateSettings($index, $setting, $options); - $this->openIndex($index); - return $result; + return $this->db->get([$index, '_stats']); } - // TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html - - // TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html - /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html + * Get a template. + * + * @param string $name Template name. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html */ - public function openIndex($index) + public function getIndexTemplate(string $name): mixed { - return $this->db->post([$index, '_open']); + return $this->db->get(['_index_template', $name]); } /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html + * Gets the index by alias. + * + * @param string $alias Alias name. */ - public function closeIndex($index) + public function getIndexesByAlias(string $alias): array { - return $this->db->post([$index, '_close']); + return array_keys($this->getIndexInfoByAlias($alias)); } /** - * @param array $options - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html - * @return mixed - * @since 2.0.4 + * Gets the index settings. + * + * @param string $index Index that the document belongs to. */ - public function scroll($options = []) + public function getSettings(string $index = '_all'): mixed { - return $this->db->get(['_search', 'scroll'], $options); + return $this->db->get([$index, '_settings']); } /** - * @param array $options - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html - * @return mixed - * @since 2.0.4 + * Gets the mapping for an index. + * + * @param string $index Index that the document belongs to. + * @param string|null $type Type that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html */ - public function clearScroll($options = []) + public function getMapping(string $index = '_all', string $type = null): mixed { - return $this->db->delete(['_search', 'scroll'], $options); + $url = [$index, '_mapping']; + if ($this->db->getDslVersion() < 7 && $type !== null) { + $url[] = $type; + } + return $this->db->get($url); } /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html + * Gets a documents _source from the index (>=v0.90.1). + * + * @param string $index Index that the document belongs to. + * @param string $id The documents id. + * @param string|null $type Type that the document belongs to. + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html#_source */ - public function getIndexStats($index = '_all') + public function getSource(string $index, string $id, string $type = null): mixed { - return $this->db->get([$index, '_stats']); + if ($this->db->getDslVersion() >= 7) { + return $this->db->get([$index, '_source', $id]); + } + return $this->db->get([$index, $type, $id]); } /** - * @param $index - * @return mixed - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html + * Checks whether an index exists. + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-exists.html */ - public function getIndexRecoveryStats($index = '_all') + public function indexExists($index): mixed { - return $this->db->get([$index, '_recovery']); + return $this->db->head([$index]); } - // http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-segments.html + /** + * Inserts a document into an index. + * + * @param string $index Index that the document belongs to. + * @param array|string $data Json string or array of data to store. + * @param string|null $id The documents id. If not specified Id will be automatically chosen. + * @param string|null $type Type that the document belongs to. + * @param array $options URL options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html + */ + public function insert( + string $index, + string|array $data, + string $id = null, + string $type = null, + array $options = [], + ): mixed { + if (empty($data)) { + $body = '{}'; + } else { + $body = is_array($data) ? Json::encode($data) : $data; + } + + if ($id !== null) { + if ($this->db->getDslVersion() >= 7) { + return $this->db->put([$index, '_doc', $id], $options, $body); + } + return $this->db->put([$index, $type, $id], $options, $body); + } + + if ($this->db->getDslVersion() >= 7) { + return $this->db->post([$index, '_doc'], $options, $body); + } + + return $this->db->post([$index, $type], $options, $body); + } /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html + * Gets multiple documents from the index. + * + * @param string $index Index that the document belongs to. + * @param string[] $ids the documents ids as values in array. + * @param string|null $type Type that the document belongs to. + * @param array $options URL options. + + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html */ - public function clearIndexCache($index) + public function mget(string $index, array $ids, string $type = null, array $options = []): mixed { - return $this->db->post([$index, '_cache', 'clear']); + $body = Json::encode(['ids' => array_values($ids)]); + + if ($this->db->getDslVersion() >= 7) { + return $this->db->get([$index, '_mget'], $options, $body); + } + return $this->db->get([$index, $type, '_mget'], $options, $body); } /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html + * Opens an index. + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html */ - public function flushIndex($index = '_all') + public function openIndex(string $index): mixed { - return $this->db->post([$index, '_flush']); + return $this->db->post([$index, '_open']); } /** - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html + * Refreshes an index. + * + * @param string $index Index that the document belongs to. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html */ - public function refreshIndex($index) + public function refreshIndex(string $index): mixed { return $this->db->post([$index, '_refresh']); } - // TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-optimize.html - - // TODO http://www.elastic.co/guide/en/elasticsearch/reference/0.90/indices-gateway-snapshot.html - /** - * @param string $index - * @param string $type - * @param array|string $mapping - * @param array $options - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html + * Removes an alias from an index. + * + * @param string $index Index that the document belongs to. + * @param string $alias Alias name. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#deleting */ - public function setMapping($index, $type, $mapping, $options = []) + public function removeAlias(string $index, string $alias): bool { - $body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null; - - return $this->db->put([$index, '_mapping', $type], $options, $body); + return (bool)$this->db->delete([$index, '_alias', $alias]); } /** - * @param string $index - * @param string $type - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html + * Scrolls through a search request. + * + * @param array $options URL options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html */ - public function getMapping($index = '_all', $type = null) + public function scroll(array $options = []): mixed { - $url = [$index, '_mapping']; - if ($type !== null) { - $url[] = $type; + $body = array_filter( + [ + 'scroll' => ArrayHelper::remove($options, 'scroll', null), + 'scroll_id' => ArrayHelper::remove($options, 'scroll_id', null), + ], + ); + + if (empty($body)) { + $body = (object) []; } - return $this->db->get($url); + + return $this->db->post(['_search', 'scroll'], $options, Json::encode($body)); } /** - * @param $index - * @param string $type - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html - */ -// public function getFieldMapping($index, $type = '_all') -// { - // // TODO implement -// return $this->db->put([$index, $type, '_mapping']); -// } + * Sends a request to the _search API and returns the result. + * + * @param string $index Index that the document belongs to. + * @param array|string $query Query to send. + * @param string|null $type Type that the document belongs to. + * @param array $options URL options. + */ + public function search( + string $index, + array|string $query = [], + string $type = null, + array $options = [] + ): mixed { + if (empty($query)) { + $query = '{}'; + } + + if (is_array($query)) { + $query = Json::encode($query); + } + + $url = [$index]; + + if ($this->db->getDslVersion() < 7 && $type !== null) { + $url[] = $type; + } + + $url[] = '_search'; + + return $this->db->get($url, $options, $query); + } /** - * @param $options - * @param $index - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html - */ - // public function analyze($options, $index = null) - // { - // // TODO implement - //// return $this->db->put([$index]); - // } + * Sets the mapping for an index. + * + * @param string $index Index that the document belongs to. + * @param array|string|null $mapping Json string or array of mapping to store. + * @param string|null $type Type that the document belongs to. + * @param array $options URL options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html + */ + public function setMapping( + string $index, + string|array|null $mapping, + string $type = null, + array $options = [] + ): mixed { + $body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null; + + if ($this->db->getDslVersion() >= 7) { + $endpoint = [$index, '_mapping']; + } else { + $endpoint = [$index, '_mapping', $type]; + } + + return $this->db->put($endpoint, $options, $body); + } /** - * @param $name - * @param $pattern - * @param $settings - * @param $mappings - * @param int $order - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html + * Sends a suggest request to the _search API and returns the result. + * + * @param string $index Index that the document belongs to. + * @param array|string $suggester The suggester body. + * @param array $options Additional options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html */ - public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) + public function suggesters(string $index = '_all', string|array $suggester = [], array $options = []): mixed { - $body = Json::encode([ - 'template' => $pattern, - 'order' => $order, - 'settings' => (object) $settings, - 'mappings' => (object) $mappings, - ]); + if (empty($suggester)) { + $suggester = '{}'; + } + + if (is_array($suggester)) { + $suggester = Json::encode($suggester); + } + + $body = '{"suggest":' . $suggester . ',"size":0}'; + $url = [$index, '_search']; + $result = $this->db->post($url, $options, $body); + + return $result['suggest']; + } + + /** + * Updates a document + * + * @param string $index Index that the document belongs to. + * @param string $id The documents id. + * @param array|string $data Json string or array of data to store. + * @param string|null $type Type that the document belongs to. + * @param array $options URL options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html + */ + public function update( + string $index, + string $id, + string|array $data, + string $type = null, + array $options = [] + ): mixed { + $body = [ + 'doc' => empty($data) ? new \stdClass() : $data, + ]; + + if (isset($options['detect_noop'])) { + $body['detect_noop'] = $options['detect_noop']; + unset($options['detect_noop']); + } + + if ($this->db->getDslVersion() >= 7) { + return $this->db->post([$index, '_update', $id], $options, Json::encode($body)); + } - return $this->db->put(['_template', $name], [], $body); + return $this->db->post([$index, $type, $id, '_update'], $options, Json::encode($body)); } /** - * @param $name - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html + * Define new analyzers for the index. + * + * For example if content analyzer hasn’t been defined on "myindex" yet you can use the following commands to add + * it: + * + * ```php + * $setting = [ + * 'analysis' => [ + * 'analyzer' => [ + * 'ngram_analyzer_with_filter' => [ + * 'tokenizer' => 'ngram_tokenizer', + * 'filter' => 'lowercase, snowball' + * ], + * ], + * 'tokenizer' => [ + * 'ngram_tokenizer' => [ + * 'type' => 'nGram', + * 'min_gram' => 3, + * 'max_gram' => 10, + * 'token_chars' => ['letter', 'digit', 'whitespace', 'punctuation', 'symbol'] + * ], + * ], + * ] + * ]; + * $elasticQuery->createCommand()->updateAnalyzers('myindex', $setting); + * ``` + * + * @param string $index Index that the document belongs to. + * @param array|string $setting Json string or array of data to store. + * @param array $options URL options. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html#update-settings-analysis */ - public function deleteTemplate($name) + public function updateAnalyzers(string $index, string|array $setting, array $options = []): mixed { - return $this->db->delete(['_template', $name]); + $this->closeIndex($index); + $result = $this->updateSettings($index, $setting, $options); + $this->openIndex($index); + return $result; } /** - * @param $name - * @return mixed - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html + * Changes a dynamic index setting in real time. + * + * Note that update analyzers required to {@see close()} the index first and {@see open()} it after the changes are + * made, use {@see updateAnalyzers()} for it. + * + * @param string $index Index that the document belongs to. + * @param array|string|null $setting Json string or array of data to store. + * @param array $options Additional options. + * + * @link http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html */ - public function getTemplate($name) + public function updateSettings(string $index, string|array|null $setting, array $options = []): mixed { - return $this->db->get(['_template', $name]); + $body = $setting !== null ? (is_string($setting) ? $setting : Json::encode($setting)) : null; + return $this->db->put([$index, '_settings'], $options, $body); } } diff --git a/src/Connection.php b/src/Connection.php index 839373d7..50b4fc6b 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -1,45 +1,110 @@ - * @since 2.0 + * @psalm-suppress PropertyNotSetInConstructor */ -class Connection extends Component +final class Connection implements LoggerAwareInterface, ProfilerAwareInterface { + use LoggerAwareTrait; + use ProfilerAwareTrait; + + /** + * @var bool Whether to autodetect available cluster nodes on {@see open()}. + */ + private bool $autodetectCluster = true; + private array $nodes = [ + ['http_address' => 'inet[/127.0.0.1:9200]'], + ]; + /** + * @var string The active node. Key of one of the {@see nodes}. + * Will be selected on {@see open()}. + */ + private string $activeNode = ''; + private array $auth = []; /** - * @event Event an event that is triggered after a DB connection is established + * @var CurlHandle The curl instance returned by @link http://php.net/manual/en/function.curl-init.php. */ - public const EVENT_AFTER_OPEN = 'afterOpen'; + private CurlHandle $curl; + private array $curlOptions = []; + private string $defaultProtocol = 'http'; + private float|null $dataTimeout = null; + private int $dslVersion = 8; + private float|null $timeOut = null; /** - * @var bool whether to autodetect available cluster nodes on [[open()]] + * @throws InvalidArgumentException */ - public $autodetectCluster = true; + public function __construct() + { + foreach ($this->nodes as &$node) { + if (!isset($node['http_address'])) { + throw new InvalidArgumentException('Elasticsearch node needs at least a http_address configured.'); + } + if (!isset($node['protocol'])) { + $node['protocol'] = $this->defaultProtocol; + } + if (!in_array($node['protocol'], ['http', 'https'])) { + throw new InvalidArgumentException('Valid node protocol settings are "http" and "https".'); + } + } + } + + /** + * Closes the connection when this component is being serialized. + */ + public function __sleep(): array + { + $this->close(); + + return array_keys(get_object_vars($this)); + } + /** - * @var array The elasticsearch cluster nodes to connect to. + * The Elasticsearch cluster nodes to connect to. * - * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true. + * This is populated with the result of a cluster nodes request when {@see autodetectCluster} is true. * * Additional special options: * @@ -48,24 +113,31 @@ class Connection extends Component * ```php * [ * 'http_address' => 'inet[/127.0.0.1:9200]', - * 'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Overrides the `auth` property of the class with specific login and password - * //'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Disabled auth regardless of `auth` property of the class + * 'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Overrides the `auth` property of the class with + * specific login and password + * //'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Disabled auth regardless of `auth` property of + * the class * ] * ``` * * - `protocol`: explicitly sets the protocol for the current node (useful when manually defining a HTTPS cluster) * - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info - */ - public $nodes = [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ]; - /** - * @var string the active node. Key of one of the [[nodes]]. Will be randomly selected on [[open()]]. + * @param string $key The node to set. + * @param mixed $value The node value. + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info */ - public $activeNode; + public function addNodeValue(string $key, mixed $value): self + { + $this->nodes[$this->activeNode][$key] = $value; + + return $this; + } + /** - * @var array Authentication data used to connect to the ElasticSearch node. + * Set credentials for authentication. + * + * @param array $values The credentials for authentication. * * Array elements: * @@ -73,108 +145,224 @@ class Connection extends Component * - `password`: the password for authentication. * * Array either MUST contain both username and password on not contain any authentication credentials. - * @see http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth + * + * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-authenticate.html */ - public $auth = []; + public function auth(array $values): self + { + $this->auth = $values; + return $this; + } + /** - * Elasticsearch has no knowledge of protocol used to access its nodes. Specifically, cluster autodetection request - * returns node hosts and ports, but not the protocols to access them. Therefore we need to specify a default protocol here, - * which can be overridden for specific nodes in the [[nodes]] property. - * If [[autodetectCluster]] is true, all nodes received from cluster will be set to use the protocol defined by [[defaultProtocol]] - * @var string Default protocol to connect to nodes - * @since 2.0.5 + * Closes the currently active DB connection. + * + * It does nothing if the connection is already closed. */ - public $defaultProtocol = 'http'; + public function close(): void + { + $this->logger?->log( + LogLevel::INFO, + 'Closing connection to Elasticsearch. Active node was: ' . + $this->nodes[$this->activeNode]['http']['publish_address'], + [__CLASS__], + ); + + $this->activeNode = ''; + + curl_close($this->curl); + unset($this->curl); + } + /** - * @var float timeout to use for connecting to an elasticsearch node. - * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. - * If not set, no explicit timeout will be set for curl. + * Creates a command for execution. + * + * @throws Exception + * @return Command the DB command + */ + public function createCommand(): Command + { + $this->open(); + + return new Command($this); + } + + /** + * Additional options used to configure curl session. + * + * @param array $values The curl options. */ - public $connectionTimeout = null; + public function curlOptions(array $values): self + { + $this->curlOptions = $values; + return $this; + } + /** - * @var float timeout to use when reading the response from an elasticsearch node. + * Set timeout to use when reading the response from an Elasticsearch node. + * * This value will be used to configure the curl `CURLOPT_TIMEOUT` option. + * * If not set, no explicit timeout will be set for curl. + * + * @param float|null $value The timeout to use when reading the response from an Elasticsearch node. */ - public $dataTimeout = null; + public function dataTimeout(float $value = null): self + { + $this->dataTimeout = $value; + return $this; + } /** - * @var resource the curl instance returned by [curl_init()](http://php.net/manual/en/function.curl-init.php). + * Elasticsearch has no knowledge of the protocol used to access its nodes. + * + * Specifically, cluster autodetect request returns node hosts and ports, but not the protocols to access them. + * + * Therefore, we need to specify a default protocol here, which can be overridden for specific nodes in the + * {@see nodes()} property. + * + * If {@see autodetectCluster} is true, all nodes received from cluster will be set to use the protocol defined by + * {@see defaultProtocol} + * + * @param string $value The default protocol to connect to nodes. */ - private $_curl; + public function defaultProtocol(string $value): self + { + $this->defaultProtocol = $value; + return $this; + } - public function init() + /** + * Set version of the domain-specific language to use with the server. + * + * @param int $value The version of the domain-specific. + */ + public function dslVersion(int $value): self { - foreach ($this->nodes as &$node) { - if (!isset($node['http_address'])) { - throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); - } - if (!isset($node['protocol'])) { - $node['protocol'] = $this->defaultProtocol; - } - if (!in_array($node['protocol'], ['http', 'https'])) { - throw new InvalidConfigException('Valid node protocol settings are "http" and "https".'); - } - } + $this->dslVersion = $value; + return $this; } /** - * Closes the connection when this component is being serialized. - * @return array + * Return the cluster state. + * + * @throws InvalidArgumentException + * @return Exception */ - public function __sleep() + public function getClusterState(): mixed { - $this->close(); + return $this->get(['_cluster', 'state']); + } - return array_keys(get_object_vars($this)); + /** + * Returns the dsl version. + */ + public function getDslVersion(): int + { + return $this->dslVersion; + } + + /** + * Returns the name of the DB driver for the current {@see dsn}. + */ + public function getDriverName(): string + { + return 'elasticsearch'; + } + + /** + * Returns the Elasticsearch node value. + * + * @param string $key The node value to get. + */ + public function getNodeValue(string $key = ''): mixed + { + if ($this->activeNode === '') { + return null; + } + + return ArrayHelper::getValue($this->nodes[$this->activeNode], $key); + } + + /** + * Return the active node. + * + * @throws Exception + * @throws InvalidArgumentException + */ + public function getNodeInfo(): mixed + { + return $this->get([]); } /** * Returns a value indicating whether the DB connection is established. - * @return bool whether the DB connection is established + * + * @return bool whether the DB connection is established. */ - public function getIsActive() + public function isActive(): bool { - return $this->activeNode !== null; + return $this->activeNode !== ''; } /** * Establishes a DB connection. + * * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails + * + * @throws Exception If connection fails or autodetectCluster is true and no active node(s) found. */ - public function open() + public function open(): void { - if ($this->activeNode !== null) { + if ($this->activeNode !== '') { return; } + if (empty($this->nodes)) { - throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); + throw new InvalidArgumentException('Elasticsearch needs at least one node to operate.'); } - $this->_curl = curl_init(); + + $this->curl = curl_init(); + if ($this->autodetectCluster) { $this->populateNodes(); } + $this->selectActiveNode(); - Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes) - . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__); - $this->initConnection(); } /** - * Populates [[nodes]] with the result of a cluster nodes request. - * @throws Exception if no active node(s) found - * @since 2.0.4 + * Set timeout to use for connecting to an Elasticsearch node. + * + * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. + * + * If not set, no explicit timeout will be set for curl. + * + * @param float|null $timeOut The timeout to use for connecting to an Elasticsearch node. */ - protected function populateNodes() + public function timeOut(float $timeOut = null): self + { + $this->timeOut = $timeOut; + return $this; + } + + /** + * Populates {@see nodes} with the result of a cluster nodes request. + * + * @throws Exception If no active node(s) found. + */ + protected function populateNodes(): void { $node = reset($this->nodes); $host = $node['http_address']; $protocol = $node['protocol'] ?? $this->defaultProtocol; + if (strncmp($host, 'inet[/', 6) === 0) { $host = substr($host, 6, -1); } + $response = $this->httpRequest('GET', "$protocol://$host/_nodes/_all/http"); + if (!empty($response['nodes'])) { $nodes = $response['nodes']; } else { @@ -182,118 +370,54 @@ protected function populateNodes() } foreach ($nodes as $key => &$node) { - // Make sure that nodes have an 'http_address' property, which is not the case if you're using AWS - // Elasticsearch service (at least as of Oct., 2015). - TO BE VERIFIED - // Temporary workaround - simply ignore all invalid nodes + /** + * Make sure that nodes have an 'http_address' property, which isn't the case if you're using AWS. + * Elasticsearch service (at least as of Oct., 2015). - TO BE VERIFIED. + * Temporary workaround - simply ignore all invalid nodes. + */ if (!isset($node['http']['publish_address'])) { unset($nodes[$key]); } + $node['http_address'] = $node['http']['publish_address']; - //Protocol is not a standard ES node property, so we add it manually + // Protocol isn't a standard ES node property, so we add it manually $node['protocol'] = $this->defaultProtocol; } if (!empty($nodes)) { $this->nodes = array_values($nodes); } else { - curl_close($this->_curl); - throw new Exception('Cluster autodetection did not find any active nodes.'); + curl_close($this->curl); + throw new RuntimeException( + 'Cluster autodetection did not find any active node. Make sure a GET /_nodes reguest on the hosts defined in the config returns the "http_address" field for each node.' + ); } } /** - * select active node randomly + * select active node. + * + * @throws Exception */ - protected function selectActiveNode() + protected function selectActiveNode(): void { $keys = array_keys($this->nodes); - $this->activeNode = $keys[rand(0, count($keys) - 1)]; - } - - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - if ($this->activeNode === null) { - return; - } - Yii::trace('Closing connection to elasticsearch. Active node was: ' - . $this->nodes[$this->activeNode]['http']['publish_address'], __CLASS__); - $this->activeNode = null; - if ($this->_curl) { - curl_close($this->_curl); - $this->_curl = null; - } - } - - /** - * Initializes the DB connection. - * This method is invoked right after the DB connection is established. - * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. - */ - protected function initConnection() - { - $this->trigger(self::EVENT_AFTER_OPEN); - } - - /** - * Returns the name of the DB driver for the current [[dsn]]. - * @return string name of the DB driver - */ - public function getDriverName() - { - return 'elasticsearch'; - } - - /** - * Creates a command for execution. - * @param array $config the configuration for the Command class - * @return Command the DB command - */ - public function createCommand($config = []) - { - $this->open(); - $config['db'] = $this; - return new Command($config); - } - - /** - * Creates a bulk command for execution. - * @param array $config the configuration for the [[BulkCommand]] class - * @return BulkCommand the DB command - * @since 2.0.5 - */ - public function createBulkCommand($config = []) - { - $this->open(); - $config['db'] = $this; - return new BulkCommand($config); - } - - /** - * Creates new query builder instance - * @return QueryBuilder - */ - public function getQueryBuilder() - { - return new QueryBuilder($this); + $this->activeNode = (string) $keys[random_int(0, count($keys) - 1)]; } /** * Performs GET HTTP request * - * @param array|string $url URL - * @param array $options URL options - * @param string $body request body - * @param bool $raw if response body contains JSON and should be decoded + * @param array|string $url URL. + * @param array $options URL options. + * @param string|null $body Request body + * @param bool $raw If response body has JSON and should be decoded. + * * @throws Exception - * @throws InvalidConfigException - * @return mixed response + * @throws InvalidArgumentException */ - public function get($url, $options = [], $body = null, $raw = false) + public function get(array|string $url, array $options = [], string $body = null, bool $raw = false): mixed { $this->open(); return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw); @@ -302,14 +426,14 @@ public function get($url, $options = [], $body = null, $raw = false) /** * Performs HEAD HTTP request * - * @param array|string $url URL - * @param array $options URL options - * @param string $body request body + * @param array|string $url URL. + * @param array $options URL options. + * @param string|null $body Request body. + * * @throws Exception - * @throws InvalidConfigException - * @return mixed response + * @throws InvalidArgumentException */ - public function head($url, $options = [], $body = null) + public function head(array|string $url, array $options = [], string $body = null): mixed { $this->open(); return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body); @@ -318,15 +442,15 @@ public function head($url, $options = [], $body = null) /** * Performs POST HTTP request * - * @param array|string $url URL - * @param array $options URL options - * @param string $body request body - * @param bool $raw if response body contains JSON and should be decoded + * @param array|string $url URL. + * @param array $options URL options. + * @param string|null $body Request body. + * @param bool $raw If response body has JSON and should be decoded + * * @throws Exception - * @throws InvalidConfigException - * @return mixed response + * @throws InvalidArgumentException */ - public function post($url, $options = [], $body = null, $raw = false) + public function post(array|string $url, array $options = [], string $body = null, bool $raw = false): mixed { $this->open(); return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw); @@ -335,49 +459,49 @@ public function post($url, $options = [], $body = null, $raw = false) /** * Performs PUT HTTP request * - * @param array|string $url URL - * @param array $options URL options - * @param string $body request body - * @param bool $raw if response body contains JSON and should be decoded + * @param array|string $url URL. + * @param array $options URL options. + * @param string|null $body Request body. + * @param bool $raw If response body has JSON and should be decoded/ + * * @throws Exception - * @throws InvalidConfigException - * @return mixed response + * @throws InvalidArgumentException */ - public function put($url, $options = [], $body = null, $raw = false) + public function put(array|string $url, array $options = [], string $body = null, bool $raw = false): mixed { $this->open(); return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw); } /** - * Performs DELETE HTTP request + * Performs DELETE HTTP request. + * + * @param array|string $url URL. + * @param array $options URL options. + * @param string|null $body Request body. + * @param bool $raw If response body has JSON and should be decoded. * - * @param array|string $url URL - * @param array $options URL options - * @param string $body request body - * @param bool $raw if response body contains JSON and should be decoded + * @throws InvalidArgumentException * @throws Exception - * @throws InvalidConfigException - * @return mixed response + * @return mixed */ - public function delete($url, $options = [], $body = null, $raw = false) + public function delete(string|array $url, array $options = [], string $body = null, bool $raw = false): mixed { $this->open(); return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw); } /** - * Creates URL + * Creates URL. * - * @param array|string $path path - * @param array $options URL options - * @return array + * @param array|string $path URL path. + * @param array $options URL options. */ - private function createUrl($path, $options = []) + private function createUrl(string|array $path, array $options = []): array { if (!is_string($path)) { - $url = implode('/', array_map(function ($a) { - return urlencode(is_array($a) ? implode(',', $a) : $a); + $url = implode('/', array_map(static function ($a) { + return urlencode(is_array($a) ? implode(',', $a) : (string) $a); }, $path)); if (!empty($options)) { $url .= '?' . http_build_query($options); @@ -385,7 +509,7 @@ private function createUrl($path, $options = []) } else { $url = $path; if (!empty($options)) { - $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options); + $url .= (!str_contains($url, '?') ? '?' : '&') . http_build_query($options); } } @@ -397,18 +521,42 @@ private function createUrl($path, $options = []) } /** - * Performs HTTP request - * - * @param string $method method name - * @param string $url URL - * @param string $requestBody request body - * @param bool $raw if response body contains JSON and should be decoded - * @throws Exception if request failed - * @throws InvalidConfigException - * @return mixed if request failed + * Try to decode error information if it's valid json, return it if not.ll */ - protected function httpRequest($method, $url, $requestBody = null, $raw = false) + protected function decodeErrorBody(string $body): mixed { + try { + $decoded = Json::decode($body); + if (isset($decoded['error']) && !is_array($decoded['error'])) { + $decoded['error'] = preg_replace( + '/\b\w+?Exception\[/', + "\\0\n ", + $decoded['error'], + ); + } + return $decoded; + } catch (InvalidArgumentException $e) { + return $body; + } + } + + /** + * Performs HTTP request. + * + * @param string $method The method name. + * @param array|string $url Request URL. + * @param string|null $requestBody Request body. + * @param bool $raw If response body has JSON and should be decoded. + * + * @throws InvalidArgumentException + * @return mixed + */ + protected function httpRequest( + string $method, + string|array $url, + string $requestBody = null, + bool $raw = false + ): mixed { $method = strtoupper($method); // response body and headers @@ -417,7 +565,7 @@ protected function httpRequest($method, $url, $requestBody = null, $raw = false) $body = ''; $options = [ - CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' ' . __CLASS__, + CURLOPT_USERAGENT => 'Yii Framework 3.0' . ' ' . __CLASS__, CURLOPT_RETURNTRANSFER => false, CURLOPT_HEADER => false, // http://www.php.net/manual/en/function.curl-setopt.php#82418 @@ -426,40 +574,47 @@ protected function httpRequest($method, $url, $requestBody = null, $raw = false) 'Content-Type: application/json', ], - CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) { + CURLOPT_WRITEFUNCTION => function (CurlHandle $curl, string $data) use (&$body): int { $body .= $data; + return mb_strlen($data, '8bit'); }, - CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers, &$headersFinished) { + CURLOPT_HEADERFUNCTION => function (CurlHandle $curl, string $data) use (&$headers, &$headersFinished): int { if ($data === '') { $headersFinished = true; } elseif ($headersFinished) { $headersFinished = false; } + if (!$headersFinished && ($pos = strpos($data, ':')) !== false) { $headers[strtolower(substr($data, 0, $pos))] = trim(substr($data, $pos + 1)); } + return mb_strlen($data, '8bit'); }, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_FORBID_REUSE => false, ]; - if (!empty($this->auth) || isset($this->nodes[$this->activeNode]['auth']) && $this->nodes[$this->activeNode]['auth'] !== false) { + foreach ($this->curlOptions as $key => $value) { + $options[$key] = $value; + } + + if (!empty($this->auth) || (isset($this->nodes[$this->activeNode]['auth']) && $this->nodes[$this->activeNode]['auth'] !== false)) { $auth = $this->nodes[$this->activeNode]['auth'] ?? $this->auth; if (empty($auth['username'])) { - throw new InvalidConfigException('Username is required to use authentication'); + throw new InvalidArgumentException('Username is required to use authentication'); } if (empty($auth['password'])) { - throw new InvalidConfigException('Password is required to use authentication'); + throw new InvalidArgumentException('Password is required to use authentication'); } $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; $options[CURLOPT_USERPWD] = $auth['username'] . ':' . $auth['password']; } - if ($this->connectionTimeout !== null) { - $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; + if ($this->timeOut !== null) { + $options[CURLOPT_CONNECTTIMEOUT] = $this->timeOut; } if ($this->dataTimeout !== null) { $options[CURLOPT_TIMEOUT] = $this->dataTimeout; @@ -467,7 +622,7 @@ protected function httpRequest($method, $url, $requestBody = null, $raw = false) if ($requestBody !== null) { $options[CURLOPT_POSTFIELDS] = $requestBody; } - if ($method == 'HEAD') { + if ($method === 'HEAD') { $options[CURLOPT_NOBODY] = true; unset($options[CURLOPT_WRITEFUNCTION]); } else { @@ -476,7 +631,7 @@ protected function httpRequest($method, $url, $requestBody = null, $raw = false) if (is_array($url)) { [$protocol, $host, $q] = $url; - if (strncmp($host, 'inet[', 5) == 0) { + if (strncmp($host, 'inet[', 5) === 0) { $host = substr($host, 5, -1); if (($pos = strpos($host, '/')) !== false) { $host = substr($host, $pos + 1); @@ -488,110 +643,80 @@ protected function httpRequest($method, $url, $requestBody = null, $raw = false) $profile = false; } - Yii::trace("Sending request to elasticsearch node: $method $url\n$requestBody", __METHOD__); + $token = 'Yii Framework 3.0' . ' ' . __CLASS__; + $connectionContext = new ConnectionContext(__METHOD__); + if ($profile !== false) { - Yii::beginProfile($profile, __METHOD__); + $this->profiler?->begin($token, $connectionContext); } + $this->logger?->log( + LogLevel::INFO, + 'Sending request to Elasticsearch node: $method $url\n$requestBody", __METHOD__', + ); + $this->resetCurlHandle(); - curl_setopt($this->_curl, CURLOPT_URL, $url); - curl_setopt_array($this->_curl, $options); - if (curl_exec($this->_curl) === false) { - throw new Exception('Elasticsearch request failed: ' . curl_errno($this->_curl) . ' - ' . curl_error($this->_curl), [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseHeaders' => $headers, - 'responseBody' => $this->decodeErrorBody($body), - ]); + + curl_setopt($this->curl, CURLOPT_URL, $url); + curl_setopt_array($this->curl, $options); + + if (curl_exec($this->curl) === false) { + throw new RuntimeException( + 'Elasticsearch request failed: ' . curl_errno($this->curl) . ' - ' . curl_error($this->curl) + ); } - $responseCode = curl_getinfo($this->_curl, CURLINFO_HTTP_CODE); + $responseCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); if ($profile !== false) { - Yii::endProfile($profile, __METHOD__); + $this->profiler?->end($token, $connectionContext); } if ($responseCode >= 200 && $responseCode < 300) { if ($method === 'HEAD') { return true; } + if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { - throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $body, - ]); + throw new RuntimeException( + "Incomplete data received from Elasticsearch: $len < {$headers['content-length']}" + ); } - if (isset($headers['content-type']) && (!strncmp($headers['content-type'], 'application/json', 16) || !strncmp($headers['content-type'], 'text/plain', 10))) { - return $raw ? $body : Json::decode($body); + if (isset($headers['content-type'])) { + if (!strncmp($headers['content-type'], 'application/json', 16)) { + return $raw ? $body : Json::decode($body); + } + if (!strncmp($headers['content-type'], 'text/plain', 10)) { + return $raw ? $body : array_filter(explode("\n", $body)); + } } - throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $this->decodeErrorBody($body), - ]); - } elseif ($responseCode == 404) { + throw new RuntimeException('Unsupported data received from Elasticsearch: ' . $headers['content-type']); + } + + if ($responseCode === 404) { return false; - } else { - throw new Exception("Elasticsearch request failed with code $responseCode. Response body:\n{$body}", [ - 'requestMethod' => $method, - 'requestUrl' => $url, - 'requestBody' => $requestBody, - 'responseCode' => $responseCode, - 'responseHeaders' => $headers, - 'responseBody' => $this->decodeErrorBody($body), - ]); } + + throw new RuntimeException( + "Elasticsearch request failed with code $responseCode. Response body:\n$body", + ); } - private function resetCurlHandle() + private function resetCurlHandle(): void { - // these functions do not get reset by curl automatically - static $unsetValues = [ + // these functions don't get reset by curl automatically + $unsetValues = [ CURLOPT_HEADERFUNCTION => null, CURLOPT_WRITEFUNCTION => null, CURLOPT_READFUNCTION => null, CURLOPT_PROGRESSFUNCTION => null, CURLOPT_POSTFIELDS => null, ]; - curl_setopt_array($this->_curl, $unsetValues); - if (function_exists('curl_reset')) { // since PHP 5.5.0 - curl_reset($this->_curl); - } - } - /** - * Try to decode error information if it is valid json, return it if not. - * @param $body - * @return mixed - */ - protected function decodeErrorBody($body) - { - try { - $decoded = Json::decode($body); - if (isset($decoded['error']) && !is_array($decoded['error'])) { - $decoded['error'] = preg_replace('/\b\w+?Exception\[/', "\\0\n ", $decoded['error']); - } - return $decoded; - } catch (InvalidParamException $e) { - return $body; - } - } + curl_setopt_array($this->curl, $unsetValues); - public function getNodeInfo() - { - return $this->get([]); - } - - public function getClusterState() - { - return $this->get(['_cluster', 'state']); + if (function_exists('curl_reset')) { // since PHP 5.5.0 + curl_reset($this->curl); + } } } diff --git a/src/DebugAction.php b/src/DebugAction.php deleted file mode 100644 index 6066f933..00000000 --- a/src/DebugAction.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @since 2.0 - */ -class DebugAction extends Action -{ - /** - * @var string the connection id to use - */ - public $db; - /** - * @var DebugPanel - */ - public $panel; - /** - * @var \Yiisoft\Yii\Debug\Controllers\DefaultController - */ - public $controller; - - public function run($logId, $tag) - { - $this->controller->loadData($tag); - - $timings = $this->panel->calculateTimings(); - ArrayHelper::multisort($timings, 3, SORT_DESC); - if (!isset($timings[$logId])) { - throw new HttpException(404, 'Log message not found.'); - } - $message = $timings[$logId][1]; - if (($pos = mb_strpos($message, '#')) !== false) { - $url = mb_substr($message, 0, $pos); - $body = mb_substr($message, $pos + 1); - } else { - $url = $message; - $body = null; - } - $method = mb_substr($url, 0, $pos = mb_strpos($url, ' ')); - $url = mb_substr($url, $pos + 1); - - $options = ['pretty' => true]; - - /* @var $db Connection */ - $db = \Yii::$app->get($this->db); - $time = microtime(true); - switch ($method) { - case 'GET': $result = $db->get($url, $options, $body, true); - break; - case 'POST': $result = $db->post($url, $options, $body, true); - break; - case 'PUT': $result = $db->put($url, $options, $body, true); - break; - case 'DELETE': $result = $db->delete($url, $options, $body, true); - break; - case 'HEAD': $result = $db->head($url, $options, $body); - break; - default: - throw new NotSupportedException("Request method '$method' is not supported by elasticsearch."); - } - $time = microtime(true) - $time; - - if ($result === true) { - $result = 'success'; - } elseif ($result === false) { - $result = 'no success'; - } - - Yii::$app->response->format = Response::FORMAT_JSON; - - return [ - 'time' => sprintf('%.1f ms', $time * 1000), - 'result' => $result, - ]; - } -} diff --git a/src/DebugPanel.php b/src/DebugPanel.php deleted file mode 100644 index b14ec422..00000000 --- a/src/DebugPanel.php +++ /dev/null @@ -1,196 +0,0 @@ - - * @since 2.0 - */ -class DebugPanel extends Panel -{ - public $db = 'elasticsearch'; - - public function init() - { - $this->actions['elasticsearch-query'] = [ - 'class' => 'Yiisoft\\Db\\ElasticSearch\\DebugAction', - 'panel' => $this, - 'db' => $this->db, - ]; - } - - /** - * @inheritdoc - */ - public function getName() - { - return 'Elasticsearch'; - } - - /** - * @inheritdoc - */ - public function getSummary() - { - $timings = $this->calculateTimings(); - $queryCount = count($timings); - $queryTime = 0; - foreach ($timings as $timing) { - $queryTime += $timing[3]; - } - $queryTime = number_format($queryTime * 1000) . ' ms'; - $url = $this->getUrl(); - $output = << - - ES $queryCount $queryTime - - -EOD; - - return $queryCount > 0 ? $output : ''; - } - - /** - * @inheritdoc - */ - public function getDetail() - { - $timings = $this->calculateTimings(); - ArrayHelper::multisort($timings, 3, SORT_DESC); - $rows = []; - $i = 0; - foreach ($timings as $logId => $timing) { - $duration = sprintf('%.1f ms', $timing[3] * 1000); - $message = $timing[1]; - $traces = $timing[4]; - if (($pos = mb_strpos($message, '#')) !== false) { - $url = mb_substr($message, 0, $pos); - $body = mb_substr($message, $pos + 1); - } else { - $url = $message; - $body = null; - } - $traceString = ''; - if (!empty($traces)) { - $traceString .= Html::ul($traces, [ - 'class' => 'trace', - 'item' => function ($trace) { - return "
  • {$trace['file']}({$trace['line']})
  • "; - }, - ]); - } - $ajaxUrl = Url::to(['elasticsearch-query', 'logId' => $logId, 'tag' => $this->tag]); - \Yii::$app->view->registerJs(<<Error: ' + errorThrown + ' - ' + textStatus + '
    ' + jqXHR.responseText); - }, - dataType: "json" - }); - - return false; -}); -JS - , View::POS_READY); - $runLink = Html::a('run query', '#', ['id' => "elastic-link-$i"]) . '
    '; - $rows[] = << - $duration -
    $url

    $body

    $traceString
    - $runLink - - -HTML; - $i++; - } - $rows = implode("\n", $rows); - - return <<Elasticsearch Queries - - - - - - - - - - -$rows - -
    TimeUrl / QueryRun Query on node
    -HTML; - } - - private $_timings; - - public function calculateTimings() - { - if ($this->_timings !== null) { - return $this->_timings; - } - $messages = $this->data['messages'] ?? []; - $timings = []; - $stack = []; - foreach ($messages as $i => $log) { - [$token, $level, $category, $timestamp] = $log; - $log[5] = $i; - if ($level == Logger::LEVEL_PROFILE_BEGIN) { - $stack[] = $log; - } elseif ($level == Logger::LEVEL_PROFILE_END) { - if (($last = array_pop($stack)) !== null && $last[0] === $token) { - $timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]]; - } - } - } - - $now = microtime(true); - while (($last = array_pop($stack)) !== null) { - $delta = $now - $last[3]; - $timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]]; - } - ksort($timings); - - return $this->_timings = $timings; - } - - /** - * @inheritdoc - */ - public function save() - { - $target = $this->module->logTarget; - $messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['Yiisoft\Db\ElasticSearch\Connection::httpRequest']); - - return ['messages' => $messages]; - } -} diff --git a/src/ElasticsearchTarget.php b/src/ElasticsearchTarget.php deleted file mode 100644 index 883f4361..00000000 --- a/src/ElasticsearchTarget.php +++ /dev/null @@ -1,169 +0,0 @@ - - * @since 2.0.5 - */ -class ElasticsearchTarget extends Target -{ - /** - * @var string Elasticsearch index name. - */ - public $index = 'yii'; - /** - * @var string Elasticsearch type name. - */ - public $type = 'log'; - /** - * @var array|Connection|string the elasticsearch connection object or the application component ID - * of the elasticsearch connection. - */ - public $db = 'elasticsearch'; - /** - * @var array $options URL options. - */ - public $options = []; - /** - * @var bool If true, context will be logged as a separate message after all other messages. - */ - public $logContext = true; - /** - * @var bool If true, context will be included in every message. - * This is convenient if you log application errors and analyze them with tools like Kibana. - */ - public $includeContext = false; - /** - * @var bool If true, context message will cached once it's been created. Makes sense to use with [[includeContext]]. - */ - public $cacheContext = false; - /** - * @var string Context message cache (can be used multiple times if context is appended to every message) - */ - protected $_contextMessage = null; - - /** - * This method will initialize the [[elasticsearch]] property to make sure it refers to a valid Elasticsearch connection. - * @throws InvalidConfigException if [[elasticsearch]] is invalid. - */ - public function init() - { - parent::init(); - $this->db = Instance::ensure($this->db, Connection::className()); - } - - /** - * @inheritdoc - */ - public function export() - { - $messages = array_map([$this, 'prepareMessage'], $this->messages); - $body = implode("\n", $messages) . "\n"; - $this->db->post([$this->index, $this->type, '_bulk'], $this->options, $body); - } - - /** - * If [[includeContext]] property is false, returns context message normally. - * If [[includeContext]] is true, returns an empty string (so that context message in [[collect]] is not generated), - * expecting that context will be appended to every message in [[prepareMessage]]. - * @return array the context information - */ - protected function getContextMessage() - { - if (null === $this->_contextMessage || !$this->cacheContext) { - $this->_contextMessage = ArrayHelper::filter($GLOBALS, $this->logVars); - } - - return $this->_contextMessage; - } - - /** - * Processes the given log messages. - * This method will filter the given messages with [[levels]] and [[categories]]. - * And if requested, it will also export the filtering result to specific medium (e.g. email). - * Depending on the [[includeContext]] attribute, a context message will be either created or ignored. - * @param array $messages log messages to be processed. See [[Logger::messages]] for the structure - * of each message. - * @param bool $final whether this method is called at the end of the current application - */ - public function collect($messages, $final) - { - $this->messages = array_merge($this->messages, static::filterMessages($messages, $this->getLevels(), $this->categories, $this->except)); - $count = count($this->messages); - if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) { - if (!$this->includeContext && $this->logContext) { - $context = $this->getContextMessage(); - if (!empty($context)) { - $this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME]; - } - } - - // set exportInterval to 0 to avoid triggering export again while exporting - $oldExportInterval = $this->exportInterval; - $this->exportInterval = 0; - $this->export(); - $this->exportInterval = $oldExportInterval; - - $this->messages = []; - } - } - - /** - * Prepares a log message. - * @param array $message The log message to be formatted. - * @return string - */ - public function prepareMessage($message) - { - [$text, $level, $category, $timestamp] = $message; - - $result = [ - 'category' => $category, - 'level' => Logger::getLevelName($level), - '@timestamp' => date('c', $timestamp), - ]; - - if (isset($message[4])) { - $result['trace'] = $message[4]; - } - - //Exceptions get parsed into an array, text and arrays are passed as is, other types are var_dumped - if ($text instanceof \Exception) { - //convert exception to array for easier analysis - $result['message'] = [ - 'message' => $text->getMessage(), - 'file' => $text->getFile(), - 'line' => $text->getLine(), - 'trace' => $text->getTraceAsString(), - ]; - } elseif (is_array($text) || is_string($text)) { - $result['message'] = $text; - } else { - $result['message'] = VarDumper::export($text); - } - - if ($this->includeContext) { - $result['context'] = $this->getContextMessage(); - } - - return implode("\n", [ - Json::encode([ - 'index' => new \stdClass(), - ]), - Json::encode($result), - ]); - } -} diff --git a/src/Exception.php b/src/Exception.php deleted file mode 100644 index 76e05fa0..00000000 --- a/src/Exception.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @since 2.0 - */ -class Exception extends \Yiisoft\Db\Exception -{ - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return 'Elasticsearch Database Exception'; - } -} diff --git a/src/Profiler/Context/AbstractContext.php b/src/Profiler/Context/AbstractContext.php new file mode 100644 index 00000000..81fdf1f7 --- /dev/null +++ b/src/Profiler/Context/AbstractContext.php @@ -0,0 +1,34 @@ +exception = $e; + return $this; + } + + public function asArray(): array + { + return [ + self::METHOD => $this->method, + self::EXCEPTION => $this->exception, + ]; + } +} diff --git a/src/Profiler/Context/CommandContext.php b/src/Profiler/Context/CommandContext.php new file mode 100644 index 00000000..34f1b3f4 --- /dev/null +++ b/src/Profiler/Context/CommandContext.php @@ -0,0 +1,35 @@ +method); + } + + public function getType(): string + { + return 'command'; + } + + public function asArray(): array + { + return parent::asArray() + [ + self::LOG_CONTEXT => $this->logContext, + self::SQL => $this->sql, + self::PARAMS => $this->params, + ]; + } +} diff --git a/src/Profiler/Context/ConnectionContext.php b/src/Profiler/Context/ConnectionContext.php new file mode 100644 index 00000000..85d05604 --- /dev/null +++ b/src/Profiler/Context/ConnectionContext.php @@ -0,0 +1,13 @@ +profiler = $profiler; + } +} diff --git a/src/Profiler/ProfilerInterface.php b/src/Profiler/ProfilerInterface.php new file mode 100644 index 00000000..42a4217e --- /dev/null +++ b/src/Profiler/ProfilerInterface.php @@ -0,0 +1,39 @@ +storedFields('id, name') - * ->from('myindex', 'users') - * ->limit(10); - * // build and execute the query - * $command = $query->createCommand(); - * $rows = $command->search(); // this way you get the raw output of elasticsearch. - * ~~~ - * - * You would normally call `$query->search()` instead of creating a command as this method - * adds the `indexBy()` feature and also removes some inconsistencies from the response. - * - * Query also provides some methods to easier get some parts of the result only: - * - * - [[one()]]: returns a single record populated with the first row of data. - * - [[all()]]: returns all records based on the query results. - * - [[count()]]: returns the number of records. - * - [[scalar()]]: returns the value of the first column in the first row of the query result. - * - [[column()]]: returns the value of the first column in the query result. - * - [[exists()]]: returns a value indicating whether the query result has data or not. - * - * NOTE: elasticsearch limits the number of records returned to 10 records by default. - * If you expect to get more records you should specify limit explicitly. - * - * @author Carsten Brandt - * @since 2.0 - */ -class Query extends Component implements QueryInterface -{ - use QueryTrait; - - /** - * @var array the fields being retrieved from the documents. For example, `['id', 'name']`. - * If not set, this option will not be applied to the query and no fields will be returned. - * In this case the `_source` field will be returned by default which can be configured using [[source]]. - * Setting this to an empty array will result in no fields being retrieved, which means that only the primaryKey - * of a record will be available in the result. - * > Note: Field values are [always returned as arrays] even if they only have one value. - * - * [always returned as arrays]: http://www.elastic.co/guide/en/elasticsearch/reference/1.x/_return_values.html#_return_values - * [script field]: http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-stored-fields.html - * @see storedFields() - * @see source - */ - public $storedFields; - - /** - * @var array the scripted fields being retrieved from the documents. - * Example: - * ```php - * $query->scriptFields = [ - * 'value_times_two' => [ - * 'script' => "doc['my_field_name'].value * 2", - * ], - * 'value_times_factor' => [ - * 'script' => "doc['my_field_name'].value * factor", - * 'params' => [ - * 'factor' => 2.0 - * ], - * ], - * ] - * ``` - * - * > Note: Field values are [always returned as arrays] even if they only have one value. - * - * [always returned as arrays]: http://www.elastic.co/guide/en/elasticsearch/reference/1.x/_return_values.html#_return_values - * [script field]: http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html - * @see scriptFields() - * @see source - */ - public $scriptFields; - - /** - * @var array this option controls how the `_source` field is returned from the documents. For example, `['id', 'name']` - * means that only the `id` and `name` field should be returned from `_source`. - * If not set, it means retrieving the full `_source` field unless [[fields]] are specified. - * Setting this option to `false` will disable return of the `_source` field, this means that only the primaryKey - * of a record will be available in the result. - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html - * @see source() - * @see fields - */ - public $source; - /** - * @var array|string The index to retrieve data from. This can be a string representing a single index - * or a an array of multiple indexes. If this is not set, indexes are being queried. - * @see from() - */ - public $index; - /** - * @var array|string The type to retrieve data from. This can be a string representing a single type - * or a an array of multiple types. If this is not set, all types are being queried. - * @see from() - */ - public $type; - /** - * @var int A search timeout, bounding the search request to be executed within the specified time value - * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. - * @see timeout() - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5 - */ - public $timeout; - /** - * @var array|string The query part of this search query. This is an array or json string that follows the format of - * the elasticsearch [Query DSL](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html). - */ - public $query; - /** - * @var array|string The filter part of this search query. This is an array or json string that follows the format of - * the elasticsearch [Query DSL](http://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html). - */ - public $filter; - /** - * @var array|string The `post_filter` part of the search query for differentially filter search results and aggregations. - * @see https://www.elastic.co/guide/en/elasticsearch/guide/current/_post_filter.html - * @since 2.0.5 - */ - public $postFilter; - /** - * @var array The highlight part of this search query. This is an array that allows to highlight search results - * on one or more fields. - * @see http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-request-highlighting.html - */ - public $highlight; - /** - * @var array List of aggregations to add to this query. - * @see http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-aggregations.html - */ - public $aggregations = []; - /** - * @var array the 'stats' part of the query. An array of groups to maintain a statistics aggregation for. - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups - */ - public $stats = []; - /** - * @var array list of suggesters to add to this query. - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html - */ - public $suggest = []; - /** - * @var float Exclude documents which have a _score less than the minimum specified in min_score - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html - * @since 2.0.4 - */ - public $minScore; - /** - * @var array list of options that will passed to commands created by this query. - * @see Command::$options - * @since 2.0.4 - */ - public $options = []; - /** - * @var bool Enables explanation for each hit on how its score was computed. - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-explain.html - * @since 2.0.5 - */ - public $explain; - - /** - * @inheritdoc - */ - public function init() - { - parent::init(); - // setting the default limit according to elasticsearch defaults - // http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5 - if ($this->limit === null) { - $this->limit = 10; - } - } - - /** - * Creates a DB command that can be used to execute this query. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return Command the created DB command instance. - */ - public function createCommand($db = null) - { - if ($db === null) { - $db = Yii::$app->get('elasticsearch'); - } - - $commandConfig = $db->getQueryBuilder()->build($this); - - return $db->createCommand($commandConfig); - } - - /** - * Executes the query and returns all results as an array. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all($db = null) - { - $result = $this->createCommand($db)->search(); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - if (empty($result['hits']['hits'])) { - return []; - } - $rows = $result['hits']['hits']; - return $this->populate($rows); - } - - /** - * Converts the raw query results into the format as specified by this query. - * This method is internally used to convert the data fetched from database - * into the format as required by this query. - * @param array $rows the raw query result from database - * @return array the converted query result - * @since 2.0.4 - */ - public function populate($rows) - { - if ($this->indexBy === null) { - return $rows; - } - $models = []; - foreach ($rows as $key => $row) { - if ($this->indexBy !== null) { - if (is_string($this->indexBy)) { - $key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy]; - } else { - $key = ($this->indexBy)($row); - } - } - $models[$key] = $row; - } - return $models; - } - - /** - * Executes the query and returns a single row of result. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array|bool the first row (in terms of an array) of the query result. False is returned if the query - * results in nothing. - */ - public function one($db = null) - { - $result = $this->createCommand($db)->search(['size' => 1]); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - if (empty($result['hits']['hits'])) { - return false; - } - return reset($result['hits']['hits']); - } - - /** - * Executes the query and returns the complete search result including e.g. hits, facets, totalCount. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @param array $options The options given with this query. Possible options are: - * - * - [routing](http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#search-routing) - * - [search_type](http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-search-type.html) - * - * @return array the query results. - */ - public function search($db = null, $options = []) - { - $result = $this->createCommand($db)->search($options); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - if (!empty($result['hits']['hits']) && $this->indexBy !== null) { - $rows = []; - foreach ($result['hits']['hits'] as $key => $row) { - if (is_string($this->indexBy)) { - $key = $row['fields'][$this->indexBy] ?? $row['_source'][$this->indexBy]; - } else { - $key = ($this->indexBy)($row); - } - $rows[$key] = $row; - } - $result['hits']['hits'] = $rows; - } - return $result; - } - - /** - * Executes the query and deletes all matching documents. - * - * Everything except query and filter will be ignored. - * - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @param array $options The options given with this query. - * @return array the query results. - */ - public function delete($db = null, $options = []) - { - return $this->createCommand($db)->deleteByQuery($options); - } - - /** - * Returns the query result as a scalar value. - * The value returned will be the specified field in the first document of the query results. - * @param string $field name of the attribute to select - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return string the value of the specified attribute in the first record of the query result. - * Null is returned if the query result is empty or the field does not exist. - */ - public function scalar($field, $db = null) - { - $record = self::one($db); - if ($record !== false) { - if ($field === '_id') { - return $record['_id']; - } - if (isset($record['_source'][$field])) { - return $record['_source'][$field]; - } - if (isset($record['fields'][$field])) { - return count($record['fields'][$field]) == 1 ? reset($record['fields'][$field]) : $record['fields'][$field]; - } - } - return null; - } - - /** - * Executes the query and returns the first column of the result. - * @param string $field the field to query over - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($field, $db = null) - { - $command = $this->createCommand($db); - $command->queryParts['_source'] = [$field]; - $result = $command->search(); - if ($result === false) { - throw new Exception('Elasticsearch search query failed.'); - } - if (empty($result['hits']['hits'])) { - return []; - } - $column = []; - foreach ($result['hits']['hits'] as $row) { - if (isset($row['fields'][$field])) { - $column[] = $row['fields'][$field]; - } elseif (isset($row['_source'][$field])) { - $column[] = $row['_source'][$field]; - } else { - $column[] = null; - } - } - return $column; - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. This parameter is ignored by this implementation. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return int number of records - */ - public function count($q = '*', $db = null) - { - // performing a query with return size of 0, is equal to getting result stats such as count - // https://www.elastic.co/guide/en/elasticsearch/reference/5.6/breaking_50_search_changes.html#_literal_search_type_literal - $count = $this->createCommand($db)->search(['size' => 0])['hits']['total']; - if ($count === false) { - throw new Exception('Elasticsearch count query failed.'); - } - return $count; - } - - /** - * Returns a value indicating whether the query result contains any row of data. - * @param Connection $db the database connection used to execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return bool whether the query result contains any row of data. - */ - public function exists($db = null) - { - return self::one($db) !== false; - } - - /** - * Adds a 'stats' part to the query. - * @param array $groups an array of groups to maintain a statistics aggregation for. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups - */ - public function stats($groups) - { - $this->stats = $groups; - return $this; - } - - /** - * Sets a highlight parameters to retrieve from the documents. - * @param array $highlight array of parameters to highlight results. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html - */ - public function highlight($highlight) - { - $this->highlight = $highlight; - return $this; - } - - /** - * @deprecated since 2.0.5 use addAggragate() instead - * - * Adds an aggregation to this query. - * @param string $name the name of the aggregation - * @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`... - * @param array|string $options the configuration options for this aggregation. Can be an array or a json string. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-aggregations.html - */ - public function addAggregation($name, $type, $options) - { - return $this->addAggregate($name, [$type => $options]); - } - - /** - * @deprecated since 2.0.5 use addAggragate() instead - * - * Adds an aggregation to this query. - * - * This is an alias for [[addAggregation]]. - * - * @param string $name the name of the aggregation - * @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`... - * @param array|string $options the configuration options for this aggregation. Can be an array or a json string. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/1.x/search-aggregations.html - */ - public function addAgg($name, $type, $options) - { - return $this->addAggregate($name, [$type => $options]); - } - - /** - * Adds an aggregation to this query. Supports nested aggregations. - * @param string $name the name of the aggregation - * @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`... - * @param array|string $options the configuration options for this aggregation. Can be an array or a json string. - * @return $this the query object itself - * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.3/search-aggregations.html - */ - public function addAggregate($name, $options) - { - $this->aggregations[$name] = $options; - return $this; - } - - /** - * Adds a suggester to this query. - * @param string $name the name of the suggester - * @param array|string $definition the configuration options for this suggester. Can be an array or a json string. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html - */ - public function addSuggester($name, $definition) - { - $this->suggest[$name] = $definition; - return $this; - } - - // TODO add validate query http://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html - - // TODO support multi query via static method http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html - - /** - * Sets the querypart of this search query. - * @param string $query - * @return $this the query object itself - */ - public function query($query) - { - $this->query = $query; - return $this; - } - - /** - * Starts a batch query. - * - * A batch query supports fetching data in batches, which can keep the memory usage under a limit. - * This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface - * and can be traversed to retrieve the data in batches. - * - * For example, - * - * ```php - * $query = (new Query)->from('user'); - * foreach ($query->batch() as $rows) { - * // $rows is an array of 10 or fewer rows from user table - * } - * ``` - * - * Batch size is determined by the `limit` setting (note that in scan mode batch limit is per shard). - * - * @param string $scrollWindow how long Elasticsearch should keep the search context alive, - * in [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units) - * @param Connection $db the database connection. If not set, the `elasticsearch` application component will be used. - * @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface - * and can be traversed to retrieve the data in batches. - * @since 2.0.4 - */ - public function batch($scrollWindow = '1m', $db = null) - { - return Yii::createObject([ - 'class' => BatchQueryResult::className(), - 'query' => $this, - 'scrollWindow' => $scrollWindow, - 'db' => $db, - 'each' => false, - ]); - } - - /** - * Starts a batch query and retrieves data row by row. - * This method is similar to [[batch()]] except that in each iteration of the result, - * only one row of data is returned. For example, - * - * ```php - * $query = (new Query)->from('user'); - * foreach ($query->each() as $row) { - * } - * ``` - * - * @param string $scrollWindow how long Elasticsearch should keep the search context alive, - * in [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units) - * @param Connection $db the database connection. If not set, the `elasticsearch` application component will be used. - * @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface - * and can be traversed to retrieve the data in batches. - * @since 2.0.4 - */ - public function each($scrollWindow = '1m', $db = null) - { - return Yii::createObject([ - 'class' => BatchQueryResult::className(), - 'query' => $this, - 'scrollWindow' => $scrollWindow, - 'db' => $db, - 'each' => true, - ]); - } - - /** - * Sets the index and type to retrieve documents from. - * @param array|string $index The index to retrieve data from. This can be a string representing a single index - * or a an array of multiple indexes. If this is `null` it means that all indexes are being queried. - * @param array|string $type The type to retrieve data from. This can be a string representing a single type - * or a an array of multiple types. If this is `null` it means that all types are being queried. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type - */ - public function from($index, $type = null) - { - $this->index = $index; - $this->type = $type; - return $this; - } - - /** - * Sets the fields to retrieve from the documents. - * > Quote from the elasticsearch doc: - * > The stored_fields parameter is about fields that are explicitly marked - * > as stored in the mapping, which is off by default and generally not recommended. - * > Use source filtering instead to select subsets of the original source document to be returned. - * - * @param array $fields the fields to be selected. - * @return $this the query object itself - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-stored-fields.html - */ - public function storedFields($fields) - { - if (is_array($fields) || $fields === null) { - $this->storedFields = $fields; - } else { - $this->storedFields = func_get_args(); - } - return $this; - } - - /** - * Sets the script fields to retrieve from the documents. - * @param array $fields the fields to be selected. - * @return $this the query object itself - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html - */ - public function scriptFields($fields) - { - if (is_array($fields) || $fields === null) { - $this->scriptFields = $fields; - } else { - $this->scriptFields = func_get_args(); - } - return $this; - } - - /** - * Sets the source filtering, specifying how the `_source` field of the document should be returned. - * @param array $source the source patterns to be selected. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html - */ - public function source($source) - { - if (is_array($source) || $source === null) { - $this->source = $source; - } else { - $this->source = func_get_args(); - } - return $this; - } - - /** - * Sets the search timeout. - * @param int $timeout A search timeout, bounding the search request to be executed within the specified time value - * and bail with the hits accumulated up to that point when expired. Defaults to no timeout. - * @return $this the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5 - */ - public function timeout($timeout) - { - $this->timeout = $timeout; - return $this; - } - - /** - * @param float $minScore Exclude documents which have a `_score` less than the minimum specified minScore - * @return static the query object itself - * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html - * @since 2.0.4 - */ - public function minScore($minScore) - { - $this->minScore = $minScore; - return $this; - } - - /** - * Sets the options to be passed to the command created by this query. - * @param array $options the options to be set. - * @throws InvalidParamException if $options is not an array - * @return $this the query object itself - * @see Command::$options - * @since 2.0.4 - */ - public function options($options) - { - if (!is_array($options)) { - throw new InvalidParamException('Array parameter expected, ' . gettype($options) . ' received.'); - } - - $this->options = $options; - return $this; - } - - /** - * Adds more options, overwriting existing options. - * @param array $options the options to be added. - * @throws InvalidParamException if $options is not an array - * @return $this the query object itself - * @see options() - * @since 2.0.4 - */ - public function addOptions($options) - { - if (!is_array($options)) { - throw new InvalidParamException('Array parameter expected, ' . gettype($options) . ' received.'); - } - - $this->options = array_merge($this->options, $options); - return $this; - } - - /** - * @inheritdoc - */ - public function andWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } elseif (isset($this->where[0]) && $this->where[0] === 'and') { - $this->where[] = $condition; - } else { - $this->where = ['and', $this->where, $condition]; - } - return $this; - } - - /** - * @inheritdoc - */ - public function orWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } elseif (isset($this->where[0]) && $this->where[0] === 'or') { - $this->where[] = $condition; - } else { - $this->where = ['or', $this->where, $condition]; - } - return $this; - } - - /** - * Set the `post_filter` part of the search query. - * @param array|string $filter - * @return $this the query object itself - * @see $postFilter - * @since 2.0.5 - */ - public function postFilter($filter) - { - $this->postFilter = $filter; - return $this; - } - - /** - * Explain for how the score of each document was computer - * @param $explain - * @return $this - * @see $explain - * @since 2.0.5 - */ - public function explain($explain) - { - $this->explain = $explain; - return $this; - } -} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php deleted file mode 100644 index 395a232c..00000000 --- a/src/QueryBuilder.php +++ /dev/null @@ -1,438 +0,0 @@ - - * @since 2.0 - */ -class QueryBuilder extends BaseObject -{ - /** - * @var Connection the database connection. - */ - public $db; - - /** - * Constructor. - * @param Connection $connection the database connection. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($connection, $config = []) - { - $this->db = $connection; - parent::__construct($config); - } - - /** - * Generates query from a [[Query]] object. - * @param Query $query the [[Query]] object from which the query will be generated - * @return array the generated SQL statement (the first array element) and the corresponding - * parameters to be bound to the SQL statement (the second array element). - */ - public function build($query) - { - $parts = []; - - if ($query->storedFields !== null) { - $parts['stored_fields'] = $query->storedFields; - } - if ($query->scriptFields !== null) { - $parts['script_fields'] = $query->scriptFields; - } - - if ($query->source !== null) { - $parts['_source'] = $query->source; - } - if ($query->limit !== null && $query->limit >= 0) { - $parts['size'] = $query->limit; - } - if ($query->offset > 0) { - $parts['from'] = (int)$query->offset; - } - if (isset($query->minScore)) { - $parts['min_score'] = (float)$query->minScore; - } - if (isset($query->explain)) { - $parts['explain'] = $query->explain; - } - - $whereQuery = $this->buildQueryFromWhere($query->where); - if ($whereQuery) { - $parts['query'] = $whereQuery; - } elseif ($query->query) { - $parts['query'] = $query->query; - } - - if (!empty($query->highlight)) { - $parts['highlight'] = $query->highlight; - } - if (!empty($query->aggregations)) { - $parts['aggregations'] = $query->aggregations; - } - if (!empty($query->stats)) { - $parts['stats'] = $query->stats; - } - if (!empty($query->suggest)) { - $parts['suggest'] = $query->suggest; - } - if (!empty($query->postFilter)) { - $parts['post_filter'] = $query->postFilter; - } - - $sort = $this->buildOrderBy($query->orderBy); - if (!empty($sort)) { - $parts['sort'] = $sort; - } - - $options = $query->options; - if ($query->timeout !== null) { - $options['timeout'] = $query->timeout; - } - - return [ - 'queryParts' => $parts, - 'index' => $query->index, - 'type' => $query->type, - 'options' => $options, - ]; - } - - /** - * adds order by condition to the query - */ - public function buildOrderBy($columns) - { - if (empty($columns)) { - return []; - } - $orders = []; - foreach ($columns as $name => $direction) { - if (is_string($direction)) { - $column = $direction; - $direction = SORT_ASC; - } else { - $column = $name; - } - if ($column == '_id') { - $column = '_uid'; - } - - // allow elasticsearch extended syntax as described in http://www.elastic.co/guide/en/elasticsearch/guide/master/_sorting.html - if (is_array($direction)) { - $orders[] = [$column => $direction]; - } else { - $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; - } - } - - return $orders; - } - - public function buildQueryFromWhere($condition) - { - $where = $this->buildCondition($condition); - if ($where) { - return [ - 'constant_score' => [ - 'filter' => $where, - ], - ]; - } - return null; - } - - /** - * Parses the condition specification and generates the corresponding SQL expression. - * - * @param array|string $condition the condition specification. Please refer to [[Query::where()]] on how to specify a condition. - * @throws \yii\base\InvalidParamException if unknown operator is used in query - * @throws \yii\base\NotSupportedException if string conditions are used in where - * @return string the generated SQL expression - */ - public function buildCondition($condition) - { - static $builders = [ - 'not' => 'buildNotCondition', - 'and' => 'buildBoolCondition', - 'or' => 'buildBoolCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - 'lt' => 'buildHalfBoundedRangeCondition', - '<' => 'buildHalfBoundedRangeCondition', - 'lte' => 'buildHalfBoundedRangeCondition', - '<=' => 'buildHalfBoundedRangeCondition', - 'gt' => 'buildHalfBoundedRangeCondition', - '>' => 'buildHalfBoundedRangeCondition', - 'gte' => 'buildHalfBoundedRangeCondition', - '>=' => 'buildHalfBoundedRangeCondition', - ]; - - if (empty($condition)) { - return []; - } - if (!is_array($condition)) { - throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtolower($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - - return $this->$method($operator, $condition); - } - throw new InvalidParamException('Found unknown operator in query: ' . $operator); - } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... - return $this->buildHashCondition($condition); - } - } - - private function buildHashCondition($condition) - { - $parts = $emptyFields = []; - foreach ($condition as $attribute => $value) { - if ($attribute == '_id') { - if ($value === null) { // there is no null pk - $parts[] = ['terms' => ['_uid' => []]]; // this condition is equal to WHERE false - } else { - $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; - } - } else { - if (is_array($value)) { // IN condition - $parts[] = ['terms' => [$attribute => $value]]; - } else { - if ($value === null) { - $emptyFields[] = [ 'exists' => [ 'field' => $attribute ] ]; - } else { - $parts[] = ['term' => [$attribute => $value]]; - } - } - } - } - - $query = [ 'must' => $parts ]; - if ($emptyFields) { - $query['must_not'] = $emptyFields; - } - return [ 'bool' => $query ]; - } - - private function buildNotCondition($operator, $operands) - { - if (count($operands) != 1) { - throw new InvalidParamException("Operator '$operator' requires exactly one operand."); - } - - $operand = reset($operands); - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - - return [ - 'bool' => [ - 'must_not' => $operand, - ], - ]; - } - - private function buildBoolCondition($operator, $operands) - { - $parts = []; - if ($operator === 'and') { - $clause = 'must'; - } elseif ($operator === 'or') { - $clause = 'should'; - } else { - throw new InvalidParamException("Operator should be 'or' or 'and'"); - } - - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if (!empty($operand)) { - $parts[] = $operand; - } - } - if ($parts) { - return [ - 'bool' => [ - $clause => $parts, - ], - ]; - } - return null; - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new InvalidParamException("Operator '$operator' requires three operands."); - } - - [$column, $value1, $value2] = $operands; - if ($column === '_id') { - throw new NotSupportedException('Between condition is not supported for the _id field.'); - } - $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; - if ($operator === 'not between') { - $filter = ['bool' => ['must_not' => $filter]]; - } - - return $filter; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1]) || !is_array($operands)) { - throw new InvalidParamException("Operator '$operator' requires array of two operands: column and values"); - } - - [$column, $values] = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'in' ? ['terms' => ['_uid' => []]] : []; // this condition is equal to WHERE false - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); - } - if (is_array($column)) { - $column = reset($column); - } - $canBeNull = false; - foreach ($values as $i => $value) { - if (is_array($value)) { - $values[$i] = $value = $value[$column] ?? null; - } - if ($value === null) { - $canBeNull = true; - unset($values[$i]); - } - } - if ($column === '_id') { - if (empty($values) && $canBeNull) { // there is no null pk - $filter = ['terms' => ['_uid' => []]]; // this condition is equal to WHERE false - } else { - $filter = ['ids' => ['values' => array_values($values)]]; - if ($canBeNull) { - $filter = [ - 'bool' => [ - 'should' => [ - $filter, - 'bool' => ['must_not' => ['exists' => ['field' => $column]]], - ], - ], - ]; - } - } - } else { - if (empty($values) && $canBeNull) { - $filter = [ - 'bool' => [ - 'must_not' => [ - 'exists' => [ 'field' => $column ], - ], - ], - ]; - } else { - $filter = [ 'terms' => [$column => array_values($values)] ]; - if ($canBeNull) { - $filter = [ - 'bool' => [ - 'should' => [ - $filter, - 'bool' => ['must_not' => ['exists' => ['field' => $column]]], - ], - ], - ]; - } - } - } - - if ($operator === 'not in') { - $filter = [ - 'bool' => [ - 'must_not' => $filter, - ], - ]; - } - - return $filter; - } - - /** - * Builds a half-bounded range condition - * (for "gt", ">", "gte", ">=", "lt", "<", "lte", "<=" operators) - * @param string $operator - * @param array $operands - * @return array Filter expression - */ - private function buildHalfBoundedRangeCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - [$column, $value] = $operands; - if ($column === '_id') { - $column = '_uid'; - } - - $range_operator = null; - - if (in_array($operator, ['gte', '>='])) { - $range_operator = 'gte'; - } elseif (in_array($operator, ['lte', '<='])) { - $range_operator = 'lte'; - } elseif (in_array($operator, ['gt', '>'])) { - $range_operator = 'gt'; - } elseif (in_array($operator, ['lt', '<'])) { - $range_operator = 'lt'; - } - - if ($range_operator === null) { - throw new InvalidParamException("Operator '$operator' is not implemented."); - } - - return [ - 'range' => [ - $column => [ - $range_operator => $value, - ], - ], - ]; - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - throw new NotSupportedException('composite in is not supported by elasticsearch.'); - } - - private function buildLikeCondition($operator, $operands) - { - throw new NotSupportedException('like conditions are not supported by elasticsearch.'); - } -} diff --git a/tests/ActiveDataProviderTest.php b/tests/ActiveDataProviderTest.php deleted file mode 100644 index f3764740..00000000 --- a/tests/ActiveDataProviderTest.php +++ /dev/null @@ -1,93 +0,0 @@ -getConnection(); - - // delete index - if ($db->createCommand()->indexExists('yiitest')) { - $db->createCommand()->deleteIndex('yiitest'); - } - $db->createCommand()->createIndex('yiitest'); - - $command = $db->createCommand(); - Customer::setUpMapping($command); - - $db->createCommand()->flushIndex('yiitest'); - - $customer = new Customer(); - $customer->id = 1; - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 2; - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 3; - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 1], false); - $customer->save(false); - - $db->createCommand()->flushIndex('yiitest'); - } - - // Tests : - - public function testQuery() - { - $query = new Query(); - $query->from('yiitest', 'customer'); - - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - ]); - $models = $provider->getModels(); - $this->assertCount(3, $models); - - $provider = new ActiveDataProvider([ - 'query' => $query, - 'db' => $this->getConnection(), - 'pagination' => [ - 'pageSize' => 1, - ], - ]); - $models = $provider->getModels(); - $this->assertCount(1, $models); - } - - public function testActiveQuery() - { - $provider = new ActiveDataProvider([ - 'query' => Customer::find(), - ]); - $models = $provider->getModels(); - $this->assertCount(3, $models); - $this->assertTrue($models[0] instanceof Customer); - $this->assertTrue($models[1] instanceof Customer); - - $provider = new ActiveDataProvider([ - 'query' => Customer::find(), - 'pagination' => [ - 'pageSize' => 1, - ], - ]); - $models = $provider->getModels(); - $this->assertCount(1, $models); - } -} diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php deleted file mode 100644 index dea14fae..00000000 --- a/tests/ActiveRecordTest.php +++ /dev/null @@ -1,978 +0,0 @@ -getConnection()->createCommand()->flushIndex('yiitest'); - } - - public function setUp() - { - parent::setUp(); - - /* @var $db Connection */ - $db = ActiveRecord::$db = $this->getConnection(); - - // delete index - if ($db->createCommand()->indexExists('yiitest')) { - $db->createCommand()->deleteIndex('yiitest'); - } - $db->createCommand()->createIndex('yiitest'); - - $command = $db->createCommand(); - Customer::setUpMapping($command); - Item::setUpMapping($command); - Order::setUpMapping($command); - OrderItem::setUpMapping($command); - OrderWithNullFK::setUpMapping($command); - OrderItemWithNullFK::setUpMapping($command); - Animal::setUpMapping($command); - - $db->createCommand()->flushIndex('yiitest'); - - $customer = new Customer(); - $customer->id = 1; - $customer->setAttributes(['email' => 'user1@example.com', 'name' => 'user1', 'address' => 'address1', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 2; - $customer->setAttributes(['email' => 'user2@example.com', 'name' => 'user2', 'address' => 'address2', 'status' => 1], false); - $customer->save(false); - $customer = new Customer(); - $customer->id = 3; - $customer->setAttributes(['email' => 'user3@example.com', 'name' => 'user3', 'address' => 'address3', 'status' => 2], false); - $customer->save(false); - - // INSERT INTO category (name) VALUES ('Books'); - // INSERT INTO category (name) VALUES ('Movies'); - - $item = new Item(); - $item->id = 1; - $item->setAttributes(['name' => 'Agile Web Application Development with Yii1.1 and PHP5', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->id = 2; - $item->setAttributes(['name' => 'Yii 1.1 Application Development Cookbook', 'category_id' => 1], false); - $item->save(false); - $item = new Item(); - $item->id = 3; - $item->setAttributes(['name' => 'Ice Age', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->id = 4; - $item->setAttributes(['name' => 'Toy Story', 'category_id' => 2], false); - $item->save(false); - $item = new Item(); - $item->id = 5; - $item->setAttributes(['name' => 'Cars', 'category_id' => 2], false); - $item->save(false); - - $order = new Order(); - $order->id = 1; - $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0, 'itemsArray' => [1, 2]], false); - $order->save(false); - $order = new Order(); - $order->id = 2; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0, 'itemsArray' => [4, 5, 3]], false); - $order->save(false); - $order = new Order(); - $order->id = 3; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0, 'itemsArray' => [2]], false); - $order->save(false); - - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); - $orderItem->save(false); - $orderItem = new OrderItem(); - $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); - $orderItem->save(false); - - $order = new OrderWithNullFK(); - $order->id = 1; - $order->setAttributes(['customer_id' => 1, 'created_at' => 1325282384, 'total' => 110.0], false); - $order->save(false); - $order = new OrderWithNullFK(); - $order->id = 2; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325334482, 'total' => 33.0], false); - $order->save(false); - $order = new OrderWithNullFK(); - $order->id = 3; - $order->setAttributes(['customer_id' => 2, 'created_at' => 1325502201, 'total' => 40.0], false); - $order->save(false); - - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 2, 'quantity' => 2, 'subtotal' => 40.0], false); - $orderItem->save(false); - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 4, 'quantity' => 1, 'subtotal' => 10.0], false); - $orderItem->save(false); - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 5, 'quantity' => 1, 'subtotal' => 15.0], false); - $orderItem->save(false); - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 2, 'item_id' => 3, 'quantity' => 1, 'subtotal' => 8.0], false); - $orderItem->save(false); - $orderItem = new OrderItemWithNullFK(); - $orderItem->setAttributes(['order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0], false); - $orderItem->save(false); - - (new Cat())->save(false); - (new Dog())->save(false); - - $db->createCommand()->flushIndex('yiitest'); - } - - public function testSaveNoChanges() - { - // this should not fail with exception - $customer = new Customer(); - // insert - $customer->save(false); - // update - $customer->save(false); - } - - public function testFindAsArray() - { - // asArray - $customer = Customer::find()->where(['id' => 2])->asArray()->one(); - $this->assertEquals([ - 'id' => 2, - 'email' => 'user2@example.com', - 'name' => 'user2', - 'address' => 'address2', - 'status' => 1, - // '_score' => 1.0 - ], $customer['_source']); - } - - public function testSearch() - { - $customers = Customer::find()->search()['hits']; - $this->assertEquals(3, $customers['total']); - $this->assertCount(3, $customers['hits']); - $this->assertTrue($customers['hits'][0] instanceof Customer); - $this->assertTrue($customers['hits'][1] instanceof Customer); - $this->assertTrue($customers['hits'][2] instanceof Customer); - - // limit vs. totalcount - $customers = Customer::find()->limit(2)->search()['hits']; - $this->assertEquals(3, $customers['total']); - $this->assertCount(2, $customers['hits']); - - // asArray - $result = Customer::find()->asArray()->search()['hits']; - $this->assertEquals(3, $result['total']); - $customers = $result['hits']; - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers[0]['_source']); - $this->assertArrayHasKey('name', $customers[0]['_source']); - $this->assertArrayHasKey('email', $customers[0]['_source']); - $this->assertArrayHasKey('address', $customers[0]['_source']); - $this->assertArrayHasKey('status', $customers[0]['_source']); - $this->assertArrayHasKey('id', $customers[1]['_source']); - $this->assertArrayHasKey('name', $customers[1]['_source']); - $this->assertArrayHasKey('email', $customers[1]['_source']); - $this->assertArrayHasKey('address', $customers[1]['_source']); - $this->assertArrayHasKey('status', $customers[1]['_source']); - $this->assertArrayHasKey('id', $customers[2]['_source']); - $this->assertArrayHasKey('name', $customers[2]['_source']); - $this->assertArrayHasKey('email', $customers[2]['_source']); - $this->assertArrayHasKey('address', $customers[2]['_source']); - $this->assertArrayHasKey('status', $customers[2]['_source']); - - // TODO test asArray() + fields() + indexBy() - // find by attributes - $result = Customer::find()->where(['name' => 'user2'])->search()['hits']; - $customer = reset($result['hits']); - $this->assertInstanceOf(Customer::class, $customer); - $this->assertEquals(2, $customer->id); - - // TODO test query() and filter() - } - - // TODO test aggregations -// public function testSearchFacets() -// { -// $result = Customer::find()->addAggregation('status_stats', ['field' => 'status'])->search(); -// $this->assertArrayHasKey('facets', $result); -// $this->assertEquals(3, $result['facets']['status_stats']['count']); -// $this->assertEquals(4, $result['facets']['status_stats']['total']); // sum of values -// $this->assertEquals(1, $result['facets']['status_stats']['min']); -// $this->assertEquals(2, $result['facets']['status_stats']['max']); -// } - - public function testGetDb() - { - $this->mockApplication(['components' => ['elasticsearch' => Connection::className()]]); - $this->assertInstanceOf(Connection::className(), ActiveRecord::getDb()); - } - - public function testGet() - { - $this->assertInstanceOf(Customer::className(), Customer::get(1)); - $this->assertNull(Customer::get(5)); - } - - public function testMget() - { - $this->assertEquals([], Customer::mget([])); - - $records = Customer::mget([1]); - $this->assertCount(1, $records); - $this->assertInstanceOf(Customer::className(), reset($records)); - - $records = Customer::mget([5]); - $this->assertCount(0, $records); - - $records = Customer::mget([1, 3, 5]); - $this->assertCount(2, $records); - $this->assertInstanceOf(Customer::className(), $records[0]); - $this->assertInstanceOf(Customer::className(), $records[1]); - } - - public function testFindLazy() - { - /* @var $customer Customer */ - $customer = Customer::findOne(2); - $orders = $customer->orders; - $this->assertCount(2, $orders); - - $orders = $customer->getOrders()->where(['between', 'created_at', 1325334000, 1325400000])->all(); - $this->assertCount(1, $orders); - $this->assertEquals(2, $orders[0]->id); - } - - public function testFindEagerViaRelation() - { - $orders = Order::find()->with('items')->orderBy('created_at')->all(); - $this->assertCount(3, $orders); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertTrue($order->isRelationPopulated('items')); - $this->assertCount(2, $order->items); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testInsertNoPk() - { - $this->assertEquals(['id'], Customer::primaryKey()); - $pkName = 'id'; - - $customer = new Customer(); - $customer->email = 'user4@example.com'; - $customer->name = 'user4'; - $customer->address = 'address4'; - - $this->assertNull($customer->primaryKey); - $this->assertNull($customer->oldPrimaryKey); - $this->assertNull($customer->$pkName); - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - $this->afterSave(); - - $this->assertNotNull($customer->primaryKey); - $this->assertNotNull($customer->oldPrimaryKey); - $this->assertNotNull($customer->$pkName); - $this->assertEquals($customer->primaryKey, $customer->oldPrimaryKey); - $this->assertEquals($customer->primaryKey, $customer->$pkName); - $this->assertFalse($customer->isNewRecord); - } - - public function testInsertPk() - { - $pkName = 'id'; - - $customer = new Customer(); - $customer->$pkName = 5; - $customer->email = 'user5@example.com'; - $customer->name = 'user5'; - $customer->address = 'address5'; - - $this->assertTrue($customer->isNewRecord); - - $customer->save(); - - $this->assertEquals(5, $customer->primaryKey); - $this->assertEquals(5, $customer->oldPrimaryKey); - $this->assertEquals(5, $customer->$pkName); - $this->assertFalse($customer->isNewRecord); - } - - public function testUpdatePk() - { - $pkName = 'id'; - - $orderItem = Order::findOne([$pkName => 2]); - $this->assertEquals(2, $orderItem->primaryKey); - $this->assertEquals(2, $orderItem->oldPrimaryKey); - $this->assertEquals(2, $orderItem->$pkName); - - // $this->setExpectedException('yii\base\InvalidCallException'); - $orderItem->$pkName = 13; - $this->assertEquals(13, $orderItem->primaryKey); - $this->assertEquals(2, $orderItem->oldPrimaryKey); - $this->assertEquals(13, $orderItem->$pkName); - $orderItem->save(); - $this->afterSave(); - $this->assertEquals(13, $orderItem->primaryKey); - $this->assertEquals(13, $orderItem->oldPrimaryKey); - $this->assertEquals(13, $orderItem->$pkName); - - $this->assertNull(Order::findOne([$pkName => 2])); - $this->assertNotNull(Order::findOne([$pkName => 13])); - } - - public function testFindLazyVia2() - { - /* @var $this TestCase|ActiveRecordTestTrait */ - /* @var $order Order */ - $orderClass = $this->getOrderClass(); - $pkName = 'id'; - - $order = new $orderClass(); - $order->$pkName = 100; - $this->assertEquals([], $order->items); - } - - public function testScriptFields() - { - $orderItems = OrderItem::find() - ->source('quantity', 'subtotal') - ->scriptFields([ - 'total' => [ - 'script' => [ - 'lang' => 'painless', - 'inline' => "doc['quantity'].value * doc['subtotal'].value", - ], - ], - ])->all(); - $this->assertNotEmpty($orderItems); - foreach ($orderItems as $item) { - $this->assertEquals($item->subtotal * $item->quantity, $item->total); - } - } - - public function testFindAsArrayFields() - { - /* @var $this TestCase|ActiveRecordTestTrait */ - // indexBy + asArray - $customers = Customer::find()->asArray() - ->storedFields(['id', 'name'])->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers[0]['fields']); - $this->assertArrayHasKey('name', $customers[0]['fields']); - $this->assertArrayNotHasKey('email', $customers[0]['fields']); - $this->assertArrayNotHasKey('address', $customers[0]['fields']); - $this->assertArrayNotHasKey('status', $customers[0]['fields']); - $this->assertArrayHasKey('id', $customers[1]['fields']); - $this->assertArrayHasKey('name', $customers[1]['fields']); - $this->assertArrayNotHasKey('email', $customers[1]['fields']); - $this->assertArrayNotHasKey('address', $customers[1]['fields']); - $this->assertArrayNotHasKey('status', $customers[1]['fields']); - $this->assertArrayHasKey('id', $customers[2]['fields']); - $this->assertArrayHasKey('name', $customers[2]['fields']); - $this->assertArrayNotHasKey('email', $customers[2]['fields']); - $this->assertArrayNotHasKey('address', $customers[2]['fields']); - $this->assertArrayNotHasKey('status', $customers[2]['fields']); - } - - public function testFindAsArraySourceFilter() - { - /* @var $this TestCase|ActiveRecordTestTrait */ - // indexBy + asArray - $customers = Customer::find()->asArray()->source(['id', 'name'])->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers[0]['_source']); - $this->assertArrayHasKey('name', $customers[0]['_source']); - $this->assertArrayNotHasKey('email', $customers[0]['_source']); - $this->assertArrayNotHasKey('address', $customers[0]['_source']); - $this->assertArrayNotHasKey('status', $customers[0]['_source']); - $this->assertArrayHasKey('id', $customers[1]['_source']); - $this->assertArrayHasKey('name', $customers[1]['_source']); - $this->assertArrayNotHasKey('email', $customers[1]['_source']); - $this->assertArrayNotHasKey('address', $customers[1]['_source']); - $this->assertArrayNotHasKey('status', $customers[1]['_source']); - $this->assertArrayHasKey('id', $customers[2]['_source']); - $this->assertArrayHasKey('name', $customers[2]['_source']); - $this->assertArrayNotHasKey('email', $customers[2]['_source']); - $this->assertArrayNotHasKey('address', $customers[2]['_source']); - $this->assertArrayNotHasKey('status', $customers[2]['_source']); - } - - public function testFindIndexBySource() - { - $customerClass = $this->getCustomerClass(); - /* @var $this TestCase|ActiveRecordTestTrait */ - // indexBy + asArray - $customers = Customer::find()->indexBy('name')->source('id', 'name')->all(); - $this->assertCount(3, $customers); - $this->assertTrue($customers['user1'] instanceof $customerClass); - $this->assertTrue($customers['user2'] instanceof $customerClass); - $this->assertTrue($customers['user3'] instanceof $customerClass); - $this->assertNotNull($customers['user1']->id); - $this->assertNotNull($customers['user1']->name); - $this->assertNull($customers['user1']->email); - $this->assertNull($customers['user1']->address); - $this->assertNull($customers['user1']->status); - $this->assertNotNull($customers['user2']->id); - $this->assertNotNull($customers['user2']->name); - $this->assertNull($customers['user2']->email); - $this->assertNull($customers['user2']->address); - $this->assertNull($customers['user2']->status); - $this->assertNotNull($customers['user3']->id); - $this->assertNotNull($customers['user3']->name); - $this->assertNull($customers['user3']->email); - $this->assertNull($customers['user3']->address); - $this->assertNull($customers['user3']->status); - - // indexBy callable + asArray - $customers = Customer::find()->indexBy(function ($customer) { - return $customer->id . '-' . $customer->name; - })->storedFields('id', 'name')->all(); - $this->assertCount(3, $customers); - $this->assertTrue($customers['1-user1'] instanceof $customerClass); - $this->assertTrue($customers['2-user2'] instanceof $customerClass); - $this->assertTrue($customers['3-user3'] instanceof $customerClass); - $this->assertNotNull($customers['1-user1']->id); - $this->assertNotNull($customers['1-user1']->name); - $this->assertNull($customers['1-user1']->email); - $this->assertNull($customers['1-user1']->address); - $this->assertNull($customers['1-user1']->status); - $this->assertNotNull($customers['2-user2']->id); - $this->assertNotNull($customers['2-user2']->name); - $this->assertNull($customers['2-user2']->email); - $this->assertNull($customers['2-user2']->address); - $this->assertNull($customers['2-user2']->status); - $this->assertNotNull($customers['3-user3']->id); - $this->assertNotNull($customers['3-user3']->name); - $this->assertNull($customers['3-user3']->email); - $this->assertNull($customers['3-user3']->address); - $this->assertNull($customers['3-user3']->status); - } - - public function testFindIndexByAsArrayFields() - { - /* @var $this TestCase|ActiveRecordTestTrait */ - // indexBy + asArray - $customers = Customer::find()->indexBy('name')->asArray()->storedFields('id', 'name')->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers['user1']['fields']); - $this->assertArrayHasKey('name', $customers['user1']['fields']); - $this->assertArrayNotHasKey('email', $customers['user1']['fields']); - $this->assertArrayNotHasKey('address', $customers['user1']['fields']); - $this->assertArrayNotHasKey('status', $customers['user1']['fields']); - $this->assertArrayHasKey('id', $customers['user2']['fields']); - $this->assertArrayHasKey('name', $customers['user2']['fields']); - $this->assertArrayNotHasKey('email', $customers['user2']['fields']); - $this->assertArrayNotHasKey('address', $customers['user2']['fields']); - $this->assertArrayNotHasKey('status', $customers['user2']['fields']); - $this->assertArrayHasKey('id', $customers['user3']['fields']); - $this->assertArrayHasKey('name', $customers['user3']['fields']); - $this->assertArrayNotHasKey('email', $customers['user3']['fields']); - $this->assertArrayNotHasKey('address', $customers['user3']['fields']); - $this->assertArrayNotHasKey('status', $customers['user3']['fields']); - - // indexBy callable + asArray - $customers = Customer::find()->indexBy(function ($customer) { - return reset($customer['fields']['id']) . '-' . reset($customer['fields']['name']); - })->asArray()->storedFields('id', 'name')->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers['1-user1']['fields']); - $this->assertArrayHasKey('name', $customers['1-user1']['fields']); - $this->assertArrayNotHasKey('email', $customers['1-user1']['fields']); - $this->assertArrayNotHasKey('address', $customers['1-user1']['fields']); - $this->assertArrayNotHasKey('status', $customers['1-user1']['fields']); - $this->assertArrayHasKey('id', $customers['2-user2']['fields']); - $this->assertArrayHasKey('name', $customers['2-user2']['fields']); - $this->assertArrayNotHasKey('email', $customers['2-user2']['fields']); - $this->assertArrayNotHasKey('address', $customers['2-user2']['fields']); - $this->assertArrayNotHasKey('status', $customers['2-user2']['fields']); - $this->assertArrayHasKey('id', $customers['3-user3']['fields']); - $this->assertArrayHasKey('name', $customers['3-user3']['fields']); - $this->assertArrayNotHasKey('email', $customers['3-user3']['fields']); - $this->assertArrayNotHasKey('address', $customers['3-user3']['fields']); - $this->assertArrayNotHasKey('status', $customers['3-user3']['fields']); - } - - public function testFindIndexByAsArray() - { - /* @var $customerClass \Yiisoft\Db\ActiveRecordInterface */ - $customerClass = $this->getCustomerClass(); - - /* @var $this TestCase|ActiveRecordTestTrait */ - // indexBy + asArray - $customers = $customerClass::find()->asArray()->indexBy('name')->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers['user1']['_source']); - $this->assertArrayHasKey('name', $customers['user1']['_source']); - $this->assertArrayHasKey('email', $customers['user1']['_source']); - $this->assertArrayHasKey('address', $customers['user1']['_source']); - $this->assertArrayHasKey('status', $customers['user1']['_source']); - $this->assertArrayHasKey('id', $customers['user2']['_source']); - $this->assertArrayHasKey('name', $customers['user2']['_source']); - $this->assertArrayHasKey('email', $customers['user2']['_source']); - $this->assertArrayHasKey('address', $customers['user2']['_source']); - $this->assertArrayHasKey('status', $customers['user2']['_source']); - $this->assertArrayHasKey('id', $customers['user3']['_source']); - $this->assertArrayHasKey('name', $customers['user3']['_source']); - $this->assertArrayHasKey('email', $customers['user3']['_source']); - $this->assertArrayHasKey('address', $customers['user3']['_source']); - $this->assertArrayHasKey('status', $customers['user3']['_source']); - - // indexBy callable + asArray - $customers = $customerClass::find()->indexBy(function ($customer) { - return $customer['_source']['id'] . '-' . $customer['_source']['name']; - })->asArray()->all(); - $this->assertCount(3, $customers); - $this->assertArrayHasKey('id', $customers['1-user1']['_source']); - $this->assertArrayHasKey('name', $customers['1-user1']['_source']); - $this->assertArrayHasKey('email', $customers['1-user1']['_source']); - $this->assertArrayHasKey('address', $customers['1-user1']['_source']); - $this->assertArrayHasKey('status', $customers['1-user1']['_source']); - $this->assertArrayHasKey('id', $customers['2-user2']['_source']); - $this->assertArrayHasKey('name', $customers['2-user2']['_source']); - $this->assertArrayHasKey('email', $customers['2-user2']['_source']); - $this->assertArrayHasKey('address', $customers['2-user2']['_source']); - $this->assertArrayHasKey('status', $customers['2-user2']['_source']); - $this->assertArrayHasKey('id', $customers['3-user3']['_source']); - $this->assertArrayHasKey('name', $customers['3-user3']['_source']); - $this->assertArrayHasKey('email', $customers['3-user3']['_source']); - $this->assertArrayHasKey('address', $customers['3-user3']['_source']); - $this->assertArrayHasKey('status', $customers['3-user3']['_source']); - } - - public function testAfterFindGet() - { - /* @var $customerClass BaseActiveRecord */ - $customerClass = $this->getCustomerClass(); - - $afterFindCalls = []; - Event::on(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND, function ($event) use (&$afterFindCalls) { - /* @var $ar BaseActiveRecord */ - $ar = $event->sender; - $afterFindCalls[] = [get_class($ar), $ar->getIsNewRecord(), $ar->getPrimaryKey(), $ar->isRelationPopulated('orders')]; - }); - - $customer = Customer::get(1); - $this->assertNotNull($customer); - $this->assertEquals([[$customerClass, false, 1, false]], $afterFindCalls); - $afterFindCalls = []; - - $customer = Customer::mget([1, 2]); - $this->assertNotNull($customer); - $this->assertEquals([ - [$customerClass, false, 1, false], - [$customerClass, false, 2, false], - ], $afterFindCalls); - $afterFindCalls = []; - - Event::off(BaseActiveRecord::className(), BaseActiveRecord::EVENT_AFTER_FIND); - } - - public function testFindEmptyPkCondition() - { - /* @var $this TestCase|ActiveRecordTestTrait */ - /* @var $orderItemClass \Yiisoft\Db\ActiveRecordInterface */ - $orderItemClass = $this->getOrderItemClass(); - $orderItem = new $orderItemClass(); - $orderItem->setAttributes(['order_id' => 1, 'item_id' => 1, 'quantity' => 1, 'subtotal' => 30.0], false); - $orderItem->save(false); - $this->afterSave(); - - $orderItems = $orderItemClass::find()->where(['_id' => [$orderItem->getPrimaryKey()]])->all(); - $this->assertCount(1, $orderItems); - - $orderItems = $orderItemClass::find()->where(['_id' => []])->all(); - $this->assertCount(0, $orderItems); - - $orderItems = $orderItemClass::find()->where(['_id' => null])->all(); - $this->assertCount(0, $orderItems); - - $orderItems = $orderItemClass::find()->where(['IN', '_id', [$orderItem->getPrimaryKey()]])->all(); - $this->assertCount(1, $orderItems); - - $orderItems = $orderItemClass::find()->where(['IN', '_id', []])->all(); - $this->assertCount(0, $orderItems); - - $orderItems = $orderItemClass::find()->where(['IN', '_id', [null]])->all(); - $this->assertCount(0, $orderItems); - } - - public function testArrayAttributes() - { - $this->assertIsArray(Order::findOne(1)->itemsArray); - $this->assertIsArray(Order::findOne(2)->itemsArray); - $this->assertIsArray(Order::findOne(3)->itemsArray); - } - - public function testArrayAttributeRelationLazy() - { - $order = Order::findOne(1); - $items = $order->itemsByArrayValue; - $this->assertCount(2, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - $this->assertTrue($items[1] instanceof Item); - $this->assertTrue($items[2] instanceof Item); - - $order = Order::findOne(2); - $items = $order->itemsByArrayValue; - $this->assertCount(3, $items); - $this->assertTrue(isset($items[3])); - $this->assertTrue(isset($items[4])); - $this->assertTrue(isset($items[5])); - $this->assertTrue($items[3] instanceof Item); - $this->assertTrue($items[4] instanceof Item); - $this->assertTrue($items[5] instanceof Item); - } - - public function testArrayAttributeRelationEager() - { - /* @var $order Order */ - $order = Order::find()->with('itemsByArrayValue')->where(['id' => 1])->one(); - $this->assertTrue($order->isRelationPopulated('itemsByArrayValue')); - $items = $order->itemsByArrayValue; - $this->assertCount(2, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - $this->assertTrue($items[1] instanceof Item); - $this->assertTrue($items[2] instanceof Item); - - /* @var $order Order */ - $order = Order::find()->with('itemsByArrayValue')->where(['id' => 2])->one(); - $this->assertTrue($order->isRelationPopulated('itemsByArrayValue')); - $items = $order->itemsByArrayValue; - $this->assertCount(3, $items); - $this->assertTrue(isset($items[3])); - $this->assertTrue(isset($items[4])); - $this->assertTrue(isset($items[5])); - $this->assertTrue($items[3] instanceof Item); - $this->assertTrue($items[4] instanceof Item); - $this->assertTrue($items[5] instanceof Item); - } - - public function testArrayAttributeRelationLink() - { - /* @var $order Order */ - $order = Order::find()->where(['id' => 1])->one(); - $items = $order->itemsByArrayValue; - $this->assertCount(2, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - - $item = Item::get(5); - $order->link('itemsByArrayValue', $item); - $this->afterSave(); - - $items = $order->itemsByArrayValue; - $this->assertCount(3, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - $this->assertTrue(isset($items[5])); - - // check also after refresh - $this->assertTrue($order->refresh()); - $items = $order->itemsByArrayValue; - $this->assertCount(3, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - $this->assertTrue(isset($items[5])); - } - - public function testArrayAttributeRelationUnLink() - { - /* @var $order Order */ - $order = Order::find()->where(['id' => 1])->one(); - $items = $order->itemsByArrayValue; - $this->assertCount(2, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - - $item = Item::get(2); - $order->unlink('itemsByArrayValue', $item); - $this->afterSave(); - - $items = $order->itemsByArrayValue; - $this->assertCount(1, $items); - $this->assertTrue(isset($items[1])); - $this->assertFalse(isset($items[2])); - - // check also after refresh - $this->assertTrue($order->refresh()); - $items = $order->itemsByArrayValue; - $this->assertCount(1, $items); - $this->assertTrue(isset($items[1])); - $this->assertFalse(isset($items[2])); - } - - /** - * https://github.com/yiisoft/yii2/issues/6065 - */ - public function testArrayAttributeRelationUnLinkBrokenArray() - { - /* @var $order Order */ - $order = Order::find()->where(['id' => 1])->one(); - - $itemIds = $order->itemsArray; - $removeId = reset($itemIds); - $item = Item::get($removeId); - $order->unlink('itemsByArrayValue', $item); - $this->afterSave(); - - $items = $order->itemsByArrayValue; - $this->assertCount(1, $items); - $this->assertFalse(isset($items[$removeId])); - - // check also after refresh - $this->assertTrue($order->refresh()); - $items = $order->itemsByArrayValue; - $this->assertCount(1, $items); - $this->assertFalse(isset($items[$removeId])); - } - - public function testArrayAttributeRelationUnLinkAll() - { - $this->expectException(\yii\base\NotSupportedException::class); - - /* @var $order Order */ - $order = Order::find()->where(['id' => 1])->one(); - $items = $order->itemsByArrayValue; - $this->assertCount(2, $items); - $this->assertTrue(isset($items[1])); - $this->assertTrue(isset($items[2])); - - $order->unlinkAll('itemsByArrayValue'); - $this->afterSave(); - - $items = $order->itemsByArrayValue; - $this->assertCount(0, $items); - - // check also after refresh - $this->assertTrue($order->refresh()); - $items = $order->itemsByArrayValue; - $this->assertCount(0, $items); - } - - public function testUnlinkAll() - { - // not supported by elasticsearch - } - - public function testUnlinkAllAndConditionSetNull() - { - $this->expectException(\yii\base\NotSupportedException::class); - - /* @var $customerClass \Yiisoft\Db\BaseActiveRecord */ - $customerClass = $this->getCustomerClass(); - /* @var $orderClass \Yiisoft\Db\BaseActiveRecord */ - $orderClass = $this->getOrderWithNullFKClass(); - - // in this test all orders are owned by customer 1 - $orderClass::updateAll(['customer_id' => 1]); - $this->afterSave(); - - $customer = $customerClass::findOne(1); - $this->assertCount(3, $customer->ordersWithNullFK); - $this->assertCount(1, $customer->expensiveOrdersWithNullFK); - $this->assertEquals(3, $orderClass::find()->count()); - $customer->unlinkAll('expensiveOrdersWithNullFK'); - } - - public function testUnlinkAllAndConditionDelete() - { - $this->expectException(\yii\base\NotSupportedException::class); - - /* @var $customerClass \Yiisoft\Db\BaseActiveRecord */ - $customerClass = $this->getCustomerClass(); - /* @var $orderClass \Yiisoft\Db\BaseActiveRecord */ - $orderClass = $this->getOrderWithNullFKClass(); - - // in this test all orders are owned by customer 1 - $orderClass::updateAll(['customer_id' => 1]); - $this->afterSave(); - - $customer = $customerClass::findOne(1); - $this->assertCount(3, $customer->ordersWithNullFK); - $this->assertCount(1, $customer->expensiveOrdersWithNullFK); - $this->assertEquals(3, $orderClass::find()->count()); - $customer->unlinkAll('expensiveOrdersWithNullFK', true); - } - - public function testPopulateRecordCallWhenQueryingOnParentClass() - { - $animal = Animal::find()->where(['type' => Dog::className()])->one(); - $this->assertEquals('bark', $animal->getDoes()); - - $animal = Animal::find()->where(['type' => Cat::className()])->one(); - $this->assertEquals('meow', $animal->getDoes()); - } - - public function testAttributeAccess() - { - /* @var $customerClass \Yiisoft\Db\ActiveRecordInterface */ - $customerClass = $this->getCustomerClass(); - $model = new $customerClass(); - - $this->assertTrue($model->canSetProperty('name')); - $this->assertTrue($model->canGetProperty('name')); - $this->assertFalse($model->canSetProperty('unExistingColumn')); - $this->assertFalse(isset($model->name)); - - $model->name = 'foo'; - $this->assertTrue(isset($model->name)); - unset($model->name); - $this->assertNull($model->name); - - // @see https://github.com/yiisoft/yii2-gii/issues/190 - $baseModel = new $customerClass(); - $this->assertFalse($baseModel->hasProperty('unExistingColumn')); - - - /* @var $customer ActiveRecord */ - $customer = new $customerClass(); - $this->assertInstanceOf($customerClass, $customer); - - $this->assertTrue($customer->canGetProperty('id')); - $this->assertTrue($customer->canSetProperty('id')); - - // tests that we really can get and set this property - $this->assertNull($customer->id); - $customer->id = 10; - $this->assertNotNull($customer->id); - - $this->assertFalse($customer->canGetProperty('non_existing_property')); - $this->assertFalse($customer->canSetProperty('non_existing_property')); - } - - public function testBooleanAttribute() - { - } - - // TODO test AR with not mapped PK - - public function illegalValuesForFindByCondition() - { - return [ - [['id' => ['`id`=`id` and 1' => 1]], ['id' => 1]], - [['id' => [ - 'legal' => 1, - '`id`=`id` and 1' => 1, - ]], ['id' => 1]], - [['id' => [ - 'nested_illegal' => [ - 'false or 1=' => 1, - ], - ]], null], - - [['id' => [ - 'or', - '1=1', - 'id' => 'id', - ]], null], - [['id' => [ - 'or', - '1=1', - 'id' => '1', - ]], ['id' => 1]], - [['id' => [ - 'name' => 'Cars', - 'email' => 'test@example.com', - ]], ['id' => 1]], - ]; - } - - /** - * @dataProvider illegalValuesForFindByCondition - */ - public function testValueEscapingInFindByCondition($filterWithInjection, $expectedResult) - { - /* @var $itemClass \Yiisoft\Db\ActiveRecordInterface */ - $itemClass = $this->getItemClass(); - - $result = $itemClass::findOne($filterWithInjection['id']); - if ($expectedResult === null) { - $this->assertNull($result); - } else { - $this->assertNotNull($result); - foreach ($expectedResult as $col => $value) { - $this->assertEquals($value, $result->$col); - } - } - } -} diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 676223b5..3d1d9dd7 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -2,40 +2,1175 @@ declare(strict_types=1); -namespace Yiisoft\Db\ElasticSearch\Tests; +namespace Yiisoft\Elasticsearch\Tests; -use Yiisoft\Db\ElasticSearch\Connection; +use PHPUnit\Framework\TestCase; +use Yiisoft\Elasticsearch\Tests\Support\TestTrait; -/** - * @group elasticsearch - */ -class CommandTest extends TestCase +final class CommandTest extends TestCase { + use TestTrait; + + public function testAddAliasAliasExistingIndexReturnsTrue(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $actualResult = $command->addAlias($index, $testAlias); + $command->deleteIndex($index); + + $this->assertTrue($actualResult); + + $db->close(); + } + + public function testAddAliasAliasNonExistingIndexReturnsFalse(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + $actualResult = $command->addAlias($index, $testAlias); + + $this->assertFalse($actualResult); + + $db->close(); + } + + public function testAliasActionsMakingOperationOverExistingIndexReturnsTrue(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + + $actualResult = $command->aliasActions([ + ['add' => ['index' => $index, 'alias' => $testAlias]], + ['remove' => ['index' => $index, 'alias' => $testAlias]], + ]); + + $command->deleteIndex($index); + + $this->assertTrue($actualResult); + + $db->close(); + } + + public function testAliasActionsMakingOperationOverNonExistingIndexReturnsFalse(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + $actualResult = $command->aliasActions([ + ['add' => ['index' => $index, 'alias' => $testAlias]], + ['remove' => ['index' => $index, 'alias' => $testAlias]], + ]); + + $this->assertFalse($actualResult); + + $db->close(); + } + + public function testAliasExistsAliasesAreSetButWithDifferentNamereturnsFalse(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test'; + $fooAlias1 = 'alias'; + $fooAlias2 = 'alias2'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $fooAlias1); + $command->addAlias($index, $fooAlias2); + $aliasExists = $command->aliasExists($testAlias); + $command->deleteIndex($index); + + $this->assertFalse($aliasExists); + + $db->close(); + } + + public function testAliasExistsAliasIsSetWithSameNameReturnsTrue(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias); + $aliasExists = $command->aliasExists($testAlias); + $command->deleteIndex($index); + + $this->assertTrue($aliasExists); + + $db->close(); + } + + public function testAliasExistsNoAliasesSetReturnsFalse(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $testAlias = 'test'; + $aliasExists = $command->aliasExists($testAlias); + + $this->assertFalse($aliasExists); + + $db->close(); + } + + public function testClearIndexCache(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'cache_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + + $this->assertSame( + ['_shards' => ['total' => 2, 'successful' => 1, 'failed' => 0]], + $command->clearIndexCache($index), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testClearScroll(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'scroll_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $scroll = $command->search('_all', options: ['scroll' => '1m']); + + $this->assertSame( + ['succeeded' => true, 'num_freed' => 5], + $command->clearScroll(['scroll_id' => $scroll['_scroll_id']]), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testCloseIndex(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'close_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + + $this->assertSame( + [ + 'acknowledged' => true, + 'shards_acknowledged' => true, + 'indices' => [ + 'close_test' => [ + 'closed' => true, + ], + ], + ], + $command->closeIndex($index), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testCreateIndexTemplate(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $template = 'test_template'; + + $this->assertSame( + ['acknowledged' => true], + $command->createIndexTemplate( + $template, + ['t*'], + ['number_of_shards' => 1, 'number_of_replicas' => 0], + ['_source' => ['enabled' => false]], + options: ['priority' => 1] + ), + ); + + $command->deleteIndexTemplate($template); + + $db->close(); + } + + public function testDelete(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'delete_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertSame( + [ + '_index' => 'delete_test', + '_id' => '2', + '_version' => 2, + 'result' => 'deleted', + '_shards' => [ + 'total' => 2, + 'successful' => 1, + 'failed' => 0, + ], + '_seq_no' => 2, + '_primary_term' => 1, + ], + $command->delete($index, '2'), + ); + + $this->assertFalse($command->exists($index, '2')); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testExist(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'exist_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertTrue($command->exists($index, '1')); + $this->assertFalse($command->exists($index, '5')); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testFlushIndex(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'flush_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertSame( + ['_shards' => ['total' => 2, 'successful' => 1, 'failed' => 0]], + $command->flushIndex($index), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testGet(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'get_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertSame( + [ + '_index' => 'get_test', + '_id' => '1', + '_version' => 1, + '_seq_no' => 0, + '_primary_term' => 1, + 'found' => true, + '_source' => [ + 'name' => 'John Doe', + ], + ], + $command->get($index, '1'), + ); + $this->assertSame( + [ + '_index' => 'get_test', + '_id' => '2', + '_version' => 1, + '_seq_no' => 1, + '_primary_term' => 1, + 'found' => true, + '_source' => [ + 'name' => 'Jane Doe', + ], + ], + $command->get($index, '2'), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testGetAliasInfoNoAliasSetReturnsEmptyArray(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $expectedResult = []; + $actualResult = $command->getAliasInfo(); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + /** - * @var Connection + * @dataProvider \Yiisoft\Elasticsearch\Tests\Provider\CommandProvider::dataForGetAliasInfo */ - private $connection; + public function testGetAliasInfoSingleAliasIsSetReturnsInfoForAlias( + string $index, + string $type, + array $mapping, + string $alias, + array $expectedResult, + array $aliasParameters + ): void { + $db = $this->getConnection(); + $command = $db->createCommand(); + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + + if ($mapping) { + $command->setMapping($index, $mapping, $type); + } + + $command->addAlias($index, $alias, $aliasParameters); + $actualResult = $command->getAliasInfo(); + $command->deleteIndex($index); + + // order is not guaranteed + sort($expectedResult); + sort($actualResult); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexAliasesMultipleAliasesAreSetReturnsDataForThoseAliases(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias1 = 'test_alias1'; + $testAlias2 = 'test_alias2'; + $expectedResult = [ + $testAlias1 => [], + $testAlias2 => [], + ]; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias1); + $command->addAlias($index, $testAlias2); + $actualResult = $command->getIndexAliases($index); + $command->deleteIndex($index); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexAliasesNoAliasesSetReturnsEmptyArray(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $expectedResult = []; + + $actualResult = $command->getIndexAliases($index); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexInfoByAliasNoAliasesSetReturnsEmptyArray(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $testAlias = 'test'; + $expectedResult = []; + + $actualResult = $command->getIndexInfoByAlias($testAlias); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexInfoByAliasOneIndexIsSetToAliasReturnsDataForThatIndex(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test'; + $expectedResult = [ + $index => [ + 'aliases' => [ + $testAlias => [], + ], + ], + ]; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias); + $actualResult = $command->getIndexInfoByAlias($testAlias); + $command->deleteIndex($index); + + $this->assertEquals($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexInfoByAliasTwoIndexesAreSetToSameAliasReturnsDataForBothIndexes(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index1 = 'alias_test1'; + $index2 = 'alias_test2'; + $testAlias = 'test'; + $expectedResult = [ + $index1 => [ + 'aliases' => [ + $testAlias => [], + ], + ], + $index2 => [ + 'aliases' => [ + $testAlias => [], + ], + ], + ]; + + if ($command->indexExists($index1)) { + $command->deleteIndex($index1); + } + + if ($command->indexExists($index2)) { + $command->deleteIndex($index2); + } + + $command->createIndex($index1); + $command->createIndex($index2); + $command->addAlias($index1, $testAlias); + $command->addAlias($index2, $testAlias); + $actualResult = $command->getIndexInfoByAlias($testAlias); + $command->deleteIndex($index1); + $command->deleteIndex($index2); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexesByAliasNoAliasesSetReturnsEmptyArray(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $expectedResult = []; + $testAlias = 'test'; + + $actualResult = $command->getIndexesByAlias($testAlias); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexesByAliasOneIndexIsSetToAliasReturnsArrayWithNameOfThatIndex(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test'; + $expectedResult = [$index]; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias); + $actualResult = $command->getIndexesByAlias($testAlias); + $command->deleteIndex($index); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexesByAliasTwoIndexesAreSetToSameAliasReturnsArrayWithNamesForBothIndexes(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index1 = 'alias_test1'; + $index2 = 'alias_test2'; + $testAlias = 'test'; + $expectedResult = [ + $index1, + $index2, + ]; + + if ($command->indexExists($index1)) { + $command->deleteIndex($index1); + } + + if ($command->indexExists($index2)) { + $command->deleteIndex($index2); + } + + $command->createIndex($index1); + $command->createIndex($index2); + $command->addAlias($index1, $testAlias); + $command->addAlias($index2, $testAlias); + $actualResult = $command->getIndexesByAlias($testAlias); + $command->deleteIndex($index1); + $command->deleteIndex($index2); + + // order is not guaranteed + sort($expectedResult); + sort($actualResult); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexAliasesSingleAliasIsSetReturnsDataForThatAlias(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + $expectedResult = [ + $testAlias => [], + ]; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias); + $actualResult = $command->getIndexAliases($index); + $command->deleteIndex($index); + + $this->assertSame($expectedResult, $actualResult); + + $db->close(); + } + + public function testGetIndexRecoveryStats(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $this->assertSame( + [ + 'command-test' => [ + 'shards' => [ + 0 => [ + 'id' => 0, + 'type' => 'EXISTING_STORE', + 'stage' => 'DONE', + 'primary' => true, + 'start_time_in_millis' => 1681650957903, + 'stop_time_in_millis' => 1681650957924, + 'total_time_in_millis' => 20, + 'source' => [ + 'bootstrap_new_history_uuid' => false, + ], + 'target' => [ + 'id' => 'EQWC5q1-Qkm28v04QVZqlQ', + 'host' => '127.0.0.1', + 'transport_address' => '127.0.0.1:9300', + 'ip' => '127.0.0.1', + 'name' => '32a7c3b4e828', + ], + 'index' => [ + 'size' => [ + 'total_in_bytes' => 225, + 'reused_in_bytes' => 225, + 'recovered_in_bytes' => 0, + 'recovered_from_snapshot_in_bytes' => 0, + 'percent' => '100.0%', + ], + 'files' => [ + 'total' => 1, + 'reused' => 1, + 'recovered' => 0, + 'percent' => '100.0%', + ], + 'total_time_in_millis' => 0, + 'source_throttle_time_in_millis' => 0, + 'target_throttle_time_in_millis' => 0, + ], + 'translog' => [ + 'recovered' => 0, + 'total' => 0, + 'percent' => '100.0%', + 'total_on_start' => 0, + 'total_time_in_millis' => 13, + ], + 'verify_index' => [ + 'check_index_time_in_millis' => 0, + 'total_time_in_millis' => 0, + ], + ], + ], + ], + ], + $command->getIndexRecoveryStats('command-test'), + ); + } - protected function setUp() + public function testGetMapping(): void { - parent::setUp(); - $this->connection = $this->getConnection(); + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'mapping_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->setMapping( + $index, + [ + 'properties' => [ + 'name' => [ + 'type' => 'text', + ], + ], + ], + ); + + $this->assertSame( + [ + 'mapping_test' => [ + 'mappings' => [ + 'properties' => [ + 'name' => [ + 'type' => 'text', + ], + ], + ], + ], + ], + $command->getMapping($index), + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testGetSource(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'source_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertSame([ + 'name' => 'John Doe', + ], $command->getSource($index, '1')); + + $this->assertSame([ + 'name' => 'Jane Doe', + ], $command->getSource($index, '2')); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testGetTemplate(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $template = 'template_test_get'; + + $command->createIndexTemplate( + $template, + ['template_*'], + ['number_of_shards' => 1, 'number_of_replicas' => 0], + ['_source' => ['enabled' => false]], + options: ['priority' => 2] + ); + + $this->assertSame( + [ + 'index_templates' => [ + 0 => [ + 'name' => 'template_test_get', + 'index_template' => [ + 'index_patterns' => [ + 0 => 'template_*', + ], + 'template' => [ + 'settings' => [ + 'index' => [ + 'number_of_shards' => '1', + 'number_of_replicas' => '0', + ], + ], + 'mappings' => [ + '_source' => [ + 'enabled' => false, + ], + ], + 'aliases' => [], + ], + 'composed_of' => [], + 'priority' => 2, + ], + ], + ], + ], + $command->getIndexTemplate($template), + ); + + $command->deleteIndexTemplate($template); + + $db->close(); + } + + public function testInsert(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'insert_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $this->assertTrue($command->exists($index, '1')); + $this->assertTrue($command->exists($index, '2')); + + $command->deleteIndex($index); + + $db->close(); } - public function testIndexStats() + public function testIndexStats(): void { - $cmd = $this->connection->createCommand(); - if (!$cmd->indexExists('yii2test2')) { - $cmd->createIndex('yii2test2'); + $db = $this->getConnection(); + $cmd = $db->createCommand(); + + if (!$cmd->indexExists('command-test')) { + $cmd->createIndex('command-test'); } $stats = $cmd->getIndexStats(); $this->assertArrayHasKey('_all', $stats, print_r(array_keys($stats), true)); $this->assertArrayHasKey('indices', $stats, print_r(array_keys($stats), true)); - $this->assertArrayHasKey('yii2test2', $stats['indices'], print_r(array_keys($stats['indices']), true)); + $this->assertArrayHasKey('command-test', $stats['indices'], print_r(array_keys($stats['indices']), true)); - $stats = $cmd->getIndexStats('yii2test2'); + $stats = $cmd->getIndexStats('command-test'); $this->assertArrayHasKey('_all', $stats, print_r(array_keys($stats), true)); $this->assertArrayHasKey('indices', $stats, print_r(array_keys($stats), true)); - $this->assertArrayHasKey('yii2test2', $stats['indices'], print_r(array_keys($stats['indices']), true)); + $this->assertArrayHasKey('command-test', $stats['indices'], print_r(array_keys($stats['indices']), true)); + + $db->close(); + } + + public function testMultipleGets(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'multiple_get_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $result = $command->mget($index, ['1', '2']); + + $this->assertCount(2, $result['docs']); + $this->assertSame( + [ + 'docs' => [ + 0 => [ + '_index' => 'multiple_get_test', + '_id' => '1', + '_version' => 1, + '_seq_no' => 0, + '_primary_term' => 1, + 'found' => true, + '_source' => [ + 'name' => 'John Doe', + ], + ], + 1 => [ + '_index' => 'multiple_get_test', + '_id' => '2', + '_version' => 1, + '_seq_no' => 1, + '_primary_term' => 1, + 'found' => true, + '_source' => [ + 'name' => 'Jane Doe', + ], + ], + ], + ], + $result, + ); + $this->assertArrayHasKey('0', $result['docs']); + $this->assertArrayHasKey('1', $result['docs']); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testRemoveAliasNoAliasIsSetForIndexReturnsFalse() + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $actualResult = $command->removeAlias($index, $testAlias); + $command->deleteIndex($index); + + $this->assertFalse($actualResult); + + $db->close(); + } + + public function testRemoveAliasAliasWasSetForIndexReturnsTrue(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'alias_test'; + $testAlias = 'test_alias'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->addAlias($index, $testAlias); + $actualResult = $command->removeAlias($index, $testAlias); + $command->deleteIndex($index); + + $this->assertTrue($actualResult); + + $db->close(); + } + + public function testScroll(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'scroll_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $command->refreshIndex($index); + + $result = $command->search($index, ['query' => ['match_all' => new \stdClass()]], options: ['scroll' => '1m']); + + $this->assertArrayHasKey('_scroll_id', $result); + + $this->assertArrayHasKey('hits', $result); + $this->assertArrayHasKey('hits', $result['hits']); + $this->assertCount(2, $result['hits']['hits']); + + $result = $command->scroll(['scroll_id' => $result['_scroll_id']]); + + $this->assertArrayHasKey('hits', $result); + $this->assertArrayHasKey('hits', $result['hits']); + $this->assertSame(2, $result['hits']['total']['value']); + + $command->deleteIndex($index); + } + + public function testSuggesters(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'suggest_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $result = $command->suggesters( + $index, + [ + 'my-suggest-1' => [ + 'text' => 'John', + 'term' => [ + 'field' => 'name', + ], + ], + 'my-suggest-2' => [ + 'text' => 'Jane', + 'term' => [ + 'field' => 'name', + ], + ], + ], + ); + + $this->assertSame( + [ + 'my-suggest-1' => [ + 0 => [ + 'text' => 'john', + 'offset' => 0, + 'length' => 4, + 'options' => [], + ], + ], + 'my-suggest-2' => [ + 0 => [ + 'text' => 'jane', + 'offset' => 0, + 'length' => 4, + 'options' => [], + ], + ], + ], + $result, + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testUpdate(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'update_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->insert($index, ['name' => 'John Doe'], '1'); + $command->insert($index, ['name' => 'Jane Doe'], '2'); + + $command->update($index, '1', ['name' => 'John Doe Jr.'], options: ['detect_noop' => true]); + + $this->assertSame(['name' => 'John Doe Jr.'], $command->getSource($index, '1')); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testUpdateAnalizers(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'update_analyzer_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->closeIndex($index); + $command->updateAnalyzers( + $index, + [ + 'analysis' => [ + 'analyzer' => [ + 'content' => [ + 'type' => 'custom', + 'tokenizer' => 'whitespace', + ], + ], + ], + ], + ); + $command->openIndex($index); + + $this->assertSame( + ['content' => ['type' => 'custom', 'tokenizer' => 'whitespace']], + $command->getSettings($index)[$index]['settings']['index']['analysis']['analyzer'], + ); + + $command->deleteIndex($index); + + $db->close(); + } + + public function testUpdateSettings(): void + { + $db = $this->getConnection(); + $command = $db->createCommand(); + + $index = 'update_settings_test'; + + if ($command->indexExists($index)) { + $command->deleteIndex($index); + } + + $command->createIndex($index); + $command->updateSettings($index, ['index' => ['number_of_replicas' => 4]]); + + $this->assertSame('4', $command->getSettings($index)[$index]['settings']['index']['number_of_replicas']); + + $command->deleteIndex($index); + + $db->close(); } } diff --git a/tests/ConnectionTest.php b/tests/ConnectionTest.php index 7ad28cf6..70be0a89 100644 --- a/tests/ConnectionTest.php +++ b/tests/ConnectionTest.php @@ -2,53 +2,138 @@ declare(strict_types=1); -namespace Yiisoft\Db\ElasticSearch\Tests; +namespace Yiisoft\Elasticsearch\Tests; -use Yiisoft\Db\ElasticSearch\Connection; +use PHPUnit\Framework\TestCase; +use Yiisoft\Elasticsearch\Connection; +use Yiisoft\Elasticsearch\Tests\Support\Assert; +use Yiisoft\Elasticsearch\Tests\Support\TestTrait; -/** - * @group elasticsearch - */ -class ConnectionTest extends TestCase +final class ConnectionTest extends TestCase { - /** - * @var Connection - */ - private $connection; + use TestTrait; - protected function setUp() + public function testAuth(): void { - parent::setUp(); - $this->connection = $this->getConnection(); + $db = $this->getConnection(); + + $db->auth(['user', 'password']); + + $this->assertSame(['user', 'password'], Assert::inaccessibleProperty($db, 'auth')); } - public function testCreateUrl() + public function testCreateUrl(): void { - $reflectedMethod = new \ReflectionMethod($this->connection, 'createUrl'); - $reflectedMethod->setAccessible(true); + $db = $this->getConnection(); - $protocol = $this->connection->nodes[$this->connection->activeNode]['protocol']; - $httpAddress = $this->connection->nodes[$this->connection->activeNode]['http_address']; - $this->assertEquals([$protocol, $httpAddress, ''], $reflectedMethod->invoke($this->connection, [])); + $protocol = 'http'; + $httpAddress = '127.0.0.1:9200'; + $db->addNodeValue('protocol', $protocol)->addNodeValue('httpAddress', $httpAddress); + + $this->assertSame('http', $db->getNodeValue('protocol')); + $this->assertSame('127.0.0.1:9200', $db->getNodeValue('httpAddress')); $this->assertEquals( [$protocol, $httpAddress, '_cat/indices'], - $reflectedMethod->invoke($this->connection, '_cat/indices') + Assert::invokeMethod($db, 'createUrl', ['_cat/indices']), ); - $this->assertEquals( [$protocol, $httpAddress, 'customer'], - $reflectedMethod->invoke($this->connection, 'customer') + Assert::invokeMethod($db, 'createUrl', ['customer']), ); - $this->assertEquals( [$protocol, $httpAddress, 'customer/external/1'], - $reflectedMethod->invoke($this->connection, ['customer', 'external', '1']) + Assert::invokeMethod($db, 'createUrl', [['customer', 'external', '1']]) ); - $this->assertEquals( [$protocol, $httpAddress, 'customer/external/1/_update'], - $reflectedMethod->invoke($this->connection, ['customer', 'external', 1, '_update',]) + Assert::invokeMethod($db, 'createUrl', [['customer', 'external', 1, '_update']]) ); + + $db->close(); + } + + public function testCurlOptions(): void + { + $db = $this->getConnection(); + + $db->curlOptions([CURLOPT_TIMEOUT => 10]); + + $this->assertSame(10, Assert::inaccessibleProperty($db, 'curlOptions')[CURLOPT_TIMEOUT]); + } + + public function testDataTimeout(): void + { + $db = $this->getConnection(); + + $db->dataTimeout(10); + + $this->assertSame(10.0, Assert::inaccessibleProperty($db, 'dataTimeout')); + } + + public function testDefaultProtocol(): void + { + $db = $this->getConnection(); + + $db->defaultProtocol('https'); + + $this->assertSame('https', Assert::inaccessibleProperty($db, 'defaultProtocol')); + } + + public function testDslVersion(): void + { + $db = $this->getConnection(); + + $db->dslVersion(7); + + $this->assertSame(7, Assert::inaccessibleProperty($db, 'dslVersion')); + } + + public function testGetClusterState(): void + { + $db = $this->getConnection(); + + $this->assertIsArray($db->getClusterState()); + } + + public function testGetDriverName(): void + { + $db = $this->getConnection(); + + $this->assertSame('elasticsearch', $db->getDriverName()); + } + + public function testGetDslVersion(): void + { + $db = $this->getConnection(); + + $this->assertSame(8, $db->getDslVersion()); + } + + public function testGetNodeInfo(): void + { + $db = $this->getConnection(); + + $this->assertIsArray($db->getNodeInfo()); + } + + public function testSerialized() + { + $db = $this->getConnection(); + + $db->open(); + $serialized = serialize($db); + $unserialized = unserialize($serialized); + + $this->assertInstanceOf(Connection::class, $unserialized); + } + + public function testTimeOut(): void + { + $db = $this->getConnection(); + + $db->timeOut(10); + + $this->assertSame(10.0, Assert::inaccessibleProperty($db, 'timeOut')); } } diff --git a/tests/Data/ActiveRecord/ActiveRecord.php b/tests/Data/ActiveRecord/ActiveRecord.php deleted file mode 100644 index 7ebd9e09..00000000 --- a/tests/Data/ActiveRecord/ActiveRecord.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @since 2.0 - */ -class ActiveRecord extends \Yiisoft\Db\ElasticSearch\ActiveRecord -{ - public static $db; - - /** - * @return \Yiisoft\Db\ElasticSearch\Connection - */ - public static function getDb() - { - return self::$db; - } - - public static function index() - { - return 'yiitest'; - } -} diff --git a/tests/Data/ActiveRecord/Animal.php b/tests/Data/ActiveRecord/Animal.php deleted file mode 100644 index b156a1e5..00000000 --- a/tests/Data/ActiveRecord/Animal.php +++ /dev/null @@ -1,73 +0,0 @@ - - * @since 2.0 - */ -class Animal extends ActiveRecord -{ - public $does; - - public static function primaryKey() - { - return ['id']; - } - - public static function type() - { - return 'test_animals'; - } - - public function attributes() - { - return ['id', 'type']; - } - - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - 'properties' => [ - 'type' => ['type' => 'string', 'index' => 'not_analyzed'], - ], - ], - ]); - } - - public function init() - { - parent::init(); - $this->type = static::class; - } - - public function getDoes() - { - return $this->does; - } - - /** - * @param type $row - * @return \yiiunit\data\ar\elasticsearch\Animal - */ - public static function instantiate($row) - { - $class = $row['_source']['type']; - return new $class(); - } -} diff --git a/tests/Data/ActiveRecord/Cat.php b/tests/Data/ActiveRecord/Cat.php deleted file mode 100644 index 1466a32f..00000000 --- a/tests/Data/ActiveRecord/Cat.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @since 2.0 - */ -class Cat extends Animal -{ - /** - * @param self $record - * @param array $row - */ - public static function populateRecord($record, $row) - { - parent::populateRecord($record, $row); - - $record->does = 'meow'; - } -} diff --git a/tests/Data/ActiveRecord/Customer.php b/tests/Data/ActiveRecord/Customer.php deleted file mode 100644 index f415f91f..00000000 --- a/tests/Data/ActiveRecord/Customer.php +++ /dev/null @@ -1,98 +0,0 @@ -hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('created_at'); - } - - public function getExpensiveOrders() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id']) - ->where([ 'gte', 'total', 50 ]) - ->orderBy('id'); - } - - public function getExpensiveOrdersWithNullFK() - { - return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id']) - ->where([ 'gte', 'total', 50 ]) - ->orderBy('id'); - } - - public function getOrdersWithNullFK() - { - return $this->hasMany(OrderWithNullFK::className(), ['customer_id' => 'id'])->orderBy('created_at'); - } - - public function getOrdersWithItems() - { - return $this->hasMany(Order::className(), ['customer_id' => 'id'])->with('orderItems'); - } - - public function afterSave($insert, $changedAttributes) - { - ActiveRecordTest::$afterSaveInsert = $insert; - ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; - parent::afterSave($insert, $changedAttributes); - } - - /** - * sets up the index for this record - * @param Command $command - * @param bool $statusIsBoolean - */ - public static function setUpMapping($command) - { - $command->setMapping(static::index(), static::type(), [ - 'properties' => [ - 'id' => ['type' => 'integer', 'store' => true], - 'name' => ['type' => 'keyword', 'index' => 'not_analyzed', 'store' => true], - 'email' => ['type' => 'keyword', 'index' => 'not_analyzed', 'store' => true], - 'address' => ['type' => 'text', 'index' => 'analyzed'], - 'status' => ['type' => 'integer', 'store' => true], - ], - ]); - } - - /** - * @inheritdoc - * @return CustomerQuery - */ - public static function find() - { - return new CustomerQuery(static::class); - } -} diff --git a/tests/Data/ActiveRecord/CustomerQuery.php b/tests/Data/ActiveRecord/CustomerQuery.php deleted file mode 100644 index bb3ece9b..00000000 --- a/tests/Data/ActiveRecord/CustomerQuery.php +++ /dev/null @@ -1,20 +0,0 @@ -andWhere(['status' => 1]); - - return $this; - } -} diff --git a/tests/Data/ActiveRecord/Dog.php b/tests/Data/ActiveRecord/Dog.php deleted file mode 100644 index 5dd21fa9..00000000 --- a/tests/Data/ActiveRecord/Dog.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @since 2.0 - */ -class Dog extends Animal -{ - /** - * @param self $record - * @param array $row - */ - public static function populateRecord($record, $row) - { - parent::populateRecord($record, $row); - - $record->does = 'bark'; - } -} diff --git a/tests/Data/ActiveRecord/Item.php b/tests/Data/ActiveRecord/Item.php deleted file mode 100644 index 7d2229ea..00000000 --- a/tests/Data/ActiveRecord/Item.php +++ /dev/null @@ -1,43 +0,0 @@ -setMapping(static::index(), static::type(), [ - static::type() => [ - 'properties' => [ - 'name' => ['type' => 'keyword', 'index' => 'not_analyzed', 'store' => true], - 'category_id' => ['type' => 'integer'], - ], - ], - ]); - } -} diff --git a/tests/Data/ActiveRecord/Order.php b/tests/Data/ActiveRecord/Order.php deleted file mode 100644 index bec55374..00000000 --- a/tests/Data/ActiveRecord/Order.php +++ /dev/null @@ -1,125 +0,0 @@ -hasOne(Customer::className(), ['id' => 'customer_id']); - } - - public function getOrderItems() - { - return $this->hasMany(OrderItem::className(), ['order_id' => 'id']); - } - - /** - * A relation to Item defined via array valued attribute - */ - public function getItemsByArrayValue() - { - return $this->hasMany(Item::className(), ['id' => 'itemsArray'])->indexBy('id'); - } - - public function getItems() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems')->orderBy('id'); - } - - public function getItemsIndexed() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems')->indexBy('id'); - } - - public function getItemsWithNullFK() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItemsWithNullFK'); - } - - public function getOrderItemsWithNullFK() - { - return $this->hasMany(OrderItemWithNullFK::className(), ['order_id' => 'id']); - } - - public function getItemsInOrder1() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_ASC]); - })->orderBy('name'); - } - - public function getItemsInOrder2() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems', function ($q) { - $q->orderBy(['subtotal' => SORT_DESC]); - })->orderBy('name'); - } - - public function getBooks() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItems') - ->where(['category_id' => 1]); - } - - public function getBooksWithNullFK() - { - return $this->hasMany(Item::className(), ['id' => 'item_id']) - ->via('orderItemsWithNullFK') - ->where(['category_id' => 1]); - } - - public function beforeSave($insert) - { - return (bool) parent::beforeSave($insert) - // $this->created_at = time(); - - - ; - } - - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - 'properties' => [ - 'customer_id' => ['type' => 'integer'], - // "created_at" => ["type" => "string", "index" => "not_analyzed"], - 'total' => ['type' => 'integer'], - ], - ], - ]); - } -} diff --git a/tests/Data/ActiveRecord/OrderItem.php b/tests/Data/ActiveRecord/OrderItem.php deleted file mode 100644 index 182331c3..00000000 --- a/tests/Data/ActiveRecord/OrderItem.php +++ /dev/null @@ -1,53 +0,0 @@ -hasOne(Order::className(), ['id' => 'order_id']); - } - - public function getItem() - { - return $this->hasOne(Item::className(), ['id' => 'item_id']); - } - - /** - * sets up the index for this record - * @param Command $command - */ - public static function setUpMapping($command) - { - $command->setMapping(static::index(), static::type(), [ - static::type() => [ - 'properties' => [ - 'order_id' => ['type' => 'integer'], - 'item_id' => ['type' => 'integer'], - 'quantity' => ['type' => 'integer'], - 'subtotal' => ['type' => 'integer'], - ], - ], - ]); - } -} diff --git a/tests/Data/ActiveRecord/OrderItemWithNullFK.php b/tests/Data/ActiveRecord/OrderItemWithNullFK.php deleted file mode 100644 index 7b185885..00000000 --- a/tests/Data/ActiveRecord/OrderItemWithNullFK.php +++ /dev/null @@ -1,12 +0,0 @@ - [ - 'autodetectCluster' => false, - 'nodes' => [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ], - ], -]; - -if (is_file(__DIR__ . '/config.local.php')) { - include(__DIR__ . '/config.local.php'); -} - -return $config; diff --git a/tests/ElasticSearchConnectionTest.php b/tests/ElasticSearchConnectionTest.php deleted file mode 100644 index 93ab4b11..00000000 --- a/tests/ElasticSearchConnectionTest.php +++ /dev/null @@ -1,29 +0,0 @@ -autodetectCluster; - $connection->nodes = [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ]; - $this->assertNull($connection->activeNode); - $connection->open(); - $this->assertNotNull($connection->activeNode); - $this->assertArrayHasKey('name', reset($connection->nodes)); -// $this->assertArrayHasKey('hostname', reset($connection->nodes)); - $this->assertArrayHasKey('version', reset($connection->nodes)); - $this->assertArrayHasKey('http_address', reset($connection->nodes)); - } -} diff --git a/tests/ElasticsearchConnectionTest.php b/tests/ElasticsearchConnectionTest.php new file mode 100644 index 00000000..91f19e00 --- /dev/null +++ b/tests/ElasticsearchConnectionTest.php @@ -0,0 +1,30 @@ +addNodeValue('http_address', 'inet[/127.0.0.1:9200]'); + + $this->assertFalse($db->isActive()); + + $db->open(); + + $this->assertTrue($db->isActive()); + $this->assertNotEmpty($db->getNodeValue('name')); + $this->assertSame('127.0.0.1', $db->getNodeValue('host')); + $this->assertSame('8.7.0', $db->getNodeValue('version')); + $this->assertSame('127.0.0.1:9200', $db->getNodeValue('http_address')); + } +} diff --git a/tests/ElasticsearchTargetTest.php b/tests/ElasticsearchTargetTest.php deleted file mode 100644 index 79b40eb7..00000000 --- a/tests/ElasticsearchTargetTest.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ - -namespace Yiisoft\Db\ElasticSearch\Tests; - -use Yiisoft\Db\ElasticSearch\ElasticsearchTarget; -use Yiisoft\Db\ElasticSearch\Query; -use Yiisoft\Log\Dispatcher; -use Yiisoft\Log\Logger; - -class ElasticsearchTargetTest extends TestCase -{ - public $logger; - public $index = 'yiilogtest'; - public $type = 'log'; - - public function testExport() - { - $logger = $this->logger; - - $logger->log('Test message', Logger::LEVEL_INFO, 'test-category'); - $logger->flush(true); - $this->getConnection()->createCommand()->refreshIndex($this->index); - - $query = new Query(); - $query->from($this->index, $this->type); - $message = $query->one($this->getConnection()); - $this->assertArrayHasKey('_source', $message); - - $source = $message['_source']; - $this->assertArrayHasKey('@timestamp', $source); - $this->assertArrayHasKey('message', $source); - $this->assertArrayHasKey('level', $source); - $this->assertArrayHasKey('category', $source); - } - - protected function setUp() - { - parent::setUp(); - - $command = $this->getConnection()->createCommand(); - - // delete index - if ($command->indexExists($this->index)) { - $command->deleteIndex($this->index); - } - - $this->logger = new Logger(); - $dispatcher = new Dispatcher([ - 'logger' => $this->logger, - 'targets' => [ - [ - 'class' => ElasticsearchTarget::className(), - 'db' => $this->getConnection(), - 'index' => $this->index, - 'type' => $this->type, - ], - ], - ]); - } - - protected function tearDown() - { - $command = $this->getConnection()->createCommand(); - $command->deleteIndex($this->index); - - parent::tearDown(); - } -} diff --git a/tests/Provider/CommandProvider.php b/tests/Provider/CommandProvider.php new file mode 100644 index 00000000..96816a26 --- /dev/null +++ b/tests/Provider/CommandProvider.php @@ -0,0 +1,125 @@ + [ + 'term' => [ + 'user' => 'satan', + ], + ], + ]; + $mapping = [ + 'properties' => [ + 'user' => ['type' => 'keyword'], + ], + ]; + $singleRouting = [ + 'routing' => '1', + ]; + $singleExpectedRouting = [ + 'index_routing' => '1', + 'search_routing' => '1', + ]; + $differentRouting = [ + 'index_routing' => '2', + 'search_routing' => '1,2', + ]; + + return [ + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => [], + ], + ], + ], + [], + ], + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => $filter, + ], + ], + ], + $filter, + ], + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => $singleExpectedRouting, + ], + ], + ], + $singleRouting, + ], + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => $differentRouting, + ], + ], + ], + $differentRouting, + ], + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => array_merge($filter, $singleExpectedRouting), + ], + ], + ], + array_merge($filter, $singleRouting), + ], + [ + $index, + $type, + $mapping, + $alias, + [ + $index => [ + 'aliases' => [ + $alias => array_merge($filter, $differentRouting), + ], + ], + ], + array_merge($filter, $differentRouting), + ], + ]; + } +} diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php deleted file mode 100644 index 9ff8bfb7..00000000 --- a/tests/QueryBuilderTest.php +++ /dev/null @@ -1,260 +0,0 @@ -getConnection()->createCommand(); - - // delete index - if ($command->indexExists('yiitest')) { - $command->deleteIndex('yiitest'); - } - - $info = $command->db->get('/'); - $this->version = $info['version']['number']; - - $this->prepareDbData(); - } - - private function prepareDbData() - { - $command = $this->getConnection()->createCommand(); - $command->insert('yiitest', 'article', ['title' => 'I love yii!', 'weight' => 1, 'created_at' => '2010-01-10'], 1); - $command->insert('yiitest', 'article', ['title' => 'Symfony2 is another framework', 'weight' => 2, 'created_at' => '2010-01-15'], 2); - $command->insert('yiitest', 'article', ['title' => 'Yii2 out now!', 'weight' => 3, 'created_at' => '2010-01-20'], 3); - $command->insert('yiitest', 'article', ['title' => 'yii test', 'weight' => 4, 'created_at' => '2012-05-11'], 4); - - $command->flushIndex('yiitest'); - } - - public function testQueryBuilderRespectsQuery() - { - $queryParts = ['field' => ['title' => 'yii']]; - $queryBuilder = new QueryBuilder($this->getConnection()); - $query = new Query(); - $query->query = $queryParts; - $build = $queryBuilder->build($query); - $this->assertArrayHasKey('queryParts', $build); - $this->assertArrayHasKey('query', $build['queryParts']); - $this->assertSame($queryParts, $build['queryParts']['query']); - $this->assertArrayNotHasKey('match_all', $build['queryParts'], 'Match all should not be set'); - } - - /** - * @group postfilter - */ - public function testQueryBuilderPostFilterQuery() - { - $postFilter = [ - 'bool' => [ - 'must' => [ - ['term' => ['title' => 'yii test']], - ], - ], - ]; - $queryBuilder = new QueryBuilder($this->getConnection()); - $query = new Query(); - $query->postFilter($postFilter); - $build = $queryBuilder->build($query); - $this->assertSame($postFilter, $build['queryParts']['post_filter']); - } - - public function testYiiCanBeFoundByQuery() - { - $queryParts = ['term' => ['title' => 'yii']]; - $query = new Query(); - $query->from('yiitest', 'article'); - $query->query = $queryParts; - $result = $query->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - } - - public function testMinScore() - { - $queryParts = [ - 'function_score' => [ - 'boost_mode' => 'replace', - 'query' => ['term' => ['title' => 'yii']], - 'functions' => [ - [ - 'script_score' => [ - 'script' => "doc['weight'].getValue()", - ], - ], - ], - ], - ]; - //without min_score should get 2 documents with weights 1 and 4 - - $query = new Query(); - $query->from('yiitest', 'article'); - $query->query($queryParts); - - $query->minScore(0.5); - $result = $query->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - - $query->minScore(2); - $result = $query->search($this->getConnection()); - $this->assertEquals(1, $result['hits']['total']); - - $query->minScore(5); - $result = $query->search($this->getConnection()); - $this->assertEquals(0, $result['hits']['total']); - } - - public function testMltSearch() - { - $queryParts = [ - 'more_like_this' => [ - 'fields' => ['title'], - 'like_text' => 'Mention YII now', - 'min_term_freq' => 1, - 'min_doc_freq' => 1, - ], - ]; - $query = new Query(); - $query->from('yiitest', 'article'); - $query->query = $queryParts; - $result = $query->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - } - - public function testHalfBoundedRange() - { - // >= 2010-01-15, 3 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['>=', 'created_at', '2010-01-15']) - ->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - - // >= 2010-01-15, 3 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['gte', 'created_at', '2010-01-15']) - ->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - - // > 2010-01-15, 2 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['>', 'created_at', '2010-01-15']) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - - // > 2010-01-15, 2 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['gt', 'created_at', '2010-01-15']) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - - // <= 2010-01-20, 3 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['<=', 'created_at', '2010-01-20']) - ->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - - // <= 2010-01-20, 3 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['lte', 'created_at', '2010-01-20']) - ->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - - // < 2010-01-20, 2 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['<', 'created_at', '2010-01-20']) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - - // < 2010-01-20, 2 results - $result = (new Query()) - ->from('yiitest', 'article') - ->where(['lt', 'created_at', '2010-01-20']) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - } - - public function testNotCondition() - { - $titles = [ - 'Symfony2 is another framework', - 'yii test', - 'nonexistent', - ]; - $result = (new Query()) - ->from('yiitest', 'article') - ->where([ 'not', [ 'in', 'title.keyword', $titles ] ]) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - } - - public function testInCondition() - { - $titles = [ - 'Symfony2 is another framework', - 'yii test', - 'nonexistent', - ]; - $result = (new Query()) - ->from('yiitest', 'article') - ->where([ 'in', 'title.keyword', $titles ]) - ->search($this->getConnection()); - $this->assertEquals(2, $result['hits']['total']); - } - - public function testBuildNotCondition() - { - $db = $this->getConnection(); - $qb = new QueryBuilder($db); - - $cond = [ 'title' => 'xyz' ]; - $operands = [ $cond ]; - - $expected = [ - 'bool' => [ - 'must_not' => [ - 'bool' => [ 'must' => [ ['term' => ['title' => 'xyz']] ] ], - ], - ], - ]; - $result = $this->invokeMethod($qb, 'buildNotCondition', ['not',$operands]); - $this->assertEquals($expected, $result); - } - - public function testBuildInCondition() - { - $db = $this->getConnection(); - $qb = new QueryBuilder($db); - - $expected = [ - 'terms' => ['foo' => ['bar1', 'bar2']], - ]; - $result = $this->invokeMethod($qb, 'buildInCondition', [ - 'in', - ['foo',['bar1','bar2']], - ]); - $this->assertEquals($expected, $result); - } -} diff --git a/tests/QueryTest.php b/tests/QueryTest.php deleted file mode 100644 index 680af958..00000000 --- a/tests/QueryTest.php +++ /dev/null @@ -1,431 +0,0 @@ -getConnection()->createCommand(); - - // delete index - if ($command->indexExists('yiitest')) { - $command->deleteIndex('yiitest'); - } - $command->createIndex('yiitest'); - - $command->setMapping('yiitest', 'user', [ - 'properties' => [ - 'name' => [ 'type' => 'keyword', 'store' => true ], - 'email' => [ 'type' => 'keyword', 'store' => true ], - 'status' => [ 'type' => 'integer', 'store' => true ], - ], - ]); - - $command->insert('yiitest', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); - $command->insert('yiitest', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); - $command->insert('yiitest', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); - $command->insert('yiitest', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); - $command->insert('yiitest', 'user', ['name' => 'user5', 'email' => 'user5@example.com', 'status' => 1], 5); - $command->insert('yiitest', 'user', ['name' => 'user6', 'email' => 'user6@example.com', 'status' => 1], 6); - $command->insert('yiitest', 'user', ['name' => 'user7', 'email' => 'user7@example.com', 'status' => 2], 7); - $command->insert('yiitest', 'user', ['name' => 'user8', 'email' => 'user8@example.com', 'status' => 1], 8); - $command->insert('yiitest', 'user', ['name' => 'user9', 'email' => 'user9@example.com', 'status' => 1], 9); - $command->insert('yiitest', 'user', ['name' => 'usera', 'email' => 'user10@example.com', 'status' => 1], 10); - $command->insert('yiitest', 'user', ['name' => 'userb', 'email' => 'user11@example.com', 'status' => 2], 11); - $command->insert('yiitest', 'user', ['name' => 'userc', 'email' => 'user12@example.com', 'status' => 1], 12); - - $command->flushIndex(); - } - - public function testFields() - { - $query = new Query(); - $query->from('yiitest', 'user'); - - $query->storedFields(['name', 'status']); - $this->assertEquals(['name', 'status'], $query->storedFields); - - $query->storedFields('name', 'status'); - $this->assertEquals(['name', 'status'], $query->storedFields); - - $result = $query->one($this->getConnection()); - $this->assertCount(2, $result['fields']); - $this->assertArrayHasKey('status', $result['fields']); - $this->assertArrayHasKey('name', $result['fields']); - $this->assertArrayHasKey('_id', $result); - - $query->storedFields([]); - $this->assertEquals([], $query->storedFields); - - $result = $query->one($this->getConnection()); - $this->assertArrayNotHasKey('fields', $result); - $this->assertArrayHasKey('_id', $result); - - $query->storedFields(null); - $this->assertNull($query->storedFields); - - $result = $query->one($this->getConnection()); - $this->assertCount(3, $result['_source']); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - } - - public function testOne() - { - $query = new Query(); - $query->from('yiitest', 'user'); - - $result = $query->one($this->getConnection()); - $this->assertCount(3, $result['_source']); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $result = $query->where(['name' => 'user1'])->one($this->getConnection()); - $this->assertCount(3, $result['_source']); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - $this->assertEquals(1, $result['_id']); - - $result = $query->where(['name' => 'user15'])->one($this->getConnection()); - $this->assertFalse($result); - } - - public function testAll() - { - $query = new Query(); - $query->from('yiitest', 'user'); - - $results = $query->limit(100)->all($this->getConnection()); - $this->assertCount(12, $results); - $result = reset($results); - $this->assertCount(3, $result['_source']); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query = new Query(); - $query->from('yiitest', 'user'); - - $results = $query->where(['name' => 'user1'])->all($this->getConnection()); - $this->assertCount(1, $results); - $result = reset($results); - $this->assertCount(3, $result['_source']); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('email', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - $this->assertEquals(1, $result['_id']); - - // indexBy - $query = new Query(); - $query->from('yiitest', 'user'); - - $results = $query->limit(100)->indexBy('name')->all($this->getConnection()); - $this->assertCount(12, $results); - ksort($results); - $this->assertEquals([ - 'user1', - 'user2', - 'user3', - 'user4', - 'user5', - 'user6', - 'user7', - 'user8', - 'user9', - 'usera', - 'userb', - 'userc', - ], array_keys($results)); - } - - public function testScalar() - { - $query = new Query(); - $query->from('yiitest', 'user'); - - $result = $query->where(['name' => 'user1'])->scalar('name', $this->getConnection()); - $this->assertEquals('user1', $result); - $result = $query->where(['name' => 'user1'])->scalar('noname', $this->getConnection()); - $this->assertNull($result); - $result = $query->where(['name' => 'user15'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - } - - public function testColumn() - { - $query = new Query(); - $query->from('yiitest', 'user'); - - $result = $query->orderBy(['name' => SORT_ASC])->limit(4)->column('name', $this->getConnection()); - $this->assertEquals(['user1', 'user2', 'user3', 'user4'], $result); - $result = $query->column('noname', $this->getConnection()); - $this->assertEquals([null, null, null, null], $result); - $result = $query->where(['name' => 'user15'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - } - - public function testAndWhere() - { - $query = new Query(); - $query->where(1) - ->andWhere(2) - ->andWhere(3); - - $expected = [ 'and', 1, 2, 3 ]; - $this->assertEquals($expected, $query->where); - } - - public function testOrWhere() - { - $query = new Query(); - $query->where(1) - ->orWhere(2) - ->orWhere(3); - - $expected = [ 'or', 1, 2, 3 ]; - $this->assertEquals($expected, $query->where); - } - - public function testFilterWhere() - { - // should work with hash format - $query = new Query(); - $query->filterWhere([ - 'id' => 0, - 'title' => ' ', - 'author_ids' => [], - ]); - $this->assertEquals(['id' => 0], $query->where); - - $query->andFilterWhere(['status' => null]); - $this->assertEquals(['id' => 0], $query->where); - - $query->orFilterWhere(['name' => '']); - $this->assertEquals(['id' => 0], $query->where); - - // should work with operator format - $query = new Query(); - $condition = ['like', 'name', 'Alex']; - $query->filterWhere($condition); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['between', 'id', null, null]); - $this->assertEquals($condition, $query->where); - - $query->orFilterWhere(['not between', 'id', null, null]); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['in', 'id', []]); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['not in', 'id', []]); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['not in', 'id', []]); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['like', 'id', '']); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['or like', 'id', '']); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['not like', 'id', ' ']); - $this->assertEquals($condition, $query->where); - - $query->andFilterWhere(['or not like', 'id', null]); - $this->assertEquals($condition, $query->where); - } - - public function testFilterWhereRecursively() - { - $query = new Query(); - $query->filterWhere([ - 'and', - ['like', 'name', ''], - ['like', 'title', ''], - ['id' => 1], - ['not', ['like', 'name', '']], - ]); - $this->assertEquals(['and', ['id' => 1]], $query->where); - } - - // TODO test facets - - // TODO test complex where() every edge of QueryBuilder - - public function testOrder() - { - $query = new Query(); - $query->orderBy('team'); - $this->assertEquals(['team' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('company'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC], $query->orderBy); - - $query->addOrderBy('age'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_ASC], $query->orderBy); - - $query->addOrderBy(['age' => SORT_DESC]); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_ASC, 'age' => SORT_DESC], $query->orderBy); - - $query->addOrderBy('age ASC, company DESC'); - $this->assertEquals(['team' => SORT_ASC, 'company' => SORT_DESC, 'age' => SORT_ASC], $query->orderBy); - } - - public function testLimitOffset() - { - $query = new Query(); - $query->limit(10)->offset(5); - $this->assertEquals(10, $query->limit); - $this->assertEquals(5, $query->offset); - } - - public function testUnion() - { - } - - /** - * @since 2.0.4 - */ - public function testBatch() - { - $names = [ - 'user1', - 'user2', - 'user3', - 'user4', - 'user5', - 'user6', - 'user7', - 'user8', - 'user9', - 'usera', - 'userb', - 'userc', - ]; - - $emails = [ - 'user1@example.com', - 'user2@example.com', - 'user3@example.com', - 'user4@example.com', - 'user5@example.com', - 'user6@example.com', - 'user7@example.com', - 'user8@example.com', - 'user9@example.com', - 'user10@example.com', - 'user11@example.com', - 'user12@example.com', - ]; - - //test each - $query = new Query(); - $query->from('yiitest', 'user')->limit(3)->orderBy(['name' => SORT_ASC])->indexBy('name')->options(['preference' => '_local']); - //NOTE: preference -> _local has no influence on query result, everything's fine as long as query doesn't fail - - $result_keys = []; - $result_values = []; - foreach ($query->each('1m', $this->getConnection()) as $key => $value) { - $result_keys[] = $key; - $result_values[] = $value['_source']['email']; - } - - $this->assertCount(12, $result_keys); - $this->assertEquals($names, $result_keys); - - $this->assertCount(12, $result_values); - $this->assertEquals($emails, $result_values); - - //test batch - $query = new Query(); - $query->from('yiitest', 'user')->limit(3)->orderBy(['name' => SORT_ASC])->indexBy('name')->options(['preference' => '_local']); - //NOTE: preference -> _local has no influence on query result, everything's fine as long as query doesn't fail - - $results = []; - foreach ($query->batch('1m', $this->getConnection()) as $batchId => $batch) { - $results = $results + $batch; - } - - $this->assertCount(12, $results); - $this->assertEquals($names, array_keys($results)); - foreach ($names as $id => $name) { - $this->assertEquals($emails[$id], $results[$name]['_source']['email']); - } - - //test scan (no ordering) - $query = new Query(); - $query->from('yiitest', 'user')->limit(3); - - $results = []; - foreach ($query->each('1m', $this->getConnection()) as $value) { - $results[] = $value['_source']['name']; - } - - $this->assertCount(12, $results); - sort($results); - $this->assertEquals($names, $results); - } - - /** - * @group postfilter - * @since 2.0.5 - */ - public function testPostFilter() - { - $postFilter = [ - 'term' => ['status' => 2], - ]; - $query = new Query(); - $query->from('yiitest', 'user'); - $query->postFilter($postFilter); - $query->addAggregation('statuses', 'terms', ['field' => 'status']); - $result = $query->search($this->getConnection()); - $this->assertEquals(3, $result['hits']['total']); - } - - /** - * @group explain - * @since 2.0.5 - */ - public function testExplain() - { - $query = new Query(); - $query->from('yiitest', 'user'); - $query->explain(true); - $result = $query->search($this->getConnection()); - $this->assertIsArray($result['hits']['hits'][0]['_explanation']); - $this->assertArrayHasKey('_explanation', $result['hits']['hits'][0]); - } - - /** - * @group explain - * @since 2.0.5 - */ - public function testNoExplain() - { - $query = new Query(); - $query->from('yiitest', 'user'); - $result = $query->search($this->getConnection()); - $this->assertArrayNotHasKey('_explanation', $result['hits']['hits'][0]); - } -} diff --git a/tests/Support/Assert.php b/tests/Support/Assert.php new file mode 100644 index 00000000..634b5280 --- /dev/null +++ b/tests/Support/Assert.php @@ -0,0 +1,77 @@ +getProperty($propertyName); + + /** @psalm-var mixed $result */ + $result = $property->getValue($object); + } + + return $result; + } + + /** + * Invokes an inaccessible method. + * + * @param object $object The object to invoke the method on. + * @param string $method The name of the method to invoke. + * @param array $args The arguments to pass to the method. + */ + public static function invokeMethod(object $object, string $method, array $args = []): mixed + { + $reflection = new ReflectionObject($object); + + $result = null; + + if ($method !== '') { + $method = $reflection->getMethod($method); + + /** @psalm-var mixed $result */ + $result = $method->invokeArgs($object, $args); + } + + return $result; + } +} diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php new file mode 100644 index 00000000..d90e3c91 --- /dev/null +++ b/tests/Support/TestTrait.php @@ -0,0 +1,25 @@ +addNodeValue('host', 'localhost'); + + if ($reset) { + $db->open(); + } + + return $db; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php deleted file mode 100644 index 5c214560..00000000 --- a/tests/TestCase.php +++ /dev/null @@ -1,98 +0,0 @@ -destroyApplication(); - } - - /** - * Destroys application in Yii::$app by setting it to null. - */ - protected function destroyApplication() - { - Yii::$app = null; - Yii::$container = new Container(); - } - - protected function setUp() - { - $this->mockApplication(); - - $config = self::getParam('elasticsearch'); - if (empty($config)) { - $this->markTestSkipped('No elasticsearch server connection configured.'); - } - parent::setUp(); - } - - /** - * @param bool $reset whether to clean up the test database - * @return Connection - */ - public function getConnection($reset = true) - { - $config = self::getParam('elasticsearch'); - $db = new Connection($config); - if ($reset) { - $db->open(); - } - - return $db; - } - - /** - * Invokes a inaccessible method. - * @param $object - * @param $method - * @param array $args - * @param bool $revoke whether to make method inaccessible after execution - * @return mixed - */ - protected function invokeMethod($object, $method, $args = [], $revoke = true) - { - $reflection = new \ReflectionObject($object); - $method = $reflection->getMethod($method); - $method->setAccessible(true); - $result = $method->invokeArgs($object, $args); - if ($revoke) { - $method->setAccessible(false); - } - - return $result; - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php deleted file mode 100644 index 9c990b6a..00000000 --- a/tests/bootstrap.php +++ /dev/null @@ -1,35 +0,0 @@ - [ - 'autodetectCluster' => false, - 'nodes' => [ - ['http_address' => 'inet[/127.0.0.1:9200]'], - ], - ], -]; - -if (is_file(__DIR__ . '/config.local.php')) { - include(__DIR__ . '/config.local.php'); -} - -return $config;