From e15860c3facbd5c8ec926d998842096c030d5d53 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sun, 24 Nov 2013 00:30:36 +0100 Subject: [PATCH] more on elasticsearch Query interface added facet search --- framework/yii/db/ActiveRecord.php | 2 +- framework/yii/db/QueryBuilder.php | 5 +- framework/yii/elasticsearch/ActiveQuery.php | 4 +- framework/yii/elasticsearch/ActiveRecord.php | 4 + framework/yii/elasticsearch/Command.php | 19 +- framework/yii/elasticsearch/Connection.php | 10 +- framework/yii/elasticsearch/Query.php | 218 +++++++++++++++------ framework/yii/elasticsearch/QueryBuilder.php | 136 ++++--------- .../framework/elasticsearch/ActiveRecordTest.php | 25 ++- .../elasticsearch/ElasticSearchConnectionTest.php | 47 ----- tests/unit/framework/elasticsearch/QueryTest.php | 180 +++++++++++++++++ 11 files changed, 415 insertions(+), 235 deletions(-) create mode 100644 tests/unit/framework/elasticsearch/QueryTest.php diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index fce9010..fd576aa 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -132,7 +132,7 @@ class ActiveRecord extends Model * - an array of name-value pairs: query by a set of column values and return a single record matching all of them. * - null: return a new [[ActiveQuery]] object for further query purpose. * - * @return ActiveQuery|ActiveQueryInterface|static|null When `$q` is null, a new [[ActiveQuery]] instance + * @return ActiveQuery|ActiveRecord|null When `$q` is null, a new [[ActiveQuery]] instance * is returned; when `$q` is a scalar or an array, an ActiveRecord object matching it will be * returned (null will be returned if there is no matching). * @throws InvalidConfigException if the AR class does not have a primary key diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php index 0a547ae..867c9e6 100644 --- a/framework/yii/db/QueryBuilder.php +++ b/framework/yii/db/QueryBuilder.php @@ -7,6 +7,7 @@ namespace yii\db; +use yii\base\InvalidParamException; use yii\base\NotSupportedException; /** @@ -761,7 +762,7 @@ class QueryBuilder extends \yii\base\Object * 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 + * @throws InvalidParamException if the condition is in bad format */ public function buildCondition($condition, &$params) { @@ -790,7 +791,7 @@ class QueryBuilder extends \yii\base\Object array_shift($condition); return $this->$method($operator, $condition, $params); } else { - throw new Exception('Found unknown operator in query: ' . $operator); + throw new InvalidParamException('Found unknown operator in query: ' . $operator); } } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... return $this->buildHashCondition($condition, $params); diff --git a/framework/yii/elasticsearch/ActiveQuery.php b/framework/yii/elasticsearch/ActiveQuery.php index fa74ba8..be948a2 100644 --- a/framework/yii/elasticsearch/ActiveQuery.php +++ b/framework/yii/elasticsearch/ActiveQuery.php @@ -71,8 +71,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface $this->index = $modelClass::index(); $this->type = $modelClass::type(); } - $query = $db->getQueryBuilder()->build($this); - return $db->createCommand($query, $this->index, $this->type); + $commandConfig = $db->getQueryBuilder()->build($this); + return $db->createCommand($commandConfig); } /** diff --git a/framework/yii/elasticsearch/ActiveRecord.php b/framework/yii/elasticsearch/ActiveRecord.php index 8cd0e34..9f2c610 100644 --- a/framework/yii/elasticsearch/ActiveRecord.php +++ b/framework/yii/elasticsearch/ActiveRecord.php @@ -110,6 +110,10 @@ class ActiveRecord extends \yii\db\ActiveRecord 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 */ diff --git a/framework/yii/elasticsearch/Command.php b/framework/yii/elasticsearch/Command.php index 2712583..5071c0c 100644 --- a/framework/yii/elasticsearch/Command.php +++ b/framework/yii/elasticsearch/Command.php @@ -6,6 +6,7 @@ namespace yii\elasticsearch; +use Guzzle\Http\Exception\ClientErrorResponseException; use yii\base\Component; use yii\db\Exception; use yii\helpers\Json; @@ -35,15 +36,16 @@ class Command extends Component * @var string|array the types to execute the query on. Defaults to null meaning all types */ public $type; - /** - * @var array|string array or json + * @var array list of arrays or json strings that become parts of a query */ - public $query; + public $queryParts; + + public $options = []; public function queryAll($options = []) { - $query = $this->query; + $query = $this->queryParts; if (empty($query)) { $query = '{}'; } @@ -55,7 +57,11 @@ class Command extends Component $this->type !== null ? $this->type : '_all', '_search' ]; - $response = $this->db->http()->post($this->createUrl($url, $options), null, $query)->send(); + 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() . $e->getResponse()->getBody(true), [], 0, $e); + } return Json::decode($response->getBody(true))['hits']; } @@ -405,7 +411,8 @@ class Command extends Component return urlencode(is_array($a) ? implode(',', $a) : $a); }, $path)); - if (!empty($options)) { + if (!empty($options) || !empty($this->options)) { + $options = array_merge($this->options, $options); $url .= '?' . http_build_query($options); } diff --git a/framework/yii/elasticsearch/Connection.php b/framework/yii/elasticsearch/Connection.php index 764a539..73d7aad 100644 --- a/framework/yii/elasticsearch/Connection.php +++ b/framework/yii/elasticsearch/Connection.php @@ -58,15 +58,11 @@ class Connection extends Component * @param string $query the SQL statement to be executed * @return Command the DB command */ - public function createCommand($query = null, $index = null, $type = null) + public function createCommand($config = []) { $this->open(); - $command = new Command(array( - 'db' => $this, - 'query' => $query, - 'index' => $index, - 'type' => $type, - )); + $config['db'] = $this; + $command = new Command($config); return $command; } diff --git a/framework/yii/elasticsearch/Query.php b/framework/yii/elasticsearch/Query.php index 4b56721..23d9de1 100644 --- a/framework/yii/elasticsearch/Query.php +++ b/framework/yii/elasticsearch/Query.php @@ -50,6 +50,16 @@ class Query extends Component implements QueryInterface */ 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. @@ -62,8 +72,8 @@ class Query extends Component implements QueryInterface $db = Yii::$app->getComponent('elasticsearch'); } - $query = $db->getQueryBuilder()->build($this); - return $db->createCommand($query, $this->index, $this->type); + $commandConfig = $db->getQueryBuilder()->build($this); + return $db->createCommand($commandConfig); } /** @@ -74,11 +84,13 @@ class Query extends Component implements QueryInterface */ public function all($db = null) { - $rows = $this->createCommand($db)->queryAll()['hits']; + $result = $this->createCommand($db)->queryAll(); + // TODO publish facet results + $rows = $result['hits']; if ($this->indexBy === null && $this->fields === null) { return $rows; } - $result = []; + $models = []; foreach ($rows as $key => $row) { if ($this->fields !== null) { $row['_source'] = isset($row['fields']) ? $row['fields'] : []; @@ -91,9 +103,9 @@ class Query extends Component implements QueryInterface $key = call_user_func($this->indexBy, $row); } } - $result[$key] = $row; + $models[$key] = $row; } - return $result; + return $models; } /** @@ -107,6 +119,7 @@ class Query extends Component implements QueryInterface { $options['size'] = 1; $result = $this->createCommand($db)->queryAll($options); + // TODO publish facet results if (empty($result['hits'])) { return false; } @@ -119,6 +132,20 @@ class Query extends Component implements QueryInterface } /** + * 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 @@ -146,11 +173,12 @@ class Query extends Component implements QueryInterface */ public function column($field, $db = null) { - $query = clone $this; - $rows = $query->fields([$field])->createCommand($db)->queryAll()['hits']; + $command = $this->createCommand($db); + $command->queryParts['fields'] = [$field]; + $rows = $command->queryAll()['hits']; $result = []; foreach ($rows as $row) { - $result[] = $row['fields'][$field]; + $result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; } return $result; } @@ -164,6 +192,10 @@ class Query extends Component implements QueryInterface */ 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; @@ -173,84 +205,156 @@ class Query extends Component implements QueryInterface return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); } - /** - * 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. + * 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 integer the sum of the specified column values + * @return boolean whether the query result contains any row of data. */ - public function sum($q, $db = null) + public function exists($db = null) { - $this->select = ["SUM($q)"]; - return $this->createCommand($db)->queryScalar(); + return self::one($db) !== false; } /** - * 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 execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return integer the average of the specified column values. + * 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 average($q, $db = null) + public function addFacet($name, $type, $options) { - $this->select = ["AVG($q)"]; - return $this->createCommand($db)->queryScalar(); + $this->facets[$name] = [$type => $options]; + return $this; } /** - * 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 execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return integer the minimum of the specified column values. + * 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 min($q, $db = null) + public function addTermFacet($name, $options) { - $this->select = ["MIN($q)"]; - return $this->createCommand($db)->queryScalar(); + return $this->addFacet($name, 'terms', $options); } /** - * 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 execute the query. - * If this parameter is not given, the `elasticsearch` application component will be used. - * @return integer the maximum of the specified column values. + * 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 max($q, $db = null) + public function addRangeFacet($name, $options) { - $this->select = ["MAX($q)"]; - return $this->createCommand($db)->queryScalar(); + return $this->addFacet($name, 'range', $options); } /** - * 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. + * 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 exists($db = null) + public function addHistogramFacet($name, $options) { - // TODO check for exists - return $this->one($db) !== null; + return $this->addFacet($name, 'histogram', $options); } /** - * 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. + * 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 delete($db = null) + public function addDateHistogramFacet($name, $options) { - // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html + 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() + { + } /** diff --git a/framework/yii/elasticsearch/QueryBuilder.php b/framework/yii/elasticsearch/QueryBuilder.php index 005d053..18e9c4e 100644 --- a/framework/yii/elasticsearch/QueryBuilder.php +++ b/framework/yii/elasticsearch/QueryBuilder.php @@ -7,15 +7,14 @@ namespace yii\elasticsearch; +use yii\base\InvalidParamException; use yii\base\NotSupportedException; /** - * QueryBuilder builds a SELECT SQL statement based on the specification given as a [[Query]] object. + * QueryBuilder builds an elasticsearch query based on the specification given as a [[Query]] object. * - * QueryBuilder can also be used to build SQL statements such as INSERT, UPDATE, DELETE, CREATE TABLE, - * from a [[Query]] object. * - * @author Qiang Xue + * @author Carsten Brandt * @since 2.0 */ class QueryBuilder extends \yii\base\Object @@ -30,105 +29,51 @@ class QueryBuilder extends \yii\base\Object * @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 = array()) + public function __construct($connection, $config = []) { $this->db = $connection; parent::__construct($config); } /** - * Generates a SELECT SQL statement from a [[Query]] object. - * @param Query $query the [[Query]] object from which the SQL statement will be generated + * 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) { - $searchQuery = array(); - $this->buildFields($searchQuery, $query->fields); -// $this->buildFrom($searchQuery, $query->from); - $this->buildCondition($searchQuery, $query->where); - $this->buildOrderBy($searchQuery, $query->orderBy); - $this->buildLimit($searchQuery, $query->limit, $query->offset); - - return $searchQuery; - } + $parts = []; - /** - * Converts an abstract column type into a physical column type. - * The conversion is done using the type map specified in [[typeMap]]. - * The following abstract column types are supported (using MySQL as an example to explain the corresponding - * physical types): - * - * - `pk`: an auto-incremental primary key type, will be converted into "int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY" - * - `bigpk`: an auto-incremental primary key type, will be converted into "bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY" - * - `string`: string type, will be converted into "varchar(255)" - * - `text`: a long string type, will be converted into "text" - * - `smallint`: a small integer type, will be converted into "smallint(6)" - * - `integer`: integer type, will be converted into "int(11)" - * - `bigint`: a big integer type, will be converted into "bigint(20)" - * - `boolean`: boolean type, will be converted into "tinyint(1)" - * - `float``: float number type, will be converted into "float" - * - `decimal`: decimal number type, will be converted into "decimal" - * - `datetime`: datetime type, will be converted into "datetime" - * - `timestamp`: timestamp type, will be converted into "timestamp" - * - `time`: time type, will be converted into "time" - * - `date`: date type, will be converted into "date" - * - `money`: money type, will be converted into "decimal(19,4)" - * - `binary`: binary data type, will be converted into "blob" - * - * If the abstract type contains two or more parts separated by spaces (e.g. "string NOT NULL"), then only - * the first part will be converted, and the rest of the parts will be appended to the converted result. - * For example, 'string NOT NULL' is converted to 'varchar(255) NOT NULL'. - * - * For some of the abstract types you can also specify a length or precision constraint - * by prepending it in round brackets directly to the type. - * For example `string(32)` will be converted into "varchar(32)" on a MySQL database. - * If the underlying DBMS does not support these kind of constraints for a type it will - * be ignored. - * - * If a type cannot be found in [[typeMap]], it will be returned without any change. - * @param string $type abstract column type - * @return string physical column type. - */ - public function getColumnType($type) - { - if (isset($this->typeMap[$type])) { - return $this->typeMap[$type]; - } elseif (preg_match('/^(\w+)\((.+?)\)(.*)$/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace('/\(.+\)/', '(' . $matches[2] . ')', $this->typeMap[$matches[1]]) . $matches[3]; - } - } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { - if (isset($this->typeMap[$matches[1]])) { - return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); - } + if ($query->fields !== null) { + $parts['fields'] = (array) $query->fields; } - return $type; - } - - /** - * @param array $columns - * @param boolean $distinct - * @param string $selectOption - * @return string the SELECT clause built from [[query]]. - */ - public function buildFields(&$query, $columns) - { - if ($columns === null) { - return; + if ($query->limit !== null && $query->limit >= 0) { + $parts['size'] = $query->limit; } - foreach ($columns as $i => $column) { - if (is_object($column)) { - $columns[$i] = (string)$column; - } + if ($query->offset > 0) { + $parts['from'] = (int) $query->offset; + } + + $this->buildCondition($parts, $query->where); + $this->buildOrderBy($parts, $query->orderBy); + + if (empty($parts['query'])) { + $parts['query'] = ["match_all" => (object)[]]; } - $query['fields'] = $columns; + + return [ + 'queryParts' => $parts, + 'index' => $query->index, + 'type' => $query->type, + 'options' => [ + 'timeout' => $query->timeout + ], + ]; } /** - * @param array $columns - * @return string the ORDER BY clause built from [[query]]. + * adds order by condition to the query */ public function buildOrderBy(&$query, $columns) { @@ -143,28 +88,13 @@ class QueryBuilder extends \yii\base\Object } elseif (is_string($direction)) { $orders[] = $direction; } else { - $orders[] = array($name => ($direction === Query::SORT_DESC ? 'desc' : 'asc')); + $orders[] = array($name => ($direction === SORT_DESC ? 'desc' : 'asc')); } } $query['sort'] = $orders; } /** - * @param integer $limit - * @param integer $offset - * @return string the LIMIT and OFFSET clauses built from [[query]]. - */ - public function buildLimit(&$query, $limit, $offset) - { - if ($limit !== null && $limit >= 0) { - $query['size'] = $limit; - } - if ($offset > 0) { - $query['from'] = (int) $offset; - } - } - - /** * 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. @@ -191,7 +121,7 @@ class QueryBuilder extends \yii\base\Object return; } if (!is_array($condition)) { - throw new NotSupportedException('String conditions are not supported by elasticsearch.'); + throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); } if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... $operator = strtoupper($condition[0]); @@ -200,7 +130,7 @@ class QueryBuilder extends \yii\base\Object array_shift($condition); $this->$method($query, $operator, $condition); } else { - throw new Exception('Found unknown operator in query: ' . $operator); + throw new InvalidParamException('Found unknown operator in query: ' . $operator); } } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... $this->buildHashCondition($query, $condition); diff --git a/tests/unit/framework/elasticsearch/ActiveRecordTest.php b/tests/unit/framework/elasticsearch/ActiveRecordTest.php index c164678..78db06f 100644 --- a/tests/unit/framework/elasticsearch/ActiveRecordTest.php +++ b/tests/unit/framework/elasticsearch/ActiveRecordTest.php @@ -296,17 +296,22 @@ class ActiveRecordTest extends ElasticSearchTestCase $this->assertEquals(1, count(Customer::find()->where(array('AND', array('name' => array('user2','user3')), array('BETWEEN', 'status', 2, 4)))->all())); } -// public function testSum() -// { -// $this->assertEquals(6, OrderItem::find()->count()); -// $this->assertEquals(7, OrderItem::find()->sum('quantity')); -// } + public function testFindNullValues() + { + $customer = Customer::find(2); + $customer->name = null; + $customer->save(false); -// public function testFindColumn() -// { -// $this->assertEquals(array('user1', 'user2', 'user3'), Customer::find()->column('name')); -//// TODO $this->assertEquals(array('user3', 'user2', 'user1'), Customer::find()->orderBy(array('name' => Query::SORT_DESC))->column('name')); -// } + $result = Customer::find()->where(['name' => null])->all(); + $this->assertEquals(1, count($result)); + $this->assertEquals(2, reset($result)->primaryKey); + } + + public function testFindColumn() + { + $this->assertEquals(array('user1', 'user2', 'user3'), Customer::find()->column('name')); + $this->assertEquals(array('user3', 'user2', 'user1'), Customer::find()->orderBy(array('name' => SORT_DESC))->column('name')); + } public function testExists() { diff --git a/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php b/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php index eb70a37..70b39b1 100644 --- a/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php +++ b/tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php @@ -16,51 +16,4 @@ class ElasticSearchConnectionTest extends ElasticSearchTestCase $db->open(); } - /** - * test connection to redis and selection of db - */ - public function testConnect() - { - $db = new Connection(); - $db->dsn = 'redis://localhost:6379'; - $db->open(); - $this->assertTrue($db->ping()); - $db->set('YIITESTKEY', 'YIITESTVALUE'); - $db->close(); - - $db = new Connection(); - $db->dsn = 'redis://localhost:6379/0'; - $db->open(); - $this->assertEquals('YIITESTVALUE', $db->get('YIITESTKEY')); - $db->close(); - - $db = new Connection(); - $db->dsn = 'redis://localhost:6379/1'; - $db->open(); - $this->assertNull($db->get('YIITESTKEY')); - $db->close(); - } - - public function keyValueData() - { - return array( - array(123), - array(-123), - array(0), - array('test'), - array("test\r\ntest"), - array(''), - ); - } - - /** - * @dataProvider keyValueData - */ - public function testStoreGet($data) - { - $db = $this->getConnection(true); - - $db->set('hi', $data); - $this->assertEquals($data, $db->get('hi')); - } } \ No newline at end of file diff --git a/tests/unit/framework/elasticsearch/QueryTest.php b/tests/unit/framework/elasticsearch/QueryTest.php new file mode 100644 index 0000000..ae83620 --- /dev/null +++ b/tests/unit/framework/elasticsearch/QueryTest.php @@ -0,0 +1,180 @@ +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); + + } + + + 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() + { + } +}