Commit 613758dd by Carsten Brandt

refactored elasticsearch COnnection and Command

parent 8792f21f
...@@ -13,10 +13,12 @@ nbproject ...@@ -13,10 +13,12 @@ nbproject
Thumbs.db Thumbs.db
# composer vendor dir # composer vendor dir
/yii/vendor /vendor
# composer itself is not needed # composer itself is not needed
composer.phar composer.phar
# composer.lock should not be committed as we always want the latest versions
/composer.lock
# Mac DS_Store Files # Mac DS_Store Files
.DS_Store .DS_Store
......
...@@ -7,6 +7,7 @@ php: ...@@ -7,6 +7,7 @@ php:
services: services:
- redis-server - redis-server
- memcached - memcached
- elasticsearch
before_script: before_script:
- composer self-update && composer --version - composer self-update && composer --version
......
...@@ -84,7 +84,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -84,7 +84,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
public function all($db = null) public function all($db = null)
{ {
$command = $this->createCommand($db); $command = $this->createCommand($db);
$result = $command->queryAll(); $result = $command->search();
if (empty($result['hits'])) { if (empty($result['hits'])) {
return []; return [];
} }
...@@ -154,7 +154,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface ...@@ -154,7 +154,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { if ($field == ActiveRecord::PRIMARY_KEY_NAME) {
$command = $this->createCommand($db); $command = $this->createCommand($db);
$command->queryParts['fields'] = []; $command->queryParts['fields'] = [];
$rows = $command->queryAll()['hits']; $rows = $command->search()['hits'];
$result = []; $result = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$result[] = $row['_id']; $result[] = $row['_id'];
......
...@@ -370,11 +370,10 @@ class ActiveRecord extends \yii\db\ActiveRecord ...@@ -370,11 +370,10 @@ class ActiveRecord extends \yii\db\ActiveRecord
} }
// TODO do this via command // TODO do this via command
$url = '/' . static::index() . '/' . static::type() . '/_bulk'; $url = [static::index(), static::type(), '_bulk'];
$response = static::getDb()->http()->post($url, null, $bulk)->send(); $response = static::getDb()->post($url, [], $bulk);
$body = Json::decode($response->getBody(true));
$n=0; $n=0;
foreach($body['items'] as $item) { foreach($response['items'] as $item) {
if ($item['update']['ok']) { if ($item['update']['ok']) {
$n++; $n++;
} }
...@@ -421,11 +420,10 @@ class ActiveRecord extends \yii\db\ActiveRecord ...@@ -421,11 +420,10 @@ class ActiveRecord extends \yii\db\ActiveRecord
} }
// TODO do this via command // TODO do this via command
$url = '/' . static::index() . '/' . static::type() . '/_bulk'; $url = [static::index(), static::type(), '_bulk'];
$response = static::getDb()->http()->post($url, null, $bulk)->send(); $response = static::getDb()->post($url, [], $bulk);
$body = Json::decode($response->getBody(true));
$n=0; $n=0;
foreach($body['items'] as $item) { foreach($response['items'] as $item) {
if ($item['delete']['found'] && $item['delete']['ok']) { if ($item['delete']['found'] && $item['delete']['ok']) {
$n++; $n++;
} }
......
...@@ -8,65 +8,53 @@ ...@@ -8,65 +8,53 @@
namespace yii\elasticsearch; namespace yii\elasticsearch;
use Guzzle\Http\Exception\ClientErrorResponseException;
use Yii;
use yii\base\Component; use yii\base\Component;
use yii\base\Exception;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\Json;
/** /**
* elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher * elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher
* *
*
* @author Carsten Brandt <mail@cebe.cc> * @author Carsten Brandt <mail@cebe.cc>
* @since 2.0 * @since 2.0
*/ */
class Connection extends Component abstract class Connection extends Component
{ {
/** /**
* @event Event an event that is triggered after a DB connection is established * @event Event an event that is triggered after a DB connection is established
*/ */
const EVENT_AFTER_OPEN = 'afterOpen'; const EVENT_AFTER_OPEN = 'afterOpen';
// TODO add autodetection of cluster nodes
// http://localhost:9200/_cluster/nodes
public $nodes = array(
array(
'host' => 'localhost',
'port' => 9200,
)
);
// http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth
public $auth = [];
// TODO use timeouts
/** /**
* @var float timeout to use for connection to redis. If not set the timeout set in php.ini will be used: ini_get("default_socket_timeout") * @var bool whether to autodetect available cluster nodes on [[open()]]
*/ */
public $connectionTimeout = null; public $autodetectCluster = true;
/** /**
* @var float timeout to use for redis socket when reading and writing data. If not set the php default value will be used. * @var array cluster nodes
* This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true.
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info
*/ */
public $dataTimeout = null; public $nodes = [
['http_address' => 'inet[/127.0.0.1:9200]'],
];
/**
* @var array the active node. key of [[nodes]]. Will be randomly selected on [[open()]].
*/
public $activeNode;
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth
public $auth = [];
public function init() public function init()
{ {
if ($this->nodes === array()) { foreach($this->nodes as $node) {
throw new InvalidConfigException('elasticsearch needs at least one node.'); if (!isset($node['http_address'])) {
throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.');
} }
} }
/**
* Creates a command for execution.
* @param string $query the SQL statement to be executed
* @return Command the DB command
*/
public function createCommand($config = [])
{
$this->open();
$config['db'] = $this;
$command = new Command($config);
return $command;
} }
/** /**
...@@ -85,7 +73,7 @@ class Connection extends Component ...@@ -85,7 +73,7 @@ class Connection extends Component
*/ */
public function getIsActive() public function getIsActive()
{ {
return false; // TODO implement return $this->activeNode !== null;
} }
/** /**
...@@ -95,48 +83,37 @@ class Connection extends Component ...@@ -95,48 +83,37 @@ class Connection extends Component
*/ */
public function open() public function open()
{ {
// TODO select one node to be the active one. if ($this->activeNode !== null) {
return;
foreach($this->nodes as $key => $node) {
if (is_array($node)) {
$this->nodes[$key] = new Node($node);
} }
if (empty($this->nodes)) {
throw new InvalidConfigException('elasticsearch needs at least one node to operate.');
} }
/* if ($this->_socket === null) { if ($this->autodetectCluster) {
if (empty($this->dsn)) { $node = reset($this->nodes);
throw new InvalidConfigException('Connection.dsn cannot be empty.'); $host = $node['http_address'];
if (strncmp($host, 'inet[/', 6) == 0) {
$host = substr($host, 6, -1);
} }
$dsn = explode('/', $this->dsn); $response = $this->httpRequest('get', 'http://' . $host . '/_cluster/nodes');
$host = $dsn[2]; $this->nodes = $response['nodes'];
if (strpos($host, ':')===false) { if (empty($this->nodes)) {
$host .= ':6379'; throw new Exception('cluster autodetection did not find any active node.');
}
$db = isset($dsn[3]) ? $dsn[3] : 0;
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__);
$this->_socket = @stream_socket_client(
$host,
$errorNumber,
$errorDescription,
$this->connectionTimeout ? $this->connectionTimeout : ini_get("default_socket_timeout")
);
if ($this->_socket) {
if ($this->dataTimeout !== null) {
stream_set_timeout($this->_socket, $timeout=(int)$this->dataTimeout, (int) (($this->dataTimeout - $timeout) * 1000000));
} }
if ($this->password !== null) {
$this->executeCommand('AUTH', array($this->password));
} }
$this->executeCommand('SELECT', array($db)); $this->selectActiveNode();
Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes)
. ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__);
$this->initConnection(); $this->initConnection();
} else {
\Yii::error("Failed to open DB connection ({$this->dsn}): " . $errorNumber . ' - ' . $errorDescription, __CLASS__);
$message = YII_DEBUG ? 'Failed to open DB connection: ' . $errorNumber . ' - ' . $errorDescription : 'Failed to open DB connection.';
throw new Exception($message, $errorDescription, (int)$errorNumber);
} }
}*/
// TODO implement /**
* select active node randomly
*/
public function selectActiveNode()
{
$keys = array_keys($this->nodes);
$this->activeNode = $keys[rand(0, count($keys) - 1)];
} }
/** /**
...@@ -145,14 +122,9 @@ class Connection extends Component ...@@ -145,14 +122,9 @@ class Connection extends Component
*/ */
public function close() public function close()
{ {
// TODO implement Yii::trace('Closing connection to elasticsearch. Active node was: '
/* if ($this->_socket !== null) { . $this->nodes[$this->activeNode]['http_address'], __CLASS__);
\Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); $this->activeNode = null;
$this->executeCommand('QUIT');
stream_socket_shutdown($this->_socket, STREAM_SHUT_RDWR);
$this->_socket = null;
$this->_transaction = null;
}*/
} }
/** /**
...@@ -174,9 +146,17 @@ class Connection extends Component ...@@ -174,9 +146,17 @@ class Connection extends Component
return 'elasticsearch'; return 'elasticsearch';
} }
public function getNodeInfo() /**
* Creates a command for execution.
* @param array $config the configuration for the Command class
* @return Command the DB command
*/
public function createCommand($config = [])
{ {
// TODO HTTP request to localhost:9200/ $this->open();
$config['db'] = $this;
$command = new Command($config);
return $command;
} }
public function getQueryBuilder() public function getQueryBuilder()
...@@ -184,13 +164,58 @@ class Connection extends Component ...@@ -184,13 +164,58 @@ class Connection extends Component
return new QueryBuilder($this); return new QueryBuilder($this);
} }
/** public function get($url, $options = [], $body = null, $validCodes = [])
* @return \Guzzle\Http\Client {
*/ $this->open();
public function http() return $this->httpRequest('get', $this->createUrl($url, $options), $body);
}
public function head($url, $options = [], $body = null)
{
$this->open();
return $this->httpRequest('head', $this->createUrl($url, $options), $body);
}
public function post($url, $options = [], $body = null)
{
$this->open();
return $this->httpRequest('post', $this->createUrl($url, $options), $body);
}
public function put($url, $options = [], $body = null)
{
$this->open();
return $this->httpRequest('put', $this->createUrl($url, $options), $body);
}
public function delete($url, $options = [], $body = null)
{
$this->open();
return $this->httpRequest('delete', $this->createUrl($url, $options), $body);
}
private function createUrl($path, $options = [])
{
$url = implode('/', array_map(function($a) {
return urlencode(is_array($a) ? implode(',', $a) : $a);
}, $path));
if (!empty($options)) {
$url .= '?' . http_build_query($options);
}
return $url;
}
protected abstract function httpRequest($type, $url, $body = null);
public function getNodeInfo()
{
return $this->get([]);
}
public function getClusterState()
{ {
$guzzle = new \Guzzle\Http\Client('http://localhost:9200/'); return $this->get(['_cluster', 'state']);
//$guzzle->setDefaultOption()
return $guzzle;
} }
} }
\ No newline at end of file
<?php
/**
*
*
* @author Carsten Brandt <mail@cebe.cc>
*/
namespace yii\elasticsearch;
use Guzzle\Http\Exception\ClientErrorResponseException;
use yii\base\Exception;
use yii\helpers\Json;
class GuzzleConnection extends Connection
{
/**
* @var \Guzzle\Http\Client
*/
private $_http;
protected function httpRequest($type, $url, $body = null)
{
if ($this->_http === null) {
$this->_http = new \Guzzle\Http\Client('http://localhost:9200/');// TODO use active node
//$guzzle->setDefaultOption()
}
$requestOptions = [];
if ($type == 'head') {
$requestOptions['exceptions'] = false;
}
if ($type == 'get' && $body !== null) {
$type = 'post';
}
try{
$response = $this->_http->createRequest(
strtoupper($type)
, $url,
null,
$body,
$requestOptions
)->send();
} catch(ClientErrorResponseException $e) {
if ($e->getResponse()->getStatusCode() == 404) {
return false;
}
throw new Exception("elasticsearch error:\n\n"
. $body . "\n\n" . $e->getMessage()
. print_r(Json::decode($e->getResponse()->getBody(true)), true), 0, $e);
}
if ($type == 'head') {
return $response->getStatusCode() == 200;
}
return Json::decode($response->getBody(true));
}
}
\ No newline at end of file
...@@ -84,7 +84,7 @@ class Query extends Component implements QueryInterface ...@@ -84,7 +84,7 @@ class Query extends Component implements QueryInterface
*/ */
public function all($db = null) public function all($db = null)
{ {
$result = $this->createCommand($db)->queryAll(); $result = $this->createCommand($db)->search();
// TODO publish facet results // TODO publish facet results
$rows = $result['hits']; $rows = $result['hits'];
if ($this->indexBy === null && $this->fields === null) { if ($this->indexBy === null && $this->fields === null) {
...@@ -118,7 +118,7 @@ class Query extends Component implements QueryInterface ...@@ -118,7 +118,7 @@ class Query extends Component implements QueryInterface
public function one($db = null) public function one($db = null)
{ {
$options['size'] = 1; $options['size'] = 1;
$result = $this->createCommand($db)->queryAll($options); $result = $this->createCommand($db)->search($options);
// TODO publish facet results // TODO publish facet results
if (empty($result['hits'])) { if (empty($result['hits'])) {
return false; return false;
...@@ -175,7 +175,7 @@ class Query extends Component implements QueryInterface ...@@ -175,7 +175,7 @@ class Query extends Component implements QueryInterface
{ {
$command = $this->createCommand($db); $command = $this->createCommand($db);
$command->queryParts['fields'] = [$field]; $command->queryParts['fields'] = [$field];
$rows = $command->queryAll()['hits']; $rows = $command->search()['hits'];
$result = []; $result = [];
foreach ($rows as $row) { foreach ($rows as $row) {
$result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; $result[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null;
...@@ -196,7 +196,9 @@ class Query extends Component implements QueryInterface ...@@ -196,7 +196,9 @@ class Query extends Component implements QueryInterface
// only when no facety are registerted. // only when no facety are registerted.
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html // http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-count.html
$count = $this->createCommand($db)->queryCount()['total']; $options = [];
$options['search_type'] = 'count';
$count = $this->createCommand($db)->search($options)['total'];
if ($this->limit === null && $this->offset === null) { if ($this->limit === null && $this->offset === null) {
return $count; return $count;
} elseif ($this->offset !== null) { } elseif ($this->offset !== null) {
......
...@@ -14,7 +14,7 @@ return [ ...@@ -14,7 +14,7 @@ return [
'elasticsearch' => [ 'elasticsearch' => [
'class' => 'yii\elasticsearch\Connection', 'class' => 'yii\elasticsearch\Connection',
'hosts' => [ 'hosts' => [
['hostname' => 'localhost', 'port' => 9200], ['http_address' => '127.0.0.1:9200'],
// configure more hosts if you have a cluster // configure more hosts if you have a cluster
], ],
], ],
......
...@@ -24,4 +24,9 @@ class ActiveRecord extends \yii\elasticsearch\ActiveRecord ...@@ -24,4 +24,9 @@ class ActiveRecord extends \yii\elasticsearch\ActiveRecord
{ {
return self::$db; return self::$db;
} }
public static function index()
{
return 'yiitest';
}
} }
...@@ -43,10 +43,12 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -43,10 +43,12 @@ class ActiveRecordTest extends ElasticSearchTestCase
/** @var Connection $db */ /** @var Connection $db */
$db = ActiveRecord::$db = $this->getConnection(); $db = ActiveRecord::$db = $this->getConnection();
// delete all indexes // delete index
$db->http()->delete('_all')->send(); if ($db->createCommand()->indexExists('yiitest')) {
$db->createCommand()->deleteIndex('yiitest');
}
$db->http()->post('items', null, Json::encode([ $db->post(['yiitest'], [], Json::encode([
'mappings' => [ 'mappings' => [
"item" => [ "item" => [
"_source" => [ "enabled" => true ], "_source" => [ "enabled" => true ],
...@@ -56,19 +58,7 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -56,19 +58,7 @@ class ActiveRecordTest extends ElasticSearchTestCase
] ]
] ]
], ],
]))->send(); ]));
$db->http()->post('customers', null, Json::encode([
'mappings' => [
"item" => [
"_source" => [ "enabled" => true ],
"properties" => [
// this is for the boolean test
"status" => ["type" => "boolean"],
]
]
],
]))->send();
$customer = new Customer(); $customer = new Customer();
$customer->id = 1; $customer->id = 1;
...@@ -281,10 +271,10 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -281,10 +271,10 @@ class ActiveRecordTest extends ElasticSearchTestCase
public function testBooleanAttribute() public function testBooleanAttribute()
{ {
$db = $this->getConnection(); $db = $this->getConnection();
$db->createCommand()->deleteIndex('customers'); $db->createCommand()->deleteIndex('yiitest');
$db->http()->post('customers', null, Json::encode([ $db->post(['yiitest'], [], Json::encode([
'mappings' => [ 'mappings' => [
"item" => [ "customer" => [
"_source" => [ "enabled" => true ], "_source" => [ "enabled" => true ],
"properties" => [ "properties" => [
// this is for the boolean test // this is for the boolean test
...@@ -292,7 +282,7 @@ class ActiveRecordTest extends ElasticSearchTestCase ...@@ -292,7 +282,7 @@ class ActiveRecordTest extends ElasticSearchTestCase
] ]
] ]
], ],
]))->send(); ]));
$customerClass = $this->getCustomerClass(); $customerClass = $this->getCustomerClass();
$customer = new $customerClass(); $customer = new $customerClass();
......
...@@ -2,13 +2,28 @@ ...@@ -2,13 +2,28 @@
namespace yiiunit\extensions\elasticsearch; namespace yiiunit\extensions\elasticsearch;
use yii\redis\Connection; use yii\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
/** /**
* @group elasticsearch * @group elasticsearch
*/ */
class ElasticSearchConnectionTest extends ElasticSearchTestCase class ElasticSearchConnectionTest extends ElasticSearchTestCase
{ {
// TODO public function testOpen()
{
$connection = new GuzzleConnection();
$connection->autodetectCluster;
$connection->nodes = [
['http_address' => 'inet[/127.0.0.1:9200]'],
];
$this->assertNull($connection->activeNode);
$connection->open();
$this->assertNotNull($connection->activeNode);
$this->assertArrayHasKey('name', reset($connection->nodes));
$this->assertArrayHasKey('hostname', reset($connection->nodes));
$this->assertArrayHasKey('version', reset($connection->nodes));
$this->assertArrayHasKey('http_address', reset($connection->nodes));
}
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ namespace yiiunit\extensions\elasticsearch; ...@@ -4,6 +4,7 @@ namespace yiiunit\extensions\elasticsearch;
use Yii; use Yii;
use yii\elasticsearch\Connection; use yii\elasticsearch\Connection;
use yii\elasticsearch\GuzzleConnection;
use yiiunit\TestCase; use yiiunit\TestCase;
Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch'); Yii::setAlias('@yii/elasticsearch', __DIR__ . '/../../../../extensions/elasticsearch');
...@@ -42,7 +43,7 @@ class ElasticSearchTestCase extends TestCase ...@@ -42,7 +43,7 @@ class ElasticSearchTestCase extends TestCase
{ {
$databases = $this->getParam('databases'); $databases = $this->getParam('databases');
$params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array(); $params = isset($databases['elasticsearch']) ? $databases['elasticsearch'] : array();
$db = new Connection; $db = new GuzzleConnection();
if ($reset) { if ($reset) {
$db->open(); $db->open();
} }
......
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