Commit e15860c3 by Carsten Brandt

more on elasticsearch Query interface added facet search

parent a0a3c36c
...@@ -132,7 +132,7 @@ class ActiveRecord extends Model ...@@ -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
......
...@@ -7,6 +7,7 @@ ...@@ -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 ...@@ -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 ...@@ -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);
......
...@@ -71,8 +71,8 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -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);
} }
/** /**
......
...@@ -110,6 +110,10 @@ class ActiveRecord extends \yii\db\ActiveRecord ...@@ -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
*/ */
......
...@@ -6,6 +6,7 @@ ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
} }
......
...@@ -58,15 +58,11 @@ class Connection extends Component ...@@ -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;
} }
......
...@@ -7,15 +7,14 @@ ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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);
......
...@@ -296,17 +296,22 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -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()
{ {
......
...@@ -16,51 +16,4 @@ class ElasticSearchConnectionTest extends ElasticSearchTestCase ...@@ -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'));
}
} }
\ No newline at end of file
<?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()
{
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment