Paul Klimov
11 years ago
149 changed files with 5475 additions and 1226 deletions
@ -0,0 +1,24 @@ |
|||||||
|
<?php |
||||||
|
use yii\helpers\Html; |
||||||
|
use yii\mail\BaseMessage; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var \yii\web\View $this |
||||||
|
* @var BaseMessage $content |
||||||
|
*/ |
||||||
|
?> |
||||||
|
<?php $this->beginPage() ?> |
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> |
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml"> |
||||||
|
<head> |
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
|
||||||
|
<title><?= Html::encode($this->title) ?></title>
|
||||||
|
<?php $this->head() ?> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<?php $this->beginBody() ?> |
||||||
|
<?= $content ?> |
||||||
|
<?php $this->endBody() ?> |
||||||
|
</body> |
||||||
|
</html> |
||||||
|
<?php $this->endPage() ?> |
@ -0,0 +1,199 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use yii\db\ActiveQueryInterface; |
||||||
|
use yii\db\ActiveQueryTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* ActiveQuery represents a [[Query]] associated with an [[ActiveRecord]] class. |
||||||
|
* |
||||||
|
* ActiveQuery instances are usually created by [[ActiveRecord::find()]]. |
||||||
|
* |
||||||
|
* ActiveQuery mainly provides the following methods to retrieve the query results: |
||||||
|
* |
||||||
|
* - [[one()]]: returns a single record populated with the first row of data. |
||||||
|
* - [[all()]]: returns all records based on the query results. |
||||||
|
* - [[count()]]: returns the number of records. |
||||||
|
* - [[scalar()]]: returns the value of the first column in the first row of the query result. |
||||||
|
* - [[column()]]: returns the value of the first column in the query result. |
||||||
|
* - [[exists()]]: returns a value indicating whether the query result has data or not. |
||||||
|
* |
||||||
|
* Because ActiveQuery extends from [[Query]], one can use query methods, such as [[where()]], |
||||||
|
* [[orderBy()]] to customize the query options. |
||||||
|
* |
||||||
|
* ActiveQuery also provides the following additional query options: |
||||||
|
* |
||||||
|
* - [[with()]]: list of relations that this query should be performed with. |
||||||
|
* - [[indexBy()]]: the name of the column by which the query result should be indexed. |
||||||
|
* - [[asArray()]]: whether to return each record as an array. |
||||||
|
* |
||||||
|
* These options can be configured using methods of the same name. For example: |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* $customers = Customer::find()->with('orders')->asArray()->all(); |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class ActiveQuery extends Query implements ActiveQueryInterface |
||||||
|
{ |
||||||
|
use ActiveQueryTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a DB command that can be used to execute this query. |
||||||
|
* @param Connection $db the DB connection used to create the DB command. |
||||||
|
* If null, the DB connection returned by [[modelClass]] will be used. |
||||||
|
* @return Command the created DB command instance. |
||||||
|
*/ |
||||||
|
public function createCommand($db = null) |
||||||
|
{ |
||||||
|
/** @var ActiveRecord $modelClass */ |
||||||
|
$modelClass = $this->modelClass; |
||||||
|
if ($db === null) { |
||||||
|
$db = $modelClass::getDb(); |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->type === null) { |
||||||
|
$this->type = $modelClass::type(); |
||||||
|
} |
||||||
|
if ($this->index === null) { |
||||||
|
$this->index = $modelClass::index(); |
||||||
|
$this->type = $modelClass::type(); |
||||||
|
} |
||||||
|
$commandConfig = $db->getQueryBuilder()->build($this); |
||||||
|
return $db->createCommand($commandConfig); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes query and returns all results as an array. |
||||||
|
* @param Connection $db the DB connection used to create the DB command. |
||||||
|
* If null, the DB connection returned by [[modelClass]] will be used. |
||||||
|
* @return array the query results. If the query results in nothing, an empty array will be returned. |
||||||
|
*/ |
||||||
|
public function all($db = null) |
||||||
|
{ |
||||||
|
$result = $this->createCommand($db)->search(); |
||||||
|
if (empty($result['hits']['hits'])) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
if ($this->fields !== null) { |
||||||
|
foreach ($result['hits']['hits'] as &$row) { |
||||||
|
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||||
|
unset($row['fields']); |
||||||
|
} |
||||||
|
unset($row); |
||||||
|
} |
||||||
|
if ($this->asArray && $this->indexBy) { |
||||||
|
foreach ($result['hits']['hits'] as &$row) { |
||||||
|
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; |
||||||
|
$row = $row['_source']; |
||||||
|
} |
||||||
|
} |
||||||
|
$models = $this->createModels($result['hits']['hits']); |
||||||
|
if ($this->asArray && !$this->indexBy) { |
||||||
|
foreach($models as $key => $model) { |
||||||
|
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; |
||||||
|
$models[$key] = $model['_source']; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!empty($this->with)) { |
||||||
|
$this->findWith($this->with, $models); |
||||||
|
} |
||||||
|
return $models; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes query and returns a single row of result. |
||||||
|
* @param Connection $db the DB connection used to create the DB command. |
||||||
|
* If null, the DB connection returned by [[modelClass]] will be used. |
||||||
|
* @return ActiveRecord|array|null a single row of query result. Depending on the setting of [[asArray]], |
||||||
|
* the query result may be either an array or an ActiveRecord object. Null will be returned |
||||||
|
* if the query results in nothing. |
||||||
|
*/ |
||||||
|
public function one($db = null) |
||||||
|
{ |
||||||
|
if (($result = parent::one($db)) === false) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if ($this->asArray) { |
||||||
|
$model = $result['_source']; |
||||||
|
$model[ActiveRecord::PRIMARY_KEY_NAME] = $result['_id']; |
||||||
|
} else { |
||||||
|
/** @var ActiveRecord $class */ |
||||||
|
$class = $this->modelClass; |
||||||
|
$model = $class::create($result); |
||||||
|
} |
||||||
|
if (!empty($this->with)) { |
||||||
|
$models = [$model]; |
||||||
|
$this->findWith($this->with, $models); |
||||||
|
$model = $models[0]; |
||||||
|
} |
||||||
|
return $model; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function search($db = null, $options = []) |
||||||
|
{ |
||||||
|
$result = $this->createCommand($db)->search($options); |
||||||
|
if (!empty($result['hits']['hits'])) { |
||||||
|
$models = $this->createModels($result['hits']['hits']); |
||||||
|
if ($this->asArray) { |
||||||
|
foreach($models as $key => $model) { |
||||||
|
$model['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $model['_id']; |
||||||
|
$models[$key] = $model['_source']; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!empty($this->with)) { |
||||||
|
$this->findWith($this->with, $models); |
||||||
|
} |
||||||
|
$result['hits']['hits'] = $models; |
||||||
|
} |
||||||
|
return $result; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function scalar($field, $db = null) |
||||||
|
{ |
||||||
|
$record = parent::one($db); |
||||||
|
if ($record !== false) { |
||||||
|
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
return $record['_id']; |
||||||
|
} elseif (isset($record['_source'][$field])) { |
||||||
|
return $record['_source'][$field]; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function column($field, $db = null) |
||||||
|
{ |
||||||
|
if ($field == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
$command = $this->createCommand($db); |
||||||
|
$command->queryParts['fields'] = []; |
||||||
|
$result = $command->search(); |
||||||
|
if (empty($result['hits']['hits'])) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$column = []; |
||||||
|
foreach ($result['hits']['hits'] as $row) { |
||||||
|
$column[] = $row['_id']; |
||||||
|
} |
||||||
|
return $column; |
||||||
|
} |
||||||
|
return parent::column($field, $db); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,474 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use yii\base\InvalidCallException; |
||||||
|
use yii\base\InvalidConfigException; |
||||||
|
use yii\base\NotSupportedException; |
||||||
|
use yii\helpers\Inflector; |
||||||
|
use yii\helpers\Json; |
||||||
|
use yii\helpers\StringHelper; |
||||||
|
|
||||||
|
/** |
||||||
|
* ActiveRecord is the base class for classes representing relational data in terms of objects. |
||||||
|
* |
||||||
|
* This class implements the ActiveRecord pattern for the fulltext search and data storage |
||||||
|
* [elasticsearch](http://www.elasticsearch.org/). |
||||||
|
* |
||||||
|
* For defining a record a subclass should at least implement the [[attributes()]] method to define |
||||||
|
* attributes. |
||||||
|
* The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()`. |
||||||
|
* The primary key is not part of the attributes. |
||||||
|
* |
||||||
|
* The following is an example model called `Customer`: |
||||||
|
* |
||||||
|
* ```php |
||||||
|
* class Customer extends \yii\elasticsearch\ActiveRecord |
||||||
|
* { |
||||||
|
* public function attributes() |
||||||
|
* { |
||||||
|
* return ['id', 'name', 'address', 'registration_date']; |
||||||
|
* } |
||||||
|
* } |
||||||
|
* ``` |
||||||
|
* |
||||||
|
* You may override [[index()]] and [[type()]] to define the index and type this record represents. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class ActiveRecord extends \yii\db\ActiveRecord |
||||||
|
{ |
||||||
|
const PRIMARY_KEY_NAME = 'id'; |
||||||
|
|
||||||
|
private $_id; |
||||||
|
private $_version; |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the database connection used by this AR class. |
||||||
|
* By default, the "elasticsearch" application component is used as the database connection. |
||||||
|
* You may override this method if you want to use a different database connection. |
||||||
|
* @return Connection the database connection used by this AR class. |
||||||
|
*/ |
||||||
|
public static function getDb() |
||||||
|
{ |
||||||
|
return \Yii::$app->getComponent('elasticsearch'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function find($q = null) |
||||||
|
{ |
||||||
|
$query = static::createQuery(); |
||||||
|
if (is_array($q)) { |
||||||
|
if (count($q) == 1 && (array_key_exists(ActiveRecord::PRIMARY_KEY_NAME, $q))) { |
||||||
|
$pk = $q[ActiveRecord::PRIMARY_KEY_NAME]; |
||||||
|
if (is_array($pk)) { |
||||||
|
return static::mget($pk); |
||||||
|
} else { |
||||||
|
return static::get($pk); |
||||||
|
} |
||||||
|
} |
||||||
|
return $query->where($q)->one(); |
||||||
|
} elseif ($q !== null) { |
||||||
|
return static::get($q); |
||||||
|
} |
||||||
|
return $query; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets a record by its primary key. |
||||||
|
* |
||||||
|
* @param mixed $primaryKey the primaryKey value |
||||||
|
* @param array $options options given in this parameter are passed to elasticsearch |
||||||
|
* as request URI parameters. |
||||||
|
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) |
||||||
|
* for more details on these options. |
||||||
|
* @return static|null The record instance or null if it was not found. |
||||||
|
*/ |
||||||
|
public static function get($primaryKey, $options = []) |
||||||
|
{ |
||||||
|
if ($primaryKey === null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$command = static::getDb()->createCommand(); |
||||||
|
$result = $command->get(static::index(), static::type(), $primaryKey, $options); |
||||||
|
if ($result['exists']) { |
||||||
|
return static::create($result); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gets a list of records by its primary keys. |
||||||
|
* |
||||||
|
* @param array $primaryKeys an array of primaryKey values |
||||||
|
* @param array $options options given in this parameter are passed to elasticsearch |
||||||
|
* as request URI parameters. |
||||||
|
* |
||||||
|
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html) |
||||||
|
* for more details on these options. |
||||||
|
* @return static|null The record instance or null if it was not found. |
||||||
|
*/ |
||||||
|
|
||||||
|
public static function mget($primaryKeys, $options = []) |
||||||
|
{ |
||||||
|
if (empty($primaryKeys)) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$command = static::getDb()->createCommand(); |
||||||
|
$result = $command->mget(static::index(), static::type(), $primaryKeys, $options); |
||||||
|
$models = []; |
||||||
|
foreach($result['docs'] as $doc) { |
||||||
|
if ($doc['exists']) { |
||||||
|
$models[] = static::create($doc); |
||||||
|
} |
||||||
|
} |
||||||
|
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} |
||||||
|
*/ |
||||||
|
public static function createQuery() |
||||||
|
{ |
||||||
|
return new ActiveQuery(['modelClass' => get_called_class()]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function createActiveRelation($config = []) |
||||||
|
{ |
||||||
|
return new ActiveRelation($config); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO implement copy and move as pk change is not possible |
||||||
|
|
||||||
|
public function getId() |
||||||
|
{ |
||||||
|
return $this->_id; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the primary key |
||||||
|
* @param mixed $value |
||||||
|
* @throws \yii\base\InvalidCallException when record is not new |
||||||
|
*/ |
||||||
|
public function setId($value) |
||||||
|
{ |
||||||
|
if ($this->isNewRecord) { |
||||||
|
$this->_id = $value; |
||||||
|
} else { |
||||||
|
throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getPrimaryKey($asArray = false) |
||||||
|
{ |
||||||
|
if ($asArray) { |
||||||
|
return [ActiveRecord::PRIMARY_KEY_NAME => $this->_id]; |
||||||
|
} else { |
||||||
|
return $this->_id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getOldPrimaryKey($asArray = false) |
||||||
|
{ |
||||||
|
$id = $this->isNewRecord ? null : $this->_id; |
||||||
|
if ($asArray) { |
||||||
|
return [ActiveRecord::PRIMARY_KEY_NAME => $id]; |
||||||
|
} else { |
||||||
|
return $this->_id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This method defines the primary. |
||||||
|
* |
||||||
|
* The primaryKey for elasticsearch documents is always `primaryKey`. It can not be changed. |
||||||
|
* |
||||||
|
* @return string[] the primary keys of this record. |
||||||
|
*/ |
||||||
|
public static function primaryKey() |
||||||
|
{ |
||||||
|
return [ActiveRecord::PRIMARY_KEY_NAME]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the list of all attribute names of the model. |
||||||
|
* This method must be overridden by child classes to define available attributes. |
||||||
|
* @return array list of attribute names. |
||||||
|
*/ |
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return string the name of the index this record is stored in. |
||||||
|
*/ |
||||||
|
public static function index() |
||||||
|
{ |
||||||
|
return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return string the name of the type of this record. |
||||||
|
*/ |
||||||
|
public static function type() |
||||||
|
{ |
||||||
|
return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates an active record object using a row of data. |
||||||
|
* This method is called by [[ActiveQuery]] to populate the query results |
||||||
|
* into Active Records. It is not meant to be used to create new records. |
||||||
|
* @param array $row attribute values (name => value) |
||||||
|
* @return ActiveRecord the newly created active record. |
||||||
|
*/ |
||||||
|
public static function create($row) |
||||||
|
{ |
||||||
|
$row['_source'][ActiveRecord::PRIMARY_KEY_NAME] = $row['_id']; |
||||||
|
$record = parent::create($row['_source']); |
||||||
|
return $record; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Inserts a document into the associated index using the attribute values of this record. |
||||||
|
* |
||||||
|
* This method performs the following steps in order: |
||||||
|
* |
||||||
|
* 1. call [[beforeValidate()]] when `$runValidation` is true. If validation |
||||||
|
* fails, it will skip the rest of the steps; |
||||||
|
* 2. call [[afterValidate()]] when `$runValidation` is true. |
||||||
|
* 3. call [[beforeSave()]]. If the method returns false, it will skip the |
||||||
|
* rest of the steps; |
||||||
|
* 4. insert the record into database. If this fails, it will skip the rest of the steps; |
||||||
|
* 5. call [[afterSave()]]; |
||||||
|
* |
||||||
|
* In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], |
||||||
|
* [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] |
||||||
|
* will be raised by the corresponding methods. |
||||||
|
* |
||||||
|
* Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. |
||||||
|
* |
||||||
|
* If the [[primaryKey|primary key]] is not set (null) during insertion, |
||||||
|
* it will be populated with a |
||||||
|
* [randomly generated value](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) |
||||||
|
* after insertion. |
||||||
|
* |
||||||
|
* For example, to insert a customer record: |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* $customer = new Customer; |
||||||
|
* $customer->name = $name; |
||||||
|
* $customer->email = $email; |
||||||
|
* $customer->insert(); |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* @param boolean $runValidation whether to perform validation before saving the record. |
||||||
|
* If the validation fails, the record will not be inserted into the database. |
||||||
|
* @param array $attributes list of attributes that need to be saved. Defaults to null, |
||||||
|
* meaning all attributes will be saved. |
||||||
|
* @param array $options options given in this parameter are passed to elasticsearch |
||||||
|
* as request URI parameters. These are among others: |
||||||
|
* |
||||||
|
* - `routing` define shard placement of this record. |
||||||
|
* - `parent` by giving the primaryKey of another record this defines a parent-child relation |
||||||
|
* - `timestamp` specifies the timestamp to store along with the document. Default is indexing time. |
||||||
|
* |
||||||
|
* Please refer to the [elasticsearch documentation](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html) |
||||||
|
* for more details on these options. |
||||||
|
* |
||||||
|
* By default the `op_type` is set to `create`. |
||||||
|
* @return boolean whether the attributes are valid and the record is inserted successfully. |
||||||
|
*/ |
||||||
|
public function insert($runValidation = true, $attributes = null, $options = ['op_type' => 'create']) |
||||||
|
{ |
||||||
|
if ($runValidation && !$this->validate($attributes)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($this->beforeSave(true)) { |
||||||
|
$values = $this->getDirtyAttributes($attributes); |
||||||
|
|
||||||
|
$response = static::getDb()->createCommand()->insert( |
||||||
|
static::index(), |
||||||
|
static::type(), |
||||||
|
$values, |
||||||
|
$this->getPrimaryKey(), |
||||||
|
$options |
||||||
|
); |
||||||
|
|
||||||
|
if (!$response['ok']) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$this->_id = $response['_id']; |
||||||
|
$this->_version = $response['_version']; |
||||||
|
$this->setOldAttributes($values); |
||||||
|
$this->afterSave(true); |
||||||
|
return true; |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Updates all records whos primary keys are given. |
||||||
|
* For example, to change the status to be 1 for all customers whose status is 2: |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* Customer::updateAll(array('status' => 1), array(2, 3, 4)); |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* @param array $attributes attribute values (name-value pairs) to be saved into the table |
||||||
|
* @param array $condition the conditions that will be put in the WHERE part of the UPDATE SQL. |
||||||
|
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter. |
||||||
|
* @param array $params this parameter is ignored in elasticsearch implementation. |
||||||
|
* @return integer the number of rows updated |
||||||
|
*/ |
||||||
|
public static function updateAll($attributes, $condition = [], $params = []) |
||||||
|
{ |
||||||
|
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { |
||||||
|
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; |
||||||
|
} else { |
||||||
|
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); |
||||||
|
} |
||||||
|
if (empty($primaryKeys)) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
$bulk = ''; |
||||||
|
foreach((array) $primaryKeys as $pk) { |
||||||
|
$action = Json::encode([ |
||||||
|
"update" => [ |
||||||
|
"_id" => $pk, |
||||||
|
"_type" => static::type(), |
||||||
|
"_index" => static::index(), |
||||||
|
], |
||||||
|
]); |
||||||
|
$data = Json::encode(array( |
||||||
|
"doc" => $attributes |
||||||
|
)); |
||||||
|
$bulk .= $action . "\n" . $data . "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO do this via command |
||||||
|
$url = [static::index(), static::type(), '_bulk']; |
||||||
|
$response = static::getDb()->post($url, [], $bulk); |
||||||
|
$n=0; |
||||||
|
foreach($response['items'] as $item) { |
||||||
|
if ($item['update']['ok']) { |
||||||
|
$n++; |
||||||
|
} |
||||||
|
} |
||||||
|
return $n; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Deletes rows in the table using the provided conditions. |
||||||
|
* WARNING: If you do not specify any condition, this method will delete ALL rows in the table. |
||||||
|
* |
||||||
|
* For example, to delete all customers whose status is 3: |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* Customer::deleteAll('status = 3'); |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* @param array $condition the conditions that will be put in the WHERE part of the DELETE SQL. |
||||||
|
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter. |
||||||
|
* @param array $params this parameter is ignored in elasticsearch implementation. |
||||||
|
* @return integer the number of rows deleted |
||||||
|
*/ |
||||||
|
public static function deleteAll($condition = [], $params = []) |
||||||
|
{ |
||||||
|
if (count($condition) == 1 && isset($condition[ActiveRecord::PRIMARY_KEY_NAME])) { |
||||||
|
$primaryKeys = (array) $condition[ActiveRecord::PRIMARY_KEY_NAME]; |
||||||
|
} else { |
||||||
|
$primaryKeys = static::find()->where($condition)->column(ActiveRecord::PRIMARY_KEY_NAME); |
||||||
|
} |
||||||
|
if (empty($primaryKeys)) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
$bulk = ''; |
||||||
|
foreach((array) $primaryKeys as $pk) { |
||||||
|
$bulk .= Json::encode([ |
||||||
|
"delete" => [ |
||||||
|
"_id" => $pk, |
||||||
|
"_type" => static::type(), |
||||||
|
"_index" => static::index(), |
||||||
|
], |
||||||
|
]) . "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO do this via command |
||||||
|
$url = [static::index(), static::type(), '_bulk']; |
||||||
|
$response = static::getDb()->post($url, [], $bulk); |
||||||
|
$n=0; |
||||||
|
foreach($response['items'] as $item) { |
||||||
|
if ($item['delete']['found'] && $item['delete']['ok']) { |
||||||
|
$n++; |
||||||
|
} |
||||||
|
} |
||||||
|
return $n; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function updateAllCounters($counters, $condition = null, $params = []) |
||||||
|
{ |
||||||
|
throw new NotSupportedException('Update Counters is not supported by elasticsearch ActiveRecord.'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function getTableSchema() |
||||||
|
{ |
||||||
|
throw new NotSupportedException('getTableSchema() is not supported by elasticsearch ActiveRecord.'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function tableName() |
||||||
|
{ |
||||||
|
return static::index() . '/' . static::type(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function findBySql($sql, $params = []) |
||||||
|
{ |
||||||
|
throw new NotSupportedException('findBySql() is not supported by elasticsearch ActiveRecord.'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a value indicating whether the specified operation is transactional in the current [[scenario]]. |
||||||
|
* This method will always return false as transactional operations are not supported by elasticsearch. |
||||||
|
* @param integer $operation the operation to check. Possible values are [[OP_INSERT]], [[OP_UPDATE]] and [[OP_DELETE]]. |
||||||
|
* @return boolean whether the specified operation is transactional in the current [[scenario]]. |
||||||
|
*/ |
||||||
|
public function isTransactional($operation) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use yii\db\ActiveRelationInterface; |
||||||
|
use yii\db\ActiveRelationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* ActiveRelation represents a relation between two Active Record classes. |
||||||
|
* |
||||||
|
* ActiveRelation instances are usually created by calling [[ActiveRecord::hasOne()]] and |
||||||
|
* [[ActiveRecord::hasMany()]]. An Active Record class declares a relation by defining |
||||||
|
* a getter method which calls one of the above methods and returns the created ActiveRelation object. |
||||||
|
* |
||||||
|
* A relation is specified by [[link]] which represents the association between columns |
||||||
|
* of different tables; and the multiplicity of the relation is indicated by [[multiple]]. |
||||||
|
* |
||||||
|
* If a relation involves a pivot table, it may be specified by [[via()]] method. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class ActiveRelation extends ActiveQuery implements ActiveRelationInterface |
||||||
|
{ |
||||||
|
use ActiveRelationTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a DB command that can be used to execute this query. |
||||||
|
* @param Connection $db the DB connection used to create the DB command. |
||||||
|
* If null, the DB connection returned by [[modelClass]] will be used. |
||||||
|
* @return Command the created DB command instance. |
||||||
|
*/ |
||||||
|
public function createCommand($db = null) |
||||||
|
{ |
||||||
|
if ($this->primaryModel !== null) { |
||||||
|
// lazy loading |
||||||
|
if (is_array($this->via)) { |
||||||
|
// via relation |
||||||
|
/** @var ActiveRelation $viaQuery */ |
||||||
|
list($viaName, $viaQuery) = $this->via; |
||||||
|
if ($viaQuery->multiple) { |
||||||
|
$viaModels = $viaQuery->all(); |
||||||
|
$this->primaryModel->populateRelation($viaName, $viaModels); |
||||||
|
} else { |
||||||
|
$model = $viaQuery->one(); |
||||||
|
$this->primaryModel->populateRelation($viaName, $model); |
||||||
|
$viaModels = $model === null ? [] : [$model]; |
||||||
|
} |
||||||
|
$this->filterByModels($viaModels); |
||||||
|
} else { |
||||||
|
$this->filterByModels([$this->primaryModel]); |
||||||
|
} |
||||||
|
} |
||||||
|
return parent::createCommand($db); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,403 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use yii\base\Component; |
||||||
|
use yii\helpers\Json; |
||||||
|
|
||||||
|
/** |
||||||
|
* The Command class implements the API for accessing the elasticsearch REST API. |
||||||
|
* |
||||||
|
* Check the [elasticsearch guide](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/index.html) |
||||||
|
* for details on these commands. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class Command extends Component |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @var Connection |
||||||
|
*/ |
||||||
|
public $db; |
||||||
|
/** |
||||||
|
* @var string|array the indexes to execute the query on. Defaults to null meaning all indexes |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-multi-index |
||||||
|
*/ |
||||||
|
public $index; |
||||||
|
/** |
||||||
|
* @var string|array the types to execute the query on. Defaults to null meaning all types |
||||||
|
*/ |
||||||
|
public $type; |
||||||
|
/** |
||||||
|
* @var array list of arrays or json strings that become parts of a query |
||||||
|
*/ |
||||||
|
public $queryParts; |
||||||
|
|
||||||
|
public $options = []; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
*/ |
||||||
|
public function search($options = []) |
||||||
|
{ |
||||||
|
$query = $this->queryParts; |
||||||
|
if (empty($query)) { |
||||||
|
$query = '{}'; |
||||||
|
} |
||||||
|
if (is_array($query)) { |
||||||
|
$query = Json::encode($query); |
||||||
|
} |
||||||
|
$url = [ |
||||||
|
$this->index !== null ? $this->index : '_all', |
||||||
|
$this->type !== null ? $this->type : '_all', |
||||||
|
'_search' |
||||||
|
]; |
||||||
|
return $this->db->get($url, array_merge($this->options, $options), $query); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Inserts a document into an index |
||||||
|
* @param string $index |
||||||
|
* @param string $type |
||||||
|
* @param string|array $data json string or array of data to store |
||||||
|
* @param null $id the documents id. If not specified Id will be automatically choosen |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-index_.html |
||||||
|
*/ |
||||||
|
public function insert($index, $type, $data, $id = null, $options = []) |
||||||
|
{ |
||||||
|
$body = is_array($data) ? Json::encode($data) : $data; |
||||||
|
|
||||||
|
if ($id !== null) { |
||||||
|
return $this->db->put([$index, $type, $id], $options, $body); |
||||||
|
} else { |
||||||
|
return $this->db->post([$index, $type], $options, $body); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* gets a document from the index |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $id |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html |
||||||
|
*/ |
||||||
|
public function get($index, $type, $id, $options = []) |
||||||
|
{ |
||||||
|
return $this->db->get([$index, $type, $id], $options, null, [200, 404]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* gets multiple documents from the index |
||||||
|
* |
||||||
|
* TODO allow specifying type and index + fields |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $ids |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-multi-get.html |
||||||
|
*/ |
||||||
|
public function mget($index, $type, $ids, $options = []) |
||||||
|
{ |
||||||
|
$body = Json::encode(['ids' => array_values($ids)]); |
||||||
|
return $this->db->get([$index, $type, '_mget'], $options, $body); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* gets a documents _source from the index (>=v0.90.1) |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $id |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html#_source |
||||||
|
*/ |
||||||
|
public function getSource($index, $type, $id) |
||||||
|
{ |
||||||
|
return $this->db->get([$index, $type, $id]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* gets a document from the index |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $id |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-get.html |
||||||
|
*/ |
||||||
|
public function exists($index, $type, $id) |
||||||
|
{ |
||||||
|
return $this->db->head([$index, $type, $id]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* deletes a document from the index |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $id |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete.html |
||||||
|
*/ |
||||||
|
public function delete($index, $type, $id, $options = []) |
||||||
|
{ |
||||||
|
return $this->db->delete([$index, $type, $id], $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* updates a document |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $id |
||||||
|
* @param array $options |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-update.html |
||||||
|
*/ |
||||||
|
// public function update($index, $type, $id, $data, $options = []) |
||||||
|
// { |
||||||
|
// // TODO implement |
||||||
|
//// return $this->db->delete([$index, $type, $id], $options); |
||||||
|
// } |
||||||
|
|
||||||
|
// TODO bulk http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-bulk.html |
||||||
|
|
||||||
|
/** |
||||||
|
* creates an index |
||||||
|
* @param $index |
||||||
|
* @param array $configuration |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-create-index.html |
||||||
|
*/ |
||||||
|
public function createIndex($index, $configuration = null) |
||||||
|
{ |
||||||
|
$body = $configuration !== null ? Json::encode($configuration) : null; |
||||||
|
return $this->db->put([$index], $body); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* deletes an index |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html |
||||||
|
*/ |
||||||
|
public function deleteIndex($index) |
||||||
|
{ |
||||||
|
return $this->db->delete([$index]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* deletes all indexes |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-delete-index.html |
||||||
|
*/ |
||||||
|
public function deleteAllIndexes() |
||||||
|
{ |
||||||
|
return $this->db->delete(['_all']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* checks whether an index exists |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-exists.html |
||||||
|
*/ |
||||||
|
public function indexExists($index) |
||||||
|
{ |
||||||
|
return $this->db->head([$index]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-types-exists.html |
||||||
|
*/ |
||||||
|
public function typeExists($index, $type) |
||||||
|
{ |
||||||
|
return $this->db->head([$index, $type]); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-aliases.html |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html |
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-settings.html |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-warmers.html |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html |
||||||
|
*/ |
||||||
|
public function openIndex($index) |
||||||
|
{ |
||||||
|
return $this->db->post([$index, '_open']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-open-close.html |
||||||
|
*/ |
||||||
|
public function closeIndex($index) |
||||||
|
{ |
||||||
|
return $this->db->post([$index, '_close']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-status.html |
||||||
|
*/ |
||||||
|
public function getIndexStatus($index = '_all') |
||||||
|
{ |
||||||
|
return $this->db->get([$index, '_status']); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-stats.html |
||||||
|
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-segments.html |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-clearcache.html |
||||||
|
*/ |
||||||
|
public function clearIndexCache($index) |
||||||
|
{ |
||||||
|
return $this->db->post([$index, '_cache', 'clear']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-flush.html |
||||||
|
*/ |
||||||
|
public function flushIndex($index = '_all') |
||||||
|
{ |
||||||
|
return $this->db->post([$index, '_flush']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-refresh.html |
||||||
|
*/ |
||||||
|
public function refreshIndex($index) |
||||||
|
{ |
||||||
|
return $this->db->post([$index, '_refresh']); |
||||||
|
} |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-optimize.html |
||||||
|
|
||||||
|
// TODO http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-gateway-snapshot.html |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @param $mapping |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html |
||||||
|
*/ |
||||||
|
public function setMapping($index, $type, $mapping) |
||||||
|
{ |
||||||
|
$body = $mapping !== null ? Json::encode($mapping) : null; |
||||||
|
return $this->db->put([$index, $type, '_mapping'], $body); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param string $index |
||||||
|
* @param string $type |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-mapping.html |
||||||
|
*/ |
||||||
|
public function getMapping($index = '_all', $type = '_all') |
||||||
|
{ |
||||||
|
return $this->db->get([$index, $type, '_mapping']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @param $type |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-put-mapping.html |
||||||
|
*/ |
||||||
|
public function deleteMapping($index, $type) |
||||||
|
{ |
||||||
|
return $this->db->delete([$index, $type]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $index |
||||||
|
* @param string $type |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html |
||||||
|
*/ |
||||||
|
public function getFieldMapping($index, $type = '_all') |
||||||
|
{ |
||||||
|
return $this->db->put([$index, $type, '_mapping']); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $options |
||||||
|
* @param $index |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-analyze.html |
||||||
|
*/ |
||||||
|
// public function analyze($options, $index = null) |
||||||
|
// { |
||||||
|
// // TODO implement |
||||||
|
//// return $this->db->put([$index]); |
||||||
|
// } |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $name |
||||||
|
* @param $pattern |
||||||
|
* @param $settings |
||||||
|
* @param $mappings |
||||||
|
* @param int $order |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||||
|
*/ |
||||||
|
public function createTemplate($name, $pattern, $settings, $mappings, $order = 0) |
||||||
|
{ |
||||||
|
$body = Json::encode([ |
||||||
|
'template' => $pattern, |
||||||
|
'order' => $order, |
||||||
|
'settings' => (object) $settings, |
||||||
|
'mappings' => (object) $mappings, |
||||||
|
]); |
||||||
|
return $this->db->put(['_template', $name], $body); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $name |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||||
|
*/ |
||||||
|
public function deleteTemplate($name) |
||||||
|
{ |
||||||
|
return $this->db->delete(['_template', $name]); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param $name |
||||||
|
* @return mixed |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-templates.html |
||||||
|
*/ |
||||||
|
public function getTemplate($name) |
||||||
|
{ |
||||||
|
return $this->db->get(['_template', $name]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,346 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use Yii; |
||||||
|
use yii\base\Component; |
||||||
|
use yii\base\InvalidConfigException; |
||||||
|
use yii\helpers\Json; |
||||||
|
|
||||||
|
/** |
||||||
|
* elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class Connection extends Component |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @event Event an event that is triggered after a DB connection is established |
||||||
|
*/ |
||||||
|
const EVENT_AFTER_OPEN = 'afterOpen'; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var bool whether to autodetect available cluster nodes on [[open()]] |
||||||
|
*/ |
||||||
|
public $autodetectCluster = true; |
||||||
|
/** |
||||||
|
* @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 $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 = []; |
||||||
|
/** |
||||||
|
* @var float timeout to use for connecting to an elasticsearch node. |
||||||
|
* This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. |
||||||
|
* If not set, no explicit timeout will be set for curl. |
||||||
|
*/ |
||||||
|
public $connectionTimeout = null; |
||||||
|
/** |
||||||
|
* @var float timeout to use when reading the response from an elasticsearch node. |
||||||
|
* This value will be used to configure the curl `CURLOPT_TIMEOUT` option. |
||||||
|
* If not set, no explicit timeout will be set for curl. |
||||||
|
*/ |
||||||
|
public $dataTimeout = null; |
||||||
|
|
||||||
|
|
||||||
|
public function init() |
||||||
|
{ |
||||||
|
foreach($this->nodes as $node) { |
||||||
|
if (!isset($node['http_address'])) { |
||||||
|
throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Closes the connection when this component is being serialized. |
||||||
|
* @return array |
||||||
|
*/ |
||||||
|
public function __sleep() |
||||||
|
{ |
||||||
|
$this->close(); |
||||||
|
return array_keys(get_object_vars($this)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a value indicating whether the DB connection is established. |
||||||
|
* @return boolean whether the DB connection is established |
||||||
|
*/ |
||||||
|
public function getIsActive() |
||||||
|
{ |
||||||
|
return $this->activeNode !== null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Establishes a DB connection. |
||||||
|
* It does nothing if a DB connection has already been established. |
||||||
|
* @throws Exception if connection fails |
||||||
|
*/ |
||||||
|
public function open() |
||||||
|
{ |
||||||
|
if ($this->activeNode !== null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (empty($this->nodes)) { |
||||||
|
throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); |
||||||
|
} |
||||||
|
if ($this->autodetectCluster) { |
||||||
|
$node = reset($this->nodes); |
||||||
|
$host = $node['http_address']; |
||||||
|
if (strncmp($host, 'inet[/', 6) == 0) { |
||||||
|
$host = substr($host, 6, -1); |
||||||
|
} |
||||||
|
$response = $this->httpRequest('GET', 'http://' . $host . '/_cluster/nodes'); |
||||||
|
$this->nodes = $response['nodes']; |
||||||
|
if (empty($this->nodes)) { |
||||||
|
throw new Exception('cluster autodetection did not find any active node.'); |
||||||
|
} |
||||||
|
} |
||||||
|
$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 |
||||||
|
*/ |
||||||
|
protected function selectActiveNode() |
||||||
|
{ |
||||||
|
$keys = array_keys($this->nodes); |
||||||
|
$this->activeNode = $keys[rand(0, count($keys) - 1)]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Closes the currently active DB connection. |
||||||
|
* It does nothing if the connection is already closed. |
||||||
|
*/ |
||||||
|
public function close() |
||||||
|
{ |
||||||
|
Yii::trace('Closing connection to elasticsearch. Active node was: ' |
||||||
|
. $this->nodes[$this->activeNode]['http_address'], __CLASS__); |
||||||
|
$this->activeNode = null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes the DB connection. |
||||||
|
* This method is invoked right after the DB connection is established. |
||||||
|
* The default implementation triggers an [[EVENT_AFTER_OPEN]] event. |
||||||
|
*/ |
||||||
|
protected function initConnection() |
||||||
|
{ |
||||||
|
$this->trigger(self::EVENT_AFTER_OPEN); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the name of the DB driver for the current [[dsn]]. |
||||||
|
* @return string name of the DB driver |
||||||
|
*/ |
||||||
|
public function getDriverName() |
||||||
|
{ |
||||||
|
return 'elasticsearch'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a command for execution. |
||||||
|
* @param array $config the configuration for the Command class |
||||||
|
* @return Command the DB command |
||||||
|
*/ |
||||||
|
public function createCommand($config = []) |
||||||
|
{ |
||||||
|
$this->open(); |
||||||
|
$config['db'] = $this; |
||||||
|
$command = new Command($config); |
||||||
|
return $command; |
||||||
|
} |
||||||
|
|
||||||
|
public function getQueryBuilder() |
||||||
|
{ |
||||||
|
return new QueryBuilder($this); |
||||||
|
} |
||||||
|
|
||||||
|
public function get($url, $options = [], $body = null) |
||||||
|
{ |
||||||
|
$this->open(); |
||||||
|
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 [$this->nodes[$this->activeNode]['http_address'], $url]; |
||||||
|
} |
||||||
|
|
||||||
|
protected function httpRequest($method, $url, $requestBody = null) |
||||||
|
{ |
||||||
|
$method = strtoupper($method); |
||||||
|
|
||||||
|
// response body and headers |
||||||
|
$headers = []; |
||||||
|
$body = ''; |
||||||
|
|
||||||
|
$options = [ |
||||||
|
CURLOPT_USERAGENT => 'Yii2 Framework ' . __CLASS__, |
||||||
|
CURLOPT_RETURNTRANSFER => false, |
||||||
|
CURLOPT_HEADER => false, |
||||||
|
// http://www.php.net/manual/en/function.curl-setopt.php#82418 |
||||||
|
CURLOPT_HTTPHEADER => ['Expect:'], |
||||||
|
|
||||||
|
CURLOPT_WRITEFUNCTION => function($curl, $data) use (&$body) { |
||||||
|
$body .= $data; |
||||||
|
return mb_strlen($data, '8bit'); |
||||||
|
}, |
||||||
|
CURLOPT_HEADERFUNCTION => function($curl, $data) use (&$headers) { |
||||||
|
foreach(explode("\r\n", $data) as $row) { |
||||||
|
if (($pos = strpos($row, ':')) !== false) { |
||||||
|
$headers[strtolower(substr($row, 0, $pos))] = trim(substr($row, $pos + 1)); |
||||||
|
} |
||||||
|
} |
||||||
|
return mb_strlen($data, '8bit'); |
||||||
|
}, |
||||||
|
CURLOPT_CUSTOMREQUEST => $method, |
||||||
|
]; |
||||||
|
if ($this->connectionTimeout !== null) { |
||||||
|
$options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; |
||||||
|
} |
||||||
|
if ($this->dataTimeout !== null) { |
||||||
|
$options[CURLOPT_TIMEOUT] = $this->dataTimeout; |
||||||
|
} |
||||||
|
if ($requestBody !== null) { |
||||||
|
$options[CURLOPT_POSTFIELDS] = $requestBody; |
||||||
|
} |
||||||
|
if ($method == 'HEAD') { |
||||||
|
$options[CURLOPT_NOBODY] = true; |
||||||
|
unset($options[CURLOPT_WRITEFUNCTION]); |
||||||
|
} |
||||||
|
|
||||||
|
if (is_array($url)) { |
||||||
|
list($host, $q) = $url; |
||||||
|
if (strncmp($host, 'inet[/', 6) == 0) { |
||||||
|
$host = substr($host, 6, -1); |
||||||
|
} |
||||||
|
$profile = $q . $requestBody; |
||||||
|
$url = 'http://' . $host . '/' . $q; |
||||||
|
} else { |
||||||
|
$profile = false; |
||||||
|
} |
||||||
|
|
||||||
|
Yii::trace("Sending request to elasticsearch node: $url\n$requestBody", __METHOD__); |
||||||
|
if ($profile !== false) { |
||||||
|
Yii::beginProfile($profile, __METHOD__); |
||||||
|
} |
||||||
|
|
||||||
|
$curl = curl_init($url); |
||||||
|
curl_setopt_array($curl, $options); |
||||||
|
if (curl_exec($curl) === false) { |
||||||
|
throw new Exception('Elasticsearch request failed: ' . curl_errno($curl) . ' - ' . curl_error($curl), [ |
||||||
|
'requestMethod' => $method, |
||||||
|
'requestUrl' => $url, |
||||||
|
'requestBody' => $requestBody, |
||||||
|
'responseHeaders' => $headers, |
||||||
|
'responseBody' => $body, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
$responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); |
||||||
|
curl_close($curl); |
||||||
|
|
||||||
|
if ($profile !== false) { |
||||||
|
Yii::endProfile($profile, __METHOD__); |
||||||
|
} |
||||||
|
|
||||||
|
if ($responseCode >= 200 && $responseCode < 300) { |
||||||
|
if ($method == 'HEAD') { |
||||||
|
return true; |
||||||
|
} else { |
||||||
|
if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { |
||||||
|
throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ |
||||||
|
'requestMethod' => $method, |
||||||
|
'requestUrl' => $url, |
||||||
|
'requestBody' => $requestBody, |
||||||
|
'responseCode' => $responseCode, |
||||||
|
'responseHeaders' => $headers, |
||||||
|
'responseBody' => $body, |
||||||
|
]); |
||||||
|
} |
||||||
|
if (isset($headers['content-type']) && !strncmp($headers['content-type'], 'application/json', 16)) { |
||||||
|
return Json::decode($body); |
||||||
|
} |
||||||
|
throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ |
||||||
|
'requestMethod' => $method, |
||||||
|
'requestUrl' => $url, |
||||||
|
'requestBody' => $requestBody, |
||||||
|
'responseCode' => $responseCode, |
||||||
|
'responseHeaders' => $headers, |
||||||
|
'responseBody' => $body, |
||||||
|
]); |
||||||
|
} |
||||||
|
} elseif ($responseCode == 404) { |
||||||
|
return false; |
||||||
|
} else { |
||||||
|
throw new Exception("Elasticsearch request failed with code $responseCode.", [ |
||||||
|
'requestMethod' => $method, |
||||||
|
'requestUrl' => $url, |
||||||
|
'requestBody' => $requestBody, |
||||||
|
'responseCode' => $responseCode, |
||||||
|
'responseHeaders' => $headers, |
||||||
|
'responseBody' => $body, |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public function getNodeInfo() |
||||||
|
{ |
||||||
|
return $this->get([]); |
||||||
|
} |
||||||
|
|
||||||
|
public function getClusterState() |
||||||
|
{ |
||||||
|
return $this->get(['_cluster', 'state']); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
/** |
||||||
|
* Exception represents an exception that is caused by elasticsearch-related operations. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class Exception extends \yii\db\Exception |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @var array additional information about the http request that caused the error. |
||||||
|
*/ |
||||||
|
public $errorInfo = []; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor. |
||||||
|
* @param string $message error message |
||||||
|
* @param array $errorInfo error info |
||||||
|
* @param integer $code error code |
||||||
|
* @param \Exception $previous The previous exception used for the exception chaining. |
||||||
|
*/ |
||||||
|
public function __construct($message, $errorInfo = [], $code = 0, \Exception $previous = null) |
||||||
|
{ |
||||||
|
$this->errorInfo = $errorInfo; |
||||||
|
parent::__construct($message, $code, $previous); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return string the user-friendly name of this exception |
||||||
|
*/ |
||||||
|
public function getName() |
||||||
|
{ |
||||||
|
return \Yii::t('yii', 'Elasticsearch Database Exception'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
The Yii framework is free software. It is released under the terms of |
||||||
|
the following BSD License. |
||||||
|
|
||||||
|
Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com) |
||||||
|
All rights reserved. |
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without |
||||||
|
modification, are permitted provided that the following conditions |
||||||
|
are met: |
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer. |
||||||
|
* Redistributions in binary form must reproduce the above copyright |
||||||
|
notice, this list of conditions and the following disclaimer in |
||||||
|
the documentation and/or other materials provided with the |
||||||
|
distribution. |
||||||
|
* Neither the name of Yii Software LLC nor the names of its |
||||||
|
contributors may be used to endorse or promote products derived |
||||||
|
from this software without specific prior written permission. |
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS |
||||||
|
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE |
||||||
|
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, |
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, |
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
||||||
|
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
||||||
|
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
||||||
|
POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,506 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use Yii; |
||||||
|
use yii\base\Component; |
||||||
|
use yii\base\NotSupportedException; |
||||||
|
use yii\db\QueryInterface; |
||||||
|
use yii\db\QueryTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* Query represents a query to the search API of elasticsearch. |
||||||
|
* |
||||||
|
* Query provides a set of methods to facilitate the specification of different parameters of the query. |
||||||
|
* These methods can be chained together. |
||||||
|
* |
||||||
|
* By calling [[createCommand()]], we can get a [[Command]] instance which can be further |
||||||
|
* used to perform/execute the DB query against a database. |
||||||
|
* |
||||||
|
* For example, |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* $query = new Query; |
||||||
|
* $query->fields('id, name') |
||||||
|
* ->from('myindex', 'users') |
||||||
|
* ->limit(10); |
||||||
|
* // build and execute the query |
||||||
|
* $command = $query->createCommand(); |
||||||
|
* $rows = $command->search(); // this way you get the raw output of elasticsearch. |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* You would normally call `$query->search()` instead of creating a command as this method |
||||||
|
* adds the `indexBy()` feature and also removes some inconsistencies from the response. |
||||||
|
* |
||||||
|
* Query also provides some methods to easier get some parts of the result only: |
||||||
|
* |
||||||
|
* - [[one()]]: returns a single record populated with the first row of data. |
||||||
|
* - [[all()]]: returns all records based on the query results. |
||||||
|
* - [[count()]]: returns the number of records. |
||||||
|
* - [[scalar()]]: returns the value of the first column in the first row of the query result. |
||||||
|
* - [[column()]]: returns the value of the first column in the query result. |
||||||
|
* - [[exists()]]: returns a value indicating whether the query result has data or not. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class Query extends Component implements QueryInterface |
||||||
|
{ |
||||||
|
use QueryTrait; |
||||||
|
|
||||||
|
/** |
||||||
|
* @var array the fields being retrieved from the documents. For example, `['id', 'name']`. |
||||||
|
* If not set, it means retrieving all fields. An empty array will result in no fields being |
||||||
|
* retrieved. This means that only the primaryKey of a record will be available in the result. |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields |
||||||
|
* @see fields() |
||||||
|
*/ |
||||||
|
public $fields; |
||||||
|
/** |
||||||
|
* @var string|array The index to retrieve data from. This can be a string representing a single index |
||||||
|
* or a an array of multiple indexes. If this is not set, indexes are being queried. |
||||||
|
* @see from() |
||||||
|
*/ |
||||||
|
public $index; |
||||||
|
/** |
||||||
|
* @var string|array The type to retrieve data from. This can be a string representing a single type |
||||||
|
* or a an array of multiple types. If this is not set, all types are being queried. |
||||||
|
* @see from() |
||||||
|
*/ |
||||||
|
public $type; |
||||||
|
/** |
||||||
|
* @var integer A search timeout, bounding the search request to be executed within the specified time value |
||||||
|
* and bail with the hits accumulated up to that point when expired. Defaults to no timeout. |
||||||
|
* @see timeout() |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||||
|
*/ |
||||||
|
public $timeout; |
||||||
|
/** |
||||||
|
* @var array|string The query part of this search query. This is an array or json string that follows the format of |
||||||
|
* the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). |
||||||
|
*/ |
||||||
|
public $query; |
||||||
|
/** |
||||||
|
* @var array|string The filter part of this search query. This is an array or json string that follows the format of |
||||||
|
* the elasticsearch [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html). |
||||||
|
*/ |
||||||
|
public $filter; |
||||||
|
|
||||||
|
public $facets = []; |
||||||
|
|
||||||
|
public function init() |
||||||
|
{ |
||||||
|
parent::init(); |
||||||
|
// setting the default limit according to elasticsearch defaults |
||||||
|
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||||
|
if ($this->limit === null) { |
||||||
|
$this->limit = 10; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a DB command that can be used to execute this 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. |
||||||
|
* @return Command the created DB command instance. |
||||||
|
*/ |
||||||
|
public function createCommand($db = null) |
||||||
|
{ |
||||||
|
if ($db === null) { |
||||||
|
$db = Yii::$app->getComponent('elasticsearch'); |
||||||
|
} |
||||||
|
|
||||||
|
$commandConfig = $db->getQueryBuilder()->build($this); |
||||||
|
return $db->createCommand($commandConfig); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes the query and returns all results as an array. |
||||||
|
* @param Connection $db the database connection used to execute the query. |
||||||
|
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||||
|
* @return array the query results. If the query results in nothing, an empty array will be returned. |
||||||
|
*/ |
||||||
|
public function all($db = null) |
||||||
|
{ |
||||||
|
$result = $this->createCommand($db)->search(); |
||||||
|
if (empty($result['hits']['hits'])) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$rows = $result['hits']['hits']; |
||||||
|
if ($this->indexBy === null && $this->fields === null) { |
||||||
|
return $rows; |
||||||
|
} |
||||||
|
$models = []; |
||||||
|
foreach ($rows as $key => $row) { |
||||||
|
if ($this->fields !== null) { |
||||||
|
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||||
|
unset($row['fields']); |
||||||
|
} |
||||||
|
if ($this->indexBy !== null) { |
||||||
|
if (is_string($this->indexBy)) { |
||||||
|
$key = $row['_source'][$this->indexBy]; |
||||||
|
} else { |
||||||
|
$key = call_user_func($this->indexBy, $row); |
||||||
|
} |
||||||
|
} |
||||||
|
$models[$key] = $row; |
||||||
|
} |
||||||
|
return $models; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes the query and returns a single row of result. |
||||||
|
* @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|boolean the first row (in terms of an array) of the query result. False is returned if the query |
||||||
|
* results in nothing. |
||||||
|
*/ |
||||||
|
public function one($db = null) |
||||||
|
{ |
||||||
|
$options['size'] = 1; |
||||||
|
$result = $this->createCommand($db)->search($options); |
||||||
|
if (empty($result['hits']['hits'])) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$record = reset($result['hits']['hits']); |
||||||
|
if ($this->fields !== null) { |
||||||
|
$record['_source'] = isset($record['fields']) ? $record['fields'] : []; |
||||||
|
unset($record['fields']); |
||||||
|
} |
||||||
|
return $record; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes the query and returns the complete search result including e.g. hits, facets, totalCount. |
||||||
|
* @param Connection $db the database connection used to execute the query. |
||||||
|
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||||
|
* @param array $options The options given with this query. Possible options are: |
||||||
|
* - [routing](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#search-routing) |
||||||
|
* - [search_type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html) |
||||||
|
* @return array the query results. |
||||||
|
*/ |
||||||
|
public function search($db = null, $options = []) |
||||||
|
{ |
||||||
|
$result = $this->createCommand($db)->search($options); |
||||||
|
if (!empty($result['hits']['hits']) && ($this->indexBy === null || $this->fields === null)) { |
||||||
|
$rows = []; |
||||||
|
foreach ($result['hits']['hits'] as $key => $row) { |
||||||
|
if ($this->fields !== null) { |
||||||
|
$row['_source'] = isset($row['fields']) ? $row['fields'] : []; |
||||||
|
unset($row['fields']); |
||||||
|
} |
||||||
|
if ($this->indexBy !== null) { |
||||||
|
if (is_string($this->indexBy)) { |
||||||
|
$key = $row['_source'][$this->indexBy]; |
||||||
|
} else { |
||||||
|
$key = call_user_func($this->indexBy, $row); |
||||||
|
} |
||||||
|
} |
||||||
|
$rows[$key] = $row; |
||||||
|
} |
||||||
|
$result['hits']['hits'] = $rows; |
||||||
|
} |
||||||
|
return $result; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO add query stats http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search.html#stats-groups |
||||||
|
|
||||||
|
// TODO add scroll/scan http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan |
||||||
|
|
||||||
|
/** |
||||||
|
* 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 implement http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/docs-delete-by-query.html |
||||||
|
throw new NotSupportedException('Delete by query is not implemented yet.'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the query result as a scalar value. |
||||||
|
* The value returned will be the specified field in the first document of the query results. |
||||||
|
* @param string $field name of the attribute to select |
||||||
|
* @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 string the value of the specified attribute in the first record of the query result. |
||||||
|
* Null is returned if the query result is empty or the field does not exist. |
||||||
|
*/ |
||||||
|
public function scalar($field, $db = null) |
||||||
|
{ |
||||||
|
$record = self::one($db); // TODO limit fields to the one required |
||||||
|
if ($record !== false && isset($record['_source'][$field])) { |
||||||
|
return $record['_source'][$field]; |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Executes the query and returns the first column of the result. |
||||||
|
* @param string $field the field to query over |
||||||
|
* @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 first column of the query result. An empty array is returned if the query results in nothing. |
||||||
|
*/ |
||||||
|
public function column($field, $db = null) |
||||||
|
{ |
||||||
|
$command = $this->createCommand($db); |
||||||
|
$command->queryParts['fields'] = [$field]; |
||||||
|
$result = $command->search(); |
||||||
|
if (empty($result['hits']['hits'])) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$column = []; |
||||||
|
foreach ($result['hits']['hits'] as $row) { |
||||||
|
$column[] = isset($row['fields'][$field]) ? $row['fields'][$field] : null; |
||||||
|
} |
||||||
|
return $column; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns the number of records. |
||||||
|
* @param string $q the COUNT expression. This parameter is ignored by this implementation. |
||||||
|
* @param Connection $db the database connection used to execute the query. |
||||||
|
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||||
|
* @return integer number of records |
||||||
|
*/ |
||||||
|
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 |
||||||
|
|
||||||
|
$options = []; |
||||||
|
$options['search_type'] = 'count'; |
||||||
|
$count = $this->createCommand($db)->search($options)['hits']['total']; |
||||||
|
if ($this->limit === null && $this->offset === null) { |
||||||
|
return $count; |
||||||
|
} elseif ($this->offset !== null) { |
||||||
|
$count = $this->offset < $count ? $count - $this->offset : 0; |
||||||
|
} |
||||||
|
return $this->limit === null ? $count : ($this->limit > $count ? $count : $this->limit); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a value indicating whether the query result contains any row of data. |
||||||
|
* @param Connection $db the database connection used to execute the query. |
||||||
|
* If this parameter is not given, the `elasticsearch` application component will be used. |
||||||
|
* @return boolean whether the query result contains any row of data. |
||||||
|
*/ |
||||||
|
public function exists($db = null) |
||||||
|
{ |
||||||
|
return self::one($db) !== false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adds a facet search to this query. |
||||||
|
* @param string $name the name of this facet |
||||||
|
* @param string $type the facet type. e.g. `terms`, `range`, `histogram`... |
||||||
|
* @param string|array $options the configuration options for this facet. Can be an array or a json string. |
||||||
|
* @return static |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-query-facet.html |
||||||
|
*/ |
||||||
|
public function addFacet($name, $type, $options) |
||||||
|
{ |
||||||
|
$this->facets[$name] = [$type => $options]; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The `terms facet` allow to specify field facets that return the N most frequent terms. |
||||||
|
* @param string $name the name of this facet |
||||||
|
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||||
|
* @return static |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-terms-facet.html |
||||||
|
*/ |
||||||
|
public function addTermFacet($name, $options) |
||||||
|
{ |
||||||
|
return $this->addFacet($name, 'terms', $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Range facet allows to specify a set of ranges and get both the number of docs (count) that fall |
||||||
|
* within each range, and aggregated data either based on the field, or using another field. |
||||||
|
* @param string $name the name of this facet |
||||||
|
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||||
|
* @return static |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-range-facet.html |
||||||
|
*/ |
||||||
|
public function addRangeFacet($name, $options) |
||||||
|
{ |
||||||
|
return $this->addFacet($name, 'range', $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* The histogram facet works with numeric data by building a histogram across intervals of the field values. |
||||||
|
* Each value is "rounded" into an interval (or placed in a bucket), and statistics are provided per |
||||||
|
* interval/bucket (count and total). |
||||||
|
* @param string $name the name of this facet |
||||||
|
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||||
|
* @return static |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-histogram-facet.html |
||||||
|
*/ |
||||||
|
public function addHistogramFacet($name, $options) |
||||||
|
{ |
||||||
|
return $this->addFacet($name, 'histogram', $options); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A specific histogram facet that can work with date field types enhancing it over the regular histogram facet. |
||||||
|
* @param string $name the name of this facet |
||||||
|
* @param array $options additional option. Please refer to the elasticsearch documentation for details. |
||||||
|
* @return static |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-facets-date-histogram-facet.html |
||||||
|
*/ |
||||||
|
public function addDateHistogramFacet($name, $options) |
||||||
|
{ |
||||||
|
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 |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the querypart of this search query. |
||||||
|
* @param string $query |
||||||
|
* @return static |
||||||
|
*/ |
||||||
|
public function query($query) |
||||||
|
{ |
||||||
|
$this->query = $query; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the filter part of this search query. |
||||||
|
* @param string $filter |
||||||
|
* @return static |
||||||
|
*/ |
||||||
|
public function filter($filter) |
||||||
|
{ |
||||||
|
$this->filter = $filter; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the index and type to retrieve documents from. |
||||||
|
* @param string|array $index The index to retrieve data from. This can be a string representing a single index |
||||||
|
* or a an array of multiple indexes. If this is `null` it means that all indexes are being queried. |
||||||
|
* @param string|array $type The type to retrieve data from. This can be a string representing a single type |
||||||
|
* or a an array of multiple types. If this is `null` it means that all types are being queried. |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type |
||||||
|
*/ |
||||||
|
public function from($index, $type = null) |
||||||
|
{ |
||||||
|
$this->index = $index; |
||||||
|
$this->type = $type; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the fields to retrieve from the documents. |
||||||
|
* @param array $fields the fields to be selected. |
||||||
|
* @return static the query object itself |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-fields.html |
||||||
|
*/ |
||||||
|
public function fields($fields) |
||||||
|
{ |
||||||
|
if (is_array($fields) || $fields === null) { |
||||||
|
$this->fields = $fields; |
||||||
|
} else { |
||||||
|
$this->fields = func_get_args(); |
||||||
|
} |
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the search timeout. |
||||||
|
* @param integer $timeout A search timeout, bounding the search request to be executed within the specified time value |
||||||
|
* and bail with the hits accumulated up to that point when expired. Defaults to no timeout. |
||||||
|
* @return static the query object itself |
||||||
|
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_3 |
||||||
|
*/ |
||||||
|
public function timeout($timeout) |
||||||
|
{ |
||||||
|
$this->timeout = $timeout; |
||||||
|
return $this; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,349 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\elasticsearch; |
||||||
|
|
||||||
|
use yii\base\InvalidParamException; |
||||||
|
use yii\base\NotSupportedException; |
||||||
|
use yii\helpers\Json; |
||||||
|
|
||||||
|
/** |
||||||
|
* QueryBuilder builds an elasticsearch query based on the specification given as a [[Query]] object. |
||||||
|
* |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class QueryBuilder extends \yii\base\Object |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @var Connection the database connection. |
||||||
|
*/ |
||||||
|
public $db; |
||||||
|
|
||||||
|
/** |
||||||
|
* Constructor. |
||||||
|
* @param Connection $connection the database connection. |
||||||
|
* @param array $config name-value pairs that will be used to initialize the object properties |
||||||
|
*/ |
||||||
|
public function __construct($connection, $config = []) |
||||||
|
{ |
||||||
|
$this->db = $connection; |
||||||
|
parent::__construct($config); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates query from a [[Query]] object. |
||||||
|
* @param Query $query the [[Query]] object from which the query will be generated |
||||||
|
* @return array the generated SQL statement (the first array element) and the corresponding |
||||||
|
* parameters to be bound to the SQL statement (the second array element). |
||||||
|
*/ |
||||||
|
public function build($query) |
||||||
|
{ |
||||||
|
$parts = []; |
||||||
|
|
||||||
|
if ($query->fields !== null) { |
||||||
|
$parts['fields'] = (array) $query->fields; |
||||||
|
} |
||||||
|
if ($query->limit !== null && $query->limit >= 0) { |
||||||
|
$parts['size'] = $query->limit; |
||||||
|
} |
||||||
|
if ($query->offset > 0) { |
||||||
|
$parts['from'] = (int) $query->offset; |
||||||
|
} |
||||||
|
|
||||||
|
if (empty($parts['query'])) { |
||||||
|
$parts['query'] = ["match_all" => (object)[]]; |
||||||
|
} |
||||||
|
|
||||||
|
$whereFilter = $this->buildCondition($query->where); |
||||||
|
if (is_string($query->filter)) { |
||||||
|
if (empty($whereFilter)) { |
||||||
|
$parts['filter'] = $query->filter; |
||||||
|
} else { |
||||||
|
$parts['filter'] = '{"and": [' . $query->filter . ', ' . Json::encode($whereFilter) . ']}'; |
||||||
|
} |
||||||
|
} elseif ($query->filter !== null) { |
||||||
|
if (empty($whereFilter)) { |
||||||
|
$parts['filter'] = $query->filter; |
||||||
|
} else { |
||||||
|
$parts['filter'] = ['and' => [$query->filter, $whereFilter]]; |
||||||
|
} |
||||||
|
} elseif (!empty($whereFilter)) { |
||||||
|
$parts['filter'] = $whereFilter; |
||||||
|
} |
||||||
|
|
||||||
|
$sort = $this->buildOrderBy($query->orderBy); |
||||||
|
if (!empty($sort)) { |
||||||
|
$parts['sort'] = $sort; |
||||||
|
} |
||||||
|
|
||||||
|
if (!empty($query->facets)) { |
||||||
|
$parts['facets'] = $query->facets; |
||||||
|
} |
||||||
|
|
||||||
|
$options = []; |
||||||
|
if ($query->timeout !== null) { |
||||||
|
$options['timeout'] = $query->timeout; |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'queryParts' => $parts, |
||||||
|
'index' => $query->index, |
||||||
|
'type' => $query->type, |
||||||
|
'options' => $options, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* adds order by condition to the query |
||||||
|
*/ |
||||||
|
public function buildOrderBy($columns) |
||||||
|
{ |
||||||
|
if (empty($columns)) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$orders = []; |
||||||
|
foreach ($columns as $name => $direction) { |
||||||
|
if (is_string($direction)) { |
||||||
|
$column = $direction; |
||||||
|
$direction = SORT_ASC; |
||||||
|
} else { |
||||||
|
$column = $name; |
||||||
|
} |
||||||
|
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
$column = '_uid'; |
||||||
|
} |
||||||
|
|
||||||
|
// allow elasticsearch extended syntax as described in http://www.elasticsearch.org/guide/reference/api/search/sort/ |
||||||
|
if (is_array($direction)) { |
||||||
|
$orders[] = [$column => $direction]; |
||||||
|
} else { |
||||||
|
$orders[] = [$column => ($direction === SORT_DESC ? 'desc' : 'asc')]; |
||||||
|
} |
||||||
|
} |
||||||
|
return $orders; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parses the condition specification and generates the corresponding SQL expression. |
||||||
|
* @param string|array $condition the condition specification. Please refer to [[Query::where()]] |
||||||
|
* on how to specify a condition. |
||||||
|
* @param array $params the binding parameters to be populated |
||||||
|
* @return string the generated SQL expression |
||||||
|
* @throws \yii\db\Exception if the condition is in bad format |
||||||
|
*/ |
||||||
|
public function buildCondition($condition) |
||||||
|
{ |
||||||
|
static $builders = array( |
||||||
|
'and' => 'buildAndCondition', |
||||||
|
'or' => 'buildAndCondition', |
||||||
|
'between' => 'buildBetweenCondition', |
||||||
|
'not between' => 'buildBetweenCondition', |
||||||
|
'in' => 'buildInCondition', |
||||||
|
'not in' => 'buildInCondition', |
||||||
|
'like' => 'buildLikeCondition', |
||||||
|
'not like' => 'buildLikeCondition', |
||||||
|
'or like' => 'buildLikeCondition', |
||||||
|
'or not like' => 'buildLikeCondition', |
||||||
|
); |
||||||
|
|
||||||
|
if (empty($condition)) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
if (!is_array($condition)) { |
||||||
|
throw new NotSupportedException('String conditions in where() are not supported by elasticsearch.'); |
||||||
|
} |
||||||
|
if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... |
||||||
|
$operator = strtolower($condition[0]); |
||||||
|
if (isset($builders[$operator])) { |
||||||
|
$method = $builders[$operator]; |
||||||
|
array_shift($condition); |
||||||
|
return $this->$method($operator, $condition); |
||||||
|
} else { |
||||||
|
throw new InvalidParamException('Found unknown operator in query: ' . $operator); |
||||||
|
} |
||||||
|
} else { // hash format: 'column1' => 'value1', 'column2' => 'value2', ... |
||||||
|
return $this->buildHashCondition($condition); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function buildHashCondition($condition) |
||||||
|
{ |
||||||
|
$parts = []; |
||||||
|
foreach($condition as $attribute => $value) { |
||||||
|
if ($attribute == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
if ($value == null) { // there is no null pk |
||||||
|
$parts[] = ['script' => ['script' => '0==1']]; |
||||||
|
} else { |
||||||
|
$parts[] = ['ids' => ['values' => is_array($value) ? $value : [$value]]]; |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (is_array($value)) { // IN condition |
||||||
|
$parts[] = ['in' => [$attribute => $value]]; |
||||||
|
} else { |
||||||
|
if ($value === null) { |
||||||
|
$parts[] = ['missing' => ['field' => $attribute, 'existence' => true, 'null_value' => true]]; |
||||||
|
} else { |
||||||
|
$parts[] = ['term' => [$attribute => $value]]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return count($parts) === 1 ? $parts[0] : ['and' => $parts]; |
||||||
|
} |
||||||
|
|
||||||
|
private function buildAndCondition($operator, $operands) |
||||||
|
{ |
||||||
|
$parts = []; |
||||||
|
foreach ($operands as $operand) { |
||||||
|
if (is_array($operand)) { |
||||||
|
$operand = $this->buildCondition($operand); |
||||||
|
} |
||||||
|
if (!empty($operand)) { |
||||||
|
$parts[] = $operand; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!empty($parts)) { |
||||||
|
return [$operator => $parts]; |
||||||
|
} else { |
||||||
|
return []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function buildBetweenCondition($operator, $operands) |
||||||
|
{ |
||||||
|
if (!isset($operands[0], $operands[1], $operands[2])) { |
||||||
|
throw new InvalidParamException("Operator '$operator' requires three operands."); |
||||||
|
} |
||||||
|
|
||||||
|
list($column, $value1, $value2) = $operands; |
||||||
|
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
throw new NotSupportedException('Between condition is not supported for primaryKey.'); |
||||||
|
} |
||||||
|
$filter = ['range' => [$column => ['gte' => $value1, 'lte' => $value2]]]; |
||||||
|
if ($operator == 'not between') { |
||||||
|
$filter = ['not' => $filter]; |
||||||
|
} |
||||||
|
return $filter; |
||||||
|
} |
||||||
|
|
||||||
|
private function buildInCondition($operator, $operands) |
||||||
|
{ |
||||||
|
if (!isset($operands[0], $operands[1])) { |
||||||
|
throw new InvalidParamException("Operator '$operator' requires two operands."); |
||||||
|
} |
||||||
|
|
||||||
|
list($column, $values) = $operands; |
||||||
|
|
||||||
|
$values = (array)$values; |
||||||
|
|
||||||
|
if (empty($values) || $column === []) { |
||||||
|
return $operator === 'in' ? ['script' => ['script' => '0==1']] : []; |
||||||
|
} |
||||||
|
|
||||||
|
if (count($column) > 1) { |
||||||
|
return $this->buildCompositeInCondition($operator, $column, $values, $params); |
||||||
|
} elseif (is_array($column)) { |
||||||
|
$column = reset($column); |
||||||
|
} |
||||||
|
$canBeNull = false; |
||||||
|
foreach ($values as $i => $value) { |
||||||
|
if (is_array($value)) { |
||||||
|
$values[$i] = $value = isset($value[$column]) ? $value[$column] : null; |
||||||
|
} |
||||||
|
if ($value === null) { |
||||||
|
$canBeNull = true; |
||||||
|
unset($values[$i]); |
||||||
|
} |
||||||
|
} |
||||||
|
if ($column == ActiveRecord::PRIMARY_KEY_NAME) { |
||||||
|
if (empty($values) && $canBeNull) { // there is no null pk |
||||||
|
$filter = ['script' => ['script' => '0==1']]; |
||||||
|
} else { |
||||||
|
$filter = ['ids' => ['values' => array_values($values)]]; |
||||||
|
if ($canBeNull) { |
||||||
|
$filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (empty($values) && $canBeNull) { |
||||||
|
$filter = ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]; |
||||||
|
} else { |
||||||
|
$filter = ['in' => [$column => array_values($values)]]; |
||||||
|
if ($canBeNull) { |
||||||
|
$filter = ['or' => [$filter, ['missing' => ['field' => $column, 'existence' => true, 'null_value' => true]]]]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if ($operator == 'not in') { |
||||||
|
$filter = ['not' => $filter]; |
||||||
|
} |
||||||
|
return $filter; |
||||||
|
} |
||||||
|
|
||||||
|
protected function buildCompositeInCondition($operator, $columns, $values) |
||||||
|
{ |
||||||
|
throw new NotSupportedException('composite in is not supported by elasticsearch.'); |
||||||
|
$vss = array(); |
||||||
|
foreach ($values as $value) { |
||||||
|
$vs = array(); |
||||||
|
foreach ($columns as $column) { |
||||||
|
if (isset($value[$column])) { |
||||||
|
$phName = self::PARAM_PREFIX . count($params); |
||||||
|
$params[$phName] = $value[$column]; |
||||||
|
$vs[] = $phName; |
||||||
|
} else { |
||||||
|
$vs[] = 'NULL'; |
||||||
|
} |
||||||
|
} |
||||||
|
$vss[] = '(' . implode(', ', $vs) . ')'; |
||||||
|
} |
||||||
|
foreach ($columns as $i => $column) { |
||||||
|
if (strpos($column, '(') === false) { |
||||||
|
$columns[$i] = $this->db->quoteColumnName($column); |
||||||
|
} |
||||||
|
} |
||||||
|
return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; |
||||||
|
} |
||||||
|
|
||||||
|
private function buildLikeCondition($operator, $operands) |
||||||
|
{ |
||||||
|
throw new NotSupportedException('like conditions is not supported by elasticsearch.'); |
||||||
|
if (!isset($operands[0], $operands[1])) { |
||||||
|
throw new Exception("Operator '$operator' requires two operands."); |
||||||
|
} |
||||||
|
|
||||||
|
list($column, $values) = $operands; |
||||||
|
|
||||||
|
$values = (array)$values; |
||||||
|
|
||||||
|
if (empty($values)) { |
||||||
|
return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0==1' : ''; |
||||||
|
} |
||||||
|
|
||||||
|
if ($operator === 'LIKE' || $operator === 'NOT LIKE') { |
||||||
|
$andor = ' AND '; |
||||||
|
} else { |
||||||
|
$andor = ' OR '; |
||||||
|
$operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; |
||||||
|
} |
||||||
|
|
||||||
|
if (strpos($column, '(') === false) { |
||||||
|
$column = $this->db->quoteColumnName($column); |
||||||
|
} |
||||||
|
|
||||||
|
$parts = array(); |
||||||
|
foreach ($values as $value) { |
||||||
|
$phName = self::PARAM_PREFIX . count($params); |
||||||
|
$params[$phName] = $value; |
||||||
|
$parts[] = "$column $operator $phName"; |
||||||
|
} |
||||||
|
|
||||||
|
return implode($andor, $parts); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,148 @@ |
|||||||
|
Elasticsearch Query and ActiveRecord for Yii 2 |
||||||
|
============================================== |
||||||
|
|
||||||
|
This extension provides the [elasticsearch](http://www.elasticsearch.org/) integration for the Yii2 framework. |
||||||
|
It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active |
||||||
|
records in elasticsearch. |
||||||
|
|
||||||
|
To use this extension, you have to configure the Connection class in your application configuration: |
||||||
|
|
||||||
|
```php |
||||||
|
return [ |
||||||
|
//.... |
||||||
|
'components' => [ |
||||||
|
'elasticsearch' => [ |
||||||
|
'class' => 'yii\elasticsearch\Connection', |
||||||
|
'nodes' => [ |
||||||
|
['http_address' => '127.0.0.1:9200'], |
||||||
|
// configure more hosts if you have a cluster |
||||||
|
], |
||||||
|
], |
||||||
|
] |
||||||
|
]; |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
Installation |
||||||
|
------------ |
||||||
|
|
||||||
|
The preferred way to install this extension is through [composer](http://getcomposer.org/download/). |
||||||
|
|
||||||
|
Either run |
||||||
|
|
||||||
|
``` |
||||||
|
php composer.phar require yiisoft/yii2-elasticsearch "*" |
||||||
|
``` |
||||||
|
|
||||||
|
or add |
||||||
|
|
||||||
|
```json |
||||||
|
"yiisoft/yii2-elasticsearch": "*" |
||||||
|
``` |
||||||
|
|
||||||
|
to the require section of your composer.json. |
||||||
|
|
||||||
|
|
||||||
|
Using the Query |
||||||
|
--------------- |
||||||
|
|
||||||
|
TBD |
||||||
|
|
||||||
|
Using the ActiveRecord |
||||||
|
---------------------- |
||||||
|
|
||||||
|
For general information on how to use yii's ActiveRecord please refer to the [guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). |
||||||
|
|
||||||
|
For defining an elasticsearch ActiveRecord class your record class needs to extend from `yii\elasticsearch\ActiveRecord` and |
||||||
|
implement at least the `attributes()` method to define the attributes of the record. |
||||||
|
The primary key (the `_id` field in elasticsearch terms) is represented by `getId()` and `setId()` and can not be changed. |
||||||
|
The primary key is not part of the attributes. |
||||||
|
|
||||||
|
primary key can be defined via [[primaryKey()]] which defaults to `id` if not specified. |
||||||
|
The primaryKey needs to be part of the attributes so make sure you have an `id` attribute defined if you do |
||||||
|
not specify your own primary key. |
||||||
|
|
||||||
|
The following is an example model called `Customer`: |
||||||
|
|
||||||
|
```php |
||||||
|
class Customer extends \yii\elasticsearch\ActiveRecord |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @return array the list of attributes for this record |
||||||
|
*/ |
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
return ['id', 'name', 'address', 'registration_date']; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @return ActiveRelation defines a relation to the Order record (can be in other database, e.g. redis or sql) |
||||||
|
*/ |
||||||
|
public function getOrders() |
||||||
|
{ |
||||||
|
return $this->hasMany(Order::className(), ['customer_id' => 'id'])->orderBy('id'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Defines a scope that modifies the `$query` to return only active(status = 1) customers |
||||||
|
*/ |
||||||
|
public static function active($query) |
||||||
|
{ |
||||||
|
$query->andWhere(array('status' => 1)); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
You may override [[index()]] and [[type()]] to define the index and type this record represents. |
||||||
|
|
||||||
|
The general usage of elasticsearch ActiveRecord is very similar to the database ActiveRecord as described in the |
||||||
|
[guide](https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md). |
||||||
|
It supports the same interface and features except the following limitations and additions(*!*): |
||||||
|
|
||||||
|
- As elasticsearch does not support SQL, the query API does not support `join()`, `groupBy()`, `having()` and `union()`. |
||||||
|
Sorting, limit, offset and conditional where are all supported. |
||||||
|
- `from()` does not select the tables, but the [index](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-index) |
||||||
|
and [type](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/glossary.html#glossary-type) to query against. |
||||||
|
- `select()` has been replaced with `fields()` which basically does the same but `fields` is more elasticsearch terminology. |
||||||
|
It defines the fields to retrieve from a document. |
||||||
|
- `via`-relations can not be defined via a table as there are not tables in elasticsearch. You can only define relations via other records. |
||||||
|
- As elasticsearch is a data storage and search engine there is of course support added for search your records. |
||||||
|
There are `query()`, `filter()` and `addFacets()` methods that allows to compose an elasticsearch query. |
||||||
|
See the usage example below on how they work and check out the [Query DSL](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl.html) |
||||||
|
on how to compose `query` and `filter` parts. |
||||||
|
- It is also possible to define relations from elasticsearch ActiveRecords to normal ActiveRecord classes and vice versa. |
||||||
|
|
||||||
|
Elasticsearch separates primary key from attributes. You need to set the `id` property of the record to set its primary key. |
||||||
|
|
||||||
|
Usage example: |
||||||
|
|
||||||
|
```php |
||||||
|
$customer = new Customer(); |
||||||
|
$customer->id = 1; |
||||||
|
$customer->attributes = ['name' => 'test']; |
||||||
|
$customer->save(); |
||||||
|
|
||||||
|
$customer = Customer::get(1); // get a record by pk |
||||||
|
$customers = Customer::get([1,2,3]); // get a records multiple by pk |
||||||
|
$customer = Customer::find()->where(['name' => 'test'])->one(); // find by query |
||||||
|
$customers = Customer::find()->active()->all(); // find all by query (using the `active` scope) |
||||||
|
|
||||||
|
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-field-query.html |
||||||
|
$result = Article::find()->query(["field" => ["title" => "yii"]])->all(); // articles whose title contains "yii" |
||||||
|
|
||||||
|
// http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-flt-query.html |
||||||
|
$query = Article::find()->query([ |
||||||
|
"fuzzy_like_this" => [ |
||||||
|
"fields" => ["title", "description"], |
||||||
|
"like_text" => "This query will return articles that are similar to this text :-)", |
||||||
|
"max_query_terms" : 12 |
||||||
|
] |
||||||
|
]); |
||||||
|
|
||||||
|
$query->all(); // gives you all the documents |
||||||
|
// you can add facets to your search: |
||||||
|
$query->addStatisticalFacet('click_stats', ['field' => 'visit_count']); |
||||||
|
$query->search(); // gives you all the records + stats about the visit_count field. e.g. mean, sum, min, max etc... |
||||||
|
``` |
||||||
|
|
||||||
|
And there is so much more in it. "it’s endless what you can build"[¹](http://www.elasticsearch.org/) |
@ -0,0 +1,28 @@ |
|||||||
|
{ |
||||||
|
"name": "yiisoft/yii2-elasticsearch", |
||||||
|
"description": "Elasticsearch integration and ActiveRecord for the Yii framework", |
||||||
|
"keywords": ["yii", "elasticsearch", "active-record", "search", "fulltext"], |
||||||
|
"type": "yii2-extension", |
||||||
|
"license": "BSD-3-Clause", |
||||||
|
"support": { |
||||||
|
"issues": "https://github.com/yiisoft/yii2/issues?labels=ext%3Aelasticsearch", |
||||||
|
"forum": "http://www.yiiframework.com/forum/", |
||||||
|
"wiki": "http://www.yiiframework.com/wiki/", |
||||||
|
"irc": "irc://irc.freenode.net/yii", |
||||||
|
"source": "https://github.com/yiisoft/yii2" |
||||||
|
}, |
||||||
|
"authors": [ |
||||||
|
{ |
||||||
|
"name": "Carsten Brandt", |
||||||
|
"email": "mail@cebe.cc" |
||||||
|
} |
||||||
|
], |
||||||
|
"require": { |
||||||
|
"yiisoft/yii2": "*", |
||||||
|
"ext-curl": "*" |
||||||
|
}, |
||||||
|
"autoload": { |
||||||
|
"psr-0": { "yii\\elasticsearch\\": "" } |
||||||
|
}, |
||||||
|
"target-dir": "yii/elasticsearch" |
||||||
|
} |
@ -0,0 +1,157 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yii\redis; |
||||||
|
|
||||||
|
use Yii; |
||||||
|
use yii\base\InvalidConfigException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Redis Session implements a session component using [redis](http://redis.io/) as the storage medium. |
||||||
|
* |
||||||
|
* Redis Session requires redis version 2.6.12 or higher to work properly. |
||||||
|
* |
||||||
|
* It needs to be configured with a redis [[Connection]] that is also configured as an application component. |
||||||
|
* By default it will use the `redis` application component. |
||||||
|
* |
||||||
|
* To use redis Session as the session application component, configure the application as follows, |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* [ |
||||||
|
* 'components' => [ |
||||||
|
* 'session' => [ |
||||||
|
* 'class' => 'yii\redis\Session', |
||||||
|
* 'redis' => [ |
||||||
|
* 'hostname' => 'localhost', |
||||||
|
* 'port' => 6379, |
||||||
|
* 'database' => 0, |
||||||
|
* ] |
||||||
|
* ], |
||||||
|
* ], |
||||||
|
* ] |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* Or if you have configured the redis [[Connection]] as an application component, the following is sufficient: |
||||||
|
* |
||||||
|
* ~~~ |
||||||
|
* [ |
||||||
|
* 'components' => [ |
||||||
|
* 'session' => [ |
||||||
|
* 'class' => 'yii\redis\Session', |
||||||
|
* // 'redis' => 'redis' // id of the connection application component |
||||||
|
* ], |
||||||
|
* ], |
||||||
|
* ] |
||||||
|
* ~~~ |
||||||
|
* |
||||||
|
* @property boolean $useCustomStorage Whether to use custom storage. This property is read-only. |
||||||
|
* |
||||||
|
* @author Carsten Brandt <mail@cebe.cc> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class Session extends \yii\web\Session |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @var Connection|string|array the Redis [[Connection]] object or the application component ID of the Redis [[Connection]]. |
||||||
|
* This can also be an array that is used to create a redis [[Connection]] instance in case you do not want do configure |
||||||
|
* redis connection as an application component. |
||||||
|
* After the Session object is created, if you want to change this property, you should only assign it |
||||||
|
* with a Redis [[Connection]] object. |
||||||
|
*/ |
||||||
|
public $redis = 'redis'; |
||||||
|
/** |
||||||
|
* @var string a string prefixed to every cache key so that it is unique. If not set, |
||||||
|
* it will use a prefix generated from [[Application::id]]. You may set this property to be an empty string |
||||||
|
* if you don't want to use key prefix. It is recommended that you explicitly set this property to some |
||||||
|
* static value if the cached data needs to be shared among multiple applications. |
||||||
|
* |
||||||
|
* To ensure interoperability, only alphanumeric characters should be used. |
||||||
|
*/ |
||||||
|
public $keyPrefix; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes the redis Session component. |
||||||
|
* This method will initialize the [[redis]] property to make sure it refers to a valid redis connection. |
||||||
|
* @throws InvalidConfigException if [[redis]] is invalid. |
||||||
|
*/ |
||||||
|
public function init() |
||||||
|
{ |
||||||
|
if (is_string($this->redis)) { |
||||||
|
$this->redis = Yii::$app->getComponent($this->redis); |
||||||
|
} else if (is_array($this->redis)) { |
||||||
|
if (!isset($this->redis['class'])) { |
||||||
|
$this->redis['class'] = Connection::className(); |
||||||
|
} |
||||||
|
$this->redis = Yii::createObject($this->redis); |
||||||
|
} |
||||||
|
if (!$this->redis instanceof Connection) { |
||||||
|
throw new InvalidConfigException("Session::redis must be either a Redis connection instance or the application component ID of a Redis connection."); |
||||||
|
} |
||||||
|
if ($this->keyPrefix === null) { |
||||||
|
$this->keyPrefix = substr(md5(Yii::$app->id), 0, 5); |
||||||
|
} elseif (!ctype_alnum($this->keyPrefix)) { |
||||||
|
throw new InvalidConfigException(get_class($this) . '::keyPrefix should only contain alphanumeric characters.'); |
||||||
|
} |
||||||
|
parent::init(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a value indicating whether to use custom session storage. |
||||||
|
* This method overrides the parent implementation and always returns true. |
||||||
|
* @return boolean whether to use custom storage. |
||||||
|
*/ |
||||||
|
public function getUseCustomStorage() |
||||||
|
{ |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Session read handler. |
||||||
|
* Do not call this method directly. |
||||||
|
* @param string $id session ID |
||||||
|
* @return string the session data |
||||||
|
*/ |
||||||
|
public function readSession($id) |
||||||
|
{ |
||||||
|
$data = $this->redis->executeCommand('GET', [$this->calculateKey($id)]); |
||||||
|
return $data === false ? '' : $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Session write handler. |
||||||
|
* Do not call this method directly. |
||||||
|
* @param string $id session ID |
||||||
|
* @param string $data session data |
||||||
|
* @return boolean whether session write is successful |
||||||
|
*/ |
||||||
|
public function writeSession($id, $data) |
||||||
|
{ |
||||||
|
return (bool) $this->redis->executeCommand('SET', [$this->calculateKey($id), $data, 'EX', $this->getTimeout()]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Session destroy handler. |
||||||
|
* Do not call this method directly. |
||||||
|
* @param string $id session ID |
||||||
|
* @return boolean whether session is destroyed successfully |
||||||
|
*/ |
||||||
|
public function destroySession($id) |
||||||
|
{ |
||||||
|
return (bool) $this->redis->executeCommand('DEL', [$this->calculateKey($id)]); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generates a unique key used for storing session data in cache. |
||||||
|
* @param string $id session variable name |
||||||
|
* @return string a safe cache key associated with the session variable name |
||||||
|
*/ |
||||||
|
protected function calculateKey($id) |
||||||
|
{ |
||||||
|
return $this->keyPrefix . md5(json_encode([__CLASS__, $id])); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* @link http://www.yiiframework.com/ |
||||||
|
* @copyright Copyright (c) 2008 Yii Software LLC |
||||||
|
* @license http://www.yiiframework.com/license/ |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace yiiunit\data\ar\elasticsearch; |
||||||
|
|
||||||
|
/** |
||||||
|
* ActiveRecord is ... |
||||||
|
* |
||||||
|
* @author Qiang Xue <qiang.xue@gmail.com> |
||||||
|
* @since 2.0 |
||||||
|
*/ |
||||||
|
class ActiveRecord extends \yii\elasticsearch\ActiveRecord |
||||||
|
{ |
||||||
|
public static $db; |
||||||
|
|
||||||
|
/** |
||||||
|
* @return \yii\elasticsearch\Connection |
||||||
|
*/ |
||||||
|
public static function getDb() |
||||||
|
{ |
||||||
|
return self::$db; |
||||||
|
} |
||||||
|
|
||||||
|
public static function index() |
||||||
|
{ |
||||||
|
return 'yiitest'; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
<?php |
||||||
|
namespace yiiunit\data\ar\elasticsearch; |
||||||
|
|
||||||
|
use yiiunit\extensions\elasticsearch\ActiveRecordTest; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class Customer |
||||||
|
* |
||||||
|
* @property integer $id |
||||||
|
* @property string $name |
||||||
|
* @property string $email |
||||||
|
* @property string $address |
||||||
|
* @property integer $status |
||||||
|
*/ |
||||||
|
class Customer extends ActiveRecord |
||||||
|
{ |
||||||
|
const STATUS_ACTIVE = 1; |
||||||
|
const STATUS_INACTIVE = 2; |
||||||
|
|
||||||
|
public $status2; |
||||||
|
|
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
return ['name', 'email', 'address', 'status']; |
||||||
|
} |
||||||
|
|
||||||
|
public function getOrders() |
||||||
|
{ |
||||||
|
return $this->hasMany(Order::className(), array('customer_id' => ActiveRecord::PRIMARY_KEY_NAME))->orderBy('create_time'); |
||||||
|
} |
||||||
|
|
||||||
|
public static function active($query) |
||||||
|
{ |
||||||
|
$query->andWhere(array('status' => 1)); |
||||||
|
} |
||||||
|
|
||||||
|
public function afterSave($insert) |
||||||
|
{ |
||||||
|
ActiveRecordTest::$afterSaveInsert = $insert; |
||||||
|
ActiveRecordTest::$afterSaveNewRecord = $this->isNewRecord; |
||||||
|
parent::afterSave($insert); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,18 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace yiiunit\data\ar\elasticsearch; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class Item |
||||||
|
* |
||||||
|
* @property integer $id |
||||||
|
* @property string $name |
||||||
|
* @property integer $category_id |
||||||
|
*/ |
||||||
|
class Item extends ActiveRecord |
||||||
|
{ |
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
return ['name', 'category_id']; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace yiiunit\data\ar\elasticsearch; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class Order |
||||||
|
* |
||||||
|
* @property integer $id |
||||||
|
* @property integer $customer_id |
||||||
|
* @property integer $create_time |
||||||
|
* @property string $total |
||||||
|
*/ |
||||||
|
class Order extends ActiveRecord |
||||||
|
{ |
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
return ['customer_id', 'create_time', 'total']; |
||||||
|
} |
||||||
|
|
||||||
|
public function getCustomer() |
||||||
|
{ |
||||||
|
return $this->hasOne(Customer::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'customer_id']); |
||||||
|
} |
||||||
|
|
||||||
|
public function getOrderItems() |
||||||
|
{ |
||||||
|
return $this->hasMany(OrderItem::className(), ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]); |
||||||
|
} |
||||||
|
|
||||||
|
public function getItems() |
||||||
|
{ |
||||||
|
return $this->hasMany(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) |
||||||
|
->via('orderItems')->orderBy('id'); |
||||||
|
} |
||||||
|
|
||||||
|
public function getItemsInOrder1() |
||||||
|
{ |
||||||
|
return $this->hasMany(Item::className(), ['id' => 'item_id']) |
||||||
|
->via('orderItems', function ($q) { |
||||||
|
$q->orderBy(['subtotal' => SORT_ASC]); |
||||||
|
})->orderBy('name'); |
||||||
|
} |
||||||
|
|
||||||
|
public function getItemsInOrder2() |
||||||
|
{ |
||||||
|
return $this->hasMany(Item::className(), ['id' => 'item_id']) |
||||||
|
->via('orderItems', function ($q) { |
||||||
|
$q->orderBy(['subtotal' => SORT_DESC]); |
||||||
|
})->orderBy('name'); |
||||||
|
} |
||||||
|
|
||||||
|
// public function getBooks() |
||||||
|
// { |
||||||
|
// return $this->hasMany('Item', [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']) |
||||||
|
// ->viaTable('tbl_order_item', ['order_id' => ActiveRecord::PRIMARY_KEY_NAME]) |
||||||
|
// ->where(['category_id' => 1]); |
||||||
|
// } |
||||||
|
|
||||||
|
public function beforeSave($insert) |
||||||
|
{ |
||||||
|
if (parent::beforeSave($insert)) { |
||||||
|
// $this->create_time = time(); |
||||||
|
return true; |
||||||
|
} else { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace yiiunit\data\ar\elasticsearch; |
||||||
|
|
||||||
|
/** |
||||||
|
* Class OrderItem |
||||||
|
* |
||||||
|
* @property integer $order_id |
||||||
|
* @property integer $item_id |
||||||
|
* @property integer $quantity |
||||||
|
* @property string $subtotal |
||||||
|
*/ |
||||||
|
class OrderItem extends ActiveRecord |
||||||
|
{ |
||||||
|
public function attributes() |
||||||
|
{ |
||||||
|
return ['order_id', 'item_id', 'quantity', 'subtotal']; |
||||||
|
} |
||||||
|
|
||||||
|
public function getOrder() |
||||||
|
{ |
||||||
|
return $this->hasOne(Order::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'order_id']); |
||||||
|
} |
||||||
|
|
||||||
|
public function getItem() |
||||||
|
{ |
||||||
|
return $this->hasOne(Item::className(), [ActiveRecord::PRIMARY_KEY_NAME => 'item_id']); |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue