diff --git a/framework/yii/elasticsearch/ActiveQuery.php b/framework/yii/elasticsearch/ActiveQuery.php index ee7f872..1937c6f 100644 --- a/framework/yii/elasticsearch/ActiveQuery.php +++ b/framework/yii/elasticsearch/ActiveQuery.php @@ -47,20 +47,9 @@ use yii\helpers\Json; * @author Carsten Brandt * @since 2.0 */ -class ActiveQuery extends \yii\base\Component +class ActiveQuery extends Query { /** - * Sort ascending - * @see orderBy - */ - const SORT_ASC = false; - /** - * Sort descending - * @see orderBy - */ - const SORT_DESC = true; - - /** * @var string the name of the ActiveRecord class. */ public $modelClass; @@ -69,193 +58,47 @@ class ActiveQuery extends \yii\base\Component */ public $with; /** - * @var string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row or model data. For more details, see [[indexBy()]]. - */ - public $indexBy; - /** * @var boolean whether to return each record as an array. If false (default), an object * of [[modelClass]] will be created to represent each record. */ public $asArray; - /** - * @var array the columns being selected. For example, `array('id', 'name')`. - * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. - * @see select() - */ - public $select; - /** - * @var array the query condition. - * @see where() - */ - public $where; - /** - * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. TODO infinite possible in ES? - */ - public $limit = 10; - /** - * @var integer zero-based offset from where the records are to be returned. - * If not set, it means starting from the beginning. - * If less than zero it means starting n elements from the end. - */ - public $offset; - /** - * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. - * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which - * can be either [[ActiveQuery::SORT_ASC]] or [[ActiveQuery::SORT_DESC]]. The array may also contain [[Expression]] objects. - * If that is the case, the expressions will be converted into strings without any change. - */ - public $orderBy; /** - * PHP magic method. - * This method allows calling static method defined in [[modelClass]] via this query object. - * It is mainly implemented for supporting the feature of scope. - * @param string $name the method name to be called - * @param array $params the parameters passed to the method - * @return mixed the method return result + * 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 __call($name, $params) + public function createCommand($db = null) { - if (method_exists($this->modelClass, $name)) { - array_unshift($params, $this); - call_user_func_array(array($this->modelClass, $name), $params); - return $this; - } else { - return parent::__call($name, $params); + /** @var $modelClass ActiveRecord */ + $modelClass = $this->modelClass; + if ($db === null) { + $db = $modelClass::getDb(); } - } - /** - * Executes query and returns all results as an array. - * @return array the query results. If the query results in nothing, an empty array will be returned. - */ - public function all() - { - // TODO add support for orderBy - $data = $this->executeScript('All'); - $rows = array(); - print_r($data); - foreach($data as $dataRow) { - $row = $dataRow['_source']; - $row['id'] = $dataRow['_id']; - $rows[] = $row; - } - if (!empty($rows)) { - $models = $this->createModels($rows); - if (!empty($this->with)) { - $this->populateRelations($models, $this->with); + $index = $modelClass::indexName(); + $type = $modelClass::indexType(); + if (is_array($this->where) && Activerecord::isPrimaryKey(array_keys($this->where))) { + // TODO what about mixed queries? + $query = array(); + foreach((array) reset($this->where) as $pk) { + $doc = array( + '_id' => $pk, + ); + $db->getQueryBuilder()->buildSelect($doc, $this->select); + $query['docs'][] = $doc; } - return $models; - } else { - return array(); - } - } - - /** - * Executes query and returns a single row of result. - * @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() - { - // TODO add support for orderBy - $data = $this->executeScript('One'); - if (!isset($data['_source'])) { - return null; - } - $row = $data['_source']; - $row['id'] = $data['_id']; - if ($this->asArray) { - $model = $row; - } else { - /** @var $class ActiveRecord */ - $class = $this->modelClass; - $model = $class::create($row); - } - if (!empty($this->with)) { - $models = array($model); - $this->populateRelations($models, $this->with); - $model = $models[0]; - } - return $model; - } - - /** - * Executes the query and returns the first column of the result. - * @param string $column name of the column to select - * @return array the first column of the query result. An empty array is returned if the query results in nothing. - */ - public function column($column) - { - // TODO add support for indexBy and orderBy - return $this->executeScript('Column', $column); - } - - /** - * Returns the number of records. - * @param string $q the COUNT expression. Defaults to '*'. - * Make sure you properly quote column names. - * @return integer number of records - */ - public function count() - { - if ($this->offset === null && $this->limit === null && $this->where === null) { - $modelClass = $this->modelClass; - /** @var Connection $db */ - $db = $modelClass::getDb(); - return $db->executeCommand('LLEN', array($modelClass::tableName())); + $command = $db->createCommand($query, $index, $type); + $command->api = '_mget'; + return $command; } else { - return $this->executeScript('Count'); + $query = $db->getQueryBuilder()->build($this); + return $db->createCommand($query, $index, $type); } } /** - * Returns the number of records. - * @param string $column the column to sum up - * @return integer number of records - */ - public function sum($column) - { - return $this->executeScript('Sum', $column); - } - - /** - * Returns the average of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @return integer the average of the specified column values. - */ - public function average($column) - { - return $this->executeScript('Average', $column); - } - - /** - * Returns the minimum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @return integer the minimum of the specified column values. - */ - public function min($column) - { - return $this->executeScript('Min', $column); - } - - /** - * Returns the maximum of the specified column values. - * @param string $column the column name or expression. - * Make sure you properly quote column names in the expression. - * @return integer the maximum of the specified column values. - */ - public function max($column) - { - return $this->executeScript('Max', $column); - } - - /** * Returns the query result as a scalar value. * The value returned will be the first column in the first row of the query results. * @param string $column name of the column to select @@ -408,109 +251,84 @@ class ActiveQuery extends \yii\base\Component // TODO: refactor. code below here is all duplicated from yii/db/ActiveQuery and yii/db/Query /** - * Sets the [[asArray]] property. - * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. - * @return ActiveQuery the query object itself - */ - public function asArray($value = true) - { - $this->asArray = $value; - return $this; - } - - /** - * Sets the SELECT part of the query. - * @param string|array $columns the columns to be selected. - * Columns can be specified in either a string (e.g. "id, name") or an array (e.g. array('id', 'name')). - * Columns can contain table prefixes (e.g. "tbl_user.id") and/or column aliases (e.g. "tbl_user.id AS user_id"). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return Query the query object itself + * PHP magic method. + * This method allows calling static method defined in [[modelClass]] via this query object. + * It is mainly implemented for supporting the feature of scope. + * @param string $name the method name to be called + * @param array $params the parameters passed to the method + * @return mixed the method return result */ - public function select($columns) + public function __call($name, $params) { - if (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + if (method_exists($this->modelClass, $name)) { + array_unshift($params, $this); + call_user_func_array(array($this->modelClass, $name), $params); + return $this; + } else { + return parent::__call($name, $params); } - $this->select = $columns; - return $this; - } - - /** - * Sets the ORDER BY part of the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see addOrderBy() - */ - public function orderBy($columns) - { - $this->orderBy = $this->normalizeOrderBy($columns); - return $this; } /** - * Adds additional ORDER BY columns to the query. - * @param string|array $columns the columns (and the directions) to be ordered by. - * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array - * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). - * The method will automatically quote the column names unless a column contains some parenthesis - * (which means the column contains a DB expression). - * @return ActiveQuery the query object itself - * @see orderBy() + * 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 addOrderBy($columns) + public function all($db = null) { - $columns = $this->normalizeOrderBy($columns); - if ($this->orderBy === null) { - $this->orderBy = $columns; - } else { - $this->orderBy = array_merge($this->orderBy, $columns); - } - return $this; - } - - protected function normalizeOrderBy($columns) - { - throw new NotSupportedException('orderBy is currently not supported'); - if (is_array($columns)) { - return $columns; - } else { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - $result = array(); - foreach ($columns as $column) { - if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { - $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; - } else { - $result[$column] = self::SORT_ASC; - } + $command = $this->createCommand($db); + $rows = $command->queryAll(); + if (!empty($rows)) { + $models = $this->createModels($rows); + if (!empty($this->with)) { + $this->populateRelations($models, $this->with); } - return $result; + return $models; + } else { + return array(); } } /** - * Sets the LIMIT part of the query. - * @param integer $limit the limit - * @return ActiveQuery the query object itself + * 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 limit($limit) + public function one($db = null) { - $this->limit = $limit; - return $this; + $command = $this->createCommand($db); + $row = $command->queryOne(); + if ($row !== false) { + if ($this->asArray) { + $model = $row; + } else { + /** @var $class ActiveRecord */ + $class = $this->modelClass; + $model = $class::create($row); + } + if (!empty($this->with)) { + $models = array($model); + $this->populateRelations($models, $this->with); + $model = $models[0]; + } + return $model; + } else { + return null; + } } /** - * Sets the OFFSET part of the query. - * @param integer $offset the offset + * Sets the [[asArray]] property. + * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. * @return ActiveQuery the query object itself */ - public function offset($offset) + public function asArray($value = true) { - $this->offset = $offset; + $this->asArray = $value; return $this; } @@ -546,141 +364,6 @@ class ActiveQuery extends \yii\base\Component return $this; } - /** - * Sets the [[indexBy]] property. - * @param string|callable $column the name of the column by which the query results should be indexed by. - * This can also be a callable (e.g. anonymous function) that returns the index value based on the given - * row or model data. The signature of the callable should be: - * - * ~~~ - * // $model is an AR instance when `asArray` is false, - * // or an array of column values when `asArray` is true. - * function ($model) - * { - * // return the index value corresponding to $model - * } - * ~~~ - * - * @return ActiveQuery the query object itself - */ - public function indexBy($column) - { - $this->indexBy = $column; - return $this; - } - - /** - * Sets the WHERE part of the query. - * - * The method requires a $condition parameter, and optionally a $params parameter - * specifying the values to be bound to the query. - * - * The $condition parameter should be either a string (e.g. 'id=1') or an array. - * If the latter, it must be in one of the following two formats: - * - * - hash format: `array('column1' => value1, 'column2' => value2, ...)` - * - operator format: `array(operator, operand1, operand2, ...)` - * - * A condition in hash format represents the following SQL expression in general: - * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, - * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used - * in the generated expression. Below are some examples: - * - * - `array('type' => 1, 'status' => 2)` generates `(type = 1) AND (status = 2)`. - * - `array('id' => array(1, 2, 3), 'status' => 2)` generates `(id IN (1, 2, 3)) AND (status = 2)`. - * - `array('status' => null) generates `status IS NULL`. - * - * A condition in operator format generates the SQL expression according to the specified operator, which - * can be one of the followings: - * - * - `and`: the operands should be concatenated together using `AND`. For example, - * `array('and', 'id=1', 'id=2')` will generate `id=1 AND id=2`. If an operand is an array, - * it will be converted into a string using the rules described here. For example, - * `array('and', 'type=1', array('or', 'id=1', 'id=2'))` will generate `type=1 AND (id=1 OR id=2)`. - * The method will NOT do any quoting or escaping. - * - * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. - * - * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the - * starting and ending values of the range that the column is in. - * For example, `array('between', 'id', 1, 10)` will generate `id BETWEEN 1 AND 10`. - * - * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` - * in the generated condition. - * - * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing - * the range of the values that the column or DB expression should be in. For example, - * `array('in', 'id', array(1, 2, 3))` will generate `id IN (1, 2, 3)`. - * The method will properly quote the column name and escape values in the range. - * - * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. - * - * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing - * the values that the column or DB expression should be like. - * For example, `array('like', 'name', '%tester%')` will generate `name LIKE '%tester%'`. - * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated - * using `AND`. For example, `array('like', 'name', array('%test%', '%sample%'))` will generate - * `name LIKE '%test%' AND name LIKE '%sample%'`. - * The method will properly quote the column name and escape values in the range. - * - * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` - * predicates when operand 2 is an array. - * - * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` - * in the generated condition. - * - * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate - * the `NOT LIKE` predicates. - * - * @param string|array $condition the conditions that should be put in the WHERE part. - * @return ActiveQuery the query object itself - * @see andWhere() - * @see orWhere() - */ - public function where($condition) - { - $this->where = $condition; - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'AND' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return ActiveQuery the query object itself - * @see where() - * @see orWhere() - */ - public function andWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('and', $this->where, $condition); - } - return $this; - } - - /** - * Adds an additional WHERE condition to the existing one. - * The new condition and the existing one will be joined using the 'OR' operator. - * @param string|array $condition the new WHERE condition. Please refer to [[where()]] - * on how to specify this parameter. - * @return ActiveQuery the query object itself - * @see where() - * @see andWhere() - */ - public function orWhere($condition) - { - if ($this->where === null) { - $this->where = $condition; - } else { - $this->where = array('or', $this->where, $condition); - } - return $this; - } - private function createModels($rows) { $models = array(); diff --git a/framework/yii/elasticsearch/ActiveRecord.php b/framework/yii/elasticsearch/ActiveRecord.php index 470ba9b..8d567e4 100644 --- a/framework/yii/elasticsearch/ActiveRecord.php +++ b/framework/yii/elasticsearch/ActiveRecord.php @@ -325,7 +325,7 @@ abstract class ActiveRecord extends \yii\db\ActiveRecord $values = $this->getDirtyAttributes($attributes); $key = reset($this->primaryKey()); $pk = $this->getAttribute($key); - unset($values[$key]); + //unset($values[$key]); // save attributes if ($pk === null) { diff --git a/framework/yii/elasticsearch/Command.php b/framework/yii/elasticsearch/Command.php new file mode 100644 index 0000000..a7f7db7 --- /dev/null +++ b/framework/yii/elasticsearch/Command.php @@ -0,0 +1,120 @@ + + */ + +namespace yii\elasticsearch; + + +use yii\base\Component; +use yii\helpers\Json; + +class Command extends Component +{ + /** + * @var Connection + */ + public $db; + + public $api = '_search'; + + /** + * @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|string array or json + */ + public $query; + + private function createUrl($endPoint = null) + { + if ($endPoint === null) { + $endPoint = $this->api; + } + if ($this->index === null && $this->type === null) { + return '/' . $endPoint; + } + $index = $this->index; + if ($index === null) { + $index = '_all'; + } elseif (is_array($index)) { + $index = implode(',', $index); + } + $type = $this->type; + if (is_array($type)) { + $type = implode(',', $type); + } + return '/' . $index . '/' . (empty($type) ? '' : $type . '/') . $endPoint; + } + + public function queryAll() + { + $query = $this->query; + if (empty($query)) { + $query = '{}'; + } + if (is_array($query)) { + $query = Json::encode($query); + } + $http = $this->db->http(); + $response = $http->post($this->createUrl(), null, $query)->send(); + $data = Json::decode($response->getBody(true)); + // TODO store query meta data for later use + $docs = array(); + switch ($this->api) { + default: + case '_search': + if (isset($data['hits']['hits'])) { + $docs = $data['hits']['hits']; + } + break; + case '_mget': + if (isset($data['docs'])) { + $docs = $data['docs']; + } + break; + } + $rows = array(); + foreach($docs as $doc) { + // TODO maybe return type info + if (isset($doc['exists']) && !$doc['exists']) { + continue; + } + $row = $doc['_source']; + $row['id'] = $doc['_id']; + $rows[] = $row; + } + return $rows; + } + + public function queryOne() + { + // TODO set limit + $rows = $this->queryAll(); + return reset($rows); + } + + public function queryCount() + { + //http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html + $query = $this->query; + if (empty($query)) { + $query = ''; + } + if (is_array($query)) { + $query = Json::encode($query); + } + $http = $this->db->http(); + $response = $http->post($this->createUrl('_count'), null, $query)->send(); + $data = Json::decode($response->getBody(true)); + // TODO store query meta data for later use + return $data['count']; + } +} \ No newline at end of file diff --git a/framework/yii/elasticsearch/Connection.php b/framework/yii/elasticsearch/Connection.php index f970eae..764a539 100644 --- a/framework/yii/elasticsearch/Connection.php +++ b/framework/yii/elasticsearch/Connection.php @@ -54,6 +54,23 @@ class Connection extends Component } /** + * Creates a command for execution. + * @param string $query the SQL statement to be executed + * @return Command the DB command + */ + public function createCommand($query = null, $index = null, $type = null) + { + $this->open(); + $command = new Command(array( + 'db' => $this, + 'query' => $query, + 'index' => $index, + 'type' => $type, + )); + return $command; + } + + /** * Closes the connection when this component is being serialized. * @return array */ diff --git a/framework/yii/elasticsearch/Query.php b/framework/yii/elasticsearch/Query.php index 99f7c07..e33c251 100644 --- a/framework/yii/elasticsearch/Query.php +++ b/framework/yii/elasticsearch/Query.php @@ -11,8 +11,417 @@ namespace yii\elasticsearch; use yii\base\Component; +use Yii; class Query extends Component { + /** + * Sort ascending + * @see orderBy + */ + const SORT_ASC = false; + /** + * Sort descending + * @see orderBy + */ + const SORT_DESC = true; + /** + * @var array the columns being selected. For example, `array('id', 'name')`. + * This is used to construct the SELECT clause in a SQL statement. If not set, if means selecting all columns. + * @see select() + */ + public $select; + /** + * @var string|array query condition. This refers to the WHERE clause in a SQL statement. + * For example, `age > 31 AND team = 1`. + * @see where() + */ + public $where; + /** + * @var integer maximum number of records to be returned. If not set or less than 0, it means no limit. + */ + public $limit; + /** + * @var integer zero-based offset from where the records are to be returned. If not set or + * less than 0, it means starting from the beginning. + */ + public $offset; + /** + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. + * If that is the case, the expressions will be converted into strings without any change. + */ + public $orderBy; + /** + * @var string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. For more details, see [[indexBy()]]. This property is only used by [[all()]]. + */ + public $indexBy; + + + /** + * Creates a DB command that can be used to execute this query. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return Command the created DB command instance. + */ + public function createCommand($db = null) + { + if ($db === null) { + $db = Yii::$app->elasticsearch; + } + $query = $db->getQueryBuilder()->build($this); + return $db->createCommand($query); + } + + /** + * Sets the [[indexBy]] property. + * @param string|callable $column the name of the column by which the query results should be indexed by. + * This can also be a callable (e.g. anonymous function) that returns the index value based on the given + * row data. The signature of the callable should be: + * + * ~~~ + * function ($row) + * { + * // return the index value corresponding to $row + * } + * ~~~ + * + * @return Query the query object itself + */ + public function indexBy($column) + { + $this->indexBy = $column; + return $this; + } + + /** + * Executes the query and returns all results as an array. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` 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) + { + $rows = $this->createCommand($db)->queryAll(); + if ($this->indexBy === null) { + return $rows; + } + $result = array(); + foreach ($rows as $row) { + if (is_string($this->indexBy)) { + $key = $row[$this->indexBy]; + } else { + $key = call_user_func($this->indexBy, $row); + } + $result[$key] = $row; + } + return $result; + } + + /** + * Executes the query and returns a single row of result. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` 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) + { + return $this->createCommand($db)->queryOne(); + } + + /** + * Returns the query result as a scalar value. + * The value returned will be the first column in the first row of the query results. + * @param $column + * @return string|boolean the value of the first column in the first row of the query result. + * False is returned if the query result is empty. + */ + public function scalar($column) + { + // TODO implement + return null; + } + +// /** +// * Executes the query and returns the first column of the result. +// * @param Connection $db the database connection used to generate the SQL statement. +// * If this parameter is not given, the `db` 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($db = null) +// { +// return $this->createCommand($db)->queryColumn(); +// } + + /** + * Returns the number of records. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return integer number of records + */ + public function count($db = null) + { + return $this->createCommand($db)->queryCount(); + } + +// /** +// * Returns the sum of the specified column values. +// * @param string $q the column name or expression. +// * Make sure you properly quote column names in the expression. +// * @param Connection $db the database connection used to generate the SQL statement. +// * If this parameter is not given, the `db` application component will be used. +// * @return integer the sum of the specified column values +// */ +// public function sum($q, $db = null) +// { +// $this->select = array("SUM($q)"); +// return $this->createCommand($db)->queryScalar(); +// } +// +// /** +// * Returns the average of the specified column values. +// * @param string $q the column name or expression. +// * Make sure you properly quote column names in the expression. +// * @param Connection $db the database connection used to generate the SQL statement. +// * If this parameter is not given, the `db` application component will be used. +// * @return integer the average of the specified column values. +// */ +// public function average($q, $db = null) +// { +// $this->select = array("AVG($q)"); +// return $this->createCommand($db)->queryScalar(); +// } +// +// /** +// * Returns the minimum of the specified column values. +// * @param string $q the column name or expression. +// * Make sure you properly quote column names in the expression. +// * @param Connection $db the database connection used to generate the SQL statement. +// * If this parameter is not given, the `db` application component will be used. +// * @return integer the minimum of the specified column values. +// */ +// public function min($q, $db = null) +// { +// $this->select = array("MIN($q)"); +// return $this->createCommand($db)->queryScalar(); +// } +// +// /** +// * Returns the maximum of the specified column values. +// * @param string $q the column name or expression. +// * Make sure you properly quote column names in the expression. +// * @param Connection $db the database connection used to generate the SQL statement. +// * If this parameter is not given, the `db` application component will be used. +// * @return integer the maximum of the specified column values. +// */ +// public function max($q, $db = null) +// { +// $this->select = array("MAX($q)"); +// return $this->createCommand($db)->queryScalar(); +// } + + /** + * Returns a value indicating whether the query result contains any row of data. + * @param Connection $db the database connection used to generate the SQL statement. + * If this parameter is not given, the `db` application component will be used. + * @return boolean whether the query result contains any row of data. + */ + public function exists() + { + return $this->one() !== null; + } + + /** + * Sets the WHERE part of the query. + * + * The method requires a $condition parameter, and optionally a $params parameter + * specifying the values to be bound to the query. + * + * The $condition parameter should be either a string (e.g. 'id=1') or an array. + * If the latter, it must be in one of the following two formats: + * + * - hash format: `array('column1' => value1, 'column2' => value2, ...)` + * - operator format: `array(operator, operand1, operand2, ...)` + * + * A condition in hash format represents the following SQL expression in general: + * `column1=value1 AND column2=value2 AND ...`. In case when a value is an array, + * an `IN` expression will be generated. And if a value is null, `IS NULL` will be used + * in the generated expression. Below are some examples: + * + * - `array('type' => 1, 'status' => 2)` generates `(type = 1) AND (status = 2)`. + * - `array('id' => array(1, 2, 3), 'status' => 2)` generates `(id IN (1, 2, 3)) AND (status = 2)`. + * - `array('status' => null) generates `status IS NULL`. + * + * A condition in operator format generates the SQL expression according to the specified operator, which + * can be one of the followings: + * + * - `and`: the operands should be concatenated together using `AND`. For example, + * `array('and', 'id=1', 'id=2')` will generate `id=1 AND id=2`. If an operand is an array, + * it will be converted into a string using the rules described here. For example, + * `array('and', 'type=1', array('or', 'id=1', 'id=2'))` will generate `type=1 AND (id=1 OR id=2)`. + * The method will NOT do any quoting or escaping. + * + * - `or`: similar to the `and` operator except that the operands are concatenated using `OR`. + * + * - `between`: operand 1 should be the column name, and operand 2 and 3 should be the + * starting and ending values of the range that the column is in. + * For example, `array('between', 'id', 1, 10)` will generate `id BETWEEN 1 AND 10`. + * + * - `not between`: similar to `between` except the `BETWEEN` is replaced with `NOT BETWEEN` + * in the generated condition. + * + * - `in`: operand 1 should be a column or DB expression, and operand 2 be an array representing + * the range of the values that the column or DB expression should be in. For example, + * `array('in', 'id', array(1, 2, 3))` will generate `id IN (1, 2, 3)`. + * The method will properly quote the column name and escape values in the range. + * + * - `not in`: similar to the `in` operator except that `IN` is replaced with `NOT IN` in the generated condition. + * + * - `like`: operand 1 should be a column or DB expression, and operand 2 be a string or an array representing + * the values that the column or DB expression should be like. + * For example, `array('like', 'name', '%tester%')` will generate `name LIKE '%tester%'`. + * When the value range is given as an array, multiple `LIKE` predicates will be generated and concatenated + * using `AND`. For example, `array('like', 'name', array('%test%', '%sample%'))` will generate + * `name LIKE '%test%' AND name LIKE '%sample%'`. + * The method will properly quote the column name and escape values in the range. + * + * - `or like`: similar to the `like` operator except that `OR` is used to concatenate the `LIKE` + * predicates when operand 2 is an array. + * + * - `not like`: similar to the `like` operator except that `LIKE` is replaced with `NOT LIKE` + * in the generated condition. + * + * - `or not like`: similar to the `not like` operator except that `OR` is used to concatenate + * the `NOT LIKE` predicates. + * + * @param string|array $condition the conditions that should be put in the WHERE part. + * @return Query the query object itself + * @see andWhere() + * @see orWhere() + */ + public function where($condition) + { + $this->where = $condition; + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'AND' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return Query the query object itself + * @see where() + * @see orWhere() + */ + public function andWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('and', $this->where, $condition); + } + return $this; + } + + /** + * Adds an additional WHERE condition to the existing one. + * The new condition and the existing one will be joined using the 'OR' operator. + * @param string|array $condition the new WHERE condition. Please refer to [[where()]] + * on how to specify this parameter. + * @return Query the query object itself + * @see where() + * @see andWhere() + */ + public function orWhere($condition) + { + if ($this->where === null) { + $this->where = $condition; + } else { + $this->where = array('or', $this->where, $condition); + } + return $this; + } + + /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return Query the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return Query the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = array(); + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + + /** + * Sets the LIMIT part of the query. + * @param integer $limit the limit. Use null or negative value to disable limit. + * @return Query the query object itself + */ + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Sets the OFFSET part of the query. + * @param integer $offset the offset. Use null or negative value to disable offset. + * @return Query the query object itself + */ + public function offset($offset) + { + $this->offset = $offset; + return $this; + } } \ No newline at end of file diff --git a/framework/yii/elasticsearch/QueryBuilder.php b/framework/yii/elasticsearch/QueryBuilder.php index da58532..4280fe6 100644 --- a/framework/yii/elasticsearch/QueryBuilder.php +++ b/framework/yii/elasticsearch/QueryBuilder.php @@ -46,7 +46,7 @@ class QueryBuilder extends \yii\base\Object { $searchQuery = array(); $this->buildSelect($searchQuery, $query->select); -// $this->buildFrom(&$searchQuery, $query->from); +// $this->buildFrom($searchQuery, $query->from); $this->buildCondition($searchQuery, $query->where); $this->buildOrderBy($searchQuery, $query->orderBy); $this->buildLimit($searchQuery, $query->limit, $query->offset); @@ -209,7 +209,12 @@ class QueryBuilder extends \yii\base\Object private function buildHashCondition(&$query, $condition) { - $query['query']['term'] = $condition; + foreach($condition as $attribute => $value) { + // ['query']['filteredQuery'] + $query['filter']['bool']['must'][] = array( + 'term' => array($attribute => $value), + ); + } return; // TODO more $parts = array(); foreach ($condition as $column => $value) { diff --git a/tests/unit/data/ar/elasticsearch/Customer.php b/tests/unit/data/ar/elasticsearch/Customer.php index 8a54ab6..5e8f8dd 100644 --- a/tests/unit/data/ar/elasticsearch/Customer.php +++ b/tests/unit/data/ar/elasticsearch/Customer.php @@ -35,6 +35,6 @@ class Customer extends ActiveRecord public static function active($query) { - $query->andWhere('status=1'); + $query->andWhere(array('status' => 1)); } } diff --git a/tests/unit/framework/elasticsearch/ActiveRecordTest.php b/tests/unit/framework/elasticsearch/ActiveRecordTest.php index da65471..27e1e92 100644 --- a/tests/unit/framework/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/framework/elasticsearch/ActiveRecordTest.php @@ -4,7 +4,8 @@ namespace yiiunit\framework\elasticsearch; use yii\db\Query; use yii\elasticsearch\Connection; -use yii\redis\ActiveQuery; +use yii\elasticsearch\ActiveQuery; +use yii\helpers\Json; use yiiunit\data\ar\elasticsearch\ActiveRecord; use yiiunit\data\ar\elasticsearch\Customer; use yiiunit\data\ar\elasticsearch\OrderItem; @@ -80,6 +81,17 @@ class ActiveRecordTest extends ElasticSearchTestCase // $orderItem = new OrderItem(); // $orderItem->setAttributes(array('order_id' => 3, 'item_id' => 2, 'quantity' => 1, 'subtotal' => 40.0), false); // $orderItem->save(false); + + for($n = 0; $n < 20; $n++) { + $r = $db->http()->post('_count')->send(); + $c = Json::decode($r->getBody(true)); + if ($c['count'] != 11) { + usleep(100000); + } else { + return; + } + } + throw new \Exception('Unable to initialize elasticsearch data.'); } public function testFind() @@ -124,13 +136,14 @@ class ActiveRecordTest extends ElasticSearchTestCase // find count, sum, average, min, max, scalar $this->assertEquals(3, Customer::find()->count()); - $this->assertEquals(6, Customer::find()->sum('id')); - $this->assertEquals(2, Customer::find()->average('id')); - $this->assertEquals(1, Customer::find()->min('id')); - $this->assertEquals(3, Customer::find()->max('id')); +// $this->assertEquals(6, Customer::find()->sum('id')); +// $this->assertEquals(2, Customer::find()->average('id')); +// $this->assertEquals(1, Customer::find()->min('id')); +// $this->assertEquals(3, Customer::find()->max('id')); // scope - $this->assertEquals(2, Customer::find()->active()->count()); + $this->assertEquals(2, count(Customer::find()->active()->all())); +// $this->assertEquals(2, Customer::find()->active()->count()); // asArray $customer = Customer::find()->where(array('id' => 2))->asArray()->one();