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])); |
||||
} |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue