From b081cf5e46db0fe2dd4fbb231ac2725ab3d00f42 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Mon, 25 Nov 2013 03:14:27 +0100 Subject: [PATCH] moved elasticsearch to extensions --- extensions/elasticsearch/ActiveQuery.php | 166 +++++++ extensions/elasticsearch/ActiveRecord.php | 478 +++++++++++++++++++++ extensions/elasticsearch/ActiveRelation.php | 61 +++ extensions/elasticsearch/Cluster.php | 16 + extensions/elasticsearch/Command.php | 430 ++++++++++++++++++ extensions/elasticsearch/Connection.php | 196 +++++++++ extensions/elasticsearch/LICENSE.md | 32 ++ extensions/elasticsearch/Node.php | 23 + extensions/elasticsearch/Query.php | 399 +++++++++++++++++ extensions/elasticsearch/QueryBuilder.php | 336 +++++++++++++++ extensions/elasticsearch/README.md | 92 ++++ extensions/elasticsearch/composer.json | 27 ++ extensions/redis/ActiveQuery.php | 2 +- extensions/redis/README.md | 2 +- framework/yii/elasticsearch/ActiveQuery.php | 166 ------- framework/yii/elasticsearch/ActiveRecord.php | 477 -------------------- framework/yii/elasticsearch/ActiveRelation.php | 61 --- framework/yii/elasticsearch/Cluster.php | 16 - framework/yii/elasticsearch/Command.php | 430 ------------------ framework/yii/elasticsearch/Connection.php | 196 --------- framework/yii/elasticsearch/Node.php | 23 - framework/yii/elasticsearch/Query.php | 399 ----------------- framework/yii/elasticsearch/QueryBuilder.php | 336 --------------- tests/unit/data/ar/elasticsearch/Customer.php | 2 +- tests/unit/data/ar/redis/Customer.php | 2 +- .../extensions/elasticsearch/ActiveRecordTest.php | 327 ++++++++++++++ .../elasticsearch/ElasticSearchConnectionTest.php | 14 + .../elasticsearch/ElasticSearchTestCase.php | 51 +++ tests/unit/extensions/elasticsearch/QueryTest.php | 182 ++++++++ .../framework/elasticsearch/ActiveRecordTest.php | 328 -------------- .../elasticsearch/ElasticSearchConnectionTest.php | 22 - .../elasticsearch/ElasticSearchTestCase.php | 48 --- tests/unit/framework/elasticsearch/QueryTest.php | 182 -------- 33 files changed, 2834 insertions(+), 2688 deletions(-) create mode 100644 extensions/elasticsearch/ActiveQuery.php create mode 100644 extensions/elasticsearch/ActiveRecord.php create mode 100644 extensions/elasticsearch/ActiveRelation.php create mode 100644 extensions/elasticsearch/Cluster.php create mode 100644 extensions/elasticsearch/Command.php create mode 100644 extensions/elasticsearch/Connection.php create mode 100644 extensions/elasticsearch/LICENSE.md create mode 100644 extensions/elasticsearch/Node.php create mode 100644 extensions/elasticsearch/Query.php create mode 100644 extensions/elasticsearch/QueryBuilder.php create mode 100644 extensions/elasticsearch/README.md create mode 100644 extensions/elasticsearch/composer.json delete mode 100644 framework/yii/elasticsearch/ActiveQuery.php delete mode 100644 framework/yii/elasticsearch/ActiveRecord.php delete mode 100644 framework/yii/elasticsearch/ActiveRelation.php delete mode 100644 framework/yii/elasticsearch/Cluster.php delete mode 100644 framework/yii/elasticsearch/Command.php delete mode 100644 framework/yii/elasticsearch/Connection.php delete mode 100644 framework/yii/elasticsearch/Node.php delete mode 100644 framework/yii/elasticsearch/Query.php delete mode 100644 framework/yii/elasticsearch/QueryBuilder.php create mode 100644 tests/unit/extensions/elasticsearch/ActiveRecordTest.php create mode 100644 tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php create mode 100644 tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php create mode 100644 tests/unit/extensions/elasticsearch/QueryTest.php delete mode 100644 tests/unit/framework/elasticsearch/ActiveRecordTest.php delete mode 100644 tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php delete mode 100644 tests/unit/framework/elasticsearch/ElasticSearchTestCase.php delete mode 100644 tests/unit/framework/elasticsearch/QueryTest.php diff --git a/extensions/elasticsearch/ActiveQuery.php b/extensions/elasticsearch/ActiveQuery.php new file mode 100644 index 0000000..2a99643 --- /dev/null +++ b/extensions/elasticsearch/ActiveQuery.php @@ -0,0 +1,166 @@ +with('orders')->asArray()->all(); + * ~~~ + * + * @author Carsten Brandt + * @since 2.0 + */ +class ActiveQuery extends Query implements ActiveQueryInterface +{ + use ActiveQueryTrait; + + /** + * 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) + { + /** @var ActiveRecord $modelClass */ + $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) + { + $command = $this->createCommand($db); + $result = $command->queryAll(); + if (empty($result['hits'])) { + return []; + } + $models = $this->createModels($result['hits']); + if ($this->asArray) { + foreach($models as $key => $model) { + $models[$key] = $model['_source']; + $models[$key][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; + } + } + if (!empty($this->with)) { + $this->findWith($this->with, $models); + } + 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) { + $model = $result['_source']; + $model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id']; + } else { + /** @var ActiveRecord $class */ + $class = $this->modelClass; + $model = $class::create($result); + } + if (!empty($this->with)) { + $models = [$model]; + $this->findWith($this->with, $models); + $model = $models[0]; + } + return $model; + } + + /** + * @inheritDocs + */ + public function scalar($field, $db = null) + { + $record = parent::one($db); + if ($record !== false) { + if ($field == ActiveRecord::PRIMARY_KEY_NAME) { + return $record['_id']; + } elseif (isset($record['_source'][$field])) { + return $record['_source'][$field]; + } + } + return null; + } + + /** + * @inheritDocs + */ + public function column($field, $db = null) + { + if ($field == ActiveRecord::PRIMARY_KEY_NAME) { + $command = $this->createCommand($db); + $command->queryParts['fields'] = []; + $rows = $command->queryAll()['hits']; + $result = []; + foreach ($rows as $row) { + $result[] = $row['_id']; + } + return $result; + } + return parent::column($field, $db); + } +} diff --git a/extensions/elasticsearch/ActiveRecord.php b/extensions/elasticsearch/ActiveRecord.php new file mode 100644 index 0000000..33b01dd --- /dev/null +++ b/extensions/elasticsearch/ActiveRecord.php @@ -0,0 +1,478 @@ + + * @since 2.0 + */ +class ActiveRecord extends \yii\db\ActiveRecord +{ + const PRIMARY_KEY_NAME = 'id'; + + private $_id; + private $_version; + + /** + * 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->getComponent('elasticsearch'); + } + + /** + * @inheritDoc + */ + public static function find($q = null) + { + $query = static::createQuery(); + if (is_array($q)) { + if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q))) { + $pk = $q[ActiveRecord::PRIMARY_KEY_NAME]; + if (is_array($pk)) { + return static::mget($pk); + } else { + return static::get($pk); + } + } + return $query->where($q)->one(); + } elseif ($q !== null) { + return static::get($q); + } + return $query; + } + + /** + * 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.elasticsearch.org/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['exists']) { + return static::create($result); + } + 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.elasticsearch.org/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 mget($primaryKeys, $options = []) + { + if (empty($primaryKeys)) { + return []; + } + $command = static::getDb()->createCommand(); + $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); + $models = []; + foreach($result['docs'] as $doc) { + if ($doc['exists']) { + $models[] = static::create($doc); + } + } + return $models; + } + + // TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html + + // TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html + + /** + * @inheritDoc + */ + public static function createQuery() + { + return new ActiveQuery(['modelClass' => get_called_class()]); + } + + /** + * @inheritDoc + */ + public static function createActiveRelation($config = []) + { + return new ActiveRelation($config); + } + + // TODO implement copy and move as pk change is not possible + + public function getId() + { + return $this->_id; + } + + /** + * Sets the primary key + * @param mixed $value + * @throws \yii\base\InvalidCallException when record is not new + */ + public function setId($value) + { + if ($this->isNewRecord) { + $this->_id = $value; + } else { + throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); + } + } + + /** + * @inheritDoc + */ + public function getPrimaryKey($asArray = false) + { + if ($asArray) { + return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id]; + } else { + return $this->_id; + } + } + + /** + * @inheritDoc + */ + public function getOldPrimaryKey($asArray = false) + { + $id = $this->isNewRecord ? null : $this->_id; + if ($asArray) { + return [ActiveRecord::PRIMARY_KEY_NAME => $id]; + } else { + return $this->_id; + } + } + + /** + * This method defines the primary. + * + * The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed. + * + * @return string[] the primary keys of this record. + */ + public static function primaryKey() + { + return [ActiveRecord::PRIMARY_KEY_NAME]; + } + + /** + * Returns the list of all attribute names of the model. + * This method must be overridden by child classes to define available attributes. + * @return array list of attribute names. + */ + public static function attributes() + { + throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); + } + + /** + * @return string the name of the index this record is stored in. + */ + public static function index() + { + return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); + } + + /** + * @return string the name of the type of this record. + */ + public static function type() + { + return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); + } + + /** + * Creates an active record object using a row of data. + * This method is called by [[ActiveQuery]] to populate the query results + * into Active Records. It is not meant to be used to create new records. + * @param array $row attribute values (name => value) + * @return ActiveRecord the newly created active record. + */ + public static function create($row) + { + $row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; + $record = parent::create($row['_source']); + return $record; + } + + /** + * 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.elasticsearch.org/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 boolean $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 + * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. + * + * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) + * for more details on these options. + * + * By default the `op_type` is set to `create`. + * @return boolean whether the attributes are valid and the record is inserted successfully. + */ + public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) + { + if ($runValidation && !$this->validate($attributes)) { + return false; + } + if ($this->beforeSave(true)) { + $values = $this->getDirtyAttributes($attributes); + + $response = static::getDb()->createCommand()->insert( + static::index(), + static::type(), + $values, + $this->getPrimaryKey(), + $options + ); + + if (!$response['ok']) { + return false; + } + $this->_id = $response['_id']; + $this->_version = $response['_version']; + $this->setOldAttributes($values); + $this->afterSave(true); + return true; + } + return false; + } + + /** + * 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(array('status' => 1), array(2, 3, 4)); + * ~~~ + * + * @param array $attributes attribute values (name-value pairs) to be saved into the table + * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @param array $params this parameter is ignored in elasticsearch implementation. + * @return integer the number of rows updated + */ + public static function updateAll($attributes, $condition = [], $params = []) + { + if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { + $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; + } else { + $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); + } + if (empty($primaryKeys)) { + return 0; + } + $bulk = ''; + foreach((array) $primaryKeys as $pk) { + $action = Json::encode([ + "update" => [ + "_id" => $pk, + "_type" => static::type(), + "_index" => static::index(), + ], + ]); + $data = Json::encode(array( + "doc" => $attributes + )); + $bulk .= $action . "\n" . $data . "\n"; + } + + // TODO do this via command + $url = '/' . static::index() . '/' . static::type() . '/_bulk'; + $response = static::getDb()->http()->post($url, null, $bulk)->send(); + $body = Json::decode($response->getBody(true)); + $n=0; + foreach($body['items'] as $item) { + if ($item['update']['ok']) { + $n++; + } + // TODO might want to update the _version in update() + } + return $n; + } + + + /** + * 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 put in the WHERE part of the DELETE SQL. + * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. + * @param array $params this parameter is ignored in elasticsearch implementation. + * @return integer the number of rows deleted + */ + public static function deleteAll($condition = [], $params = []) + { + if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { + $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; + } else { + $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); + } + if (empty($primaryKeys)) { + return 0; + } + $bulk = ''; + foreach((array) $primaryKeys as $pk) { + $bulk .= Json::encode([ + "delete" => [ + "_id" => $pk, + "_type" => static::type(), + "_index" => static::index(), + ], + ]) . "\n"; + } + + // TODO do this via command + $url = '/' . static::index() . '/' . static::type() . '/_bulk'; + $response = static::getDb()->http()->post($url, null, $bulk)->send(); + $body = Json::decode($response->getBody(true)); + $n=0; + foreach($body['items'] as $item) { + if ($item['delete']['found'] && $item['delete']['ok']) { + $n++; + } + } + return $n; + } + + /** + * @inheritdoc + */ + public static function updateAllCounters($counters, $condition = null, $params = []) + { + throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.'); + } + + /** + * @inheritdoc + */ + public static function getTableSchema() + { + throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.'); + } + + /** + * @inheritDoc + */ + public static function tableName() + { + return static::index() . '/' . static::type(); + } + + /** + * @inheritdoc + */ + public static function findBySql($sql, $params = []) + { + throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.'); + } + + /** + * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. + * This method will always return false as transactional operations are not supported by elasticsearch. + * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. + * @return boolean whether the specified operation is transactional in the current [[scenario]]. + */ + public function isTransactional($operation) + { + return false; + } +} diff --git a/extensions/elasticsearch/ActiveRelation.php b/extensions/elasticsearch/ActiveRelation.php new file mode 100644 index 0000000..a102697 --- /dev/null +++ b/extensions/elasticsearch/ActiveRelation.php @@ -0,0 +1,61 @@ + + * @since 2.0 + */ +class ActiveRelation extends ActiveQuery implements ActiveRelationInterface +{ + use ActiveRelationTrait; + + /** + * 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 ActiveRelation $viaQuery */ + list($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]); + } + } + return parent::createCommand($db); + } +} diff --git a/extensions/elasticsearch/Cluster.php b/extensions/elasticsearch/Cluster.php new file mode 100644 index 0000000..fda4175 --- /dev/null +++ b/extensions/elasticsearch/Cluster.php @@ -0,0 +1,16 @@ + + */ + +namespace yii\elasticsearch; + + +use yii\base\Object; + +class Cluster extends Object +{ + // TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster.html +} \ No newline at end of file diff --git a/extensions/elasticsearch/Command.php b/extensions/elasticsearch/Command.php new file mode 100644 index 0000000..35334f4 --- /dev/null +++ b/extensions/elasticsearch/Command.php @@ -0,0 +1,430 @@ + + */ + +namespace yii\elasticsearch; + + +use Guzzle\Http\Exception\ClientErrorResponseException; +use yii\base\Component; +use yii\db\Exception; +use yii\helpers\Json; + +// camelCase vs. _ +// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/common-options.html#_result_casing + + +/** + * Class Command + * + * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html + * + */ +class Command extends Component +{ + /** + * @var Connection + */ + public $db; + /** + * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index + */ + public $index; + /** + * @var string|array the types to execute the query on. Defaults to null meaning all types + */ + public $type; + /** + * @var array list of arrays or json strings that become parts of a query + */ + public $queryParts; + + public $options = []; + + public function queryAll($options = []) + { + $query = $this->queryParts; + if (empty($query)) { + $query = '{}'; + } + if (is_array($query)) { + $query = Json::encode($query); + } + $url = [ + $this->index !== null ? $this->index : '_all', + $this->type !== null ? $this->type : '_all', + '_search' + ]; + try { + $response = $this->db->http()->post($this->createUrl($url, $options), null, $query)->send(); + } catch(ClientErrorResponseException $e) { + throw new Exception("elasticsearch error:\n\n" + . $query . "\n\n" . $e->getMessage() + . print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e); + } + return Json::decode($response->getBody(true))['hits']; + } + + public function queryCount($options = []) + { + $options['search_type'] = 'count'; + return $this->queryAll($options); + } + + + /** + * Inserts a document into an index + * @param string $index + * @param string $type + * @param string|array $data json string or array of data to store + * @param null $id the documents id. If not specified Id will be automatically choosen + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html + */ + public function insert($index, $type, $data, $id = null, $options = []) + { + $body = is_array($data) ? Json::encode($data) : $data; + + try { + if ($id !== null) { + $response = $this->db->http()->put($this->createUrl([$index, $type, $id], $options), null, $body)->send(); + } else { + $response = $this->db->http()->post($this->createUrl([$index, $type], $options), null, $body)->send(); + } + } catch(ClientErrorResponseException $e) { + throw new Exception("elasticsearch error:\n\n" + . $body . "\n\n" . $e->getMessage() + . print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e); + } + return Json::decode($response->getBody(true)); + } + + /** + * gets a document from the index + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function get($index, $type, $id, $options = []) + { + $httpOptions = [ + 'exceptions' => false, + ]; + $response = $this->db->http()->get($this->createUrl([$index, $type, $id], $options), null, $httpOptions)->send(); + if ($response->getStatusCode() == 200 || $response->getStatusCode() == 404) { + return Json::decode($response->getBody(true)); + } else { + throw new Exception('Elasticsearch request failed.'); + } + } + + /** + * gets multiple documents from the index + * + * TODO allow specifying type and index + fields + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function mget($index, $type, $ids, $options = []) + { + $httpOptions = [ + 'exceptions' => false, + ]; + $body = Json::encode(['ids' => array_values($ids)]); + $response = $this->db->http()->post( // TODO guzzle does not manage to send get request with content + $this->createUrl([$index, $type, '_mget'], $options), + null, + $body, + $httpOptions + )->send(); + if ($response->getStatusCode() == 200) { + return Json::decode($response->getBody(true)); + } else { + throw new Exception('Elasticsearch request failed.'); + } + } + + /** + * gets a documents _source from the index (>=v0.90.1) + * @param $index + * @param $type + * @param $id + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source + */ + public function getSource($index, $type, $id) + { + $response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); + return Json::decode($response->getBody(true)); + } + + // TODO mget http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html + + /** + * gets a document from the index + * @param $index + * @param $type + * @param $id + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html + */ + public function exists($index, $type, $id) + { + $response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); + return $response->getStatusCode() == 200; + } + + /** + * deletes a document from the index + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html + */ + public function delete($index, $type, $id, $options = []) + { + $response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); + return Json::decode($response->getBody(true)); + } + + /** + * updates a document + * @param $index + * @param $type + * @param $id + * @param array $options + * @return mixed + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html + */ + public function update($index, $type, $id, $data, $options = []) + { + // TODO + $response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); + return Json::decode($response->getBody(true)); + } + + // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html + + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html + */ + public function createIndex($index, $configuration = null) + { + $body = $configuration !== null ? Json::encode($configuration) : null; + $response = $this->db->http()->put($this->createUrl([$index]), null, $body)->send(); + return Json::decode($response->getBody(true)); + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html + */ + public function deleteIndex($index) + { + $response = $this->db->http()->delete($this->createUrl([$index]))->send(); + return Json::decode($response->getBody(true)); + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html + */ + public function deleteAllIndexes() + { + $response = $this->db->http()->delete($this->createUrl(['_all']))->send(); + return Json::decode($response->getBody(true)); + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html + */ + public function indexExists($index) + { + $response = $this->db->http()->head($this->createUrl([$index]))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html + */ + public function typeExists($index, $type) + { + $response = $this->db->http()->head($this->createUrl([$index, $type]))->send(); + return $response->getStatusCode() == 200; + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html + */ + public function openIndex($index) + { + $response = $this->db->http()->post($this->createUrl([$index, '_open']))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html + */ + public function closeIndex($index) + { + $response = $this->db->http()->post($this->createUrl([$index, '_close']))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html + */ + public function getIndexStatus($index = '_all') + { + $response = $this->db->http()->get($this->createUrl([$index, '_status']))->send(); + return Json::decode($response->getBody(true)); + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html + // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html + */ + public function clearIndexCache($index) + { + $response = $this->db->http()->post($this->createUrl([$index, '_cache', 'clear']))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html + */ + public function flushIndex($index = '_all') + { + $response = $this->db->http()->post($this->createUrl([$index, '_flush']))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html + */ + public function refreshIndex($index) + { + $response = $this->db->http()->post($this->createUrl([$index, '_refresh']))->send(); + return $response->getStatusCode() == 200; + } + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html + + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html + + /** + * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html + */ + public function setMapping($index, $type, $mapping) + { + $body = $mapping !== null ? Json::encode($mapping) : null; + $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']), null, $body)->send(); + return $response->getStatusCode() == 200; + } + + /** + * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html + */ + public function getMapping($index = '_all', $type = '_all') + { + $response = $this->db->http()->get($this->createUrl([$index, $type, '_mapping']))->send(); + return Json::decode($response->getBody(true)); + } + + /** + * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html + */ + public function deleteMapping($index, $type) + { + $response = $this->db->http()->delete($this->createUrl([$index, $type]))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html + */ + public function getFieldMapping($index, $type = '_all') + { + // TODO + $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send(); + return Json::decode($response->getBody(true)); + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html + */ + public function analyze($options, $index = null) + { + // TODO + $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send(); + return Json::decode($response->getBody(true)); + + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) + { + $body = Json::encode([ + 'template' => $pattern, + 'order' => $order, + 'settings' => (object) $settings, + 'mappings' => (object) $settings, + ]); + $response = $this->db->http()->put($this->createUrl(['_template', $name]), null, $body)->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function deleteTemplate($name) + { + $response = $this->db->http()->delete($this->createUrl(['_template', $name]))->send(); + return $response->getStatusCode() == 200; + } + + /** + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html + */ + public function getTemplate($name) + { + $response = $this->db->http()->get($this->createUrl(['_template', $name]))->send(); + return Json::decode($response->getBody(true)); + } + + private function createUrl($path, $options = []) + { + $url = implode('/', array_map(function($a) { + return urlencode(is_array($a) ? implode(',', $a) : $a); + }, $path)); + + if (!empty($options) || !empty($this->options)) { + $options = array_merge($this->options, $options); + $url .= '?' . http_build_query($options); + } + + return $url; + } +} \ No newline at end of file diff --git a/extensions/elasticsearch/Connection.php b/extensions/elasticsearch/Connection.php new file mode 100644 index 0000000..46c1efb --- /dev/null +++ b/extensions/elasticsearch/Connection.php @@ -0,0 +1,196 @@ + + * @since 2.0 + */ +class Connection extends Component +{ + /** + * @event Event an event that is triggered after a DB connection is established + */ + const EVENT_AFTER_OPEN = 'afterOpen'; + + // TODO add autodetection of cluster nodes + // http://localhost:9200/_cluster/nodes + public $nodes = array( + array( + 'host' => 'localhost', + 'port' => 9200, + ) + ); + + // http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth + public $auth = []; + + // TODO use timeouts + /** + * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") + */ + public $connectionTimeout = null; + /** + * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. + */ + public $dataTimeout = null; + + + + public function init() + { + if ($this->nodes === array()) { + throw new InvalidConfigException('elasticsearch needs at least one node.'); + } + } + + /** + * Creates a command for execution. + * @param string $query the SQL statement to be executed + * @return Command the DB command + */ + public function createCommand($config = []) + { + $this->open(); + $config['db'] = $this; + $command = new Command($config); + return $command; + } + + /** + * Closes the connection when this component is being serialized. + * @return array + */ + public function __sleep() + { + $this->close(); + return array_keys(get_object_vars($this)); + } + + /** + * Returns a value indicating whether the DB connection is established. + * @return boolean whether the DB connection is established + */ + public function getIsActive() + { + return false; // TODO implement + } + + /** + * Establishes a DB connection. + * It does nothing if a DB connection has already been established. + * @throws Exception if connection fails + */ + public function open() + { + // TODO select one node to be the active one. + + + foreach($this->nodes as $key => $node) { + if (is_array($node)) { + $this->nodes[$key] = new Node($node); + } + } +/* if ($this->_socket === null) { + if (empty($this->dsn)) { + throw new InvalidConfigException('Connection.dsn cannot be empty.'); + } + $dsn = explode('/', $this->dsn); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':6379'; + } + $db = isset($dsn[3]) ? $dsn[3] : 0; + + \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + $this->_socket = @stream_socket_client( + $host, + $errorNumber, + $errorDescription, + $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") + ); + if ($this->_socket) { + if ($this->dataTimeout !== null) { + stream_set_timeout($this->_socket, $timeout=(int)$this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); + } + if ($this->password !== null) { + $this->executeCommand('AUTH', array($this->password)); + } + $this->executeCommand('SELECT', array($db)); + $this->initConnection(); + } else { + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); + $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; + throw new Exception($message, $errorDescription, (int)$errorNumber); + } + }*/ + // TODO implement + } + + /** + * Closes the currently active DB connection. + * It does nothing if the connection is already closed. + */ + public function close() + { + // TODO implement +/* if ($this->_socket !== null) { + \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + $this->executeCommand('QUIT'); + stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); + $this->_socket = null; + $this->_transaction = 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'; + } + + public function getNodeInfo() + { + // TODO HTTP request to localhost:9200/ + } + + public function getQueryBuilder() + { + return new QueryBuilder($this); + } + + /** + * @return \Guzzle\Http\Client + */ + public function http() + { + $guzzle = new \Guzzle\Http\Client('http://localhost:9200/'); + //$guzzle->setDefaultOption() + return $guzzle; + } +} \ No newline at end of file diff --git a/extensions/elasticsearch/LICENSE.md b/extensions/elasticsearch/LICENSE.md new file mode 100644 index 0000000..e98f03d --- /dev/null +++ b/extensions/elasticsearch/LICENSE.md @@ -0,0 +1,32 @@ +The Yii framework is free software. It is released under the terms of +the following BSD License. + +Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + * Neither the name of Yii Software LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/elasticsearch/Node.php b/extensions/elasticsearch/Node.php new file mode 100644 index 0000000..60d5956 --- /dev/null +++ b/extensions/elasticsearch/Node.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +class Node extends Object +{ + public $host; + public $port; +} \ No newline at end of file diff --git a/extensions/elasticsearch/Query.php b/extensions/elasticsearch/Query.php new file mode 100644 index 0000000..23d9de1 --- /dev/null +++ b/extensions/elasticsearch/Query.php @@ -0,0 +1,399 @@ + + * @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, it means retrieving all fields. An empty array will result in no fields being + * retrieved. This means that only the primaryKey of a record will be available in the result. + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields + * @see fields() + */ + public $fields; + /** + * @var string|array 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 string|array 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 integer 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.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 + */ + public $timeout; + + public $query; + + public $filter; + + public $facets = []; + + public $facetResults = []; + + public $totalCount; + + /** + * 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->getComponent('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)->queryAll(); + // TODO publish facet results + $rows = $result['hits']; + if ($this->indexBy === null && $this->fields === null) { + return $rows; + } + $models = []; + foreach ($rows as $key => $row) { + if ($this->fields !== null) { + $row['_source'] = isset($row['fields']) ? $row['fields'] : []; + unset($row['fields']); + } + if ($this->indexBy !== null) { + if (is_string($this->indexBy)) { + $key = $row['_source'][$this->indexBy]; + } else { + $key = call_user_func($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|boolean 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) + { + $options['size'] = 1; + $result = $this->createCommand($db)->queryAll($options); + // TODO publish facet results + if (empty($result['hits'])) { + return false; + } + $record = reset($result['hits']); + if ($this->fields !== null) { + $record['_source'] = isset($record['fields']) ? $record['fields'] : []; + unset($record['fields']); + } + return $record; + } + + /** + * Executes the query and deletes all matching documents. + * + * This will not run facet queries. + * + * @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 delete($db = null) + { + // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + } + + /** + * 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 && isset($record['_source'][$field])) { + return $record['_source'][$field]; + } else { + 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['fields'] = [$field]; + $rows = $command->queryAll()['hits']; + $result = []; + foreach ($rows as $row) { + $result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; + } + return $result; + } + + /** + * 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 integer number of records + */ + public function count($q = '*', $db = null) + { + // TODO consider sending to _count api instead of _search for performance + // only when no facety are registerted. + // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html + + $count = $this->createCommand($db)->queryCount()['total']; + if ($this->limit === null && $this->offset === null) { + return $count; + } elseif ($this->offset !== null) { + $count = $this->offset < $count ? $count - $this->offset : 0; + } + return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); + } + + /** + * 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 boolean whether the query result contains any row of data. + */ + public function exists($db = null) + { + return self::one($db) !== false; + } + + /** + * Adds a facet search to this query. + * @param string $name the name of this facet + * @param string $type the facet type. e.g. `terms`, `range`, `histogram`... + * @param string|array $options the configuration options for this facet. Can be an array or a json string. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html + */ + public function addFacet($name, $type, $options) + { + $this->facets[$name] = [$type => $options]; + return $this; + } + + /** + * The `terms facet` allow to specify field facets that return the N most frequent terms. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html + */ + public function addTermFacet($name, $options) + { + return $this->addFacet($name, 'terms', $options); + } + + /** + * Range facet allows to specify a set of ranges and get both the number of docs (count) that fall + * within each range, and aggregated data either based on the field, or using another field. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html + */ + public function addRangeFacet($name, $options) + { + return $this->addFacet($name, 'range', $options); + } + + /** + * The histogram facet works with numeric data by building a histogram across intervals of the field values. + * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per + * interval/bucket (count and total). + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html + */ + public function addHistogramFacet($name, $options) + { + return $this->addFacet($name, 'histogram', $options); + } + + /** + * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html + */ + public function addDateHistogramFacet($name, $options) + { + return $this->addFacet($name, 'date_histogram', $options); + } + + /** + * A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter. + * The filter itself can be expressed using the Query DSL. + * @param string $name the name of this facet + * @param string $filter the query in Query DSL + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html + */ + public function addFilterFacet($name, $filter) + { + return $this->addFacet($name, 'filter', $filter); + } + + /** + * A facet query allows to return a count of the hits matching the facet query. + * The query itself can be expressed using the Query DSL. + * @param string $name the name of this facet + * @param string $query the query in Query DSL + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html + */ + public function addQueryFacet($name, $query) + { + return $this->addFacet($name, 'query', $query); + } + + /** + * Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count, + * total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html + */ + public function addStatisticalFacet($name, $options) + { + return $this->addFacet($name, 'statistical', $options); + } + + /** + * The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field, + * per term value driven by another field. + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html + */ + public function addTermsStatsFacet($name, $options) + { + return $this->addFacet($name, 'terms_stats', $options); + } + + /** + * The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point` + * including count of the number of hits that fall within each range, and aggregation information (like `total`). + * @param string $name the name of this facet + * @param array $options additional option. Please refer to the elasticsearch documentation for details. + * @return static + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html + */ + public function addGeoDistanceFacet($name, $options) + { + return $this->addFacet($name, 'geo_distance', $options); + } + + // TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html + + // TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html + + // TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html + + public function query() + { + + } + + /** + * Sets the index and type to retrieve documents from. + * @param string|array $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 string|array $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. + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type + */ + public function from($index, $type = null) + { + $this->index = $index; + $this->type = $type; + } + + /** + * Sets the fields to retrieve from the documents. + * @param array $fields the fields to be selected. + * @return static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html + */ + public function fields($fields) + { + $this->fields = $fields; + return $this; + } + + /** + * Sets the search timeout. + * @param integer $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 static the query object itself + * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 + */ + public function timeout($timeout) + { + $this->timeout = $timeout; + return $this; + } + +} \ No newline at end of file diff --git a/extensions/elasticsearch/QueryBuilder.php b/extensions/elasticsearch/QueryBuilder.php new file mode 100644 index 0000000..c008de1 --- /dev/null +++ b/extensions/elasticsearch/QueryBuilder.php @@ -0,0 +1,336 @@ + + * @since 2.0 + */ +class QueryBuilder extends \yii\base\Object +{ + /** + * @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->fields !== null) { + $parts['fields'] = (array) $query->fields; + } + if ($query->limit !== null && $query->limit >= 0) { + $parts['size'] = $query->limit; + } + if ($query->offset > 0) { + $parts['from'] = (int) $query->offset; + } + + $filters = empty($query->filter) ? [] : [$query->filter]; + $whereFilter = $this->buildCondition($query->where); + if (!empty($whereFilter)) { + $filters[] = $whereFilter; + } + if (!empty($filters)) { + $parts['filter'] = count($filters) > 1 ? ['and' => $filters] : $filters[0]; + } + + $sort = $this->buildOrderBy($query->orderBy); + if (!empty($sort)) { + $parts['sort'] = $sort; + } + + if (empty($parts['query'])) { + $parts['query'] = ["match_all" => (object)[]]; + } + + $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 == ActiveRecord::PRIMARY_KEY_NAME) { + $column = '_id'; + } + + // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ + if (is_array($direction)) { + $orders[] = [$column => $direction]; + } else { + $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; + } + } + return $orders; + } + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($condition) + { + static $builders = array( + 'and' => 'buildAndCondition', + 'or' => 'buildAndCondition', + 'between' => 'buildBetweenCondition', + 'not between' => 'buildBetweenCondition', + 'in' => 'buildInCondition', + 'not in' => 'buildInCondition', + 'like' => 'buildLikeCondition', + 'not like' => 'buildLikeCondition', + 'or like' => 'buildLikeCondition', + 'or not like' => 'buildLikeCondition', + ); + + 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); + } else { + 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 = []; + foreach($condition as $attribute => $value) { + if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) { + if ($value == null) { // there is no null pk + $parts[] = ['script' => ['script' => '0==1']]; + } else { + $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; + } + } else { + if (is_array($value)) { // IN condition + $parts[] = ['in' => [$attribute => $value]]; + } else { + if ($value === null) { + $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; + } else { + $parts[] = ['term' => [$attribute => $value]]; + } + } + } + } + return count($parts) === 1 ? $parts[0] : ['and' => $parts]; + } + + private function buildAndCondition($operator, $operands) + { + $parts = []; + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand); + } + if (!empty($operand)) { + $parts[] = $operand; + } + } + if (!empty($parts)) { + return [$operator => $parts]; + } else { + return []; + } + } + + private function buildBetweenCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new InvalidParamException("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + if ($column == ActiveRecord::PRIMARY_KEY_NAME) { + throw new NotSupportedException('Between condition is not supported for primaryKey.'); + } + $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; + if ($operator == 'not between') { + $filter = ['not' => $filter]; + } + return $filter; + } + + private function buildInCondition($operator, $operands) + { + if (!isset($operands[0], $operands[1])) { + throw new InvalidParamException("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if (empty($values) || $column === []) { + return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } elseif (is_array($column)) { + $column = reset($column); + } + $canBeNull = false; + foreach ($values as $i => $value) { + if (is_array($value)) { + $values[$i] = $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $canBeNull = true; + unset($values[$i]); + } + } + if ($column == ActiveRecord::PRIMARY_KEY_NAME) { + if (empty($values) && $canBeNull) { // there is no null pk + $filter = ['script' => ['script' => '0==1']]; + } else { + $filter = ['ids' => ['values' => array_values($values)]]; + if ($canBeNull) { + $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; + } + } + } else { + if (empty($values) && $canBeNull) { + $filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; + } else { + $filter = ['in' => [$column => array_values($values)]]; + if ($canBeNull) { + $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; + } + } + } + if ($operator == 'not in') { + $filter = ['not' => $filter]; + } + return $filter; + } + + protected function buildCompositeInCondition($operator, $columns, $values) + { + throw new NotSupportedException('composite in is not supported by elasticsearch.'); + $vss = array(); + foreach ($values as $value) { + $vs = array(); + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands) + { + throw new NotSupportedException('like conditions is not supported by elasticsearch.'); + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if (empty($values)) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0==1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = array(); + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } +} diff --git a/extensions/elasticsearch/README.md b/extensions/elasticsearch/README.md new file mode 100644 index 0000000..7988de1 --- /dev/null +++ b/extensions/elasticsearch/README.md @@ -0,0 +1,92 @@ +Elasticsearch Query and ActiveRecord for Yii 2 +============================================== + +This extension provides the [elasticsearch](http://www.elasticsearch.org/) integration for the Yii2 framework. +It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active +records in elasticsearch. + +To use this extension, you have to configure the Connection class in your application configuration: + +```php +return [ + //.... + 'components' => [ + 'elasticsearch' => [ + 'class' => 'yii\elasticsearch\Connection', + 'hosts' => [ + ['hostname' => 'localhost', 'port' => 9200], + // configure more hosts if you have a cluster + ], + ], + ] +]; +``` + + +Installation +------------ + +The preferred way to install this extension is through [composer](http://getcomposer.org/download/). + +Either run + +``` +php composer.phar require yiisoft/yii2-elasticsearch "*" +``` + +or add + +```json +"yiisoft/yii2-elasticsearch": "*" +``` + +to the require section of your composer.json. + + +Using the Query +--------------- + +TBD + +Using the ActiveRecord +---------------------- + +For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). + +For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and +implement at least the `attributes()` method to define the attributes of the record. +The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()` and can not be changed. +The primary key is not part of the attributes. + + primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified. +The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do +not specify your own primary key. + +The following is an example model called `Customer`: + +```php +class Customer extends \yii\elasticsearch\ActiveRecord +{ + public function attributes() + { + return ['id', 'name', 'address', 'registration_date']; + } +} +``` + +You may override [[index()]] and [[type()]] to define the index and type this record represents. + +The general usage of elasticsearch ActiveRecord is very similar to the database ActiveRecord as described in the +[guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). +It supports the same interface and features except the following limitations and additions(*!*): + +- As elasticsearch does not support SQL, the query API does not support `join()`, `groupBy()`, `having()` and `union()`. + Sorting, limit, offset and conditional where are all supported. +- `from()` does not select the tables, but the [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) + and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against. +- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology. + It defines the fields to retrieve from a document. +- `via`-relations can not be defined via a table as there are not tables in elasticsearch. You can only define relations via other records. +- As elasticsearch is a data storage and search engine there is of course support added for search your records. + TBD ... +- It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa. \ No newline at end of file diff --git a/extensions/elasticsearch/composer.json b/extensions/elasticsearch/composer.json new file mode 100644 index 0000000..9f5ed3a --- /dev/null +++ b/extensions/elasticsearch/composer.json @@ -0,0 +1,27 @@ +{ + "name": "yiisoft/yii2-elasticsearch", + "description": "Elasticsearch integration and ActiveRecord for the Yii framework", + "keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"], + "type": "yii2-extension", + "license": "BSD-3-Clause", + "support": { + "issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aelasticsearch", + "forum": "http://www.yiiframework.com/forum/", + "wiki": "http://www.yiiframework.com/wiki/", + "irc": "irc://irc.freenode.net/yii", + "source": "https://github.com/yiisoft/yii2" + }, + "authors": [ + { + "name": "Carsten Brandt", + "email": "mail@cebe.cc" + } + ], + "require": { + "yiisoft/yii2": "*" + }, + "autoload": { + "psr-0": { "yii\\elasticsearch\\": "" } + }, + "target-dir": "yii/elasticsearch" +} diff --git a/extensions/redis/ActiveQuery.php b/extensions/redis/ActiveQuery.php index 2174901..755fc6f 100644 --- a/extensions/redis/ActiveQuery.php +++ b/extensions/redis/ActiveQuery.php @@ -226,7 +226,7 @@ class ActiveQuery extends \yii\base\Component implements ActiveQueryInterface { $record = $this->one($db); if ($record !== null) { - return $record->$attribute; + return $record->hasAttribute($attribute) ? $record->$attribute : null; } else { return null; } diff --git a/extensions/redis/README.md b/extensions/redis/README.md index 28cecf1..86450dc 100644 --- a/extensions/redis/README.md +++ b/extensions/redis/README.md @@ -2,7 +2,7 @@ Redis Cache and ActiveRecord for Yii 2 ====================================== This extension provides the [redis](http://redis.io/) key-value store support for the Yii2 framework. -It includes a `Cache` class and implents the `ActiveRecord` pattern that allows you to store active +It includes a `Cache` class and implements the `ActiveRecord` pattern that allows you to store active records in redis. To use this extension, you have to configure the Connection class in your application configuration: diff --git a/framework/yii/elasticsearch/ActiveQuery.php b/framework/yii/elasticsearch/ActiveQuery.php deleted file mode 100644 index 2a99643..0000000 --- a/framework/yii/elasticsearch/ActiveQuery.php +++ /dev/null @@ -1,166 +0,0 @@ -with('orders')->asArray()->all(); - * ~~~ - * - * @author Carsten Brandt - * @since 2.0 - */ -class ActiveQuery extends Query implements ActiveQueryInterface -{ - use ActiveQueryTrait; - - /** - * 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) - { - /** @var ActiveRecord $modelClass */ - $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) - { - $command = $this->createCommand($db); - $result = $command->queryAll(); - if (empty($result['hits'])) { - return []; - } - $models = $this->createModels($result['hits']); - if ($this->asArray) { - foreach($models as $key => $model) { - $models[$key] = $model['_source']; - $models[$key][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; - } - } - if (!empty($this->with)) { - $this->findWith($this->with, $models); - } - 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) { - $model = $result['_source']; - $model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id']; - } else { - /** @var ActiveRecord $class */ - $class = $this->modelClass; - $model = $class::create($result); - } - if (!empty($this->with)) { - $models = [$model]; - $this->findWith($this->with, $models); - $model = $models[0]; - } - return $model; - } - - /** - * @inheritDocs - */ - public function scalar($field, $db = null) - { - $record = parent::one($db); - if ($record !== false) { - if ($field == ActiveRecord::PRIMARY_KEY_NAME) { - return $record['_id']; - } elseif (isset($record['_source'][$field])) { - return $record['_source'][$field]; - } - } - return null; - } - - /** - * @inheritDocs - */ - public function column($field, $db = null) - { - if ($field == ActiveRecord::PRIMARY_KEY_NAME) { - $command = $this->createCommand($db); - $command->queryParts['fields'] = []; - $rows = $command->queryAll()['hits']; - $result = []; - foreach ($rows as $row) { - $result[] = $row['_id']; - } - return $result; - } - return parent::column($field, $db); - } -} diff --git a/framework/yii/elasticsearch/ActiveRecord.php b/framework/yii/elasticsearch/ActiveRecord.php deleted file mode 100644 index 6a036cb..0000000 --- a/framework/yii/elasticsearch/ActiveRecord.php +++ /dev/null @@ -1,477 +0,0 @@ - - * @since 2.0 - */ -class ActiveRecord extends \yii\db\ActiveRecord -{ - const PRIMARY_KEY_NAME = 'id'; - - private $_id; - private $_version; - - /** - * 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->getComponent('elasticsearch'); - } - - /** - * @inheritDoc - */ - public static function find($q = null) - { - $query = static::createQuery(); - if (is_array($q)) { - if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q))) { - $pk = $q[ActiveRecord::PRIMARY_KEY_NAME]; - if (is_array($pk)) { - return static::mget($pk); - } else { - return static::get($pk); - } - } - return $query->where($q)->one(); - } elseif ($q !== null) { - return static::get($q); - } - return $query; - } - - /** - * 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.elasticsearch.org/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['exists']) { - return static::create($result); - } - 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.elasticsearch.org/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 mget($primaryKeys, $options = []) - { - if (empty($primaryKeys)) { - return []; - } - $command = static::getDb()->createCommand(); - $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); - $models = []; - foreach($result['docs'] as $doc) { - if ($doc['exists']) { - $models[] = static::create($doc); - } - } - return $models; - } - - // TODO add more like this feature http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-more-like-this.html - - // TODO add percolate functionality http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-percolate.html - - /** - * @inheritDoc - */ - public static function createQuery() - { - return new ActiveQuery(['modelClass' => get_called_class()]); - } - - /** - * @inheritDoc - */ - public static function createActiveRelation($config = []) - { - return new ActiveRelation($config); - } - - // TODO implement copy and move as pk change is not possible - - public function getId() - { - return $this->_id; - } - - /** - * Sets the primary key - * @param mixed $value - * @throws \yii\base\InvalidCallException when record is not new - */ - public function setId($value) - { - if ($this->isNewRecord) { - $this->_id = $value; - } else { - throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); - } - } - - /** - * @inheritDoc - */ - public function getPrimaryKey($asArray = false) - { - if ($asArray) { - return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id]; - } else { - return $this->_id; - } - } - - /** - * @inheritDoc - */ - public function getOldPrimaryKey($asArray = false) - { - $id = $this->isNewRecord ? null : $this->_id; - if ($asArray) { - return [ActiveRecord::PRIMARY_KEY_NAME => $id]; - } else { - return $this->_id; - } - } - - /** - * This method defines the primary. - * - * The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed. - * - * @return string[] the primary keys of this record. - */ - public static function primaryKey() - { - return [ActiveRecord::PRIMARY_KEY_NAME]; - } - - /** - * Returns the list of all attribute names of the model. - * This method must be overridden by child classes to define available attributes. - * @return array list of attribute names. - */ - public static function attributes() - { - throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); - } - - /** - * @return string the name of the index this record is stored in. - */ - public static function index() - { - return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); - } - - /** - * @return string the name of the type of this record. - */ - public static function type() - { - return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); - } - - /** - * Creates an active record object using a row of data. - * This method is called by [[ActiveQuery]] to populate the query results - * into Active Records. It is not meant to be used to create new records. - * @param array $row attribute values (name => value) - * @return ActiveRecord the newly created active record. - */ - public static function create($row) - { - $row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; - $record = parent::create($row['_source']); - return $record; - } - - /** - * 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.elasticsearch.org/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 boolean $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 - * - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. - * - * Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) - * for more details on these options. - * - * By default the `op_type` is set to `create`. - * @return boolean whether the attributes are valid and the record is inserted successfully. - */ - public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) - { - if ($runValidation && !$this->validate($attributes)) { - return false; - } - if ($this->beforeSave(true)) { - $values = $this->getDirtyAttributes($attributes); - - $response = static::getDb()->createCommand()->insert( - static::index(), - static::type(), - $values, - $this->getPrimaryKey(), - $options - ); - - if (!$response['ok']) { - return false; - } - $this->_id = $response['_id']; - $this->_version = $response['_version']; - $this->setOldAttributes($values); - $this->afterSave(true); - return true; - } - return false; - } - - /** - * 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(array('status' => 1), array(2, 3, 4)); - * ~~~ - * - * @param array $attributes attribute values (name-value pairs) to be saved into the table - * @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @param array $params this parameter is ignored in elasticsearch implementation. - * @return integer the number of rows updated - */ - public static function updateAll($attributes, $condition = [], $params = []) - { - if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { - $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; - } else { - $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); - } - if (empty($primaryKeys)) { - return 0; - } - $bulk = ''; - foreach((array) $primaryKeys as $pk) { - $action = Json::encode([ - "update" => [ - "_id" => $pk, - "_type" => static::type(), - "_index" => static::index(), - ], - ]); - $data = Json::encode(array( - "doc" => $attributes - )); - $bulk .= $action . "\n" . $data . "\n"; - } - - // TODO do this via command - $url = '/' . static::index() . '/' . static::type() . '/_bulk'; - $response = static::getDb()->http()->post($url, null, $bulk)->send(); - $body = Json::decode($response->getBody(true)); - $n=0; - foreach($body['items'] as $item) { - if ($item['update']['ok']) { - $n++; - } - // TODO might want to update the _version in update() - } - return $n; - } - - - /** - * 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 put in the WHERE part of the DELETE SQL. - * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. - * @param array $params this parameter is ignored in elasticsearch implementation. - * @return integer the number of rows deleted - */ - public static function deleteAll($condition = [], $params = []) - { - if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { - $primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; - } else { - $primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); - } - if (empty($primaryKeys)) { - return 0; - } - $bulk = ''; - foreach((array) $primaryKeys as $pk) { - $bulk .= Json::encode([ - "delete" => [ - "_id" => $pk, - "_type" => static::type(), - "_index" => static::index(), - ], - ]) . "\n"; - } - - // TODO do this via command - $url = '/' . static::index() . '/' . static::type() . '/_bulk'; - $response = static::getDb()->http()->post($url, null, $bulk)->send(); - $body = Json::decode($response->getBody(true)); - $n=0; - foreach($body['items'] as $item) { - if ($item['delete']['found'] && $item['delete']['ok']) { - $n++; - } - } - return $n; - } - - /** - * @inheritdoc - */ - public static function updateAllCounters($counters, $condition = null, $params = []) - { - throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.'); - } - - /** - * @inheritdoc - */ - public static function getTableSchema() - { - throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.'); - } - - /** - * @inheritDoc - */ - public static function tableName() - { - return static::index() . '/' . static::type(); - } - - /** - * @inheritdoc - */ - public static function findBySql($sql, $params = []) - { - throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.'); - } - - /** - * Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. - * This method will always return false as transactional operations are not supported by elasticsearch. - * @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. - * @return boolean whether the specified operation is transactional in the current [[scenario]]. - */ - public function isTransactional($operation) - { - return false; - } -} diff --git a/framework/yii/elasticsearch/ActiveRelation.php b/framework/yii/elasticsearch/ActiveRelation.php deleted file mode 100644 index a102697..0000000 --- a/framework/yii/elasticsearch/ActiveRelation.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @since 2.0 - */ -class ActiveRelation extends ActiveQuery implements ActiveRelationInterface -{ - use ActiveRelationTrait; - - /** - * 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 ActiveRelation $viaQuery */ - list($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]); - } - } - return parent::createCommand($db); - } -} diff --git a/framework/yii/elasticsearch/Cluster.php b/framework/yii/elasticsearch/Cluster.php deleted file mode 100644 index fda4175..0000000 --- a/framework/yii/elasticsearch/Cluster.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ - -namespace yii\elasticsearch; - - -use yii\base\Object; - -class Cluster extends Object -{ - // TODO implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster.html -} \ No newline at end of file diff --git a/framework/yii/elasticsearch/Command.php b/framework/yii/elasticsearch/Command.php deleted file mode 100644 index 35334f4..0000000 --- a/framework/yii/elasticsearch/Command.php +++ /dev/null @@ -1,430 +0,0 @@ - - */ - -namespace yii\elasticsearch; - - -use Guzzle\Http\Exception\ClientErrorResponseException; -use yii\base\Component; -use yii\db\Exception; -use yii\helpers\Json; - -// camelCase vs. _ -// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/common-options.html#_result_casing - - -/** - * Class Command - * - * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html - * - */ -class Command extends Component -{ - /** - * @var Connection - */ - public $db; - /** - * @var string|array the indexes to execute the query on. Defaults to null meaning all indexes - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index - */ - public $index; - /** - * @var string|array the types to execute the query on. Defaults to null meaning all types - */ - public $type; - /** - * @var array list of arrays or json strings that become parts of a query - */ - public $queryParts; - - public $options = []; - - public function queryAll($options = []) - { - $query = $this->queryParts; - if (empty($query)) { - $query = '{}'; - } - if (is_array($query)) { - $query = Json::encode($query); - } - $url = [ - $this->index !== null ? $this->index : '_all', - $this->type !== null ? $this->type : '_all', - '_search' - ]; - try { - $response = $this->db->http()->post($this->createUrl($url, $options), null, $query)->send(); - } catch(ClientErrorResponseException $e) { - throw new Exception("elasticsearch error:\n\n" - . $query . "\n\n" . $e->getMessage() - . print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e); - } - return Json::decode($response->getBody(true))['hits']; - } - - public function queryCount($options = []) - { - $options['search_type'] = 'count'; - return $this->queryAll($options); - } - - - /** - * Inserts a document into an index - * @param string $index - * @param string $type - * @param string|array $data json string or array of data to store - * @param null $id the documents id. If not specified Id will be automatically choosen - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html - */ - public function insert($index, $type, $data, $id = null, $options = []) - { - $body = is_array($data) ? Json::encode($data) : $data; - - try { - if ($id !== null) { - $response = $this->db->http()->put($this->createUrl([$index, $type, $id], $options), null, $body)->send(); - } else { - $response = $this->db->http()->post($this->createUrl([$index, $type], $options), null, $body)->send(); - } - } catch(ClientErrorResponseException $e) { - throw new Exception("elasticsearch error:\n\n" - . $body . "\n\n" . $e->getMessage() - . print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e); - } - return Json::decode($response->getBody(true)); - } - - /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html - */ - public function get($index, $type, $id, $options = []) - { - $httpOptions = [ - 'exceptions' => false, - ]; - $response = $this->db->http()->get($this->createUrl([$index, $type, $id], $options), null, $httpOptions)->send(); - if ($response->getStatusCode() == 200 || $response->getStatusCode() == 404) { - return Json::decode($response->getBody(true)); - } else { - throw new Exception('Elasticsearch request failed.'); - } - } - - /** - * gets multiple documents from the index - * - * TODO allow specifying type and index + fields - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html - */ - public function mget($index, $type, $ids, $options = []) - { - $httpOptions = [ - 'exceptions' => false, - ]; - $body = Json::encode(['ids' => array_values($ids)]); - $response = $this->db->http()->post( // TODO guzzle does not manage to send get request with content - $this->createUrl([$index, $type, '_mget'], $options), - null, - $body, - $httpOptions - )->send(); - if ($response->getStatusCode() == 200) { - return Json::decode($response->getBody(true)); - } else { - throw new Exception('Elasticsearch request failed.'); - } - } - - /** - * gets a documents _source from the index (>=v0.90.1) - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source - */ - public function getSource($index, $type, $id) - { - $response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); - return Json::decode($response->getBody(true)); - } - - // TODO mget http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html - - /** - * gets a document from the index - * @param $index - * @param $type - * @param $id - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html - */ - public function exists($index, $type, $id) - { - $response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); - return $response->getStatusCode() == 200; - } - - /** - * deletes a document from the index - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html - */ - public function delete($index, $type, $id, $options = []) - { - $response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); - return Json::decode($response->getBody(true)); - } - - /** - * updates a document - * @param $index - * @param $type - * @param $id - * @param array $options - * @return mixed - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html - */ - public function update($index, $type, $id, $data, $options = []) - { - // TODO - $response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); - return Json::decode($response->getBody(true)); - } - - // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html - - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html - */ - public function createIndex($index, $configuration = null) - { - $body = $configuration !== null ? Json::encode($configuration) : null; - $response = $this->db->http()->put($this->createUrl([$index]), null, $body)->send(); - return Json::decode($response->getBody(true)); - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html - */ - public function deleteIndex($index) - { - $response = $this->db->http()->delete($this->createUrl([$index]))->send(); - return Json::decode($response->getBody(true)); - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html - */ - public function deleteAllIndexes() - { - $response = $this->db->http()->delete($this->createUrl(['_all']))->send(); - return Json::decode($response->getBody(true)); - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html - */ - public function indexExists($index) - { - $response = $this->db->http()->head($this->createUrl([$index]))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html - */ - public function typeExists($index, $type) - { - $response = $this->db->http()->head($this->createUrl([$index, $type]))->send(); - return $response->getStatusCode() == 200; - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html - */ - public function openIndex($index) - { - $response = $this->db->http()->post($this->createUrl([$index, '_open']))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html - */ - public function closeIndex($index) - { - $response = $this->db->http()->post($this->createUrl([$index, '_close']))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html - */ - public function getIndexStatus($index = '_all') - { - $response = $this->db->http()->get($this->createUrl([$index, '_status']))->send(); - return Json::decode($response->getBody(true)); - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html - // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html - */ - public function clearIndexCache($index) - { - $response = $this->db->http()->post($this->createUrl([$index, '_cache', 'clear']))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html - */ - public function flushIndex($index = '_all') - { - $response = $this->db->http()->post($this->createUrl([$index, '_flush']))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html - */ - public function refreshIndex($index) - { - $response = $this->db->http()->post($this->createUrl([$index, '_refresh']))->send(); - return $response->getStatusCode() == 200; - } - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html - - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html - - /** - * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html - */ - public function setMapping($index, $type, $mapping) - { - $body = $mapping !== null ? Json::encode($mapping) : null; - $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']), null, $body)->send(); - return $response->getStatusCode() == 200; - } - - /** - * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html - */ - public function getMapping($index = '_all', $type = '_all') - { - $response = $this->db->http()->get($this->createUrl([$index, $type, '_mapping']))->send(); - return Json::decode($response->getBody(true)); - } - - /** - * http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html - */ - public function deleteMapping($index, $type) - { - $response = $this->db->http()->delete($this->createUrl([$index, $type]))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html - */ - public function getFieldMapping($index, $type = '_all') - { - // TODO - $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send(); - return Json::decode($response->getBody(true)); - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html - */ - public function analyze($options, $index = null) - { - // TODO - $response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send(); - return Json::decode($response->getBody(true)); - - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) - { - $body = Json::encode([ - 'template' => $pattern, - 'order' => $order, - 'settings' => (object) $settings, - 'mappings' => (object) $settings, - ]); - $response = $this->db->http()->put($this->createUrl(['_template', $name]), null, $body)->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function deleteTemplate($name) - { - $response = $this->db->http()->delete($this->createUrl(['_template', $name]))->send(); - return $response->getStatusCode() == 200; - } - - /** - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html - */ - public function getTemplate($name) - { - $response = $this->db->http()->get($this->createUrl(['_template', $name]))->send(); - return Json::decode($response->getBody(true)); - } - - private function createUrl($path, $options = []) - { - $url = implode('/', array_map(function($a) { - return urlencode(is_array($a) ? implode(',', $a) : $a); - }, $path)); - - if (!empty($options) || !empty($this->options)) { - $options = array_merge($this->options, $options); - $url .= '?' . http_build_query($options); - } - - return $url; - } -} \ No newline at end of file diff --git a/framework/yii/elasticsearch/Connection.php b/framework/yii/elasticsearch/Connection.php deleted file mode 100644 index 46c1efb..0000000 --- a/framework/yii/elasticsearch/Connection.php +++ /dev/null @@ -1,196 +0,0 @@ - - * @since 2.0 - */ -class Connection extends Component -{ - /** - * @event Event an event that is triggered after a DB connection is established - */ - const EVENT_AFTER_OPEN = 'afterOpen'; - - // TODO add autodetection of cluster nodes - // http://localhost:9200/_cluster/nodes - public $nodes = array( - array( - 'host' => 'localhost', - 'port' => 9200, - ) - ); - - // http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth - public $auth = []; - - // TODO use timeouts - /** - * @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") - */ - public $connectionTimeout = null; - /** - * @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. - */ - public $dataTimeout = null; - - - - public function init() - { - if ($this->nodes === array()) { - throw new InvalidConfigException('elasticsearch needs at least one node.'); - } - } - - /** - * Creates a command for execution. - * @param string $query the SQL statement to be executed - * @return Command the DB command - */ - public function createCommand($config = []) - { - $this->open(); - $config['db'] = $this; - $command = new Command($config); - return $command; - } - - /** - * Closes the connection when this component is being serialized. - * @return array - */ - public function __sleep() - { - $this->close(); - return array_keys(get_object_vars($this)); - } - - /** - * Returns a value indicating whether the DB connection is established. - * @return boolean whether the DB connection is established - */ - public function getIsActive() - { - return false; // TODO implement - } - - /** - * Establishes a DB connection. - * It does nothing if a DB connection has already been established. - * @throws Exception if connection fails - */ - public function open() - { - // TODO select one node to be the active one. - - - foreach($this->nodes as $key => $node) { - if (is_array($node)) { - $this->nodes[$key] = new Node($node); - } - } -/* if ($this->_socket === null) { - if (empty($this->dsn)) { - throw new InvalidConfigException('Connection.dsn cannot be empty.'); - } - $dsn = explode('/', $this->dsn); - $host = $dsn[2]; - if (strpos($host, ':')===false) { - $host .= ':6379'; - } - $db = isset($dsn[3]) ? $dsn[3] : 0; - - \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); - $this->_socket = @stream_socket_client( - $host, - $errorNumber, - $errorDescription, - $this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout") - ); - if ($this->_socket) { - if ($this->dataTimeout !== null) { - stream_set_timeout($this->_socket, $timeout=(int)$this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000)); - } - if ($this->password !== null) { - $this->executeCommand('AUTH', array($this->password)); - } - $this->executeCommand('SELECT', array($db)); - $this->initConnection(); - } else { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__); - $message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.'; - throw new Exception($message, $errorDescription, (int)$errorNumber); - } - }*/ - // TODO implement - } - - /** - * Closes the currently active DB connection. - * It does nothing if the connection is already closed. - */ - public function close() - { - // TODO implement -/* if ($this->_socket !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); - $this->executeCommand('QUIT'); - stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR); - $this->_socket = null; - $this->_transaction = 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'; - } - - public function getNodeInfo() - { - // TODO HTTP request to localhost:9200/ - } - - public function getQueryBuilder() - { - return new QueryBuilder($this); - } - - /** - * @return \Guzzle\Http\Client - */ - public function http() - { - $guzzle = new \Guzzle\Http\Client('http://localhost:9200/'); - //$guzzle->setDefaultOption() - return $guzzle; - } -} \ No newline at end of file diff --git a/framework/yii/elasticsearch/Node.php b/framework/yii/elasticsearch/Node.php deleted file mode 100644 index 60d5956..0000000 --- a/framework/yii/elasticsearch/Node.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @since 2.0 - */ -class Node extends Object -{ - public $host; - public $port; -} \ No newline at end of file diff --git a/framework/yii/elasticsearch/Query.php b/framework/yii/elasticsearch/Query.php deleted file mode 100644 index 23d9de1..0000000 --- a/framework/yii/elasticsearch/Query.php +++ /dev/null @@ -1,399 +0,0 @@ - - * @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, it means retrieving all fields. An empty array will result in no fields being - * retrieved. This means that only the primaryKey of a record will be available in the result. - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields - * @see fields() - */ - public $fields; - /** - * @var string|array 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 string|array 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 integer 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.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 - */ - public $timeout; - - public $query; - - public $filter; - - public $facets = []; - - public $facetResults = []; - - public $totalCount; - - /** - * 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->getComponent('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)->queryAll(); - // TODO publish facet results - $rows = $result['hits']; - if ($this->indexBy === null && $this->fields === null) { - return $rows; - } - $models = []; - foreach ($rows as $key => $row) { - if ($this->fields !== null) { - $row['_source'] = isset($row['fields']) ? $row['fields'] : []; - unset($row['fields']); - } - if ($this->indexBy !== null) { - if (is_string($this->indexBy)) { - $key = $row['_source'][$this->indexBy]; - } else { - $key = call_user_func($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|boolean 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) - { - $options['size'] = 1; - $result = $this->createCommand($db)->queryAll($options); - // TODO publish facet results - if (empty($result['hits'])) { - return false; - } - $record = reset($result['hits']); - if ($this->fields !== null) { - $record['_source'] = isset($record['fields']) ? $record['fields'] : []; - unset($record['fields']); - } - return $record; - } - - /** - * Executes the query and deletes all matching documents. - * - * This will not run facet queries. - * - * @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 delete($db = null) - { - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html - } - - /** - * 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 && isset($record['_source'][$field])) { - return $record['_source'][$field]; - } else { - 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['fields'] = [$field]; - $rows = $command->queryAll()['hits']; - $result = []; - foreach ($rows as $row) { - $result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; - } - return $result; - } - - /** - * 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 integer number of records - */ - public function count($q = '*', $db = null) - { - // TODO consider sending to _count api instead of _search for performance - // only when no facety are registerted. - // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html - - $count = $this->createCommand($db)->queryCount()['total']; - if ($this->limit === null && $this->offset === null) { - return $count; - } elseif ($this->offset !== null) { - $count = $this->offset < $count ? $count - $this->offset : 0; - } - return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); - } - - /** - * 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 boolean whether the query result contains any row of data. - */ - public function exists($db = null) - { - return self::one($db) !== false; - } - - /** - * Adds a facet search to this query. - * @param string $name the name of this facet - * @param string $type the facet type. e.g. `terms`, `range`, `histogram`... - * @param string|array $options the configuration options for this facet. Can be an array or a json string. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html - */ - public function addFacet($name, $type, $options) - { - $this->facets[$name] = [$type => $options]; - return $this; - } - - /** - * The `terms facet` allow to specify field facets that return the N most frequent terms. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html - */ - public function addTermFacet($name, $options) - { - return $this->addFacet($name, 'terms', $options); - } - - /** - * Range facet allows to specify a set of ranges and get both the number of docs (count) that fall - * within each range, and aggregated data either based on the field, or using another field. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html - */ - public function addRangeFacet($name, $options) - { - return $this->addFacet($name, 'range', $options); - } - - /** - * The histogram facet works with numeric data by building a histogram across intervals of the field values. - * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per - * interval/bucket (count and total). - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html - */ - public function addHistogramFacet($name, $options) - { - return $this->addFacet($name, 'histogram', $options); - } - - /** - * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html - */ - public function addDateHistogramFacet($name, $options) - { - return $this->addFacet($name, 'date_histogram', $options); - } - - /** - * A filter facet (not to be confused with a facet filter) allows you to return a count of the hits matching the filter. - * The filter itself can be expressed using the Query DSL. - * @param string $name the name of this facet - * @param string $filter the query in Query DSL - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-filter-facet.html - */ - public function addFilterFacet($name, $filter) - { - return $this->addFacet($name, 'filter', $filter); - } - - /** - * A facet query allows to return a count of the hits matching the facet query. - * The query itself can be expressed using the Query DSL. - * @param string $name the name of this facet - * @param string $query the query in Query DSL - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html - */ - public function addQueryFacet($name, $query) - { - return $this->addFacet($name, 'query', $query); - } - - /** - * Statistical facet allows to compute statistical data on a numeric fields. The statistical data include count, - * total, sum of squares, mean (average), minimum, maximum, variance, and standard deviation. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-statistical-facet.html - */ - public function addStatisticalFacet($name, $options) - { - return $this->addFacet($name, 'statistical', $options); - } - - /** - * The `terms_stats` facet combines both the terms and statistical allowing to compute stats computed on a field, - * per term value driven by another field. - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-stats-facet.html - */ - public function addTermsStatsFacet($name, $options) - { - return $this->addFacet($name, 'terms_stats', $options); - } - - /** - * The `geo_distance` facet is a facet providing information for ranges of distances from a provided `geo_point` - * including count of the number of hits that fall within each range, and aggregation information (like `total`). - * @param string $name the name of this facet - * @param array $options additional option. Please refer to the elasticsearch documentation for details. - * @return static - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-geo-distance-facet.html - */ - public function addGeoDistanceFacet($name, $options) - { - return $this->addFacet($name, 'geo_distance', $options); - } - - // TODO add suggesters http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-suggesters.html - - // TODO add validate query http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-validate.html - - // TODO support multi query via static method http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-multi-search.html - - public function query() - { - - } - - /** - * Sets the index and type to retrieve documents from. - * @param string|array $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 string|array $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. - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type - */ - public function from($index, $type = null) - { - $this->index = $index; - $this->type = $type; - } - - /** - * Sets the fields to retrieve from the documents. - * @param array $fields the fields to be selected. - * @return static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html - */ - public function fields($fields) - { - $this->fields = $fields; - return $this; - } - - /** - * Sets the search timeout. - * @param integer $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 static the query object itself - * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 - */ - public function timeout($timeout) - { - $this->timeout = $timeout; - return $this; - } - -} \ No newline at end of file diff --git a/framework/yii/elasticsearch/QueryBuilder.php b/framework/yii/elasticsearch/QueryBuilder.php deleted file mode 100644 index c008de1..0000000 --- a/framework/yii/elasticsearch/QueryBuilder.php +++ /dev/null @@ -1,336 +0,0 @@ - - * @since 2.0 - */ -class QueryBuilder extends \yii\base\Object -{ - /** - * @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->fields !== null) { - $parts['fields'] = (array) $query->fields; - } - if ($query->limit !== null && $query->limit >= 0) { - $parts['size'] = $query->limit; - } - if ($query->offset > 0) { - $parts['from'] = (int) $query->offset; - } - - $filters = empty($query->filter) ? [] : [$query->filter]; - $whereFilter = $this->buildCondition($query->where); - if (!empty($whereFilter)) { - $filters[] = $whereFilter; - } - if (!empty($filters)) { - $parts['filter'] = count($filters) > 1 ? ['and' => $filters] : $filters[0]; - } - - $sort = $this->buildOrderBy($query->orderBy); - if (!empty($sort)) { - $parts['sort'] = $sort; - } - - if (empty($parts['query'])) { - $parts['query'] = ["match_all" => (object)[]]; - } - - $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 == ActiveRecord::PRIMARY_KEY_NAME) { - $column = '_id'; - } - - // allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ - if (is_array($direction)) { - $orders[] = [$column => $direction]; - } else { - $orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; - } - } - return $orders; - } - - /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @param array $params the binding parameters to be populated - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = array( - 'and' => 'buildAndCondition', - 'or' => 'buildAndCondition', - 'between' => 'buildBetweenCondition', - 'not between' => 'buildBetweenCondition', - 'in' => 'buildInCondition', - 'not in' => 'buildInCondition', - 'like' => 'buildLikeCondition', - 'not like' => 'buildLikeCondition', - 'or like' => 'buildLikeCondition', - 'or not like' => 'buildLikeCondition', - ); - - 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); - } else { - 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 = []; - foreach($condition as $attribute => $value) { - if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) { - if ($value == null) { // there is no null pk - $parts[] = ['script' => ['script' => '0==1']]; - } else { - $parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; - } - } else { - if (is_array($value)) { // IN condition - $parts[] = ['in' => [$attribute => $value]]; - } else { - if ($value === null) { - $parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; - } else { - $parts[] = ['term' => [$attribute => $value]]; - } - } - } - } - return count($parts) === 1 ? $parts[0] : ['and' => $parts]; - } - - private function buildAndCondition($operator, $operands) - { - $parts = []; - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if (!empty($operand)) { - $parts[] = $operand; - } - } - if (!empty($parts)) { - return [$operator => $parts]; - } else { - return []; - } - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new InvalidParamException("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - if ($column == ActiveRecord::PRIMARY_KEY_NAME) { - throw new NotSupportedException('Between condition is not supported for primaryKey.'); - } - $filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; - if ($operator == 'not between') { - $filter = ['not' => $filter]; - } - return $filter; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new InvalidParamException("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values) || $column === []) { - return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values, $params); - } elseif (is_array($column)) { - $column = reset($column); - } - $canBeNull = false; - foreach ($values as $i => $value) { - if (is_array($value)) { - $values[$i] = $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $canBeNull = true; - unset($values[$i]); - } - } - if ($column == ActiveRecord::PRIMARY_KEY_NAME) { - if (empty($values) && $canBeNull) { // there is no null pk - $filter = ['script' => ['script' => '0==1']]; - } else { - $filter = ['ids' => ['values' => array_values($values)]]; - if ($canBeNull) { - $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; - } - } - } else { - if (empty($values) && $canBeNull) { - $filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; - } else { - $filter = ['in' => [$column => array_values($values)]]; - if ($canBeNull) { - $filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; - } - } - } - if ($operator == 'not in') { - $filter = ['not' => $filter]; - } - return $filter; - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - throw new NotSupportedException('composite in is not supported by elasticsearch.'); - $vss = array(); - foreach ($values as $value) { - $vs = array(); - foreach ($columns as $column) { - if (isset($value[$column])) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value[$column]; - $vs[] = $phName; - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands) - { - throw new NotSupportedException('like conditions is not supported by elasticsearch.'); - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if (empty($values)) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0==1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = array(); - foreach ($values as $value) { - $phName = self::PARAM_PREFIX . count($params); - $params[$phName] = $value; - $parts[] = "$column $operator $phName"; - } - - return implode($andor, $parts); - } -} diff --git a/tests/unit/data/ar/elasticsearch/Customer.php b/tests/unit/data/ar/elasticsearch/Customer.php index 6f7f79c..7d00dd3 100644 --- a/tests/unit/data/ar/elasticsearch/Customer.php +++ b/tests/unit/data/ar/elasticsearch/Customer.php @@ -1,7 +1,7 @@ getConnection()->createCommand()->flushIndex(); + } + + public function setUp() + { + parent::setUp(); + + /** @var Connection $db */ + $db = ActiveRecord::$db = $this->getConnection(); + + // delete all indexes + $db->http()->delete('_all')->send(); + + $db->http()->post('items', null, Json::encode([ + 'mappings' => [ + "item" => [ + "_source" => [ "enabled" => true ], + "properties" => [ + // allow proper sorting by name + "name" => ["type" => "string", "index" => "not_analyzed"], + ] + ] + ], + ]))->send(); + + $db->http()->post('customers', null, Json::encode([ + 'mappings' => [ + "item" => [ + "_source" => [ "enabled" => true ], + "properties" => [ + // this is for the boolean test + "status" => ["type" => "boolean"], + ] + ] + ], + ]))->send(); + + $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 tbl_category (name) VALUES ('Books'); +// INSERT INTO tbl_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, 'create_time' => 1325282384, 'total' => 110.0], false); + $order->save(false); + $order = new Order(); + $order->id = 2; + $order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false); + $order->save(false); + $order = new Order(); + $order->id = 3; + $order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], 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); + + Customer::getDb()->createCommand()->flushIndex(); + } + + 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->assertEquals(1, count($records)); + $this->assertInstanceOf(Customer::className(), reset($records)); + + $records = Customer::mget([5]); + $this->assertEquals(0, count($records)); + + $records = Customer::mget([1,3,5]); + $this->assertEquals(2, count($records)); + $this->assertInstanceOf(Customer::className(), $records[0]); + $this->assertInstanceOf(Customer::className(), $records[1]); + } + + public function testFindLazy() + { + /** @var $customer Customer */ + $customer = Customer::find(2); + $orders = $customer->orders; + $this->assertEquals(2, count($orders)); + + $orders = $customer->getOrders()->where(['between', 'create_time', 1325334000, 1325400000])->all(); + $this->assertEquals(1, count($orders)); + $this->assertEquals(2, $orders[0]->id); + } + + public function testFindEagerViaRelation() + { + // this test is currently failing randomly because of https://github.com/yiisoft/yii2/issues/1310 + $orders = Order::find()->with('items')->orderBy('create_time')->all(); + $this->assertEquals(3, count($orders)); + $order = $orders[0]; + $this->assertEquals(1, $order->id); + $this->assertEquals(2, count($order->items)); + $this->assertEquals(1, $order->items[0]->id); + $this->assertEquals(2, $order->items[1]->id); + } + + public function testInsertNoPk() + { + $this->assertEquals([ActiveRecord::PRIMARY_KEY_NAME], Customer::primaryKey()); + $pkName = ActiveRecord::PRIMARY_KEY_NAME; + + $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->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 = ActiveRecord::PRIMARY_KEY_NAME; + + $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 = ActiveRecord::PRIMARY_KEY_NAME; + + $pk = [$pkName => 2]; + $orderItem = Order::find($pk); + $this->assertEquals(2, $orderItem->primaryKey); + $this->assertEquals(2, $orderItem->oldPrimaryKey); + $this->assertEquals(2, $orderItem->$pkName); + + $this->setExpectedException('yii\base\InvalidCallException'); + $orderItem->$pkName = 13; + $orderItem->save(); + } + + public function testFindLazyVia2() + { + /** @var TestCase|ActiveRecordTestTrait $this */ + /** @var Order $order */ + $orderClass = $this->getOrderClass(); + $pkName = ActiveRecord::PRIMARY_KEY_NAME; + + $order = new $orderClass(); + $order->$pkName = 100; + $this->assertEquals([], $order->items); + } + + public function testUpdateCounters() + { + // Update Counters is not supported by elasticsearch +// $this->setExpectedException('yii\base\NotSupportedException'); +// ActiveRecordTestTrait::testUpdateCounters(); + } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + $db = $this->getConnection(); + $db->createCommand()->deleteIndex('customers'); + $db->http()->post('customers', null, Json::encode([ + 'mappings' => [ + "item" => [ + "_source" => [ "enabled" => true ], + "properties" => [ + // this is for the boolean test + "status" => ["type" => "boolean"], + ] + ] + ], + ]))->send(); + + $customerClass = $this->getCustomerClass(); + $customer = new $customerClass(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(true, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(false, $customer->status); + + $customer = new Customer(); + $customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false); + $customer->save(false); + $customer = new Customer(); + $customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false); + $customer->save(false); + $this->afterSave(); + + $customers = $this->callCustomerFind()->where(['status' => true])->all(); + $this->assertEquals(1, count($customers)); + + $customers = $this->callCustomerFind()->where(['status' => false])->all(); + $this->assertEquals(2, count($customers)); + } +} \ No newline at end of file diff --git a/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php new file mode 100644 index 0000000..7e04d90 --- /dev/null +++ b/tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php @@ -0,0 +1,14 @@ +mockApplication(); + + $databases = $this->getParam('databases'); + $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null; + if ($params === null || !isset($params['dsn'])) { + $this->markTestSkipped('No elasticsearch server connection configured.'); + } + $dsn = explode('/', $params['dsn']); + $host = $dsn[2]; + if (strpos($host, ':')===false) { + $host .= ':9200'; + } + if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { + $this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); + } + + parent::setUp(); + } + + /** + * @param bool $reset whether to clean up the test database + * @return Connection + */ + public function getConnection($reset = true) + { + $databases = $this->getParam('databases'); + $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array(); + $db = new Connection; + if ($reset) { + $db->open(); + } + return $db; + } +} \ No newline at end of file diff --git a/tests/unit/extensions/elasticsearch/QueryTest.php b/tests/unit/extensions/elasticsearch/QueryTest.php new file mode 100644 index 0000000..a520433 --- /dev/null +++ b/tests/unit/extensions/elasticsearch/QueryTest.php @@ -0,0 +1,182 @@ +getConnection()->createCommand(); + + $command->deleteAllIndexes(); + + $command->insert('test', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); + $command->insert('test', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); + $command->insert('test', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); + $command->insert('test', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); + + $command->flushIndex(); + } + + public function testFields() + { + $query = new Query; + $query->from('test', 'user'); + + $query->fields(['name', 'status']); + $this->assertEquals(['name', 'status'], $query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals(2, count($result['_source'])); + $this->assertArrayHasKey('status', $result['_source']); + $this->assertArrayHasKey('name', $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $query->fields([]); + $this->assertEquals([], $query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals([], $result['_source']); + $this->assertArrayHasKey('_id', $result); + + $query->fields(null); + $this->assertNull($query->fields); + + $result = $query->one($this->getConnection()); + $this->assertEquals(3, count($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('test', 'user'); + + $result = $query->one($this->getConnection()); + $this->assertEquals(3, count($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->assertEquals(3, count($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' => 'user5'])->one($this->getConnection()); + $this->assertFalse($result); + } + + public function testAll() + { + $query = new Query; + $query->from('test', 'user'); + + $results = $query->all($this->getConnection()); + $this->assertEquals(4, count($results)); + $result = reset($results); + $this->assertEquals(3, count($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('test', 'user'); + + $results = $query->where(['name' => 'user1'])->all($this->getConnection()); + $this->assertEquals(1, count($results)); + $result = reset($results); + $this->assertEquals(3, count($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('test', 'user'); + + $results = $query->indexBy('name')->all($this->getConnection()); + $this->assertEquals(4, count($results)); + ksort($results); + $this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results)); + } + + public function testScalar() + { + $query = new Query; + $query->from('test', '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' => 'user5'])->scalar('name', $this->getConnection()); + $this->assertNull($result); + } + + public function testColumn() + { + $query = new Query; + $query->from('test', 'user'); + + $result = $query->orderBy(['name' => SORT_ASC])->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' => 'user5'])->scalar('name', $this->getConnection()); + $this->assertNull($result); + + } + + // 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() + { + } +} diff --git a/tests/unit/framework/elasticsearch/ActiveRecordTest.php b/tests/unit/framework/elasticsearch/ActiveRecordTest.php deleted file mode 100644 index 2264de3..0000000 --- a/tests/unit/framework/elasticsearch/ActiveRecordTest.php +++ /dev/null @@ -1,328 +0,0 @@ -getConnection()->createCommand()->flushIndex(); - } - - public function setUp() - { - parent::setUp(); - - /** @var Connection $db */ - $db = ActiveRecord::$db = $this->getConnection(); - - // delete all indexes - $db->http()->delete('_all')->send(); - - $db->http()->post('items', null, Json::encode([ - 'mappings' => [ - "item" => [ - "_source" => [ "enabled" => true ], - "properties" => [ - // allow proper sorting by name - "name" => ["type" => "string", "index" => "not_analyzed"], - ] - ] - ], - ]))->send(); - - $db->http()->post('customers', null, Json::encode([ - 'mappings' => [ - "item" => [ - "_source" => [ "enabled" => true ], - "properties" => [ - // this is for the boolean test - "status" => ["type" => "boolean"], - ] - ] - ], - ]))->send(); - - $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 tbl_category (name) VALUES ('Books'); -// INSERT INTO tbl_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, 'create_time' => 1325282384, 'total' => 110.0], false); - $order->save(false); - $order = new Order(); - $order->id = 2; - $order->setAttributes(['customer_id' => 2, 'create_time' => 1325334482, 'total' => 33.0], false); - $order->save(false); - $order = new Order(); - $order->id = 3; - $order->setAttributes(['customer_id' => 2, 'create_time' => 1325502201, 'total' => 40.0], 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); - - Customer::getDb()->createCommand()->flushIndex(); - } - - 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->assertEquals(1, count($records)); - $this->assertInstanceOf(Customer::className(), reset($records)); - - $records = Customer::mget([5]); - $this->assertEquals(0, count($records)); - - $records = Customer::mget([1,3,5]); - $this->assertEquals(2, count($records)); - $this->assertInstanceOf(Customer::className(), $records[0]); - $this->assertInstanceOf(Customer::className(), $records[1]); - } - - public function testFindLazy() - { - /** @var $customer Customer */ - $customer = Customer::find(2); - $orders = $customer->orders; - $this->assertEquals(2, count($orders)); - - $orders = $customer->getOrders()->where(['between', 'create_time', 1325334000, 1325400000])->all(); - $this->assertEquals(1, count($orders)); - $this->assertEquals(2, $orders[0]->id); - } - - public function testFindEagerViaRelation() - { - // this test is currently failing randomly because of https://github.com/yiisoft/yii2/issues/1310 - $orders = Order::find()->with('items')->orderBy('create_time')->all(); - $this->assertEquals(3, count($orders)); - $order = $orders[0]; - $this->assertEquals(1, $order->id); - $this->assertEquals(2, count($order->items)); - $this->assertEquals(1, $order->items[0]->id); - $this->assertEquals(2, $order->items[1]->id); - } - - public function testInsertNoPk() - { - $this->assertEquals([ActiveRecord::PRIMARY_KEY_NAME], Customer::primaryKey()); - $pkName = ActiveRecord::PRIMARY_KEY_NAME; - - $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->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 = ActiveRecord::PRIMARY_KEY_NAME; - - $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 = ActiveRecord::PRIMARY_KEY_NAME; - - $pk = [$pkName => 2]; - $orderItem = Order::find($pk); - $this->assertEquals(2, $orderItem->primaryKey); - $this->assertEquals(2, $orderItem->oldPrimaryKey); - $this->assertEquals(2, $orderItem->$pkName); - - $this->setExpectedException('yii\base\InvalidCallException'); - $orderItem->$pkName = 13; - $orderItem->save(); - } - - public function testFindLazyVia2() - { - /** @var TestCase|ActiveRecordTestTrait $this */ - /** @var Order $order */ - $orderClass = $this->getOrderClass(); - $pkName = ActiveRecord::PRIMARY_KEY_NAME; - - $order = new $orderClass(); - $order->$pkName = 100; - $this->assertEquals([], $order->items); - } - - public function testUpdateCounters() - { - // Update Counters is not supported by elasticsearch -// $this->setExpectedException('yii\base\NotSupportedException'); -// ActiveRecordTestTrait::testUpdateCounters(); - } - - /** - * Some PDO implementations(e.g. cubrid) do not support boolean values. - * Make sure this does not affect AR layer. - */ - public function testBooleanAttribute() - { - $db = $this->getConnection(); - $db->createCommand()->deleteIndex('customers'); - $db->http()->post('customers', null, Json::encode([ - 'mappings' => [ - "item" => [ - "_source" => [ "enabled" => true ], - "properties" => [ - // this is for the boolean test - "status" => ["type" => "boolean"], - ] - ] - ], - ]))->send(); - - $customerClass = $this->getCustomerClass(); - $customer = new $customerClass(); - $customer->name = 'boolean customer'; - $customer->email = 'mail@example.com'; - $customer->status = true; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(true, $customer->status); - - $customer->status = false; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(false, $customer->status); - - $customer = new Customer(); - $customer->setAttributes(['email' => 'user2b@example.com', 'name' => 'user2b', 'status' => true], false); - $customer->save(false); - $customer = new Customer(); - $customer->setAttributes(['email' => 'user3b@example.com', 'name' => 'user3b', 'status' => false], false); - $customer->save(false); - $this->afterSave(); - - $customers = $this->callCustomerFind()->where(['status' => true])->all(); - $this->assertEquals(1, count($customers)); - - $customers = $this->callCustomerFind()->where(['status' => false])->all(); - $this->assertEquals(2, count($customers)); - } -} \ No newline at end of file diff --git a/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php deleted file mode 100644 index af8b9ff..0000000 --- a/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php +++ /dev/null @@ -1,22 +0,0 @@ -open(); - } - -} \ No newline at end of file diff --git a/tests/unit/framework/elasticsearch/ElasticSearchTestCase.php b/tests/unit/framework/elasticsearch/ElasticSearchTestCase.php deleted file mode 100644 index 88e24b5..0000000 --- a/tests/unit/framework/elasticsearch/ElasticSearchTestCase.php +++ /dev/null @@ -1,48 +0,0 @@ -mockApplication(); - - $databases = $this->getParam('databases'); - $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : null; - if ($params === null || !isset($params['dsn'])) { - $this->markTestSkipped('No elasticsearch server connection configured.'); - } - $dsn = explode('/', $params['dsn']); - $host = $dsn[2]; - if (strpos($host, ':')===false) { - $host .= ':9200'; - } - if(!@stream_socket_client($host, $errorNumber, $errorDescription, 0.5)) { - $this->markTestSkipped('No elasticsearch server running at ' . $params['dsn'] . ' : ' . $errorNumber . ' - ' . $errorDescription); - } - - parent::setUp(); - } - - /** - * @param bool $reset whether to clean up the test database - * @return Connection - */ - public function getConnection($reset = true) - { - $databases = $this->getParam('databases'); - $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array(); - $db = new Connection; - if ($reset) { - $db->open(); - } - return $db; - } -} \ No newline at end of file diff --git a/tests/unit/framework/elasticsearch/QueryTest.php b/tests/unit/framework/elasticsearch/QueryTest.php deleted file mode 100644 index 44d91ea..0000000 --- a/tests/unit/framework/elasticsearch/QueryTest.php +++ /dev/null @@ -1,182 +0,0 @@ -getConnection()->createCommand(); - - $command->deleteAllIndexes(); - - $command->insert('test', 'user', ['name' => 'user1', 'email' => 'user1@example.com', 'status' => 1], 1); - $command->insert('test', 'user', ['name' => 'user2', 'email' => 'user2@example.com', 'status' => 1], 2); - $command->insert('test', 'user', ['name' => 'user3', 'email' => 'user3@example.com', 'status' => 2], 3); - $command->insert('test', 'user', ['name' => 'user4', 'email' => 'user4@example.com', 'status' => 1], 4); - - $command->flushIndex(); - } - - public function testFields() - { - $query = new Query; - $query->from('test', 'user'); - - $query->fields(['name', 'status']); - $this->assertEquals(['name', 'status'], $query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals(2, count($result['_source'])); - $this->assertArrayHasKey('status', $result['_source']); - $this->assertArrayHasKey('name', $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query->fields([]); - $this->assertEquals([], $query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals([], $result['_source']); - $this->assertArrayHasKey('_id', $result); - - $query->fields(null); - $this->assertNull($query->fields); - - $result = $query->one($this->getConnection()); - $this->assertEquals(3, count($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('test', 'user'); - - $result = $query->one($this->getConnection()); - $this->assertEquals(3, count($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->assertEquals(3, count($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' => 'user5'])->one($this->getConnection()); - $this->assertFalse($result); - } - - public function testAll() - { - $query = new Query; - $query->from('test', 'user'); - - $results = $query->all($this->getConnection()); - $this->assertEquals(4, count($results)); - $result = reset($results); - $this->assertEquals(3, count($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('test', 'user'); - - $results = $query->where(['name' => 'user1'])->all($this->getConnection()); - $this->assertEquals(1, count($results)); - $result = reset($results); - $this->assertEquals(3, count($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('test', 'user'); - - $results = $query->indexBy('name')->all($this->getConnection()); - $this->assertEquals(4, count($results)); - ksort($results); - $this->assertEquals(['user1', 'user2', 'user3', 'user4'], array_keys($results)); - } - - public function testScalar() - { - $query = new Query; - $query->from('test', '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' => 'user5'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - } - - public function testColumn() - { - $query = new Query; - $query->from('test', 'user'); - - $result = $query->orderBy(['name' => SORT_ASC])->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' => 'user5'])->scalar('name', $this->getConnection()); - $this->assertNull($result); - - } - - // 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() - { - } -}