Browse Source

refactored elasticsearch COnnection and Command

tags/2.0.0-beta
Carsten Brandt 11 years ago
parent
commit
613758dda5
  1. 6
      .gitignore
  2. 1
      .travis.yml
  3. 4
      extensions/elasticsearch/ActiveQuery.php
  4. 14
      extensions/elasticsearch/ActiveRecord.php
  5. 158
      extensions/elasticsearch/Command.php
  6. 209
      extensions/elasticsearch/Connection.php
  7. 57
      extensions/elasticsearch/GuzzleConnection.php
  8. 10
      extensions/elasticsearch/Query.php
  9. 2
      extensions/elasticsearch/README.md
  10. 5
      tests/unit/data/ar/elasticsearch/ActiveRecord.php
  11. 30
      tests/unit/extensions/elasticsearch/ActiveRecordTest.php
  12. 19
      tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php
  13. 3
      tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php

6
.gitignore vendored

@ -13,13 +13,15 @@ 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
# local phpunit config # local phpunit config
/phpunit.xml /phpunit.xml

1
.travis.yml

@ -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

4
extensions/elasticsearch/ActiveQuery.php

@ -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
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'];

14
extensions/elasticsearch/ActiveRecord.php

@ -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
} }
// 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++;
} }

158
extensions/elasticsearch/Command.php

@ -43,7 +43,11 @@ class Command extends Component
public $options = []; public $options = [];
public function queryAll($options = []) /**
* @param array $options
* @return mixed
*/
public function search($options = [])
{ {
$query = $this->queryParts; $query = $this->queryParts;
if (empty($query)) { if (empty($query)) {
@ -57,23 +61,9 @@ class Command extends Component
$this->type !== null ? $this->type : '_all', $this->type !== null ? $this->type : '_all',
'_search' '_search'
]; ];
try { return $this->db->get($url, array_merge($this->options, $options), $query)['hits'];
$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()
. print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e);
}
return Json::decode($response->getBody(true))['hits'];
}
public function queryCount($options = [])
{
$options['search_type'] = 'count';
return $this->queryAll($options);
} }
/** /**
* Inserts a document into an index * Inserts a document into an index
* @param string $index * @param string $index
@ -88,18 +78,11 @@ class Command extends Component
{ {
$body = is_array($data) ? Json::encode($data) : $data; $body = is_array($data) ? Json::encode($data) : $data;
try { if ($id !== null) {
if ($id !== null) { return $this->db->put([$index, $type, $id], $options, $body);
$response = $this->db->http()->put($this->createUrl([$index, $type, $id], $options), null, $body)->send(); } else {
} else { return $this->db->post([$index, $type], $options, $body);
$response = $this->db->http()->post($this->createUrl([$index, $type], $options), null, $body)->send();
}
} catch(ClientErrorResponseException $e) {
throw new Exception("elasticsearch error:\n\n"
. $body . "\n\n" . $e->getMessage()
. print_r(Json::decode($e->getResponse()->getBody(true)), true), [], 0, $e);
} }
return Json::decode($response->getBody(true));
} }
/** /**
@ -113,15 +96,7 @@ class Command extends Component
*/ */
public function get($index, $type, $id, $options = []) public function get($index, $type, $id, $options = [])
{ {
$httpOptions = [ return $this->db->get([$index, $type, $id], $options, null, [200, 404]);
'exceptions' => false,
];
$response = $this->db->http()->get($this->createUrl([$index, $type, $id], $options), null, $httpOptions)->send();
if ($response->getStatusCode() == 200 || $response->getStatusCode() == 404) {
return Json::decode($response->getBody(true));
} else {
throw new Exception('Elasticsearch request failed.');
}
} }
/** /**
@ -133,25 +108,12 @@ class Command extends Component
* @param $id * @param $id
* @param array $options * @param array $options
* @return mixed * @return mixed
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html
*/ */
public function mget($index, $type, $ids, $options = []) public function mget($index, $type, $ids, $options = [])
{ {
$httpOptions = [
'exceptions' => false,
];
$body = Json::encode(['ids' => array_values($ids)]); $body = Json::encode(['ids' => array_values($ids)]);
$response = $this->db->http()->post( // TODO guzzle does not manage to send get request with content return $this->db->get([$index, $type, '_mget'], $options, $body);
$this->createUrl([$index, $type, '_mget'], $options),
null,
$body,
$httpOptions
)->send();
if ($response->getStatusCode() == 200) {
return Json::decode($response->getBody(true));
} else {
throw new Exception('Elasticsearch request failed.');
}
} }
/** /**
@ -164,12 +126,9 @@ class Command extends Component
*/ */
public function getSource($index, $type, $id) public function getSource($index, $type, $id)
{ {
$response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); return $this->db->get([$index, $type, $id]);
return Json::decode($response->getBody(true));
} }
// TODO mget http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html
/** /**
* gets a document from the index * gets a document from the index
* @param $index * @param $index
@ -180,8 +139,7 @@ class Command extends Component
*/ */
public function exists($index, $type, $id) public function exists($index, $type, $id)
{ {
$response = $this->db->http()->head($this->createUrl([$index, $type, $id]))->send(); return $this->db->head([$index, $type, $id]);
return $response->getStatusCode() == 200;
} }
/** /**
@ -195,8 +153,7 @@ class Command extends Component
*/ */
public function delete($index, $type, $id, $options = []) public function delete($index, $type, $id, $options = [])
{ {
$response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); return $this->db->delete([$index, $type, $id], $options);
return Json::decode($response->getBody(true));
} }
/** /**
@ -211,21 +168,18 @@ class Command extends Component
public function update($index, $type, $id, $data, $options = []) public function update($index, $type, $id, $data, $options = [])
{ {
// TODO // TODO
$response = $this->db->http()->delete($this->createUrl([$index, $type, $id], $options))->send(); // return $this->db->delete([$index, $type, $id], $options);
return Json::decode($response->getBody(true));
} }
// TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html // TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html
/** /**
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html * @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html
*/ */
public function createIndex($index, $configuration = null) public function createIndex($index, $configuration = null)
{ {
$body = $configuration !== null ? Json::encode($configuration) : null; $body = $configuration !== null ? Json::encode($configuration) : null;
$response = $this->db->http()->put($this->createUrl([$index]), null, $body)->send(); return $this->db->put([$index], $body);
return Json::decode($response->getBody(true));
} }
/** /**
@ -233,8 +187,7 @@ class Command extends Component
*/ */
public function deleteIndex($index) public function deleteIndex($index)
{ {
$response = $this->db->http()->delete($this->createUrl([$index]))->send(); return $this->db->delete([$index]);
return Json::decode($response->getBody(true));
} }
/** /**
@ -242,8 +195,7 @@ class Command extends Component
*/ */
public function deleteAllIndexes() public function deleteAllIndexes()
{ {
$response = $this->db->http()->delete($this->createUrl(['_all']))->send(); return $this->db->delete(['_all']);
return Json::decode($response->getBody(true));
} }
/** /**
@ -251,8 +203,7 @@ class Command extends Component
*/ */
public function indexExists($index) public function indexExists($index)
{ {
$response = $this->db->http()->head($this->createUrl([$index]))->send(); return $this->db->head([$index]);
return $response->getStatusCode() == 200;
} }
/** /**
@ -260,8 +211,7 @@ class Command extends Component
*/ */
public function typeExists($index, $type) public function typeExists($index, $type)
{ {
$response = $this->db->http()->head($this->createUrl([$index, $type]))->send(); return $this->db->head([$index, $type]);
return $response->getStatusCode() == 200;
} }
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html
@ -276,8 +226,7 @@ class Command extends Component
*/ */
public function openIndex($index) public function openIndex($index)
{ {
$response = $this->db->http()->post($this->createUrl([$index, '_open']))->send(); return $this->db->post([$index, '_open']);
return $response->getStatusCode() == 200;
} }
/** /**
@ -285,8 +234,7 @@ class Command extends Component
*/ */
public function closeIndex($index) public function closeIndex($index)
{ {
$response = $this->db->http()->post($this->createUrl([$index, '_close']))->send(); return $this->db->post([$index, '_close']);
return $response->getStatusCode() == 200;
} }
/** /**
@ -294,8 +242,7 @@ class Command extends Component
*/ */
public function getIndexStatus($index = '_all') public function getIndexStatus($index = '_all')
{ {
$response = $this->db->http()->get($this->createUrl([$index, '_status']))->send(); return $this->db->get([$index, '_status']);
return Json::decode($response->getBody(true));
} }
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html
@ -306,8 +253,7 @@ class Command extends Component
*/ */
public function clearIndexCache($index) public function clearIndexCache($index)
{ {
$response = $this->db->http()->post($this->createUrl([$index, '_cache', 'clear']))->send(); return $this->db->post([$index, '_cache', 'clear']);
return $response->getStatusCode() == 200;
} }
/** /**
@ -315,8 +261,7 @@ class Command extends Component
*/ */
public function flushIndex($index = '_all') public function flushIndex($index = '_all')
{ {
$response = $this->db->http()->post($this->createUrl([$index, '_flush']))->send(); return $this->db->post([$index, '_flush']);
return $response->getStatusCode() == 200;
} }
/** /**
@ -324,8 +269,7 @@ class Command extends Component
*/ */
public function refreshIndex($index) public function refreshIndex($index)
{ {
$response = $this->db->http()->post($this->createUrl([$index, '_refresh']))->send(); return $this->db->post([$index, '_refresh']);
return $response->getStatusCode() == 200;
} }
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html // TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html
@ -338,8 +282,7 @@ class Command extends Component
public function setMapping($index, $type, $mapping) public function setMapping($index, $type, $mapping)
{ {
$body = $mapping !== null ? Json::encode($mapping) : null; $body = $mapping !== null ? Json::encode($mapping) : null;
$response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']), null, $body)->send(); return $this->db->put([$index, $type, '_mapping'], $body);
return $response->getStatusCode() == 200;
} }
/** /**
@ -347,8 +290,7 @@ class Command extends Component
*/ */
public function getMapping($index = '_all', $type = '_all') public function getMapping($index = '_all', $type = '_all')
{ {
$response = $this->db->http()->get($this->createUrl([$index, $type, '_mapping']))->send(); return $this->db->get([$index, $type, '_mapping']);
return Json::decode($response->getBody(true));
} }
/** /**
@ -356,8 +298,7 @@ class Command extends Component
*/ */
public function deleteMapping($index, $type) public function deleteMapping($index, $type)
{ {
$response = $this->db->http()->delete($this->createUrl([$index, $type]))->send(); return $this->db->delete([$index, $type]);
return $response->getStatusCode() == 200;
} }
/** /**
@ -365,9 +306,7 @@ class Command extends Component
*/ */
public function getFieldMapping($index, $type = '_all') public function getFieldMapping($index, $type = '_all')
{ {
// TODO return $this->db->put([$index, $type, '_mapping']);
$response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send();
return Json::decode($response->getBody(true));
} }
/** /**
@ -375,10 +314,8 @@ class Command extends Component
*/ */
public function analyze($options, $index = null) public function analyze($options, $index = null)
{ {
// TODO // TODO implement
$response = $this->db->http()->put($this->createUrl([$index, $type, '_mapping']))->send(); // return $this->db->put([$index]);
return Json::decode($response->getBody(true));
} }
/** /**
@ -390,10 +327,10 @@ class Command extends Component
'template' => $pattern, 'template' => $pattern,
'order' => $order, 'order' => $order,
'settings' => (object) $settings, 'settings' => (object) $settings,
'mappings' => (object) $settings, 'mappings' => (object) $mappings,
]); ]);
$response = $this->db->http()->put($this->createUrl(['_template', $name]), null, $body)->send(); return $this->db->put(['_template', $name], $body);
return $response->getStatusCode() == 200;
} }
/** /**
@ -401,8 +338,8 @@ class Command extends Component
*/ */
public function deleteTemplate($name) public function deleteTemplate($name)
{ {
$response = $this->db->http()->delete($this->createUrl(['_template', $name]))->send(); return $this->db->delete(['_template', $name]);
return $response->getStatusCode() == 200;
} }
/** /**
@ -410,21 +347,6 @@ class Command extends Component
*/ */
public function getTemplate($name) public function getTemplate($name)
{ {
$response = $this->db->http()->get($this->createUrl(['_template', $name]))->send(); return $this->db->get(['_template', $name]);
return Json::decode($response->getBody(true));
}
private function createUrl($path, $options = [])
{
$url = implode('/', array_map(function($a) {
return urlencode(is_array($a) ? implode(',', $a) : $a);
}, $path));
if (!empty($options) || !empty($this->options)) {
$options = array_merge($this->options, $options);
$url .= '?' . http_build_query($options);
}
return $url;
} }
} }

209
extensions/elasticsearch/Connection.php

@ -8,68 +8,56 @@
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;
}
/**
* Closes the connection when this component is being serialized. * Closes the connection when this component is being serialized.
* @return array * @return array
*/ */
@ -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
*/ */
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 ($this->_socket === null) { if (empty($this->nodes)) {
if (empty($this->dsn)) { throw new InvalidConfigException('elasticsearch needs at least one node to operate.');
throw new InvalidConfigException('Connection.dsn cannot be empty.'); }
} if ($this->autodetectCluster) {
$dsn = explode('/', $this->dsn); $node = reset($this->nodes);
$host = $dsn[2]; $host = $node['http_address'];
if (strpos($host, ':')===false) { if (strncmp($host, 'inet[/', 6) == 0) {
$host .= ':6379'; $host = substr($host, 6, -1);
} }
$db = isset($dsn[3]) ? $dsn[3] : 0; $response = $this->httpRequest('get', 'http://' . $host . '/_cluster/nodes');
$this->nodes = $response['nodes'];
\Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); if (empty($this->nodes)) {
$this->_socket = @stream_socket_client( throw new Exception('cluster autodetection did not find any active node.');
$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->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 $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();
}
/**
* 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
*/ */
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
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
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;
} }
} }

57
extensions/elasticsearch/GuzzleConnection.php

@ -0,0 +1,57 @@
<?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));
}
}

10
extensions/elasticsearch/Query.php

@ -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
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
{ {
$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
// 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) {

2
extensions/elasticsearch/README.md

@ -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
], ],
], ],

5
tests/unit/data/ar/elasticsearch/ActiveRecord.php

@ -24,4 +24,9 @@ class ActiveRecord extends \yii\elasticsearch\ActiveRecord
{ {
return self::$db; return self::$db;
} }
public static function index()
{
return 'yiitest';
}
} }

30
tests/unit/extensions/elasticsearch/ActiveRecordTest.php

@ -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
] ]
] ]
], ],
]))->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
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
] ]
] ]
], ],
]))->send(); ]));
$customerClass = $this->getCustomerClass(); $customerClass = $this->getCustomerClass();
$customer = new $customerClass(); $customer = new $customerClass();

19
tests/unit/extensions/elasticsearch/ElasticSearchConnectionTest.php

@ -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));
}
} }

3
tests/unit/extensions/elasticsearch/ElasticSearchTestCase.php

@ -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
{ {
$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();
} }

Loading…
Cancel
Save