Commit 8ee9b528 authored by shajiaiming's avatar shajiaiming

锁仓model

parent 6e875f8b
<?php
namespace api\controllers;
use Yii;
use api\base\BaseController;
use common\models\psources\LockCreator;
class LockController extends BaseController
{
public function actionLockCreator()
{
$lock_creator = new LockCreator();
$lock_creator->primaryKey = 2;//primaryKey 定义 _id
$lock_creator->creator = 'ming';
$lock_creator->create = [
'means' => 'fix_amount',
'fix_amount' => [
'period' => 2400,
'amount' => 153500
],
'total_count' => 4600000,
'start_time' => 6430000,
'asset_symbol' => 'bty',
'asset_exec' => 'coins',
];
$lock_creator->unfreeze_id = 'the-id';
$lock_creator->block = [
'hash' => '',
'height' => 2,
'ts' => 6632560,
'index' => 2
];
$lock_creator->action_type = 1;
$lock_creator->beneficiary = 'yi';
$lock_creator->success = true;
var_dump($lock_creator->save());
exit;
}
public function actionSearchLockCreator()
{
$lock_creator = new LockCreator();
$query_arr = [
"bool" => [
"must" => [
#"match" => ['creator' => '']
['match' => ['creator' => 'fei']],
['match' => ['beneficiary' => 'xuan']],
#['match' => ['unfreeze_id' => "24"]],
]
],
];
$res = $lock_creator::find()->query($query_arr)->asArray()->all();
var_dump($res);
exit;
}
public function actionDeleteCreator()
{
$lock_creator = new LockCreator();
var_dump($lock_creator->deleteIndex());
exit;
}
public function actionLockBlock()
{
}
public function actionLockBeneficiary()
{
}
}
\ No newline at end of file
<?php
namespace app\models;
use yii\elasticsearch\ActiveRecord;
class LockBeneficiary extends ActiveRecord
{
// 索引名相当于库名
public static function index()
{
return 'wangjian';
}
// 类别名相当于表名
public static function type()
{
return 'test';
}
// 属性
public function attributes()
{
$mapConfig = self::mapConfig();
return array_keys($mapConfig['properties']);
}
/**
*[mapConfig mapping配置]
*返回这个模型的映射
*/
public static function mapConfig()
{
return [
'properties' => [
'beneficiary' => ['type' => 'string'],
'terminate' => [
'type' => 'nested',
'properties' => [
'amount_left' => ['type' => 'long'],
'amount_back' => ['type' => 'long']
]
],
'success' => ['type' => 'long'],
'block' => [
'type' => 'nested',
'properties' => [
'hash' => ['type' => 'string'],
'index' => ['type' => 'long'],
'height' => ['type' => 'long'],
'ts' => ['type' => 'long']
]
],
'unfreeze_id' => ['type' => 'long'],
'action_type' => ['type' => 'long'],
'creator' => ['type' => 'string']
]
];
}
public static function mapping()
{
return [
static::type() => self::mapConfig(),
];
}
/**
* 设置(更新)此模型的映射
*/
public static function updateMapping()
{
$db = self::getDb();
$command = $db->createCommand();
if (!$command->indexExists(self::index())) {
$command->createIndex(self::index());
}
$command->setMapping(self::index(), self::type(), self::mapping());
}
//获取此模型的映射
public static function getMapping()
{
$db = self::getDb();
$command = $db->createCommand();
return $command->getMapping();
}
}
\ No newline at end of file
<?php
namespace common\models\psources;
use yii\elasticsearch\ActiveRecord;
class LockBlock extends ActiveRecord
{
// 索引名相当于库名
public static function index()
{
return 'wangjian';
}
// 类别名相当于表名
public static function type()
{
return 'test';
}
// 属性
public function attributes()
{
$mapConfig = self::mapConfig();
return array_keys($mapConfig['properties']);
}
/**
*[mapConfig mapping配置]
*返回这个模型的映射
*/
public static function mapConfig()
{
return [
'properties' => [
'block' => [
'type' => 'nested',
'properties' => [
'index' => ['type' => 'long'],
'height' => ['type' => 'long'],
'ts' => ['type' => 'long'],
'hash' => ['type' => 'string']
]
],
'beneficiary' => ['type' => 'string'],
'unfreeze_id' => ['type' => 'long'],
'creator' => ['type' => 'string'],
'action_type' => ['type' => 'long'],
'success' => ['type' => 'long'],
'withdraw' => [
'type' => 'nested',
'properties' => [
'amount' => ['type' => 'long']
]
]
]
];
}
public static function mapping()
{
return [
static::type() => self::mapConfig(),
];
}
/**
* 设置(更新)此模型的映射
*/
public static function updateMapping()
{
$db = self::getDb();
$command = $db->createCommand();
if (!$command->indexExists(self::index())) {
$command->createIndex(self::index());
}
$command->setMapping(self::index(), self::type(), self::mapping());
}
//获取此模型的映射
public static function getMapping()
{
$db = self::getDb();
$command = $db->createCommand();
return $command->getMapping();
}
}
\ No newline at end of file
<?php
namespace common\models\psources;
use yii\elasticsearch\ActiveRecord;
class LockCreator extends ActiveRecord
{
// 索引名相当于库名
public static function index()
{
return 'suo_cang';
}
// 类别名相当于表名
public static function type()
{
return 'lock_creator';
}
// 属性
public function attributes()
{
$mapConfig = self::mapConfig();
return array_keys($mapConfig['properties']);
}
/**
*[mapConfig mapping配置]
*返回这个模型的映射
*/
public static function mapConfig()
{
return [
'properties' => [
'creator' => ['type' => 'string'],
'create' => [
'type' => 'nested',
'properties' => [
'means' => ['type' => 'long'],
'fix_amount' => [
'type' => 'nested',
'properties' => [
'period' => ['type' => 'long'],
'amount' => ['type' => 'long']
]
],
'total_count' => ['type' => 'long'],
'start_time' => ['type' => 'long'],
'asset_symbol' => ['type' => 'string'],
'asset_exec' => ['type' => 'string']
]
],
'unfreeze_id' => ['type' => 'string'],
'block' => [
'type' => 'nested',
'properties' => [
'hash' => ['type' => 'string'],
'height' => ['type' => 'long'],
'ts' => ['type' => 'long'],
'index' => ['type' => 'long']
]
],
'action_type' => ['type' => 'long'],
'beneficiary' => ['type' => 'string'],
'success' => ['type' => 'string']
]
];
}
public static function mapping()
{
return [
static::type() => self::mapConfig(),
];
}
/**
* 设置(更新)此模型的映射
*/
public static function updateMapping()
{
$db = self::getDb();
$command = $db->createCommand();
if (!$command->indexExists(self::index())) {
$command->createIndex(self::index());
}
$command->setMapping(self::index(), self::type(), self::mapping());
}
//获取此模型的映射
public static function getMapping()
{
$db = self::getDb();
$command = $db->createCommand();
return $command->getMapping();
}
/**
* Delete this model's index
*/
public static function deleteIndex()
{
$db = static::getDb();
$command = $db->createCommand();
$command->deleteIndex(static::index(), static::type());
}
public static function updateRecord($book_id, $columns)
{
try {
$record = self::get($book_id);
foreach ($columns as $key => $value) {
$record->$key = $value;
}
return $record->update();
} catch (\Exception $e) { //handle error here return false; } }
return false;
}
}
}
\ No newline at end of file
......@@ -20,6 +20,7 @@ return array(
'yii\\queue\\' => array($vendorDir . '/yiisoft/yii2-queue/src'),
'yii\\gii\\' => array($vendorDir . '/yiisoft/yii2-gii/src'),
'yii\\faker\\' => array($vendorDir . '/yiisoft/yii2-faker'),
'yii\\elasticsearch\\' => array($vendorDir . '/yiisoft/yii2-elasticsearch'),
'yii\\debug\\' => array($vendorDir . '/yiisoft/yii2-debug'),
'yii\\composer\\' => array($vendorDir . '/yiisoft/yii2-composer'),
'yii\\bootstrap\\' => array($vendorDir . '/yiisoft/yii2-bootstrap/src'),
......
......@@ -33,6 +33,7 @@ class ComposerStaticInit33057934f3e7eaaa1ce2d53797277936
'yii\\queue\\' => 10,
'yii\\gii\\' => 8,
'yii\\faker\\' => 10,
'yii\\elasticsearch\\' => 18,
'yii\\debug\\' => 10,
'yii\\composer\\' => 13,
'yii\\bootstrap\\' => 14,
......@@ -189,6 +190,10 @@ class ComposerStaticInit33057934f3e7eaaa1ce2d53797277936
array (
0 => __DIR__ . '/..' . '/yiisoft/yii2-faker',
),
'yii\\elasticsearch\\' =>
array (
0 => __DIR__ . '/..' . '/yiisoft/yii2-elasticsearch',
),
'yii\\debug\\' =>
array (
0 => __DIR__ . '/..' . '/yiisoft/yii2-debug',
......
......@@ -4816,6 +4816,57 @@
]
},
{
"name": "yiisoft/yii2-elasticsearch",
"version": "2.0.5",
"version_normalized": "2.0.5.0",
"source": {
"type": "git",
"url": "https://github.com/yiisoft/yii2-elasticsearch.git",
"reference": "82d66d17543040dda3c64f299ae251658156c2c1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/yiisoft/yii2-elasticsearch/zipball/82d66d17543040dda3c64f299ae251658156c2c1",
"reference": "82d66d17543040dda3c64f299ae251658156c2c1",
"shasum": ""
},
"require": {
"ext-curl": "*",
"yiisoft/yii2": "~2.0.14"
},
"time": "2018-03-20T11:34:58+00:00",
"type": "yii2-extension",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"yii\\elasticsearch\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc"
}
],
"description": "Elasticsearch integration and ActiveRecord for the Yii framework",
"keywords": [
"active-record",
"elasticsearch",
"fulltext",
"search",
"yii2"
]
},
{
"name": "yiisoft/yii2-faker",
"version": "2.0.4",
"version_normalized": "2.0.4.0",
......
......@@ -300,4 +300,13 @@ return array (
'@linslin/yii2/curl' => $vendorDir . '/linslin/yii2-curl',
),
),
'yiisoft/yii2-elasticsearch' =>
array (
'name' => 'yiisoft/yii2-elasticsearch',
'version' => '2.0.5.0',
'alias' =>
array (
'@yii/elasticsearch' => $vendorDir . '/yiisoft/yii2-elasticsearch',
),
),
);
<?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\db\ActiveQueryInterface;
/**
* ActiveDataProvider is an enhanced version of [[\yii\data\ActiveDataProvider]] specific to the ElasticSearch.
* It allows to fetch not only rows and total rows count, but full query results including aggregations and so on.
*
* Note: this data provider fetches result models and total count using single ElasticSearch query, so results total
* count will be fetched after pagination limit applying, which eliminates ability to verify if requested page number
* actually exist. Data provider disables [[yii\data\Pagination::validatePage]] automatically because of this.
*
* @property array $aggregations All aggregations results. This property is read-only.
* @property array $queryResults Full query results.
*
* @author Paul Klimov <klimov.paul@gmail.com>
* @since 2.0.5
*/
class ActiveDataProvider extends \yii\data\ActiveDataProvider
{
/**
* @var array the full query results.
*/
private $_queryResults;
/**
* @param array $results full query results
*/
public function setQueryResults($results)
{
$this->_queryResults = $results;
}
/**
* @return array full query results
*/
public function getQueryResults()
{
if (!is_array($this->_queryResults)) {
$this->prepare();
}
return $this->_queryResults;
}
/**
* @return array all aggregations results
*/
public function getAggregations()
{
$results = $this->getQueryResults();
return isset($results['aggregations']) ? $results['aggregations'] : [];
}
/**
* Returns results of the specified aggregation.
* @param string $name aggregation name.
* @return array aggregation results.
* @throws InvalidCallException if requested aggregation does not present in query results.
*/
public function getAggregation($name)
{
$aggregations = $this->getAggregations();
if (!isset($aggregations[$name])) {
throw new InvalidCallException("Aggregation '{$name}' does not present.");
}
return $aggregations[$name];
}
/**
* @inheritdoc
*/
protected function prepareModels()
{
if (!$this->query instanceof Query) {
throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
}
$query = clone $this->query;
if (($pagination = $this->getPagination()) !== false) {
// pagination fails to validate page number, because total count is unknown at this stage
$pagination->validatePage = false;
$query->limit($pagination->getLimit())->offset($pagination->getOffset());
}
if (($sort = $this->getSort()) !== false) {
$query->addOrderBy($sort->getOrders());
}
if (is_array(($results = $query->search($this->db)))) {
$this->setQueryResults($results);
if ($pagination !== false) {
$pagination->totalCount = $this->getTotalCount();
}
return $results['hits']['hits'];
}
$this->setQueryResults([]);
return [];
}
/**
* @inheritdoc
*/
protected function prepareTotalCount()
{
if (!$this->query instanceof Query) {
throw new InvalidConfigException('The "query" property must be an instance "' . Query::className() . '" or its subclasses.');
}
$results = $this->getQueryResults();
return isset($results['hits']['total']) ? (int)$results['hits']['total'] : 0;
}
/**
* @inheritdoc
*/
protected function prepareKeys($models)
{
$keys = [];
if ($this->key !== null) {
foreach ($models as $model) {
if (is_string($this->key)) {
$keys[] = $model[$this->key];
} else {
$keys[] = call_user_func($this->key, $model);
}
}
return $keys;
} elseif ($this->query instanceof ActiveQueryInterface) {
/* @var $class \yii\db\ActiveRecord */
$class = $this->query->modelClass;
$pks = $class::primaryKey();
if (count($pks) === 1) {
foreach ($models as $model) {
$keys[] = $model->primaryKey;
}
} else {
foreach ($models as $model) {
$kk = [];
foreach ($pks as $pk) {
$kk[$pk] = $model[$pk];
}
$keys[] = $kk;
}
}
return $keys;
} else {
return array_keys($models);
}
}
/**
* @inheritdoc
*/
public function refresh()
{
parent::refresh();
$this->_queryResults = null;
}
}
<?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\InvalidConfigException;
use yii\test\BaseActiveFixture;
/**
* ActiveFixture represents a fixture for testing backed up by an [[modelClass|ActiveRecord class]] or an elastic search index.
*
* Either [[modelClass]] or [[index]] and [[type]] must be set. You should also provide fixture data in the file
* specified by [[dataFile]] or overriding [[getData()]] if you want to use code to generate the fixture data.
*
* When the fixture is being loaded, it will first call [[resetIndex()]] to remove any existing data in the index for the [[type]].
* It will then populate the index with the data returned by [[getData()]].
*
* After the fixture is loaded, you can access the loaded data via the [[data]] property. If you set [[modelClass]],
* you will also be able to retrieve an instance of [[modelClass]] with the populated data via [[getModel()]].
*
* @author Carsten Brandt <mail@cebe.cc>
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0.2
*/
class ActiveFixture extends BaseActiveFixture
{
/**
* @var Connection|string the DB connection object or the application component ID of the DB connection.
* After the DbFixture object is created, if you want to change this property, you should only assign it
* with a DB connection object.
*/
public $db = 'elasticsearch';
/**
* @var string the name of the index that this fixture is about. If this property is not set,
* the name will be determined via [[modelClass]].
* @see modelClass
*/
public $index;
/**
* @var string the name of the type that this fixture is about. If this property is not set,
* the name will be determined via [[modelClass]].
* @see modelClass
*/
public $type;
/**
* @var string|boolean the file path or path alias of the data file that contains the fixture data
* to be returned by [[getData()]]. If this is not set, it will default to `FixturePath/data/Index/Type.php`,
* where `FixturePath` stands for the directory containing this fixture class, `Index` stands for the elasticsearch [[index]] name
* and `Type` stands for the [[type]] associated with this fixture.
* You can set this property to be false to prevent loading any data.
*/
public $dataFile;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
if (!isset($this->modelClass) && (!isset($this->index) || !isset($this->type))) {
throw new InvalidConfigException('Either "modelClass" or "index" and "type" must be set.');
}
/* @var $modelClass ActiveRecord */
$modelClass = $this->modelClass;
if ($this->index === null) {
$this->index = $modelClass::index();
}
if ($this->type === null) {
$this->type = $modelClass::type();
}
}
/**
* Loads the fixture.
*
* The default implementation will first clean up the index by calling [[resetIndex()]].
* It will then populate the index with the data returned by [[getData()]].
*
* If you override this method, you should consider calling the parent implementation
* so that the data returned by [[getData()]] can be populated into the index.
*/
public function load()
{
$this->resetIndex();
$this->data = [];
$mapping = $this->db->createCommand()->getMapping($this->index, $this->type);
if (isset($mapping[$this->index]['mappings'][$this->type]['_id']['path'])) {
$idField = $mapping[$this->index]['mappings'][$this->type]['_id']['path'];
} else {
$idField = '_id';
}
foreach ($this->getData() as $alias => $row) {
$options = [];
$id = isset($row[$idField]) ? $row[$idField] : null;
if ($idField === '_id') {
unset($row[$idField]);
}
if (isset($row['_parent'])) {
$options['parent'] = $row['_parent'];
unset($row['_parent']);
}
try {
$response = $this->db->createCommand()->insert($this->index, $this->type, $row, $id, $options);
} catch(\yii\db\Exception $e) {
throw new \yii\base\Exception("Failed to insert fixture data \"$alias\": " . $e->getMessage() . "\n" . print_r($e->errorInfo, true), $e->getCode(), $e);
}
if ($id === null) {
$row[$idField] = $response['_id'];
}
$this->data[$alias] = $row;
}
// ensure all data is flushed and immediately available in the test
$this->db->createCommand()->flushIndex($this->index);
}
/**
* Returns the fixture data.
*
* The default implementation will try to return the fixture data by including the external file specified by [[dataFile]].
* The file should return an array of data rows (column name => column value), each corresponding to a row in the index.
*
* If the data file does not exist, an empty array will be returned.
*
* @return array the data rows to be inserted into the database index.
*/
protected function getData()
{
if ($this->dataFile === null) {
$class = new \ReflectionClass($this);
$dataFile = dirname($class->getFileName()) . "/data/{$this->index}/{$this->type}.php";
return is_file($dataFile) ? require($dataFile) : [];
} else {
return parent::getData();
}
}
/**
* Removes all existing data from the specified index and type.
* This method is called before populating fixture data into the index associated with this fixture.
*/
protected function resetIndex()
{
$this->db->createCommand([
'index' => $this->index,
'type' => $this->type,
'queryParts' => ['query' => ['match_all' => new \stdClass()]],
])->deleteByQuery();
}
}
<?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;
use yii\db\ActiveRelationTrait;
/**
* ActiveQuery represents a [[Query]] associated with an [[ActiveRecord]] class.
*
* An ActiveQuery can be a normal query or be used in a relational context.
*
* ActiveQuery instances are usually created by [[ActiveRecord::find()]].
* Relational queries are created by [[ActiveRecord::hasOne()]] and [[ActiveRecord::hasMany()]].
*
* Normal Query
* ------------
*
* 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:
*
* ```php
* $customers = Customer::find()->with('orders')->asArray()->all();
* ```
* > NOTE: elasticsearch limits the number of records returned to 10 records by default.
* > If you expect to get more records you should specify limit explicitly.
*
* Relational query
* ----------------
*
* In relational context ActiveQuery represents a relation between two Active Record classes.
*
* Relational ActiveQuery 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 ActiveQuery 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 junction table, it may be specified by [[via()]].
* This methods may only be called in a relational context. Same is true for [[inverseOf()]], which
* marks a relation as inverse of another relation.
*
* > Note: elasticsearch limits the number of records returned by any query to 10 records by default.
* > If you expect to get more records you should specify limit explicitly in relation definition.
* > This is also important for relations that use [[via()]] so that if via records are limited to 10
* > the relations records can also not be more than 10.
*
* > Note: Currently [[with]] is not supported in combination with [[asArray]].
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ActiveQuery extends Query implements ActiveQueryInterface
{
use ActiveQueryTrait;
use ActiveRelationTrait;
/**
* @event Event an event that is triggered when the query is initialized via [[init()]].
*/
const EVENT_INIT = 'init';
/**
* Constructor.
* @param array $modelClass the model class associated with this query
* @param array $config configurations to be applied to the newly created query object
*/
public function __construct($modelClass, $config = [])
{
$this->modelClass = $modelClass;
parent::__construct($config);
}
/**
* Initializes the object.
* This method is called at the end of the constructor. The default implementation will trigger
* an [[EVENT_INIT]] event. If you override this method, make sure you call the parent implementation at the end
* to ensure triggering of the event.
*/
public function init()
{
parent::init();
$this->trigger(self::EVENT_INIT);
}
/**
* 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 $viaQuery ActiveQuery */
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]);
}
}
/* @var $modelClass ActiveRecord */
$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)
{
return parent::all($db);
}
/**
* Converts found rows into model instances
* @param array $rows
* @return array|ActiveRecord[]
* @since 2.0.4
*/
private function createModels($rows)
{
$models = [];
if ($this->asArray) {
if ($this->indexBy === null) {
return $rows;
}
foreach ($rows as $row) {
if (is_string($this->indexBy)) {
$key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $row['_source'][$this->indexBy];
} else {
$key = call_user_func($this->indexBy, $row);
}
$models[$key] = $row;
}
} else {
/* @var $class ActiveRecord */
$class = $this->modelClass;
if ($this->indexBy === null) {
foreach ($rows as $row) {
$model = $class::instantiate($row);
$modelClass = get_class($model);
$modelClass::populateRecord($model, $row);
$models[] = $model;
}
} else {
foreach ($rows as $row) {
$model = $class::instantiate($row);
$modelClass = get_class($model);
$modelClass::populateRecord($model, $row);
if (is_string($this->indexBy)) {
$key = $model->{$this->indexBy};
} else {
$key = call_user_func($this->indexBy, $model);
}
$models[$key] = $model;
}
}
}
return $models;
}
/**
* @inheritdoc
* @since 2.0.4
*/
public function populate($rows)
{
if (empty($rows)) {
return [];
}
$models = $this->createModels($rows);
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
if (!$this->asArray) {
foreach ($models as $model) {
$model->afterFind();
}
}
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) {
// TODO implement with()
// /* @var $modelClass ActiveRecord */
// $modelClass = $this->modelClass;
// $model = $result['_source'];
// $pk = $modelClass::primaryKey()[0];
// if ($pk === '_id') {
// $model['_id'] = $result['_id'];
// }
// $model['_score'] = $result['_score'];
// if (!empty($this->with)) {
// $models = [$model];
// $this->findWith($this->with, $models);
// $model = $models[0];
// }
return $result;
} else {
/* @var $class ActiveRecord */
$class = $this->modelClass;
$model = $class::instantiate($result);
$class = get_class($model);
$class::populateRecord($model, $result);
if (!empty($this->with)) {
$models = [$model];
$this->findWith($this->with, $models);
$model = $models[0];
}
$model->afterFind();
return $model;
}
}
/**
* @inheritdoc
*/
public function search($db = null, $options = [])
{
$result = $this->createCommand($db)->search($options);
// TODO implement with() for asArray
if (!empty($result['hits']['hits']) && !$this->asArray) {
$models = $this->createModels($result['hits']['hits']);
if (!empty($this->with)) {
$this->findWith($this->with, $models);
}
foreach ($models as $model) {
$model->afterFind();
}
$result['hits']['hits'] = $models;
}
return $result;
}
/**
* @inheritdoc
*/
public function column($field, $db = null)
{
if ($field == '_id') {
$command = $this->createCommand($db);
$command->queryParts['fields'] = [];
$command->queryParts['_source'] = false;
$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);
}
}
<?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\InvalidArgumentException;
use yii\base\InvalidCallException;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\db\BaseActiveRecord;
use yii\db\StaleObjectException;
use yii\helpers\ArrayHelper;
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](https://www.elastic.co/products/elasticsearch).
*
* 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.
*
* @property array|null $highlight A list of arrays with highlighted excerpts indexed by field names. This
* property is read-only.
* @property float $score Returns the score of this record when it was retrieved via a [[find()]] query. This
* property is read-only.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class ActiveRecord extends BaseActiveRecord
{
private $_id;
private $_score;
private $_version;
private $_highlight;
private $_explanation;
/**
* 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->get('elasticsearch');
}
/**
* @inheritdoc
* @return ActiveQuery the newly created [[ActiveQuery]] instance.
*/
public static function find()
{
return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
}
/**
* @inheritdoc
*/
public static function findOne($condition)
{
if (!is_array($condition)) {
return static::get($condition);
}
if (!ArrayHelper::isAssociative($condition)) {
$records = static::mget(array_values($condition));
return empty($records) ? null : reset($records);
}
$condition = static::filterCondition($condition);
return static::find()->andWhere($condition)->one();
}
/**
* @inheritdoc
*/
public static function findAll($condition)
{
if (!ArrayHelper::isAssociative($condition)) {
return static::mget(is_array($condition) ? array_values($condition) : [$condition]);
}
$condition = static::filterCondition($condition);
return static::find()->andWhere($condition)->all();
}
/**
* Filter out condition parts that are array valued, to prevent building arbitrary conditions.
* @param array $condition
*/
private static function filterCondition($condition)
{
foreach($condition as $k => $v) {
if (is_array($v)) {
$condition[$k] = array_values($v);
foreach($v as $vv) {
if (is_array($vv)) {
throw new InvalidArgumentException('Nested arrays are not allowed in condition for findAll() and findOne().');
}
}
}
}
return $condition;
}
/**
* 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.elastic.co/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['found']) {
$model = static::instantiate($result);
static::populateRecord($model, $result);
$model->afterFind();
return $model;
}
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.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html)
* for more details on these options.
* @return array The record instances, or empty array if nothing was found
*/
public static function mget(array $primaryKeys, $options = [])
{
if (empty($primaryKeys)) {
return [];
}
if (count($primaryKeys) === 1) {
$model = static::get(reset($primaryKeys));
return $model === null ? [] : [$model];
}
$command = static::getDb()->createCommand();
$result = $command->mget(static::index(), static::type(), $primaryKeys, $options);
$models = [];
foreach ($result['docs'] as $doc) {
if ($doc['found']) {
$model = static::instantiate($doc);
static::populateRecord($model, $doc);
$model->afterFind();
$models[] = $model;
}
}
return $models;
}
// TODO add more like this feature http://www.elastic.co/guide/en/elasticsearch/reference/current/search-more-like-this.html
// TODO add percolate functionality http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html
// TODO implement copy and move as pk change is not possible
/**
* @return float returns the score of this record when it was retrieved via a [[find()]] query.
*/
public function getScore()
{
return $this->_score;
}
/**
* @return array|null A list of arrays with highlighted excerpts indexed by field names.
*/
public function getHighlight()
{
return $this->_highlight;
}
/**
* @return array|null An explanation for each hit on how its score was computed.
* @since 2.0.5
*/
public function getExplanation()
{
return $this->_explanation;
}
/**
* Sets the primary key
* @param mixed $value
* @throws \yii\base\InvalidCallException when record is not new
*/
public function setPrimaryKey($value)
{
$pk = static::primaryKey()[0];
if ($this->getIsNewRecord() || $pk != '_id') {
$this->$pk = $value;
} else {
throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.');
}
}
/**
* @inheritdoc
*/
public function getPrimaryKey($asArray = false)
{
$pk = static::primaryKey()[0];
if ($asArray) {
return [$pk => $this->$pk];
} else {
return $this->$pk;
}
}
/**
* @inheritdoc
*/
public function getOldPrimaryKey($asArray = false)
{
$pk = static::primaryKey()[0];
if ($this->getIsNewRecord()) {
$id = null;
} elseif ($pk == '_id') {
$id = $this->_id;
} else {
$id = $this->getOldAttribute($pk);
}
if ($asArray) {
return [$pk => $id];
} else {
return $id;
}
}
/**
* This method defines the attribute that uniquely identifies a record.
*
* The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the
* ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
*
* You may override this method to define the primary key name when you have defined
* [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html)
* for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]].
*
* Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature
* of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
* single string.
*
* @return string[] array of primary key attributes. Only the first element of the array will be used.
*/
public static function primaryKey()
{
return ['_id'];
}
/**
* Returns the list of all attribute names of the model.
*
* This method must be overridden by child classes to define available attributes.
*
* Attributes are names of fields of the corresponding elasticsearch document.
* The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes.
* You may define [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html)
* for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes.
*
* @return string[] list of attribute names.
* @throws \yii\base\InvalidConfigException if not overridden in a child class.
*/
public function attributes()
{
throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.');
}
/**
* A list of attributes that should be treated as array valued when retrieved through [[ActiveQuery::fields]].
*
* If not listed by this method, attributes retrieved through [[ActiveQuery::fields]] will converted to a scalar value
* when the result array contains only one value.
*
* @return string[] list of attribute names. Must be a subset of [[attributes()]].
*/
public function arrayAttributes()
{
return [];
}
/**
* @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()), '-');
}
/**
* @inheritdoc
*
* @param ActiveRecord $record the record to be populated. In most cases this will be an instance
* created by [[instantiate()]] beforehand.
* @param array $row attribute values (name => value)
*/
public static function populateRecord($record, $row)
{
$attributes = [];
if (isset($row['_source'])) {
$attributes = $row['_source'];
}
if (isset($row['fields'])) {
// reset fields in case it is scalar value
$arrayAttributes = $record->arrayAttributes();
foreach($row['fields'] as $key => $value) {
if (!isset($arrayAttributes[$key]) && count($value) == 1) {
$row['fields'][$key] = reset($value);
}
}
$attributes = array_merge($attributes, $row['fields']);
}
parent::populateRecord($record, $attributes);
$pk = static::primaryKey()[0];//TODO should always set ID in case of fields are not returned
if ($pk === '_id') {
$record->_id = $row['_id'];
}
$record->_highlight = isset($row['highlight']) ? $row['highlight'] : null;
$record->_score = isset($row['_score']) ? $row['_score'] : null;
$record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available...
$record->_explanation = isset($row['_explanation']) ? $row['_explanation'] : null;
}
/**
* Creates an active record instance.
*
* This method is called together with [[populateRecord()]] by [[ActiveQuery]].
* It is not meant to be used for creating new records directly.
*
* You may override this method if the instance being created
* depends on the row data to be populated into the record.
* For example, by creating a record based on the value of a column,
* you may implement the so-called single-table inheritance mapping.
* @param array $row row data to be populated into the record.
* This array consists of the following keys:
* - `_source`: refers to the attributes of the record.
* - `_type`: the type this record is stored in.
* - `_index`: the index this record is stored in.
* @return static the newly created active record
*/
public static function instantiate($row)
{
return new static;
}
/**
* 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.elastic.co/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 bool $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.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html)
* for more details on these options.
*
* By default the `op_type` is set to `create`.
* @return bool 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)) {
return false;
}
$values = $this->getDirtyAttributes($attributes);
$response = static::getDb()->createCommand()->insert(
static::index(),
static::type(),
$values,
$this->getPrimaryKey(),
$options
);
$pk = static::primaryKey()[0];
$this->$pk = $response['_id'];
if ($pk != '_id') {
$values[$pk] = $response['_id'];
}
$this->_version = $response['_version'];
$this->_score = null;
$changedAttributes = array_fill_keys(array_keys($values), null);
$this->setOldAttributes($values);
$this->afterSave(true, $changedAttributes);
return true;
}
/**
* @inheritdoc
*
* @param bool $runValidation whether to perform validation before saving the record.
* If the validation fails, the record will not be inserted into the database.
* @param array $attributeNames list of attribute names that need to be saved. Defaults to null,
* meaning all attributes that are loaded from DB 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
* - `timeout` timeout waiting for a shard to become available.
* - `replication` the replication type for the delete/index operation (sync or async).
* - `consistency` the write consistency of the index/delete operation.
* - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
* - `detect_noop` this parameter will become part of the request body and will prevent the index from getting updated when nothing has changed.
*
* Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html#_parameters_3)
* for more details on these options.
*
* The following parameters are Yii specific:
*
* - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
* has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
* See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html) for details.
*
* Make sure the record has been fetched with a [[version]] before. This is only the case
* for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
*
* @return int|bool the number of rows affected, or false if validation fails
* or [[beforeSave()]] stops the updating process.
* @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
* @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
* @throws Exception in case update failed.
*/
public function update($runValidation = true, $attributeNames = null, $options = [])
{
if ($runValidation && !$this->validate($attributeNames)) {
return false;
}
return $this->updateInternal($attributeNames, $options);
}
/**
* @see update()
* @param array $attributes attributes to update
* @param array $options options given in this parameter are passed to elasticsearch
* as request URI parameters. See [[update()]] for details.
* @return int|false the number of rows affected, or false if [[beforeSave()]] stops the updating process.
* @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated.
* @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled.
* @throws Exception in case update failed.
*/
protected function updateInternal($attributes = null, $options = [])
{
if (!$this->beforeSave(false)) {
return false;
}
$values = $this->getDirtyAttributes($attributes);
if (empty($values)) {
$this->afterSave(false, $values);
return 0;
}
if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
if ($this->_version === null) {
throw new InvalidParamException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::update() for details.');
}
$options['version'] = $this->_version;
unset($options['optimistic_locking']);
}
try {
$result = static::getDb()->createCommand()->update(
static::index(),
static::type(),
$this->getOldPrimaryKey(false),
$values,
$options
);
} catch(Exception $e) {
// HTTP 409 is the response in case of failed optimistic locking
// http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
throw new StaleObjectException('The object being updated is outdated.', $e->errorInfo, $e->getCode(), $e);
}
throw $e;
}
if (is_array($result) && isset($result['_version'])) {
$this->_version = $result['_version'];
}
$changedAttributes = [];
foreach ($values as $name => $value) {
$changedAttributes[$name] = $this->getOldAttribute($name);
$this->setOldAttribute($name, $value);
}
$this->afterSave(false, $changedAttributes);
if ($result === false) {
return 0;
} else {
return 1;
}
}
/**
* Performs a quick and highly efficient scroll/scan query to get the list of primary keys that
* satisfy the given condition. If condition is a list of primary keys
* (e.g.: `['_id' => ['1', '2', '3']]`), the query is not performed for performance considerations.
* @param array $condition please refer to [[ActiveQuery::where()]] on how to specify this parameter
* @return array primary keys that correspond to given conditions
* @see updateAll()
* @see updateAllCounters()
* @see deleteAll()
* @since 2.0.4
*/
protected static function primaryKeysByCondition($condition)
{
$pkName = static::primaryKey()[0];
if (count($condition) == 1 && isset($condition[$pkName])) {
$primaryKeys = (array)$condition[$pkName];
} else {
//fetch only document metadata (no fields), 1000 documents per shard
$query = static::find()->where($condition)->asArray()->source(false)->limit(1000);
$primaryKeys = [];
foreach ($query->each('1m') as $document) {
$primaryKeys[] = $document['_id'];
}
}
return $primaryKeys;
}
/**
* 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(['status' => 1], ['status' => 2]);
* ~~~
*
* @param array $attributes attribute values (name-value pairs) to be saved into the table
* @param array $condition the conditions that will be passed to the `where()` method when building the query.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
* @see [[ActiveRecord::primaryKeysByCondition()]]
* @return int the number of rows updated
* @throws Exception on error.
*/
public static function updateAll($attributes, $condition = [])
{
$primaryKeys = static::primaryKeysByCondition($condition);
if (empty($primaryKeys)) {
return 0;
}
$bulkCommand = static::getDb()->createBulkCommand([
"index" => static::index(),
"type" => static::type(),
]);
foreach ($primaryKeys as $pk) {
$bulkCommand->addAction(["update" => ["_id" => $pk]], ["doc" => $attributes]);
}
$response = $bulkCommand->execute();
$n = 0;
$errors = [];
foreach ($response['items'] as $item) {
if (isset($item['update']['status']) && $item['update']['status'] == 200) {
$n++;
} else {
$errors[] = $item['update'];
}
}
if (!empty($errors) || isset($response['errors']) && $response['errors']) {
throw new Exception(__METHOD__ . ' failed updating records.', $errors);
}
return $n;
}
/**
* Updates all matching records using the provided counter changes and conditions.
* For example, to add 1 to age of all customers whose status is 2,
*
* ~~~
* Customer::updateAllCounters(['age' => 1], ['status' => 2]);
* ~~~
*
* @param array $counters the counters to be updated (attribute name => increment value).
* Use negative values if you want to decrement the counters.
* @param array $condition the conditions that will be passed to the `where()` method when building the query.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
* @see [[ActiveRecord::primaryKeysByCondition()]]
* @return int the number of rows updated
* @throws Exception on error.
*/
public static function updateAllCounters($counters, $condition = [])
{
$primaryKeys = static::primaryKeysByCondition($condition);
if (empty($primaryKeys) || empty($counters)) {
return 0;
}
$bulkCommand = static::getDb()->createBulkCommand([
"index" => static::index(),
"type" => static::type(),
]);
foreach ($primaryKeys as $pk) {
$script = '';
foreach ($counters as $counter => $value) {
$script .= "ctx._source.{$counter} += {$counter};\n";
}
$bulkCommand->addAction(["update" => ["_id" => $pk]], ["script" => $script, "params" => $counters, "lang" => "groovy"]);
}
$response = $bulkCommand->execute();
$n = 0;
$errors = [];
foreach ($response['items'] as $item) {
if (isset($item['update']['status']) && $item['update']['status'] == 200) {
$n++;
} else {
$errors[] = $item['update'];
}
}
if (!empty($errors) || isset($response['errors']) && $response['errors']) {
throw new Exception(__METHOD__ . ' failed updating records counters.', $errors);
}
return $n;
}
/**
* @inheritdoc
*
* @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
* - `timeout` timeout waiting for a shard to become available.
* - `replication` the replication type for the delete/index operation (sync or async).
* - `consistency` the write consistency of the index/delete operation.
* - `refresh` refresh the relevant primary and replica shards (not the whole index) immediately after the operation occurs, so that the updated document appears in search results immediately.
*
* Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html)
* for more details on these options.
*
* The following parameters are Yii specific:
*
* - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it
* has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]].
* See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html#delete-versioning) for details.
*
* Make sure the record has been fetched with a [[version]] before. This is only the case
* for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly.
*
* @return int|bool the number of rows deleted, or false if the deletion is unsuccessful for some reason.
* Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
* @throws StaleObjectException if optimistic locking is enabled and the data being deleted is outdated.
* @throws Exception in case delete failed.
*/
public function delete($options = [])
{
if (!$this->beforeDelete()) {
return false;
}
if (isset($options['optimistic_locking']) && $options['optimistic_locking']) {
if ($this->_version === null) {
throw new InvalidParamException('Unable to use optimistic locking on a record that has no version set. Refer to the docs of ActiveRecord::delete() for details.');
}
$options['version'] = $this->_version;
unset($options['optimistic_locking']);
}
try {
$result = static::getDb()->createCommand()->delete(
static::index(),
static::type(),
$this->getOldPrimaryKey(false),
$options
);
} catch(Exception $e) {
// HTTP 409 is the response in case of failed optimistic locking
// http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html
if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) {
throw new StaleObjectException('The object being deleted is outdated.', $e->errorInfo, $e->getCode(), $e);
}
throw $e;
}
$this->setOldAttributes(null);
$this->afterDelete();
if ($result === false) {
return 0;
} else {
return 1;
}
}
/**
* 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 passed to the `where()` method when building the query.
* Please refer to [[ActiveQuery::where()]] on how to specify this parameter.
* @see [[ActiveRecord::primaryKeysByCondition()]]
* @return int the number of rows deleted
* @throws Exception on error.
*/
public static function deleteAll($condition = [])
{
$primaryKeys = static::primaryKeysByCondition($condition);
if (empty($primaryKeys)) {
return 0;
}
$bulkCommand = static::getDb()->createBulkCommand([
"index" => static::index(),
"type" => static::type(),
]);
foreach ($primaryKeys as $pk) {
$bulkCommand->addDeleteAction($pk);
}
$response = $bulkCommand->execute();
$n = 0;
$errors = [];
foreach ($response['items'] as $item) {
if (isset($item['delete']['status']) && $item['delete']['status'] == 200) {
if (isset($item['delete']['found']) && $item['delete']['found']) {
$n++;
}
} else {
$errors[] = $item['delete'];
}
}
if (!empty($errors) || isset($response['errors']) && $response['errors']) {
throw new Exception(__METHOD__ . ' failed deleting records.', $errors);
}
return $n;
}
/**
* This method has no effect in Elasticsearch ActiveRecord.
*
* Elasticsearch ActiveRecord uses [native Optimistic locking](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html).
* See [[update()]] for more details.
*/
public function optimisticLock()
{
return null;
}
/**
* Destroys the relationship in current model.
*
* This method is not supported by elasticsearch.
*/
public function unlinkAll($name, $delete = false)
{
throw new NotSupportedException('unlinkAll() is not supported by elasticsearch, use unlink() instead.');
}
}
<?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\BaseObject;
/**
* BatchQueryResult represents a batch query from which you can retrieve data in batches.
*
* You usually do not instantiate BatchQueryResult directly. Instead, you obtain it by
* calling [[Query::batch()]] or [[Query::each()]]. Because BatchQueryResult implements the [[\Iterator]] interface,
* you can iterate it to obtain a batch of data in each iteration.
*
* Batch size is determined by the [[Query::$limit]] setting. [[Query::$offset]] setting is ignored.
* New batches will be obtained until the server runs out of results.
*
* If [[Query::$orderBy]] parameter is not set, batches will be processed using the highly efficient "scan" mode.
* In this case, [[Query::$limit]] setting determines batch size per shard.
* See [elasticsearch guide](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html)
* for more information.
*
* Example:
* ```php
* $query = (new Query)->from('user');
* foreach ($query->batch() as $i => $users) {
* // $users represents the rows in the $i-th batch
* }
* foreach ($query->each() as $user) {
* }
* ```
*
* @author Konstantin Sirotkin <beowulfenator@gmail.com>
* @since 2.0.4
*/
class BatchQueryResult extends BaseObject implements \Iterator
{
/**
* @var Connection the DB connection to be used when performing batch query.
* If null, the `elasticsearch` application component will be used.
*/
public $db;
/**
* @var Query the query object associated with this batch query.
* Do not modify this property directly unless after [[reset()]] is called explicitly.
*/
public $query;
/**
* @var boolean whether to return a single row during each iteration.
* If false, a whole batch of rows will be returned in each iteration.
*/
public $each = false;
/**
* @var DataReader the data reader associated with this batch query.
*/
private $_dataReader;
/**
* @var array the data retrieved in the current batch
*/
private $_batch;
/**
* @var mixed the value for the current iteration
*/
private $_value;
/**
* @var string|integer the key for the current iteration
*/
private $_key;
/**
* @var string the amount of time to keep the scroll window open
* (in ElasticSearch [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units).
*/
public $scrollWindow = '1m';
/*
* @var string internal ElasticSearch scroll id
*/
private $_lastScrollId = null;
/**
* Destructor.
*/
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
/**
* Resets the batch query.
* This method will clean up the existing batch query so that a new batch query can be performed.
*/
public function reset()
{
if(isset($this->_lastScrollId)) {
$this->query->createCommand($this->db)->clearScroll(['scroll_id' => $this->_lastScrollId]);
}
$this->_batch = null;
$this->_value = null;
$this->_key = null;
$this->_lastScrollId = null;
}
/**
* Resets the iterator to the initial state.
* This method is required by the interface [[\Iterator]].
*/
public function rewind()
{
$this->reset();
$this->next();
}
/**
* Moves the internal pointer to the next dataset.
* This method is required by the interface [[\Iterator]].
*/
public function next()
{
if ($this->_batch === null || !$this->each || $this->each && next($this->_batch) === false) {
$this->_batch = $this->fetchData();
reset($this->_batch);
}
if ($this->each) {
$this->_value = current($this->_batch);
if ($this->query->indexBy !== null) {
$this->_key = key($this->_batch);
} elseif (key($this->_batch) !== null) {
$this->_key++;
} else {
$this->_key = null;
}
} else {
$this->_value = $this->_batch;
$this->_key = $this->_key === null ? 0 : $this->_key + 1;
}
}
/**
* Fetches the next batch of data.
* @return array the data fetched
*/
protected function fetchData()
{
if (null === $this->_lastScrollId) {
//first query - do search
$options = ['scroll' => $this->scrollWindow];
if(!$this->query->orderBy) {
$options['search_type'] = 'scan';
}
$result = $this->query->createCommand($this->db)->search($options);
//if using "scan" mode, make another request immediately
//(search request returned 0 results)
if(!$this->query->orderBy) {
$result = $this->query->createCommand($this->db)->scroll([
'scroll_id' => $result['_scroll_id'],
'scroll' => $this->scrollWindow,
]);
}
} else {
//subsequent queries - do scroll
$result = $this->query->createCommand($this->db)->scroll([
'scroll_id' => $this->_lastScrollId,
'scroll' => $this->scrollWindow,
]);
}
//get last scroll id
$this->_lastScrollId = $result['_scroll_id'];
//get data
return $this->query->populate($result['hits']['hits']);
}
/**
* Returns the index of the current dataset.
* This method is required by the interface [[\Iterator]].
* @return int the index of the current row.
*/
public function key()
{
return $this->_key;
}
/**
* Returns the current dataset.
* This method is required by the interface [[\Iterator]].
* @return mixed the current dataset.
*/
public function current()
{
return $this->_value;
}
/**
* Returns whether there is a valid dataset at the current position.
* This method is required by the interface [[\Iterator]].
* @return bool whether there is a valid dataset at the current position.
*/
public function valid()
{
return !empty($this->_batch);
}
}
<?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\base\InvalidCallException;
use yii\helpers\Json;
/**
* The [[BulkCommand]] class implements the API for accessing the elasticsearch bulk REST API.
*
* Further details on bulk API is available in
* [elasticsearch guide](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html).
*
* @author Konstantin Sirotkin <beowulfenator@gmail.com>
* @since 2.0.5
*/
class BulkCommand extends Component
{
/**
* @var Connection
*/
public $db;
/**
* @var string Default index to execute the queries on. Defaults to null meaning that index needs to be specified in every action.
*/
public $index;
/**
* @var string Default type to execute the queries on. Defaults to null meaning that type needs to be specified in every action.
*/
public $type;
/**
* @var array|string Actions to be executed in this bulk command, given as either an array of arrays or as one newline-delimited string.
* All actions except delete span two lines.
*/
public $actions;
/**
* @var array Options to be appended to the query URL.
*/
public $options = [];
/**
* Executes the bulk command.
* @return mixed
* @throws yii\base\InvalidCallException
*/
public function execute()
{
//valid endpoints are /_bulk, /{index}/_bulk, and {index}/{type}/_bulk
if ($this->index === null && $this->type === null) {
$endpoint = ['_bulk'];
} elseif ($this->index !== null && $this->type === null) {
$endpoint = [$this->index, '_bulk'];
} elseif ($this->index !== null && $this->type !== null) {
$endpoint = [$this->index, $this->type, '_bulk'];
} else {
throw new InvalidCallException('Invalid endpoint: if type is defined, index must be defined too.');
}
if (empty($this->actions)) {
$body = '{}';
} elseif (is_array($this->actions)) {
$body = '';
foreach ($this->actions as $action) {
$body .= Json::encode($action) . "\n";
}
} else {
$body = $this->actions;
}
return $this->db->post($endpoint, $this->options, $body);
}
/**
* Adds an action to the command. Will overwrite existing actions if they are specified as a string.
* @param array $action Action expressed as an array (will be encoded to JSON automatically).
*/
public function addAction($line1, $line2 = null)
{
if (!is_array($this->actions)) {
$this->actions = [];
}
$this->actions[] = $line1;
if ($line2 !== null) {
$this->actions[] = $line2;
}
}
/**
* Adds a delete action to the command.
* @param string $id Document ID
* @param string $index Index that the document belogs to. Can be set to null if the command has
* a default index ([[BulkCommand::$index]]) assigned.
* @param string $type Type that the document belogs to. Can be set to null if the command has
* a default type ([[BulkCommand::$type]]) assigned.
*/
public function addDeleteAction($id, $index = null, $type = null)
{
$actionData = ['_id' => $id];
if (!empty($index)) {
$actionData['_index'] = $index;
}
if (!empty($type)) {
$actionData['_type'] = $type;
}
$this->addAction(['delete' => $actionData]);
}
}
Yii Framework 2 elasticsearch extension Change Log
==================================================
2.0.5 March 20, 2018
--------------------
- Bug #120: Fix debug panel markup to be compatible with Yii 2.0.10 (drdim)
- Bug #125: Fixed `ActiveDataProvider::refresh()` to also reset `$queryResults` data (sizeg)
- Bug #134: Fix infinite query loop "ActiveDataProvider" when the index does not exist (eolitich)
- Bug #149: Changed `yii\base\Object` to `yii\base\BaseObject` (dmirogin)
- Bug: (CVE-2018-8074): Fixed possibility of manipulated condition when unfiltered input is passed to `ActiveRecord::findOne()` or `findAll()` (cebe)
- Bug: Updated debug panel classes to be consistent with yii 2.0.7 (beowulfenator)
- Bug: Added accessor method for the default elasticsearch primary key (kyle-mccarthy)
- Enh #15: Special data provider `yii\elasticsearch\ActiveDataProvider` created (klimov-paul)
- Enh #43: Elasticsearch log target (trntv, beowulfenator)
- Enh #47: Added support for post_filter option in search queries (mxkh)
- Enh #60: Minor updates to guide (devypt, beowulfenator)
- Enh #82: Support HTTPS protocol (dor-denis, beowulfenator)
- Enh #83: Support for "gt", ">", "gte", ">=", "lt", "<", "lte", "<=" operators in query (i-lie, beowulfenator)
- Enh #119: Added support for explanation on query (kyle-mccarthy)
- Enh #150: Explicitily send `Content-Type` header in HTTP requests to elasticsearch (lubobill1990)
- Enh: Bulk API implemented and used in AR (tibee, beowulfenator)
- Enh: Deserialization of raw response when text/plain is supported (Tezd)
- Enh: Added ability to work with aliases through Command class (Tezd)
2.0.4 March 17, 2016
--------------------
- Bug #8: Fixed issue with running out of sockets when running a large number of requests by reusing curl handles (cebe)
- Bug #13: Fixed wrong API call for getting all types or searching all types, `_all` works only for indexes (cebe)
- Bug #19: `DeleteAll` now deletes all entries, not first 10 (beowulfenator)
- Bug #48: `UpdateAll` now updates all entries, not first 10 (beowulfenator)
- Bug #65: Fixed warning `array to string conversion` when parsing error response (rhertogh, silverfire)
- Bug #73: Fixed debug panel exception when no data was recorded for elasticsearch panel (jafaripur)
- Enh #2: Added `min_score` option to query (knut)
- Enh #28: AWS Elasticsearch service compatibility (andrey-bahrachev)
- Enh #33: Implemented `Command::updateSettings()` and `Command::updateAnalyzers()` (githubjeka)
- Enh #50: Implemented HTTP auth (silverfire)
- Enh #62: Added support for scroll API in `batch()` and `each()` (beowulfenator, 13leaf)
- Enh #70: `Query` and `ActiveQuery` now have `$options` attribute that is passed to commands generated by queries (beowulfenator)
- Enh: Unified model creation from result set in `Query` and `ActiveQuery` with `populate()` (beowulfenator)
2.0.3 March 01, 2015
--------------------
- no changes in this release.
2.0.2 January 11, 2015
----------------------
- Enh: Added `ActiveFixture` class for testing fixture support for elasticsearch (cebe, viilveer)
2.0.1 December 07, 2014
-----------------------
- Bug #5662: Elasticsearch AR updateCounters() now uses explicitly `groovy` script for updating making it compatible with ES >1.3.0 (cebe)
- Bug #6065: `ActiveRecord::unlink()` was failing in some situations when working with relations via array valued attributes (cebe)
- Enh #5758: Allow passing custom options to `ActiveRecord::update()` and `::delete()` including support for routing needed for updating records with parent relation (cebe)
- Enh: Add support for optimistic locking (cebe)
2.0.0 October 12, 2014
----------------------
- Enh #3381: Added ActiveRecord::arrayAttributes() to define attributes that should be treated as array when retrieved via `fields` (cebe)
2.0.0-rc September 27, 2014
---------------------------
- Bug #3587: Fixed an issue with storing empty records (cebe)
- Bug #4187: Elasticsearch dynamic scripting is disabled in 1.2.0, so do not use it in query builder (cebe)
- Enh #3527: Added `highlight` property to Query and ActiveRecord. (Borales)
- Enh #4048: Added `init` event to `ActiveQuery` classes (qiangxue)
- Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
- Enh: Make error messages more readable in HTML output (cebe)
- Enh: Added support for query stats (cebe)
- Enh: Added support for query suggesters (cebe, tvdavid)
- Enh: Added support for delete by query (cebe, tvdavid)
- Chg #4451: Removed support for facets and replaced them with aggregations (cebe, tadaszelvys)
- Chg: asArray in ActiveQuery is now equal to using the normal Query. This means, that the output structure has changed and `with` is supported anymore. (cebe)
- Chg: Deletion of a record is now also considered successful if the record did not exist. (cebe)
- Chg: Requirement changes: Yii now requires elasticsearch version 1.0 or higher (cebe)
2.0.0-beta April 13, 2014
-------------------------
- Bug #1993: afterFind event in AR is now called after relations have been populated (cebe, creocoder)
- Bug #2324: Fixed QueryBuilder bug when building a query with "query" option (mintao)
- Enh #1313: made index and type available in `ActiveRecord::instantiate()` to allow creating records based on elasticsearch type when doing cross index/type search (cebe)
- Enh #1382: Added a debug toolbar panel for elasticsearch (cebe)
- Enh #1765: Added support for primary key path mapping, pk can now be part of the attributes when mapping is defined (cebe)
- Enh #2002: Added filterWhere() method to yii\elasticsearch\Query to allow easy addition of search filter conditions by ignoring empty search fields (samdark, cebe)
- Enh #2892: ActiveRecord dirty attributes are now reset after call to `afterSave()` so information about changed attributes is available in `afterSave`-event (cebe)
- Chg #1765: Changed handling of ActiveRecord primary keys, removed getId(), use getPrimaryKey() instead (cebe)
- Chg #2281: Renamed `ActiveRecord::create()` to `populateRecord()` and changed signature. This method will not call instantiate() anymore (cebe)
- Chg #2146: Removed `ActiveRelation` class and moved the functionality to `ActiveQuery`.
All relational queries are now directly served by `ActiveQuery` allowing to use
custom scopes in relations (cebe)
2.0.0-alpha, December 1, 2013
-----------------------------
- Initial release.
<?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\base\InvalidCallException;
use yii\helpers\Json;
/**
* The Command class implements the API for accessing the elasticsearch REST API.
*
* Check the [elasticsearch guide](http://www.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type
*/
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;
/**
* @var array options to be appended to the query URL, such as "search_type" for search or "timeout" for delete
*/
public $options = [];
/**
* Sends a request to the _search API and returns the result
* @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'];
if ($this->type !== null) {
$url[] = $this->type;
}
$url[] = '_search';
return $this->db->get($url, array_merge($this->options, $options), $query);
}
/**
* Sends a request to the delete by query
* @param array $options
* @return mixed
*/
public function deleteByQuery($options = [])
{
if (!isset($this->queryParts['query'])) {
throw new InvalidCallException('Can not call deleteByQuery when no query is given.');
}
$query = [
'query' => $this->queryParts['query'],
];
if (isset($this->queryParts['filter'])) {
$query['filter'] = $this->queryParts['filter'];
}
$query = Json::encode($query);
$url = [$this->index !== null ? $this->index : '_all'];
if ($this->type !== null) {
$url[] = $this->type;
}
$url[] = '_query';
return $this->db->delete($url, array_merge($this->options, $options), $query);
}
/**
* Sends a request to the _suggest API and returns the result
* @param string|array $suggester the suggester body
* @param array $options
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
*/
public function suggest($suggester, $options = [])
{
if (empty($suggester)) {
$suggester = '{}';
}
if (is_array($suggester)) {
$suggester = Json::encode($suggester);
}
$url = [
$this->index !== null ? $this->index : '_all',
'_suggest'
];
return $this->db->post($url, array_merge($this->options, $options), $suggester);
}
/**
* 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 chosen
* @param array $options
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html
*/
public function insert($index, $type, $data, $id = null, $options = [])
{
if (empty($data)) {
$body = '{}';
} else {
$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.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html
*/
public function get($index, $type, $id, $options = [])
{
return $this->db->get([$index, $type, $id], $options);
}
/**
* 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.elastic.co/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.elastic.co/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.elastic.co/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.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html
*/
public function update($index, $type, $id, $data, $options = [])
{
$body = [
'doc' => empty($data) ? new \stdClass() : $data,
];
if (isset($options["detect_noop"])) {
$body["detect_noop"] = $options["detect_noop"];
unset($options["detect_noop"]);
}
return $this->db->post([$index, $type, $id, '_update'], $options, Json::encode($body));
}
// TODO bulk http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
/**
* creates an index
* @param $index
* @param array $configuration
* @return mixed
* @see http://www.elastic.co/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.elastic.co/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.elastic.co/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.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/indices-types-exists.html
*/
public function typeExists($index, $type)
{
return $this->db->head([$index, $type]);
}
/**
* @param string $alias
*
* @return bool
*/
public function aliasExists($alias)
{
$indexes = $this->getIndexesByAlias($alias);
return !empty($indexes);
}
/**
* @return array
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
*/
public function getAliasInfo()
{
$aliasInfo = $this->db->get(['_alias', '*']);
return $aliasInfo ?: [];
}
/**
* @param string $alias
*
* @return array
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
*/
public function getIndexInfoByAlias($alias)
{
$responseData = $this->db->get(['_alias', $alias]);
if (empty($responseData)) {
return [];
}
return $responseData;
}
/**
* @param string $alias
*
* @return array
*/
public function getIndexesByAlias($alias)
{
return array_keys($this->getIndexInfoByAlias($alias));
}
/**
* @param string $index
*
* @return array
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-retrieving
*/
public function getIndexAliases($index)
{
$responseData = $this->db->get([$index, '_alias', '*']);
if (empty($responseData)) {
return [];
}
return $responseData[$index]['aliases'];
}
/**
* @param $index
* @param $alias
* @param array $aliasParameters
*
* @return bool
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#alias-adding
*/
public function addAlias($index, $alias, $aliasParameters = [])
{
return (bool)$this->db->put([$index, '_alias', $alias], [], json_encode((object)$aliasParameters));
}
/**
* @param string $index
* @param string $alias
*
* @return bool
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#deleting
*/
public function removeAlias($index, $alias)
{
return (bool)$this->db->delete([$index, '_alias', $alias]);
}
/**
* Runs alias manipulations.
* If you want to add alias1 to index1
* and remove alias2 from index2 you can use following commands:
* ~~~
* $actions = [
* ['add' => ['index' => 'index1', 'alias' => 'alias1']],
* ['remove' => ['index' => 'index2', 'alias' => 'alias2']],
* ];
* ~~~
* @param array $actions
*
* @return bool
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.0/indices-aliases.html#indices-aliases
*/
public function aliasActions(array $actions)
{
return (bool)$this->db->post(['_aliases'], [], json_encode(['actions' => $actions]));
}
/**
* Change specific index level settings in real time.
* Note that update analyzers required to [[close()]] the index first and [[open()]] it after the changes are made,
* use [[updateAnalyzers()]] for it.
*
* @param string $index
* @param string|array $setting
* @param array $options URL options
* @return mixed
* @see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/indices-update-settings.html
* @since 2.0.4
*/
public function updateSettings($index, $setting, $options = [])
{
$body = $setting !== null ? (is_string($setting) ? $setting : Json::encode($setting)) : null;
return $this->db->put([$index, '_settings'], $options, $body);
}
/**
* Define new analyzers for the index.
* For example if content analyzer hasn’t been defined on "myindex" yet
* you can use the following commands to add it:
*
* ~~~
* $setting = [
* 'analysis' => [
* 'analyzer' => [
* 'ngram_analyzer_with_filter' => [
* 'tokenizer' => 'ngram_tokenizer',
* 'filter' => 'lowercase, snowball'
* ],
* ],
* 'tokenizer' => [
* 'ngram_tokenizer' => [
* 'type' => 'nGram',
* 'min_gram' => 3,
* 'max_gram' => 10,
* 'token_chars' => ['letter', 'digit', 'whitespace', 'punctuation', 'symbol']
* ],
* ],
* ]
* ];
* $elasticQuery->createCommand()->updateAnalyzers('myindex', $setting);
* ~~~
*
* @param string $index
* @param string|array $setting
* @param array $options URL options
* @return mixed
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html#update-settings-analysis
* @since 2.0.4
*/
public function updateAnalyzers($index, $setting, $options = [])
{
$this->closeIndex($index);
$result = $this->updateSettings($index, $setting, $options);
$this->openIndex($index);
return $result;
}
// TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-settings.html
// TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-warmers.html
/**
* @param $index
* @return mixed
* @see http://www.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
*/
public function closeIndex($index)
{
return $this->db->post([$index, '_close']);
}
/**
* @param array $options
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
* @return mixed
* @since 2.0.4
*/
public function scroll($options = [])
{
return $this->db->get(['_search', 'scroll'], $options);
}
/**
* @param array $options
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-scroll.html
* @return mixed
* @since 2.0.4
*/
public function clearScroll($options = [])
{
return $this->db->delete(['_search', 'scroll'], $options);
}
/**
* @param $index
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-status.html
*/
public function getIndexStatus($index = '_all')
{
return $this->db->get([$index, '_status']);
}
// TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html
// http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-segments.html
/**
* @param $index
* @return mixed
* @see http://www.elastic.co/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.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
*/
public function refreshIndex($index)
{
return $this->db->post([$index, '_refresh']);
}
// TODO http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-optimize.html
// TODO http://www.elastic.co/guide/en/elasticsearch/reference/0.90/indices-gateway-snapshot.html
/**
* @param string $index
* @param string $type
* @param string|array $mapping
* @param array $options
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html
*/
public function setMapping($index, $type, $mapping, $options = [])
{
$body = $mapping !== null ? (is_string($mapping) ? $mapping : Json::encode($mapping)) : null;
return $this->db->put([$index, '_mapping', $type], $options, $body);
}
/**
* @param string $index
* @param string $type
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-mapping.html
*/
public function getMapping($index = '_all', $type = null)
{
$url = [$index, '_mapping'];
if ($type !== null) {
$url[] = $type;
}
return $this->db->get($url);
}
/**
* @param $index
* @param $type
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-put-mapping.html
*/
public function deleteMapping($index, $type)
{
return $this->db->delete([$index, '_mapping', $type]);
}
/**
* @param $index
* @param string $type
* @return mixed
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/indices-get-field-mapping.html
*/
// public function getFieldMapping($index, $type = '_all')
// {
// // TODO implement
// return $this->db->put([$index, $type, '_mapping']);
// }
/**
* @param $options
* @param $index
* @return mixed
* @see http://www.elastic.co/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.elastic.co/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.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
*/
public function getTemplate($name)
{
return $this->db->get(['_template', $name]);
}
}
<?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\base\InvalidParamException;
use yii\helpers\Json;
/**
* elasticsearch Connection is used to connect to an elasticsearch cluster version 0.20 or higher
*
* @property string $driverName Name of the DB driver. This property is read-only.
* @property bool $isActive Whether the DB connection is established. This property is read-only.
* @property QueryBuilder $queryBuilder This property is read-only.
*
* @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 boolean whether to autodetect available cluster nodes on [[open()]]
*/
public $autodetectCluster = true;
/**
* @var array The elasticsearch cluster nodes to connect to.
*
* This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true.
*
* Additional special options:
*
* - `auth`: overrides [[auth]] property. For example:
*
* ```php
* [
* 'http_address' => 'inet[/127.0.0.1:9200]',
* 'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Overrides the `auth` property of the class with specific login and password
* //'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Disabled auth regardless of `auth` property of the class
* ]
* ```
*
* - `protocol`: explicitly sets the protocol for the current node (useful when manually defining a HTTPS cluster)
*
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info
*/
public $nodes = [
['http_address' => 'inet[/127.0.0.1:9200]'],
];
/**
* @var string the active node. Key of one of the [[nodes]]. Will be randomly selected on [[open()]].
*/
public $activeNode;
/**
* @var array Authentication data used to connect to the ElasticSearch node.
*
* Array elements:
*
* - `username`: the username for authentication.
* - `password`: the password for authentication.
*
* Array either MUST contain both username and password on not contain any authentication credentials.
* @see http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth
*/
public $auth = [];
/**
* Elasticsearch has no knowledge of protocol used to access its nodes. Specifically, cluster autodetection request
* returns node hosts and ports, but not the protocols to access them. Therefore we need to specify a default protocol here,
* which can be overridden for specific nodes in the [[nodes]] property.
* If [[autodetectCluster]] is true, all nodes received from cluster will be set to use the protocol defined by [[defaultProtocol]]
* @var string Default protocol to connect to nodes
* @since 2.0.5
*/
public $defaultProtocol = 'http';
/**
* @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;
/**
* @var resource the curl instance returned by [curl_init()](http://php.net/manual/en/function.curl-init.php).
*/
private $_curl;
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.');
}
if (!isset($node['protocol'])) {
$node['protocol'] = $this->defaultProtocol;
}
if (!in_array($node['protocol'], ['http', 'https'])) {
throw new InvalidConfigException('Valid node protocol settings are "http" and "https".');
}
}
}
/**
* 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 bool 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.');
}
$this->_curl = curl_init();
if ($this->autodetectCluster) {
$this->populateNodes();
}
$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();
}
/**
* Populates [[nodes]] with the result of a cluster nodes request.
* @throws Exception if no active node(s) found
* @since 2.0.4
*/
protected function populateNodes()
{
$node = reset($this->nodes);
$host = $node['http_address'];
$protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol;
if (strncmp($host, 'inet[/', 6) === 0) {
$host = substr($host, 6, -1);
}
$response = $this->httpRequest('GET', "$protocol://$host/_nodes");
if (!empty($response['nodes'])) {
$nodes = $response['nodes'];
} else {
$nodes = [];
}
foreach ($nodes as $key => &$node) {
// Make sure that nodes have an 'http_address' property, which is not the case
// if you're using AWS Elasticsearch service (at least as of Oct., 2015, still the case in July, 2017).
// it should be there according to the docs: https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html
$node['http_address'] = $node['http']['publish_address'];
if (!isset($node['http_address'])) {
unset($nodes[$key]);
}
// Protocol is not a standard ES node property, so we add it manually
$node['protocol'] = $this->defaultProtocol;
}
if (!empty($nodes)) {
$this->nodes = array_values($nodes);
} else {
curl_close($this->_curl);
throw new Exception('Cluster autodetection did not find any active node. Make sure a GET /_nodes reguest on the hosts defined in the config returns the "http_address" field for each node.');
}
}
/**
* 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()
{
if ($this->activeNode === null) {
return;
}
Yii::trace('Closing connection to elasticsearch. Active node was: '
. $this->nodes[$this->activeNode]['http_address'], __CLASS__);
$this->activeNode = null;
if ($this->_curl) {
curl_close($this->_curl);
$this->_curl = 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;
}
/**
* Creates a bulk command for execution.
* @param array $config the configuration for the [[BulkCommand]] class
* @return BulkCommand the DB command
* @since 2.0.5
*/
public function createBulkCommand($config = [])
{
$this->open();
$config['db'] = $this;
$command = new BulkCommand($config);
return $command;
}
/**
* Creates new query builder instance
* @return QueryBuilder
*/
public function getQueryBuilder()
{
return new QueryBuilder($this);
}
/**
* Performs GET HTTP request
*
* @param string|array $url URL
* @param array $options URL options
* @param string $body request body
* @param bool $raw if response body contains JSON and should be decoded
* @return mixed response
* @throws Exception
* @throws InvalidConfigException
*/
public function get($url, $options = [], $body = null, $raw = false)
{
$this->open();
return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw);
}
/**
* Performs HEAD HTTP request
*
* @param string|array $url URL
* @param array $options URL options
* @param string $body request body
* @return mixed response
* @throws Exception
* @throws InvalidConfigException
*/
public function head($url, $options = [], $body = null)
{
$this->open();
return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body);
}
/**
* Performs POST HTTP request
*
* @param string|array $url URL
* @param array $options URL options
* @param string $body request body
* @param bool $raw if response body contains JSON and should be decoded
* @return mixed response
* @throws Exception
* @throws InvalidConfigException
*/
public function post($url, $options = [], $body = null, $raw = false)
{
$this->open();
return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw);
}
/**
* Performs PUT HTTP request
*
* @param string|array $url URL
* @param array $options URL options
* @param string $body request body
* @param bool $raw if response body contains JSON and should be decoded
* @return mixed response
* @throws Exception
* @throws InvalidConfigException
*/
public function put($url, $options = [], $body = null, $raw = false)
{
$this->open();
return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw);
}
/**
* Performs DELETE HTTP request
*
* @param string|array $url URL
* @param array $options URL options
* @param string $body request body
* @param bool $raw if response body contains JSON and should be decoded
* @return mixed response
* @throws Exception
* @throws InvalidConfigException
*/
public function delete($url, $options = [], $body = null, $raw = false)
{
$this->open();
return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw);
}
/**
* Creates URL
*
* @param string|array $path path
* @param array $options URL options
* @return array
*/
private function createUrl($path, $options = [])
{
if (!is_string($path)) {
$url = implode('/', array_map(function ($a) {
return urlencode(is_array($a) ? implode(',', $a) : $a);
}, $path));
if (!empty($options)) {
$url .= '?' . http_build_query($options);
}
} else {
$url = $path;
if (!empty($options)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options);
}
}
$node = $this->nodes[$this->activeNode];
$protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol;
$host = $node['http_address'];
return [$protocol, $host, $url];
}
/**
* Performs HTTP request
*
* @param string $method method name
* @param string $url URL
* @param string $requestBody request body
* @param bool $raw if response body contains JSON and should be decoded
* @return mixed if request failed
* @throws Exception if request failed
* @throws InvalidConfigException
*/
protected function httpRequest($method, $url, $requestBody = null, $raw = false)
{
$method = strtoupper($method);
// response body and headers
$headers = [];
$headersFinished = false;
$body = '';
$options = [
CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' ' . __CLASS__,
CURLOPT_RETURNTRANSFER => false,
CURLOPT_HEADER => false,
// http://www.php.net/manual/en/function.curl-setopt.php#82418
CURLOPT_HTTPHEADER => ['Expect:', 'Content-Type: application/json'],
CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) {
$body .= $data;
return mb_strlen($data, '8bit');
},
CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers, &$headersFinished) {
if ($data === '') {
$headersFinished = true;
} elseif ($headersFinished) {
$headersFinished = false;
}
if (!$headersFinished && ($pos = strpos($data, ':')) !== false) {
$headers[strtolower(substr($data, 0, $pos))] = trim(substr($data, $pos + 1));
}
return mb_strlen($data, '8bit');
},
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_FORBID_REUSE => false,
];
if (!empty($this->auth) || isset($this->nodes[$this->activeNode]['auth']) && $this->nodes[$this->activeNode]['auth'] !== false) {
$auth = isset($this->nodes[$this->activeNode]['auth']) ? $this->nodes[$this->activeNode]['auth'] : $this->auth;
if (empty($auth['username'])) {
throw new InvalidConfigException('Username is required to use authentication');
}
if (empty($auth['password'])) {
throw new InvalidConfigException('Password is required to use authentication');
}
$options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC;
$options[CURLOPT_USERPWD] = $auth['username'] . ':' . $auth['password'];
}
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]);
} else {
$options[CURLOPT_NOBODY] = false;
}
if (is_array($url)) {
list($protocol, $host, $q) = $url;
if (strncmp($host, 'inet[', 5) == 0) {
$host = substr($host, 5, -1);
if (($pos = strpos($host, '/')) !== false) {
$host = substr($host, $pos + 1);
}
}
$profile = "$method $q#$requestBody";
$url = "$protocol://$host/$q";
} else {
$profile = false;
}
Yii::trace("Sending request to elasticsearch node: $method $url\n$requestBody", __METHOD__);
if ($profile !== false) {
Yii::beginProfile($profile, __METHOD__);
}
$this->resetCurlHandle();
curl_setopt($this->_curl, CURLOPT_URL, $url);
curl_setopt_array($this->_curl, $options);
if (curl_exec($this->_curl) === false) {
throw new Exception('Elasticsearch request failed: ' . curl_errno($this->_curl) . ' - ' . curl_error($this->_curl), [
'requestMethod' => $method,
'requestUrl' => $url,
'requestBody' => $requestBody,
'responseHeaders' => $headers,
'responseBody' => $this->decodeErrorBody($body),
]);
}
$responseCode = curl_getinfo($this->_curl, CURLINFO_HTTP_CODE);
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'])) {
if (!strncmp($headers['content-type'], 'application/json', 16)) {
return $raw ? $body : Json::decode($body);
}
if (!strncmp($headers['content-type'], 'text/plain', 10)) {
return $raw ? $body : array_filter(explode("\n", $body));
}
}
throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [
'requestMethod' => $method,
'requestUrl' => $url,
'requestBody' => $requestBody,
'responseCode' => $responseCode,
'responseHeaders' => $headers,
'responseBody' => $this->decodeErrorBody($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' => $this->decodeErrorBody($body),
]);
}
}
private function resetCurlHandle()
{
// these functions do not get reset by curl automatically
static $unsetValues = [
CURLOPT_HEADERFUNCTION => null,
CURLOPT_WRITEFUNCTION => null,
CURLOPT_READFUNCTION => null,
CURLOPT_PROGRESSFUNCTION => null,
];
curl_setopt_array($this->_curl, $unsetValues);
if (function_exists('curl_reset')) { // since PHP 5.5.0
curl_reset($this->_curl);
}
}
/**
* Try to decode error information if it is valid json, return it if not.
* @param $body
* @return mixed
*/
protected function decodeErrorBody($body)
{
try {
$decoded = Json::decode($body);
if (isset($decoded['error']) && !is_array($decoded['error'])) {
$decoded['error'] = preg_replace('/\b\w+?Exception\[/', "<span style=\"color: red;\">\\0</span>\n ", $decoded['error']);
}
return $decoded;
} catch(InvalidParamException $e) {
return $body;
}
}
public function getNodeInfo()
{
return $this->get([]);
}
public function getClusterState()
{
return $this->get(['_cluster', 'state']);
}
}
<?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\Action;
use yii\base\NotSupportedException;
use yii\helpers\ArrayHelper;
use yii\web\HttpException;
use yii\web\Response;
use Yii;
/**
* Debug Action is used by [[DebugPanel]] to perform elasticsearch queries using ajax.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class DebugAction extends Action
{
/**
* @var string the connection id to use
*/
public $db;
/**
* @var DebugPanel
*/
public $panel;
/**
* @var \yii\debug\controllers\DefaultController
*/
public $controller;
public function run($logId, $tag)
{
$this->controller->loadData($tag);
$timings = $this->panel->calculateTimings();
ArrayHelper::multisort($timings, 3, SORT_DESC);
if (!isset($timings[$logId])) {
throw new HttpException(404, 'Log message not found.');
}
$message = $timings[$logId][1];
if (($pos = mb_strpos($message, "#")) !== false) {
$url = mb_substr($message, 0, $pos);
$body = mb_substr($message, $pos + 1);
} else {
$url = $message;
$body = null;
}
$method = mb_substr($url, 0, $pos = mb_strpos($url, ' '));
$url = mb_substr($url, $pos + 1);
$options = ['pretty' => true];
/* @var $db Connection */
$db = \Yii::$app->get($this->db);
$time = microtime(true);
switch ($method) {
case 'GET': $result = $db->get($url, $options, $body, true); break;
case 'POST': $result = $db->post($url, $options, $body, true); break;
case 'PUT': $result = $db->put($url, $options, $body, true); break;
case 'DELETE': $result = $db->delete($url, $options, $body, true); break;
case 'HEAD': $result = $db->head($url, $options, $body); break;
default:
throw new NotSupportedException("Request method '$method' is not supported by elasticsearch.");
}
$time = microtime(true) - $time;
if ($result === true) {
$result = '<span class="label label-success">success</span>';
} elseif ($result === false) {
$result = '<span class="label label-danger">no success</span>';
}
Yii::$app->response->format = Response::FORMAT_JSON;
return [
'time' => sprintf('%.1f ms', $time * 1000),
'result' => $result,
];
}
}
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\elasticsearch;
use yii\debug\Panel;
use yii\helpers\ArrayHelper;
use yii\helpers\Url;
use yii\log\Logger;
use yii\helpers\Html;
use yii\web\View;
/**
* Debugger panel that collects and displays elasticsearch queries performed.
*
* @author Carsten Brandt <mail@cebe.cc>
* @since 2.0
*/
class DebugPanel extends Panel
{
public $db = 'elasticsearch';
public function init()
{
$this->actions['elasticsearch-query'] = [
'class' => 'yii\\elasticsearch\\DebugAction',
'panel' => $this,
'db' => $this->db,
];
}
/**
* @inheritdoc
*/
public function getName()
{
return 'Elasticsearch';
}
/**
* @inheritdoc
*/
public function getSummary()
{
$timings = $this->calculateTimings();
$queryCount = count($timings);
$queryTime = 0;
foreach ($timings as $timing) {
$queryTime += $timing[3];
}
$queryTime = number_format($queryTime * 1000) . ' ms';
$url = $this->getUrl();
$output = <<<EOD
<div class="yii-debug-toolbar__block">
<a href="$url" title="Executed $queryCount elasticsearch queries which took $queryTime.">
ES <span class="yii-debug-toolbar__label yii-debug-toolbar__ajax_counter yii-debug-toolbar__label_info">$queryCount</span> <span class="yii-debug-toolbar__label">$queryTime</span>
</a>
</div>
EOD;
return $queryCount > 0 ? $output : '';
}
/**
* @inheritdoc
*/
public function getDetail()
{
$timings = $this->calculateTimings();
ArrayHelper::multisort($timings, 3, SORT_DESC);
$rows = [];
$i = 0;
foreach ($timings as $logId => $timing) {
$duration = sprintf('%.1f ms', $timing[3] * 1000);
$message = $timing[1];
$traces = $timing[4];
if (($pos = mb_strpos($message, "#")) !== false) {
$url = mb_substr($message, 0, $pos);
$body = mb_substr($message, $pos + 1);
} else {
$url = $message;
$body = null;
}
$traceString = '';
if (!empty($traces)) {
$traceString .= Html::ul($traces, [
'class' => 'trace',
'item' => function ($trace) {
return "<li>{$trace['file']}({$trace['line']})</li>";
},
]);
}
$ajaxUrl = Url::to(['elasticsearch-query', 'logId' => $logId, 'tag' => $this->tag]);
\Yii::$app->view->registerJs(<<<JS
$('#elastic-link-$i').on('click', function () {
var result = $('#elastic-result-$i');
result.html('Sending request...');
result.parent('tr').show();
$.ajax({
type: "POST",
url: "$ajaxUrl",
success: function (data) {
$('#elastic-time-$i').html(data.time);
$('#elastic-result-$i').html(data.result);
},
error: function (jqXHR, textStatus, errorThrown) {
$('#elastic-time-$i').html('');
$('#elastic-result-$i').html('<span style="color: #c00;">Error: ' + errorThrown + ' - ' + textStatus + '</span><br />' + jqXHR.responseText);
},
dataType: "json"
});
return false;
});
JS
, View::POS_READY);
$runLink = Html::a('run query', '#', ['id' => "elastic-link-$i"]) . '<br/>';
$rows[] = <<<HTML
<tr>
<td style="width: 10%;">$duration</td>
<td style="width: 75%;"><div><b>$url</b><br/><p>$body</p>$traceString</div></td>
<td style="width: 15%;">$runLink</td>
</tr>
<tr style="display: none;"><td id="elastic-time-$i"></td><td colspan="3" id="elastic-result-$i"></td></tr>
HTML;
$i++;
}
$rows = implode("\n", $rows);
return <<<HTML
<h1>Elasticsearch Queries</h1>
<table class="table table-condensed table-bordered table-striped table-hover" style="table-layout: fixed;">
<thead>
<tr>
<th style="width: 10%;">Time</th>
<th style="width: 75%;">Url / Query</th>
<th style="width: 15%;">Run Query on node</th>
</tr>
</thead>
<tbody>
$rows
</tbody>
</table>
HTML;
}
private $_timings;
public function calculateTimings()
{
if ($this->_timings !== null) {
return $this->_timings;
}
$messages = isset($this->data['messages']) ? $this->data['messages'] : [];
$timings = [];
$stack = [];
foreach ($messages as $i => $log) {
list($token, $level, $category, $timestamp) = $log;
$log[5] = $i;
if ($level == Logger::LEVEL_PROFILE_BEGIN) {
$stack[] = $log;
} elseif ($level == Logger::LEVEL_PROFILE_END) {
if (($last = array_pop($stack)) !== null && $last[0] === $token) {
$timings[$last[5]] = [count($stack), $token, $last[3], $timestamp - $last[3], $last[4]];
}
}
}
$now = microtime(true);
while (($last = array_pop($stack)) !== null) {
$delta = $now - $last[3];
$timings[$last[5]] = [count($stack), $last[0], $last[2], $delta, $last[4]];
}
ksort($timings);
return $this->_timings = $timings;
}
/**
* @inheritdoc
*/
public function save()
{
$target = $this->module->logTarget;
$messages = $target->filterMessages($target->messages, Logger::LEVEL_PROFILE, ['yii\elasticsearch\Connection::httpRequest']);
return ['messages' => $messages];
}
}
<?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\InvalidConfigException;
use yii\di\Instance;
use yii\helpers\ArrayHelper;
use yii\helpers\Json;
use yii\helpers\VarDumper;
use yii\log\Logger;
use yii\log\Target;
/**
* ElasticsearchTarget stores log messages in a elasticsearch index.
*
* @author Eugene Terentev <eugene@terentev.net>
* @since 2.0.5
*/
class ElasticsearchTarget extends Target
{
/**
* @var string Elasticsearch index name.
*/
public $index = 'yii';
/**
* @var string Elasticsearch type name.
*/
public $type = 'log';
/**
* @var Connection|array|string the elasticsearch connection object or the application component ID
* of the elasticsearch connection.
*/
public $db = 'elasticsearch';
/**
* @var array $options URL options.
*/
public $options = [];
/**
* @var boolean If true, context will be logged as a separate message after all other messages.
*/
public $logContext = true;
/**
* @var boolean If true, context will be included in every message.
* This is convenient if you log application errors and analyze them with tools like Kibana.
*/
public $includeContext = false;
/**
* @var boolean If true, context message will cached once it's been created. Makes sense to use with [[includeContext]].
*/
public $cacheContext = false;
/**
* @var string Context message cache (can be used multiple times if context is appended to every message)
*/
protected $_contextMessage = null;
/**
* This method will initialize the [[elasticsearch]] property to make sure it refers to a valid Elasticsearch connection.
* @throws InvalidConfigException if [[elasticsearch]] is invalid.
*/
public function init()
{
parent::init();
$this->db = Instance::ensure($this->db, Connection::className());
}
/**
* @inheritdoc
*/
public function export()
{
$messages = array_map([$this, 'prepareMessage'], $this->messages);
$body = implode("\n", $messages) . "\n";
$this->db->post([$this->index, $this->type, '_bulk'], $this->options, $body);
}
/**
* If [[includeContext]] property is false, returns context message normally.
* If [[includeContext]] is true, returns an empty string (so that context message in [[collect]] is not generated),
* expecting that context will be appended to every message in [[prepareMessage]].
* @return array the context information
*/
protected function getContextMessage()
{
if (null === $this->_contextMessage || !$this->cacheContext) {
$this->_contextMessage = ArrayHelper::filter($GLOBALS, $this->logVars);
}
return $this->_contextMessage;
}
/**
* Processes the given log messages.
* This method will filter the given messages with [[levels]] and [[categories]].
* And if requested, it will also export the filtering result to specific medium (e.g. email).
* Depending on the [[includeContext]] attribute, a context message will be either created or ignored.
* @param array $messages log messages to be processed. See [[Logger::messages]] for the structure
* of each message.
* @param bool $final whether this method is called at the end of the current application
*/
public function collect($messages, $final)
{
$this->messages = array_merge($this->messages, static::filterMessages($messages, $this->getLevels(), $this->categories, $this->except));
$count = count($this->messages);
if ($count > 0 && ($final || $this->exportInterval > 0 && $count >= $this->exportInterval)) {
if (!$this->includeContext && $this->logContext) {
$context = $this->getContextMessage();
if (!empty($context)) {
$this->messages[] = [$context, Logger::LEVEL_INFO, 'application', YII_BEGIN_TIME];
}
}
// set exportInterval to 0 to avoid triggering export again while exporting
$oldExportInterval = $this->exportInterval;
$this->exportInterval = 0;
$this->export();
$this->exportInterval = $oldExportInterval;
$this->messages = [];
}
}
/**
* Prepares a log message.
* @param array $message The log message to be formatted.
* @return string
*/
public function prepareMessage($message)
{
list($text, $level, $category, $timestamp) = $message;
$result = [
'category' => $category,
'level' => Logger::getLevelName($level),
'@timestamp' => date('c', $timestamp),
];
if (isset($message[4])) {
$result['trace'] = $message[4];
}
//Exceptions get parsed into an array, text and arrays are passed as is, other types are var_dumped
if ($text instanceof \Exception) {
//convert exception to array for easier analysis
$result['message'] = [
'message' => $text->getMessage(),
'file' => $text->getFile(),
'line' => $text->getLine(),
'trace' => $text->getTraceAsString(),
];
} elseif (is_array($text) || is_string($text)) {
$result['message'] = $text;
} else {
$result['message'] = VarDumper::export($text);
}
if ($this->includeContext) {
$result['context'] = $this->getContextMessage();
}
$message = implode("\n", [
Json::encode([
'index' => new \stdClass()
]),
Json::encode($result)
]);
return $message;
}
}
<?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
{
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'Elasticsearch Database Exception';
}
}
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.
<?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\InvalidParamException;
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.
*
* NOTE: elasticsearch limits the number of records returned to 10 records by default.
* If you expect to get more records you should specify limit explicitly.
*
* @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, this option will not be applied to the query and no fields will be returned.
* In this case the `_source` field will be returned by default which can be configured using [[source]].
* Setting this to an empty array will result in no fields being retrieved, which means that only the primaryKey
* of a record will be available in the result.
*
* For each field you may also add an array representing a [script field]. Example:
*
* ```php
* $query->fields = [
* 'id',
* 'name',
* 'value_times_two' => [
* 'script' => "doc['my_field_name'].value * 2",
* ],
* 'value_times_factor' => [
* 'script' => "doc['my_field_name'].value * factor",
* 'params' => [
* 'factor' => 2.0
* ],
* ],
* ]
* ```
*
* > Note: Field values are [always returned as arrays] even if they only have one value.
*
* [always returned as arrays]: http://www.elastic.co/guide/en/elasticsearch/reference/1.x/_return_values.html#_return_values
* [script field]: http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
*
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-fields.html#search-request-fields
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-script-fields.html
* @see fields()
* @see source
*/
public $fields;
/**
* @var array this option controls how the `_source` field is returned from the documents. For example, `['id', 'name']`
* means that only the `id` and `name` field should be returned from `_source`.
* If not set, it means retrieving the full `_source` field unless [[fields]] are specified.
* Setting this option to `false` will disable return of the `_source` field, this means that only the primaryKey
* of a record will be available in the result.
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
* @see source()
* @see fields
*/
public $source;
/**
* @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.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5
*/
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.elastic.co/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.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html).
*/
public $filter;
/**
* @var string|array The `post_filter` part of the search query for differentially filter search results and aggregations.
* @see https://www.elastic.co/guide/en/elasticsearch/guide/current/_post_filter.html
* @since 2.0.5
*/
public $postFilter;
/**
* @var array The highlight part of this search query. This is an array that allows to highlight search results
* on one or more fields.
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
*/
public $highlight;
/**
* @var array List of aggregations to add to this query.
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
*/
public $aggregations = [];
/**
* @var array the 'stats' part of the query. An array of groups to maintain a statistics aggregation for.
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups
*/
public $stats = [];
/**
* @var array list of suggesters to add to this query.
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
*/
public $suggest = [];
/**
* @var float Exclude documents which have a _score less than the minimum specified in min_score
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html
* @since 2.0.4
*/
public $minScore;
/**
* @var array list of options that will passed to commands created by this query.
* @see Command::$options
* @since 2.0.4
*/
public $options = [];
/**
* @var bool Enables explanation for each hit on how its score was computed.
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-explain.html
* @since 2.0.5
*/
public $explain;
/**
* @inheritdoc
*/
public function init()
{
parent::init();
// setting the default limit according to elasticsearch defaults
// http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5
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->get('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'];
return $this->populate($rows);
}
/**
* Converts the raw query results into the format as specified by this query.
* This method is internally used to convert the data fetched from database
* into the format as required by this query.
* @param array $rows the raw query result from database
* @return array the converted query result
* @since 2.0.4
*/
public function populate($rows)
{
if ($this->indexBy === null) {
return $rows;
}
$models = [];
foreach ($rows as $key => $row) {
if ($this->indexBy !== null) {
if (is_string($this->indexBy)) {
$key = isset($row['fields'][$this->indexBy]) ? reset($row['fields'][$this->indexBy]) : $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|bool 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)
{
$result = $this->createCommand($db)->search(['size' => 1]);
if (empty($result['hits']['hits'])) {
return false;
}
$record = reset($result['hits']['hits']);
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.elastic.co/guide/en/elasticsearch/reference/current/search.html#search-routing)
* - [search_type](http://www.elastic.co/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) {
$rows = [];
foreach ($result['hits']['hits'] as $key => $row) {
if (is_string($this->indexBy)) {
$key = isset($row['fields'][$this->indexBy]) ? $row['fields'][$this->indexBy] : $row['_source'][$this->indexBy];
} else {
$key = call_user_func($this->indexBy, $row);
}
$rows[$key] = $row;
}
$result['hits']['hits'] = $rows;
}
return $result;
}
// TODO add scroll/scan http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-search-type.html#scan
/**
* Executes the query and deletes all matching documents.
*
* Everything except query and filter will be ignored.
*
* @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.
* @return array the query results.
*/
public function delete($db = null, $options = [])
{
return $this->createCommand($db)->deleteByQuery($options);
}
/**
* 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);
if ($record !== false) {
if ($field === '_id') {
return $record['_id'];
} elseif (isset($record['_source'][$field])) {
return $record['_source'][$field];
} elseif (isset($record['fields'][$field])) {
return count($record['fields'][$field]) == 1 ? reset($record['fields'][$field]) : $record['fields'][$field];
}
}
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['_source'] = [$field];
$result = $command->search();
if (empty($result['hits']['hits'])) {
return [];
}
$column = [];
foreach ($result['hits']['hits'] as $row) {
if (isset($row['fields'][$field])) {
$column[] = $row['fields'][$field];
} elseif (isset($row['_source'][$field])) {
$column[] = $row['_source'][$field];
} else {
$column[] = 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 int 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.elastic.co/guide/en/elasticsearch/reference/current/search-count.html
// http://www.elastic.co/guide/en/elasticsearch/reference/1.x/_search_requests.html
$options = [];
$options['search_type'] = 'count';
return $this->createCommand($db)->search($options)['hits']['total'];
}
/**
* 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 bool whether the query result contains any row of data.
*/
public function exists($db = null)
{
return self::one($db) !== false;
}
/**
* Adds a 'stats' part to the query.
* @param array $groups an array of groups to maintain a statistics aggregation for.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search.html#stats-groups
*/
public function stats($groups)
{
$this->stats = $groups;
return $this;
}
/**
* Sets a highlight parameters to retrieve from the documents.
* @param array $highlight array of parameters to highlight results.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-highlighting.html
*/
public function highlight($highlight)
{
$this->highlight = $highlight;
return $this;
}
/**
* @deprecated since 2.0.5 use addAggragate() instead
*
* Adds an aggregation to this query.
* @param string $name the name of the aggregation
* @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`...
* @param string|array $options the configuration options for this aggregation. Can be an array or a json string.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
*/
public function addAggregation($name, $type, $options)
{
return $this->addAggregate($name, [$type => $options]);
}
/**
* @deprecated since 2.0.5 use addAggragate() instead
*
* Adds an aggregation to this query.
*
* This is an alias for [[addAggregation]].
*
* @param string $name the name of the aggregation
* @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`...
* @param string|array $options the configuration options for this aggregation. Can be an array or a json string.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
*/
public function addAgg($name, $type, $options)
{
return $this->addAggregate($name, [$type => $options]);
}
/**
* Adds an aggregation to this query. Supports nested aggregations.
* @param string $name the name of the aggregation
* @param string $type the aggregation type. e.g. `terms`, `range`, `histogram`...
* @param string|array $options the configuration options for this aggregation. Can be an array or a json string.
* @return $this the query object itself
* @see https://www.elastic.co/guide/en/elasticsearch/reference/2.3/search-aggregations.html
*/
public function addAggregate($name, $options)
{
$this->aggregations[$name] = $options;
return $this;
}
/**
* Adds a suggester to this query.
* @param string $name the name of the suggester
* @param string|array $definition the configuration options for this suggester. Can be an array or a json string.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html
*/
public function addSuggester($name, $definition)
{
$this->suggest[$name] = $definition;
return $this;
}
// TODO add validate query http://www.elastic.co/guide/en/elasticsearch/reference/current/search-validate.html
// TODO support multi query via static method http://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html
/**
* Sets the query part of this search query.
* @param string|array $query
* @return $this the query object itself
*/
public function query($query)
{
$this->query = $query;
return $this;
}
/**
* Starts a batch query.
*
* A batch query supports fetching data in batches, which can keep the memory usage under a limit.
* This method will return a [[BatchQueryResult]] object which implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
*
* For example,
*
* ```php
* $query = (new Query)->from('user');
* foreach ($query->batch() as $rows) {
* // $rows is an array of 10 or fewer rows from user table
* }
* ```
*
* Batch size is determined by the `limit` setting (note that in scan mode batch limit is per shard).
*
* @param string $scrollWindow how long Elasticsearch should keep the search context alive,
* in [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units)
* @param Connection $db the database connection. If not set, the `elasticsearch` application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
* @since 2.0.4
*/
public function batch($scrollWindow = '1m', $db = null)
{
return Yii::createObject([
'class' => BatchQueryResult::className(),
'query' => $this,
'scrollWindow' => $scrollWindow,
'db' => $db,
'each' => false,
]);
}
/**
* Starts a batch query and retrieves data row by row.
* This method is similar to [[batch()]] except that in each iteration of the result,
* only one row of data is returned. For example,
*
* ```php
* $query = (new Query)->from('user');
* foreach ($query->each() as $row) {
* }
* ```
*
* @param string $scrollWindow how long Elasticsearch should keep the search context alive,
* in [time units](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#time-units)
* @param Connection $db the database connection. If not set, the `elasticsearch` application component will be used.
* @return BatchQueryResult the batch query result. It implements the [[\Iterator]] interface
* and can be traversed to retrieve the data in batches.
* @since 2.0.4
*/
public function each($scrollWindow = '1m', $db = null)
{
return Yii::createObject([
'class' => BatchQueryResult::className(),
'query' => $this,
'scrollWindow' => $scrollWindow,
'db' => $db,
'each' => true,
]);
}
/**
* Sets the filter part of this search query.
* @param string $filter
* @return $this the query object itself
*/
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.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-multi-index-type
*/
public function from($index, $type = null)
{
$this->index = $index;
$this->type = $type;
return $this;
}
/**
* Sets the fields to retrieve from the documents.
* @param array $fields the fields to be selected.
* @return $this the query object itself
* @see http://www.elastic.co/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 source filtering, specifying how the `_source` field of the document should be returned.
* @param array $source the source patterns to be selected.
* @return $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-source-filtering.html
*/
public function source($source)
{
if (is_array($source) || $source === null) {
$this->source = $source;
} else {
$this->source = func_get_args();
}
return $this;
}
/**
* Sets the search timeout.
* @param int $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 $this the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html#_parameters_5
*/
public function timeout($timeout)
{
$this->timeout = $timeout;
return $this;
}
/**
* @param float $minScore Exclude documents which have a `_score` less than the minimum specified minScore
* @return static the query object itself
* @see http://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-min-score.html
* @since 2.0.4
*/
public function minScore($minScore)
{
$this->minScore = $minScore;
return $this;
}
/**
* Sets the options to be passed to the command created by this query.
* @param array $options the options to be set.
* @return $this the query object itself
* @throws InvalidParamException if $options is not an array
* @see Command::$options
* @since 2.0.4
*/
public function options($options)
{
if (!is_array($options)) {
throw new InvalidParamException('Array parameter expected, ' . gettype($options) . ' received.');
}
$this->options = $options;
return $this;
}
/**
* Adds more options, overwriting existing options.
* @param array $options the options to be added.
* @return $this the query object itself
* @throws InvalidParamException if $options is not an array
* @see options()
* @since 2.0.4
*/
public function addOptions($options)
{
if (!is_array($options)) {
throw new InvalidParamException('Array parameter expected, ' . gettype($options) . ' received.');
}
$this->options = array_merge($this->options, $options);
return $this;
}
/**
* Set the `post_filter` part of the search query.
* @param string|array $filter
* @return $this the query object itself
* @see $postFilter
* @since 2.0.5
*/
public function postFilter($filter)
{
$this->postFilter = $filter;
return $this;
}
/**
* Explain for how the score of each document was computer
* @param $explain
* @return $this
* @see $explain
* @since 2.0.5
*/
public function explain($explain)
{
$this->explain = $explain;
return $this;
}
}
<?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\BaseObject;
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 BaseObject
{
/**
* @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 === []) {
$parts['fields'] = [];
} elseif ($query->fields !== null) {
$fields = [];
$scriptFields = [];
foreach ($query->fields as $key => $field) {
if (is_int($key)) {
$fields[] = $field;
} else {
$scriptFields[$key] = $field;
}
}
if (!empty($fields)) {
$parts['fields'] = $fields;
}
if (!empty($scriptFields)) {
$parts['script_fields'] = $scriptFields;
}
}
if ($query->source !== null) {
$parts['_source'] = $query->source;
}
if ($query->limit !== null && $query->limit >= 0) {
$parts['size'] = $query->limit;
}
if ($query->offset > 0) {
$parts['from'] = (int)$query->offset;
}
if (isset($query->minScore)) {
$parts['min_score'] = (float)$query->minScore;
}
if (isset($query->explain)) {
$parts['explain'] = $query->explain;
}
if (empty($query->query)) {
$parts['query'] = ["match_all" => (object)[]];
} else {
$parts['query'] = $query->query;
}
$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;
}
if (!empty($query->highlight)) {
$parts['highlight'] = $query->highlight;
}
if (!empty($query->aggregations)) {
$parts['aggregations'] = $query->aggregations;
}
if (!empty($query->stats)) {
$parts['stats'] = $query->stats;
}
if (!empty($query->suggest)) {
$parts['suggest'] = $query->suggest;
}
if (!empty($query->postFilter)) {
$parts['post_filter'] = $query->postFilter;
}
$sort = $this->buildOrderBy($query->orderBy);
if (!empty($sort)) {
$parts['sort'] = $sort;
}
$options = $query->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 == '_id') {
$column = '_uid';
}
// allow elasticsearch extended syntax as described in http://www.elastic.co/guide/en/elasticsearch/guide/master/_sorting.html
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.
* @throws \yii\base\InvalidParamException if unknown operator is used in query
* @throws \yii\base\NotSupportedException if string conditions are used in where
* @return string the generated SQL expression
*/
public function buildCondition($condition)
{
static $builders = [
'not' => 'buildNotCondition',
'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',
'lt' => 'buildHalfBoundedRangeCondition',
'<' => 'buildHalfBoundedRangeCondition',
'lte' => 'buildHalfBoundedRangeCondition',
'<=' => 'buildHalfBoundedRangeCondition',
'gt' => 'buildHalfBoundedRangeCondition',
'>' => 'buildHalfBoundedRangeCondition',
'gte' => 'buildHalfBoundedRangeCondition',
'>=' => 'buildHalfBoundedRangeCondition',
];
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 == '_id') {
if ($value === null) { // there is no null pk
$parts[] = ['terms' => ['_uid' => []]]; // this condition is equal to WHERE false
} 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 buildNotCondition($operator, $operands)
{
if (count($operands) != 1) {
throw new InvalidParamException("Operator '$operator' requires exactly one operand.");
}
$operand = reset($operands);
if (is_array($operand)) {
$operand = $this->buildCondition($operand);
}
return [$operator => $operand];
}
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 == '_id') {
throw new NotSupportedException('Between condition is not supported for the _id field.');
}
$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' ? ['terms' => ['_uid' => []]] : []; // this condition is equal to WHERE false
}
if (count($column) > 1) {
return $this->buildCompositeInCondition($operator, $column, $values);
} 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 == '_id') {
if (empty($values) && $canBeNull) { // there is no null pk
$filter = ['terms' => ['_uid' => []]]; // this condition is equal to WHERE false
} 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;
}
/**
* Builds a half-bounded range condition
* (for "gt", ">", "gte", ">=", "lt", "<", "lte", "<=" operators)
* @param string $operator
* @param array $operands
* @return array Filter expression
*/
private function buildHalfBoundedRangeCondition($operator, $operands)
{
if (!isset($operands[0], $operands[1])) {
throw new InvalidParamException("Operator '$operator' requires two operands.");
}
list($column, $value) = $operands;
if ($column == '_id') {
$column = '_uid';
}
$range_operator = null;
if (in_array($operator, ['gte', '>='])) {
$range_operator = 'gte';
} elseif (in_array($operator, ['lte', '<='])) {
$range_operator = 'lte';
} elseif (in_array($operator, ['gt', '>'])) {
$range_operator = 'gt';
} elseif (in_array($operator, ['lt', '<'])) {
$range_operator = 'lt';
}
if ($range_operator === null) {
throw new InvalidParamException("Operator '$operator' is not implemented.");
}
$filter = [
'range' => [
$column => [
$range_operator => $value
]
]
];
return $filter;
}
protected function buildCompositeInCondition($operator, $columns, $values)
{
throw new NotSupportedException('composite in is not supported by elasticsearch.');
}
private function buildLikeCondition($operator, $operands)
{
throw new NotSupportedException('like conditions are not supported by elasticsearch.');
}
}
<p align="center">
<a href="https://www.elastic.co/products/elasticsearch" target="_blank" rel="external">
<img src="https://static-www.elastic.co/assets/blt45b0886c90beceee/logo-elastic.svg" height="80px">
</a>
<h1 align="center">Elasticsearch Query and ActiveRecord for Yii 2</h1>
<br>
</p>
This extension provides the [elasticsearch](https://www.elastic.co/products/elasticsearch) integration for the [Yii framework 2.0](http://www.yiiframework.com).
It includes basic querying/search support and also implements the `ActiveRecord` pattern that allows you to store active
records in elasticsearch.
For license information check the [LICENSE](LICENSE.md)-file.
Documentation is at [docs/guide/README.md](docs/guide/README.md).
[![Latest Stable Version](https://poser.pugx.org/yiisoft/yii2-elasticsearch/v/stable.png)](https://packagist.org/packages/yiisoft/yii2-elasticsearch)
[![Total Downloads](https://poser.pugx.org/yiisoft/yii2-elasticsearch/downloads.png)](https://packagist.org/packages/yiisoft/yii2-elasticsearch)
[![Build Status](https://travis-ci.org/yiisoft/yii2-elasticsearch.svg?branch=master)](https://travis-ci.org/yiisoft/yii2-elasticsearch)
Requirements
------------
Dependent on the version of elasticsearch you are using you need a different version of this extension.
- Extension version 2.0.x works with elasticsearch version 1.0 to 4.x.
- Extension version 2.1.x requires at least elasticsearch version 5.0.
Installation
------------
The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
Either run
```
php composer.phar require --prefer-dist yiisoft/yii2-elasticsearch
```
or add
```json
"yiisoft/yii2-elasticsearch": "~2.0.0"
```
to the require section of your composer.json.
Configuration
-------------
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
],
],
]
];
```
{
"name": "yiisoft/yii2-elasticsearch",
"description": "Elasticsearch integration and ActiveRecord for the Yii framework",
"keywords": ["yii2", "elasticsearch", "active-record", "search", "fulltext"],
"type": "yii2-extension",
"license": "BSD-3-Clause",
"support": {
"issues": "https://github.com/yiisoft/yii2-elasticsearch/issues",
"forum": "http://www.yiiframework.com/forum/",
"wiki": "http://www.yiiframework.com/wiki/",
"irc": "irc://irc.freenode.net/yii",
"source": "https://github.com/yiisoft/yii2-elasticsearch"
},
"authors": [
{
"name": "Carsten Brandt",
"email": "mail@cebe.cc"
}
],
"require": {
"yiisoft/yii2": "~2.0.14",
"ext-curl": "*"
},
"autoload": {
"psr-4": { "yii\\elasticsearch\\": "" }
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment