Browse Source

more on elasticsearch Query interface added facet search

tags/2.0.0-beta
Carsten Brandt 11 years ago
parent
commit
e15860c3fa
  1. 2
      framework/yii/db/ActiveRecord.php
  2. 5
      framework/yii/db/QueryBuilder.php
  3. 4
      framework/yii/elasticsearch/ActiveQuery.php
  4. 4
      framework/yii/elasticsearch/ActiveRecord.php
  5. 19
      framework/yii/elasticsearch/Command.php
  6. 10
      framework/yii/elasticsearch/Connection.php
  7. 218
      framework/yii/elasticsearch/Query.php
  8. 136
      framework/yii/elasticsearch/QueryBuilder.php
  9. 25
      tests/unit/framework/elasticsearch/ActiveRecordTest.php
  10. 47
      tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php
  11. 180
      tests/unit/framework/elasticsearch/QueryTest.php

2
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. * - 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. * - 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 * 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). * returned (null will be returned if there is no matching).
* @throws InvalidConfigException if the AR class does not have a primary key * @throws InvalidConfigException if the AR class does not have a primary key

5
framework/yii/db/QueryBuilder.php

@ -7,6 +7,7 @@
namespace yii\db; namespace yii\db;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException; use yii\base\NotSupportedException;
/** /**
@ -761,7 +762,7 @@ class QueryBuilder extends \yii\base\Object
* on how to specify a condition. * on how to specify a condition.
* @param array $params the binding parameters to be populated * @param array $params the binding parameters to be populated
* @return string the generated SQL expression * @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) public function buildCondition($condition, &$params)
{ {
@ -790,7 +791,7 @@ class QueryBuilder extends \yii\base\Object
array_shift($condition); array_shift($condition);
return $this->$method($operator, $condition, $params); return $this->$method($operator, $condition, $params);
} else { } 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', ... } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
return $this->buildHashCondition($condition, $params); return $this->buildHashCondition($condition, $params);

4
framework/yii/elasticsearch/ActiveQuery.php

@ -71,8 +71,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface
$this->index = $modelClass::index(); $this->index = $modelClass::index();
$this->type = $modelClass::type(); $this->type = $modelClass::type();
} }
$query = $db->getQueryBuilder()->build($this); $commandConfig = $db->getQueryBuilder()->build($this);
return $db->createCommand($query, $this->index, $this->type); return $db->createCommand($commandConfig);
} }
/** /**

4
framework/yii/elasticsearch/ActiveRecord.php

@ -110,6 +110,10 @@ class ActiveRecord extends \yii\db\ActiveRecord
return $models; 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 * @inheritDoc
*/ */

19
framework/yii/elasticsearch/Command.php

@ -6,6 +6,7 @@
namespace yii\elasticsearch; namespace yii\elasticsearch;
use Guzzle\Http\Exception\ClientErrorResponseException;
use yii\base\Component; use yii\base\Component;
use yii\db\Exception; use yii\db\Exception;
use yii\helpers\Json; 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 * @var string|array the types to execute the query on. Defaults to null meaning all types
*/ */
public $type; 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 = []) public function queryAll($options = [])
{ {
$query = $this->query; $query = $this->queryParts;
if (empty($query)) { if (empty($query)) {
$query = '{}'; $query = '{}';
} }
@ -55,7 +57,11 @@ class Command extends Component
$this->type !== null ? $this->type : '_all', $this->type !== null ? $this->type : '_all',
'_search' '_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']; return Json::decode($response->getBody(true))['hits'];
} }
@ -405,7 +411,8 @@ class Command extends Component
return urlencode(is_array($a) ? implode(',', $a) : $a); return urlencode(is_array($a) ? implode(',', $a) : $a);
}, $path)); }, $path));
if (!empty($options)) { if (!empty($options) || !empty($this->options)) {
$options = array_merge($this->options, $options);
$url .= '?' . http_build_query($options); $url .= '?' . http_build_query($options);
} }

10
framework/yii/elasticsearch/Connection.php

@ -58,15 +58,11 @@ class Connection extends Component
* @param string $query the SQL statement to be executed * @param string $query the SQL statement to be executed
* @return Command the DB command * @return Command the DB command
*/ */
public function createCommand($query = null, $index = null, $type = null) public function createCommand($config = [])
{ {
$this->open(); $this->open();
$command = new Command(array( $config['db'] = $this;
'db' => $this, $command = new Command($config);
'query' => $query,
'index' => $index,
'type' => $type,
));
return $command; return $command;
} }

218
framework/yii/elasticsearch/Query.php

@ -50,6 +50,16 @@ class Query extends Component implements QueryInterface
*/ */
public $timeout; public $timeout;
public $query;
public $filter;
public $facets = [];
public $facetResults = [];
public $totalCount;
/** /**
* Creates a DB command that can be used to execute this query. * Creates a DB command that can be used to execute this query.
* @param Connection $db the database connection used to execute the 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'); $db = Yii::$app->getComponent('elasticsearch');
} }
$query = $db->getQueryBuilder()->build($this); $commandConfig = $db->getQueryBuilder()->build($this);
return $db->createCommand($query, $this->index, $this->type); return $db->createCommand($commandConfig);
} }
/** /**
@ -74,11 +84,13 @@ class Query extends Component implements QueryInterface
*/ */
public function all($db = null) 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) { if ($this->indexBy === null && $this->fields === null) {
return $rows; return $rows;
} }
$result = []; $models = [];
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if ($this->fields !== null) { if ($this->fields !== null) {
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; $row['_source'] = isset($row['fields']) ? $row['fields'] : [];
@ -91,9 +103,9 @@ class Query extends Component implements QueryInterface
$key = call_user_func($this->indexBy, $row); $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; $options['size'] = 1;
$result = $this->createCommand($db)->queryAll($options); $result = $this->createCommand($db)->queryAll($options);
// TODO publish facet results
if (empty($result['hits'])) { if (empty($result['hits'])) {
return false; 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. * Returns the query result as a scalar value.
* The value returned will be the specified field in the first document of the query results. * 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 string $field name of the attribute to select
@ -146,11 +173,12 @@ class Query extends Component implements QueryInterface
*/ */
public function column($field, $db = null) public function column($field, $db = null)
{ {
$query = clone $this; $command = $this->createCommand($db);
$rows = $query->fields([$field])->createCommand($db)->queryAll()['hits']; $command->queryParts['fields'] = [$field];
$rows = $command->queryAll()['hits'];
$result = []; $result = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$result[] = $row['fields'][$field]; $result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null;
} }
return $result; return $result;
} }
@ -164,6 +192,10 @@ class Query extends Component implements QueryInterface
*/ */
public function count($q = '*', $db = null) 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']; $count = $this->createCommand($db)->queryCount()['total'];
if ($this->limit === null && $this->offset === null) { if ($this->limit === null && $this->offset === null) {
return $count; return $count;
@ -173,84 +205,156 @@ class Query extends Component implements QueryInterface
return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit);
} }
/** /**
* Returns the sum of the specified column values. * Returns a value indicating whether the query result contains any row of data.
* @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. * @param Connection $db the database connection used to execute the query.
* If this parameter is not given, the `elasticsearch` application component will be used. * 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 self::one($db) !== false;
return $this->createCommand($db)->queryScalar();
} }
/** /**
* Returns the average of the specified column values. * Adds a facet search to this query.
* @param string $q the column name or expression. * @param string $name the name of this facet
* Make sure you properly quote column names in the expression. * @param string $type the facet type. e.g. `terms`, `range`, `histogram`...
* @param Connection $db the database connection used to execute the query. * @param string|array $options the configuration options for this facet. Can be an array or a json string.
* If this parameter is not given, the `elasticsearch` application component will be used. * @return static
* @return integer the average of the specified column values. * @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)"]; $this->facets[$name] = [$type => $options];
return $this->createCommand($db)->queryScalar(); return $this;
} }
/** /**
* Returns the minimum of the specified column values. * The `terms facet` allow to specify field facets that return the N most frequent terms.
* @param string $q the column name or expression. * @param string $name the name of this facet
* Make sure you properly quote column names in the expression. * @param array $options additional option. Please refer to the elasticsearch documentation for details.
* @param Connection $db the database connection used to execute the query. * @return static
* If this parameter is not given, the `elasticsearch` application component will be used. * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html
* @return integer the minimum of the specified column values.
*/ */
public function min($q, $db = null) public function addTermFacet($name, $options)
{ {
$this->select = ["MIN($q)"]; return $this->addFacet($name, 'terms', $options);
return $this->createCommand($db)->queryScalar();
} }
/** /**
* Returns 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
* @param string $q the column name or expression. * within each range, and aggregated data either based on the field, or using another field.
* Make sure you properly quote column names in the expression. * @param string $name the name of this facet
* @param Connection $db the database connection used to execute the query. * @param array $options additional option. Please refer to the elasticsearch documentation for details.
* If this parameter is not given, the `elasticsearch` application component will be used. * @return static
* @return integer the maximum of the specified column values. * @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->addFacet($name, 'range', $options);
return $this->createCommand($db)->queryScalar();
} }
/** /**
* Returns a value indicating 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.
* @param Connection $db the database connection used to execute the query. * Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per
* If this parameter is not given, the `elasticsearch` application component will be used. * interval/bucket (count and total).
* @return boolean whether the query result contains any row of data. * @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->addFacet($name, 'histogram', $options);
return $this->one($db) !== null;
} }
/** /**
* Executes the query and returns all results as an array. * A specific histogram facet that can work with date field types enhancing it over the regular histogram facet.
* @param Connection $db the database connection used to execute the query. * @param string $name the name of this facet
* If this parameter is not given, the `elasticsearch` application component will be used. * @param array $options additional option. Please refer to the elasticsearch documentation for details.
* @return array the query results. If the query results in nothing, an empty array will be returned. * @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()
{
} }
/** /**

136
framework/yii/elasticsearch/QueryBuilder.php

@ -7,15 +7,14 @@
namespace yii\elasticsearch; namespace yii\elasticsearch;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException; 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 <qiang.xue@gmail.com> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class QueryBuilder extends \yii\base\Object class QueryBuilder extends \yii\base\Object
@ -30,105 +29,51 @@ class QueryBuilder extends \yii\base\Object
* @param Connection $connection the database connection. * @param Connection $connection the database connection.
* @param array $config name-value pairs that will be used to initialize the object properties * @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; $this->db = $connection;
parent::__construct($config); parent::__construct($config);
} }
/** /**
* Generates a SELECT SQL statement from a [[Query]] object. * Generates query from a [[Query]] object.
* @param Query $query the [[Query]] object from which the SQL statement will be generated * @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 * @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). * parameters to be bound to the SQL statement (the second array element).
*/ */
public function build($query) public function build($query)
{ {
$searchQuery = array(); $parts = [];
$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;
}
/** if ($query->fields !== null) {
* Converts an abstract column type into a physical column type. $parts['fields'] = (array) $query->fields;
* 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);
}
} }
return $type; if ($query->limit !== null && $query->limit >= 0) {
} $parts['size'] = $query->limit;
/**
* @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;
} }
foreach ($columns as $i => $column) { if ($query->offset > 0) {
if (is_object($column)) { $parts['from'] = (int) $query->offset;
$columns[$i] = (string)$column; }
}
$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 * adds order by condition to the query
* @return string the ORDER BY clause built from [[query]].
*/ */
public function buildOrderBy(&$query, $columns) public function buildOrderBy(&$query, $columns)
{ {
@ -143,28 +88,13 @@ class QueryBuilder extends \yii\base\Object
} elseif (is_string($direction)) { } elseif (is_string($direction)) {
$orders[] = $direction; $orders[] = $direction;
} else { } else {
$orders[] = array($name => ($direction === Query::SORT_DESC ? 'desc' : 'asc')); $orders[] = array($name => ($direction === SORT_DESC ? 'desc' : 'asc'));
} }
} }
$query['sort'] = $orders; $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. * Parses the condition specification and generates the corresponding SQL expression.
* @param string|array $condition the condition specification. Please refer to [[Query::where()]] * @param string|array $condition the condition specification. Please refer to [[Query::where()]]
* on how to specify a condition. * on how to specify a condition.
@ -191,7 +121,7 @@ class QueryBuilder extends \yii\base\Object
return; return;
} }
if (!is_array($condition)) { 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, ... if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ...
$operator = strtoupper($condition[0]); $operator = strtoupper($condition[0]);
@ -200,7 +130,7 @@ class QueryBuilder extends \yii\base\Object
array_shift($condition); array_shift($condition);
$this->$method($query, $operator, $condition); $this->$method($query, $operator, $condition);
} else { } 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', ... } else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ...
$this->buildHashCondition($query, $condition); $this->buildHashCondition($query, $condition);

25
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())); $this->assertEquals(1, count(Customer::find()->where(array('AND', array('name' => array('user2','user3')), array('BETWEEN', 'status', 2, 4)))->all()));
} }
// public function testSum() public function testFindNullValues()
// { {
// $this->assertEquals(6, OrderItem::find()->count()); $customer = Customer::find(2);
// $this->assertEquals(7, OrderItem::find()->sum('quantity')); $customer->name = null;
// } $customer->save(false);
// public function testFindColumn() $result = Customer::find()->where(['name' => null])->all();
// { $this->assertEquals(1, count($result));
// $this->assertEquals(array('user1', 'user2', 'user3'), Customer::find()->column('name')); $this->assertEquals(2, reset($result)->primaryKey);
//// TODO $this->assertEquals(array('user3', 'user2', 'user1'), Customer::find()->orderBy(array('name' => Query::SORT_DESC))->column('name')); }
// }
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() public function testExists()
{ {

47
tests/unit/framework/elasticsearch/ElasticSearchConnectionTest.php

@ -16,51 +16,4 @@ class ElasticSearchConnectionTest extends ElasticSearchTestCase
$db->open(); $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'));
}
} }

180
tests/unit/framework/elasticsearch/QueryTest.php

@ -0,0 +1,180 @@
<?php
namespace yiiunit\framework\elasticsearch;
use yii\elasticsearch\Query;
/**
* @group db
* @group mysql
*/
class QueryTest extends ElasticSearchTestCase
{
protected function setUp()
{
parent::setUp();
$command = $this->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()
{
}
}
Loading…
Cancel
Save