├── README.md ├── composer.json ├── config └── fecshop_elasticsearch.php ├── controllers └── ElasticsearchController.php ├── models └── elasticSearch │ ├── Category.php │ └── Product.php ├── services └── search │ └── ElasticSearch.php └── yii2-elasticsearch ├── ActiveRecord.php ├── Command.php ├── Connection.php └── README.md /README.md: -------------------------------------------------------------------------------- 1 | Fecshop ElasticSearch 2 | ====================== 3 | 4 | > fecshop elasticsearch 功能部分,用于将分类,产品,搜索页面,底层使用elasticSearch支持 5 | 6 | 7 | ### 环境安装 8 | 9 | 1.安装elasticSearch 6.1 10 | 11 | http://www.fecshop.com/topic/672 12 | 13 | 2.安装elasticSearch 可视化工具 kibana 14 | 15 | http://www.fecshop.com/topic/668 16 | 17 | 18 | ### 安装fecshop elasticSearch扩展,也就是本扩展 19 | 20 | 1.安装 21 | 22 | ``` 23 | composer require --prefer-dist fancyecommerce/fecshop_elasticsearch 24 | ``` 25 | 26 | or 在根目录的`composer.json`中添加 27 | 28 | ``` 29 | "fancyecommerce/fecshop_elasticsearch": "~1.xx" // 使用最新版本号 ,在这里查看:https://github.com/fecshop/yii2_fecshop_elasticsearch/releases 30 | 31 | ``` 32 | 33 | 然后执行 34 | 35 | ``` 36 | composer update 37 | ``` 38 | 39 | ### yii2-elasticSearch 扩展bug处理 40 | 41 | yii2-elasticSearch目前还不支持 ElasticSearch 6 的处理,会有问题,而且yii2项目组好像很忙:https://github.com/yiisoft/yii2-elasticsearch/issues/167#issuecomment-364055614 42 | , 因此对其进行了略微改动(比较粗暴的改动,仅支持本插件) 43 | 44 | 1.在根目录`composer.json`中添加 45 | `"yiisoft/yii2-elasticsearch": "2.1@dev",` 46 | ,然后执行`composer update` 47 | 48 | 2.更新后,然后将 vendor/fancyecommerce/fecshop_elasticsearch/yii2-elasticSearch 49 | 下的三个php文件覆盖到`/vendor/yiisoft/yii2-elasticsearch` 下即可 50 | 51 | 3.当yii2-elasticSearch 项目组修复了这个问题后,此处将不需要执行(这个只能等官方了) 52 | 53 | ### 配置 54 | 55 | 1.添加当前扩展的配置到fecshop 56 | 57 | 将 `vendor/fancyecommerce/fecshop_elasticsearch/config/fecshop_elasticsearch.php`文件复制到 `common/config/fecshop_third_extensions/`下面 58 | ,然后打开这个文件 59 | 60 | 1.1在 `nodes` 处配置`ip`和`port`,配置ES的连接 61 | 62 | 1.2在`searchLang`处,配置支持的语言,也就是把您的网站的语言都填写过来,那么,这些语言就会使用 63 | `elasticSearch`搜索。 64 | 65 | 66 | 2.将mongodb和xunsearch搜索的语言改为elasticSearch 67 | 68 | 打开文件 `common/config/fecshop_local_services/Search.php` 69 | 70 | 将 mongodb 和 xunsearch 部分的搜索语言部分注释掉, 71 | 如果您想要某些语言继续使用mongodb或xunsearch搜索,那么可以保留某些语言, 72 | 各个搜索引擎的`searchLang`中的语言都是唯一的,不要一种语言出现在2个搜索引擎里面 73 | 74 | ``` 75 | // mongodb 76 | /* 77 | 'searchLang' => [ 78 | 'en' => 'english', 79 | 'fr' => 'french', 80 | 'de' => 'german', 81 | 'es' => 'spanish', 82 | 'ru' => 'russian', 83 | 'pt' => 'portuguese', 84 | ], 85 | */ 86 | 87 | // xunsearch 88 | /* 89 | 'searchLang' => [ 90 | 'zh' => 'chinese', 91 | ], 92 | */ 93 | 94 | ``` 95 | 96 | 3.初始化数据 97 | 98 | fecshop 根目录下执行 99 | 100 | 3.1、新建elasticSearch的mapping(必须执行) 101 | 102 | ``` 103 | ./yii elasticsearch/updatemapping 104 | ``` 105 | 106 | 3.2、删除es的产品index(当您的mapping中的某个字段需要修改,直接修改是无效的,只能删除index库,然后重建) 107 | 108 | ``` 109 | ./yii elasticsearch/clean 110 | ``` 111 | 112 | 3.3、同步产品到elasticSearch(必须执行) 113 | 114 | ``` 115 | cd vendor/fancyecommerce/fecshop/shell/search/ 116 | sh fullSearchSync.sh 117 | ``` 118 | 119 | 3.4、然后,es部分就可以访问了 120 | 121 | 3.5、当您的产品信息有改动的时候,产品保存的时候,会自动同步到ES的 122 | 123 | ### 扩展 124 | 125 | 126 | 搜索的model路径为: `vendor/fancyecommerce/fecshop_elasticsearch/models/elasticSearch/Product.php` 127 | ,如果您想更改里面的内容,您可以重写这个model,进行更改, 128 | 重写model可以参看: 129 | 130 | [通过rewriteMap进行重写Block Model 层](http://www.fecshop.com/doc/fecshop-guide/develop/cn-1.0/guide-fecshop-rewrite-func.html#8rewritemapblock-model) 131 | 132 | 133 | ### 备注 134 | 135 | 1.支持的语言 136 | 137 | https://github.com/fecshop/yii2_fecshop_elasticsearch/blob/master/models/elasticSearch/Product.php 138 | 139 | 暂时支持这些语言 140 | 141 | 142 | ``` 143 | 'zh' => 'cjk', // 中国 144 | 'kr' => 'cjk', // 韩国 145 | 'jp' => 'cjk', // 日本 146 | 'en' => 'english', // 147 | 'fr' => 'french', 148 | 'de' => 'german', 149 | 'it' => 'italian', 150 | 'pt' => 'portuguese', 151 | 'es' => 'spanish', 152 | 'ru' => 'russian', 153 | 'nl' => 'dutch', 154 | 'br' => 'brazilian', 155 | ``` 156 | 157 | Es6默认就支持的analysis,详细参看:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html#english-analyzer 158 | 159 | 2.产品搜索index 160 | 161 | 一个语言一个index(elasticSearch的index,有一点点类似mysql的数据库,type,一点点类似表 162 | ,不过完全不同。) 163 | 164 | 165 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fancyecommerce/fecshop_elasticsearch", 3 | "description": "fecshop elasticsearch extension for category, product, search page ", 4 | "keywords": [ 5 | "yii2", 6 | "fecshop", 7 | "elasticsearch" 8 | ], 9 | "homepage": "https://github.com/fecshop/yii2_fecshop_elasticsearch", 10 | "type": "yii2-extension", 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "source": "https://github.com/fecshop/yii2_fecshop_elasticsearch" 14 | }, 15 | "authors": [ 16 | { 17 | "name": "terry", 18 | "email": "2358269014@qq.com" 19 | } 20 | ], 21 | "minimum-stability": "stable", 22 | "require": { 23 | "yiisoft/yii2-elasticsearch": "~2.0.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "fecshop\\elasticsearch\\": "" 28 | } 29 | }, 30 | "repositories": [ 31 | { 32 | "type": "composer", 33 | "url": "https://asset-packagist.org" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /config/fecshop_elasticsearch.php: -------------------------------------------------------------------------------- 1 | true, 15 | // 各个入口的配置 16 | 'app' => [ 17 | // 1.公用层 18 | 'common' => [ 19 | // 在公用层的开关,设置成false后,公用层的配置将失效 20 | 'enable' => true, 21 | // 公用层的具体配置下载下面 22 | 'config' => [ 23 | 'components' => [ 24 | 'elasticsearch' => [ 25 | 'class' => 'yii\elasticsearch\Connection', 26 | 'nodes' => [ 27 | // 配置elasticSearch的ip和host 28 | ['http_address' => '127.0.0.1:9200'], 29 | // configure more hosts if you have a cluster 30 | ], 31 | ], 32 | ], 33 | 'services' => [ 34 | 'search' => [ 35 | 'childService' => [ 36 | 'elasticSearch' => [ 37 | 'class' => 'fecshop\elasticsearch\services\search\ElasticSearch', 38 | 'enableService' => true, 39 | // 下面这些语言将使用Es搜索引擎,当然,您需要在其他的搜索引擎中剔除下面的搜索语言 40 | 'searchLang' => [ 41 | 'en' => 'english', 42 | 'fr' => 'french', 43 | 'de' => 'german', 44 | 'es' => 'spanish', 45 | 'ru' => 'russian', 46 | 'pt' => 'portuguese', 47 | 'zh' => 'chinese', 48 | ], 49 | ], 50 | ], 51 | ], 52 | ], 53 | ], 54 | ], 55 | 56 | 57 | 'console' => [ 58 | // 在公用层的开关,设置成false后,公用层的配置将失效 59 | 'enable' => true, 60 | // 公用层的具体配置下载下面 61 | 'config' => [ 62 | 'controllerMap'=>[ 63 | 'elasticsearch'=>[ 64 | 'class'=>'fecshop\elasticsearch\controllers\ElasticsearchController' 65 | ], 66 | ], 67 | 68 | ], 69 | ], 70 | 71 | 72 | 73 | ], 74 | 75 | ]; 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /controllers/ElasticsearchController.php: -------------------------------------------------------------------------------- 1 | 17 | * @since 1.0 18 | */ 19 | class ElasticsearchController extends Controller 20 | { 21 | protected $_numPerPage = 50; 22 | 23 | /** 24 | * 通过脚本,把产品的相应语言信息添加到相应的新表中。 25 | * 然后在相应语言搜索的时候,自动去相应的表中查数据。 26 | * 为什么需要搞这么多表呢?因为mongodb的全文搜索(fullSearch)索引,在一个表中只能有一个。 27 | * 这个索引可以是多个字段的组合索引, 28 | * 因此,对于多语言的产品搜索就需要搞几个表了。 29 | * 下面的功能: 30 | * 1. 将产品的name description price img score sku spu等信息更新过来。 31 | 32 | */ 33 | public function actionSyncproduct($pageNum) 34 | { 35 | $filter['numPerPage'] = $this->_numPerPage; 36 | $filter['pageNum'] = $pageNum; 37 | $filter['asArray'] = true; 38 | $products = Yii::$service->product->coll($filter); 39 | $product_ids = []; 40 | $langs = Yii::$service->fecshoplang->getAllLangCode(); 41 | 42 | foreach ($products['coll'] as $one) { 43 | //var_dump($one); 44 | //exit; 45 | foreach ($langs as $langCode) { 46 | $_id = $one['_id']; 47 | $name = Yii::$service->fecshoplang->getLangAttrVal($one['name'], 'name', $langCode); 48 | $description = Yii::$service->fecshoplang->getLangAttrVal($one['description'], 'description', $langCode); 49 | $short_description = Yii::$service->fecshoplang->getLangAttrVal($one['short_description'], 'short_description', $langCode); 50 | 51 | $spu = $one['spu']; 52 | $sku = $one['sku']; 53 | $score = $one['score']; 54 | $status = $one['status']; 55 | $is_in_stock = $one['is_in_stock']; 56 | $url_key = $one['url_key']; 57 | $price = $one['price']; 58 | $cost_price = $one['cost_price']; 59 | $special_price = $one['special_price']; 60 | $special_from = $one['special_from']; 61 | $special_to = $one['special_to']; 62 | $image = serialize($one['image']); 63 | $created_at = $one['created_at']; 64 | $sync_updated_at = $one['sync_updated_at']; 65 | $final_price = Yii::$service->product->price->getFinalPrice($price, $special_price, $special_from, $special_to); 66 | 67 | EsProduct::initLang($langCode); 68 | $esOne = EsProduct::findOne($_id); 69 | if (!$esOne['sku']) { // !$esOne->getPrimaryKey() 70 | $esOne = new EsProduct; 71 | $esOne['_id'] = $_id; 72 | } 73 | $esOne['name'] = $name; 74 | $esOne['description'] = $description; 75 | $esOne['short_description'] = $short_description; 76 | $esOne['spu'] = $spu; 77 | $esOne['sku'] = $sku; 78 | $esOne['score'] = $score; 79 | $esOne['status'] = $status; 80 | $esOne['is_in_stock'] = $is_in_stock; 81 | $esOne['url_key'] = $url_key; 82 | $esOne['price'] = $price; 83 | $esOne['cost_price'] = $cost_price; 84 | $esOne['special_price'] = $special_price; 85 | $esOne['special_from'] = $special_from; 86 | $esOne['special_to'] = $special_to; 87 | $esOne['image'] = $image; 88 | $esOne['created_at'] = $created_at; 89 | $esOne['sync_updated_at'] = $sync_updated_at; 90 | $esOne['final_price'] = $final_price; 91 | $esOne->save(); 92 | } 93 | } 94 | } 95 | // 清空es中的产品 96 | public function actionClean(){ 97 | Yii::$service->search->elasticSearch->esDeleteAllProduct(); 98 | 99 | } 100 | 101 | /** 102 | * 得到产品的总数。 103 | */ 104 | public function actionProductcount() 105 | { 106 | $count = Yii::$service->product->collCount($filter); 107 | echo $count; 108 | } 109 | 110 | public function actionProductpagenum() 111 | { 112 | $count = Yii::$service->product->collCount($filter); 113 | echo ceil($count / $this->_numPerPage); 114 | } 115 | // 更新es的mapping 116 | public function actionUpdatemapping(){ 117 | Yii::$service->search->elasticSearch->updateMapping(); 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /models/elasticSearch/Category.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fecshop/yii2_fecshop_elasticsearch/5adbff5e0c336f17d685131758c50f44d7b6fdbe/models/elasticSearch/Category.php -------------------------------------------------------------------------------- /models/elasticSearch/Product.php: -------------------------------------------------------------------------------- 1 | 15 | * @since 1.0 16 | */ 17 | class Product extends ActiveRecord 18 | { 19 | protected $_attr; 20 | protected static $_lang; 21 | // elasticSearch 语言 analysis 分词器 22 | protected static $_lang_analysis; 23 | /** 24 | * 配置数组,语言简码 和 对应的es分词器名称analysis 25 | * 主流语言都已经配置,如果您想配置其他的语言,自行添加analysis 26 | * 详细参看:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html#english-analyzer 27 | */ 28 | public static $langAnalysis = [ 29 | 'zh' => 'cjk', // 中国 30 | 'kr' => 'cjk', // 韩国 31 | 'jp' => 'cjk', // 日本 32 | 'en' => 'english', // 33 | 'fr' => 'french', 34 | 'de' => 'german', 35 | 'it' => 'italian', 36 | 'pt' => 'portuguese', 37 | 'es' => 'spanish', 38 | 'ru' => 'russian', 39 | 'nl' => 'dutch', 40 | 'br' => 'brazilian', 41 | ]; 42 | /** 43 | * Language Analyzers 44 | * A set of analyzers aimed at analyzing specific language text. The following types are supported: 45 | * arabic, armenian, basque, bengali, brazilian, bulgarian, catalan, cjk, czech, danish, dutch, english, finnish, french, galician, german, greek, hindi, hungarian, indonesian, irish, italian, latvian, lithuanian, norwegian, persian, portuguese, romanian, russian, sorani, spanish, swedish, turkish, thai. 46 | * cjk : 中日韩 47 | * english :英语 48 | * french :法语 49 | * german :德语 50 | * italian :意大利语 51 | * portuguese :葡萄牙语 52 | * spanish :西班牙语 53 | * russian :俄语 54 | * dutch :荷兰语 55 | * brazilian :巴西语 56 | * 详细参看:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html#english-analyzer 57 | * 58 | */ 59 | public static function initLang($lang){ 60 | $arr = self::$langAnalysis; 61 | if (isset($arr[$lang]) && $arr[$lang]) { 62 | self::$_lang = $lang; 63 | self::$_lang_analysis = $arr[$lang]; 64 | } 65 | } 66 | /** 67 | * 主键 68 | */ 69 | public static function primaryKey(){ 70 | return ['_id']; 71 | } 72 | 73 | /** 74 | * es component 75 | */ 76 | public static function getDb() 77 | { 78 | return \Yii::$app->get('elasticsearch'); 79 | } 80 | 81 | /** 82 | * index , 有一点类似数据库的db,但完全不是一个概念 83 | */ 84 | public static function index() 85 | { 86 | if (!self::$_lang) { 87 | throw new InvalidConfigException('you must run func initLang($lang) first!'); 88 | } 89 | return 'fecshop_product_'.self::$_lang; 90 | } 91 | /** 92 | * 获得属性 93 | */ 94 | public function attributes() 95 | { 96 | 97 | if (!$this->_attr) { 98 | $mapConfig = self::mapConfig(true); 99 | $this->_attr = array_keys($mapConfig['properties']); 100 | } 101 | return $this->_attr; 102 | } 103 | /** 104 | * elasticsearch map config 105 | * 关于elasticSearch的mapping,参看:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html 106 | * 下面是fecshop默认添加的mapping,如果您想添加其他字段的mapping, 107 | 在return函数中自行添加, 108 | 因为 109 | */ 110 | public static function mapConfig($noAnalysis = false){ 111 | if ($noAnalysis) { 112 | $analysis = ''; 113 | } else { 114 | if (!self::$_lang_analysis) { 115 | throw new InvalidConfigException('you must run func initLang($lang) first!'); 116 | } 117 | $analysis = self::$_lang_analysis; 118 | } 119 | return [ 120 | 'properties' => [ 121 | 'm_id' => ['type' => 'keyword',], 122 | //'product_id' => ['type' => 'string',"index" => "not_analyzed", "analyzer": $analysis], 123 | //'product_id' => ['type' => 'keyword',], 124 | 'name' => ['type' => 'text', "analyzer" => $analysis], 125 | 'short_description' => ['type' => 'text', "analyzer" => $analysis], 126 | 'description' => ['type' => 'text', "analyzer" => $analysis], 127 | 128 | 'spu' => ['type' => 'keyword'], 129 | 'sku' => ['type' => 'keyword'], 130 | 'score' => ['type' => 'integer'], 131 | 'status' => ['type' => 'integer'], 132 | 'is_in_stock' => ['type' => 'integer'], 133 | 'url_key' => ['type' => 'keyword'], 134 | 'price' => ['type' => 'float'], 135 | 'cost_price' => ['type' => 'float'], 136 | 'special_price' => ['type' => 'float'], 137 | 'special_from' => ['type' => 'integer'], 138 | 'special_to' => ['type' => 'integer'], 139 | 'final_price' => ['type' => 'float'], 140 | 'image' => ['type' => 'text', 'index' => false], 141 | 'created_at' => ['type' => 'integer'], 142 | 'sync_updated_at' => ['type' => 'integer'], 143 | 144 | 'color' => ['type' => 'keyword',], // 需要聚合的字段,需要加入:'fielddata': true 145 | 'size' => ['type' => 'keyword',], 146 | ] 147 | ]; 148 | } 149 | /** 150 | * mapping 151 | */ 152 | public static function mapping() 153 | { 154 | return [ 155 | static::type() => self::mapConfig(), 156 | ]; 157 | } 158 | 159 | /** 160 | * Set (update) mappings for this model 161 | */ 162 | public static function updateMapping(){ 163 | $db = self::getDb(); 164 | $command = $db->createCommand(); 165 | if(!$command->indexExists(self::index())){ 166 | $command->createIndex(self::index()); 167 | } 168 | $command->setMapping(self::index(), self::type(), self::mapping()); 169 | } 170 | /** 171 | * get mapping 172 | */ 173 | public static function getMapping(){ 174 | $db = self::getDb(); 175 | $command = $db->createCommand(); 176 | return $command->getMapping(); 177 | } 178 | 179 | /** 180 | * 删除index 181 | */ 182 | public static function deleteIndex() 183 | { 184 | $db = static::getDb(); 185 | $command = $db->createCommand(); 186 | $command->deleteIndex(static::index(), static::type()); 187 | } 188 | 189 | 190 | } -------------------------------------------------------------------------------- /services/search/ElasticSearch.php: -------------------------------------------------------------------------------- 1 | 19 | * @since 1.0 20 | */ 21 | class ElasticSearch extends Service implements SearchInterface 22 | { 23 | // Es搜索引擎支持的语言,也就是那些语言,使用Es搜索引擎。 24 | public $searchLang; 25 | // 匹配类型,目前使用的是 cross_fields, 其他的搜索类型详细,您可以参看: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types 26 | public $type = 'cross_fields'; 27 | // 产品model 28 | protected $_productModelName = '\fecshop\models\mongodb\Product'; 29 | protected $_productModel; 30 | // Es Product model 31 | protected $_searchModelName = '\fecshop\elasticsearch\models\elasticSearch\Product'; 32 | protected $_searchModel; 33 | 34 | /** 35 | * Es不同于Mongo等,他可以一次将搜索的产品列表,以及聚合属性以及属性的count,一次计算出来 36 | * 因此,在产品产品的函数中,就把聚合数据计算出来,存放到该变量 37 | * 后面的函数取聚合数据,直接从这个变量中取出来即可。 38 | */ 39 | public $filter_values; 40 | 41 | public function init() 42 | { 43 | parent::init(); 44 | list($this->_productModelName,$this->_productModel) = \Yii::mapGet($this->_productModelName); 45 | list($this->_searchModelName,$this->_searchModel) = \Yii::mapGet($this->_searchModelName); 46 | } 47 | /** 48 | * Mongodb初始化索引.Es不需要该函数,Es只需要新建mapping就可以了。 49 | */ 50 | protected function actionInitFullSearchIndex() 51 | { 52 | } 53 | 54 | /** 55 | * 将产品信息同步到xunSearch引擎中. 56 | */ 57 | protected function actionSyncProductInfo($product_ids, $numPerPage) 58 | { 59 | if (is_array($product_ids) && !empty($product_ids)) { 60 | $productPrimaryKey = Yii::$service->product->getPrimaryKey(); 61 | $elasticSearchModel = new $this->_searchModelName(); 62 | $filter['select'] = $elasticSearchModel->attributes(); 63 | $filter['asArray'] = true; 64 | $filter['where'][] = ['in', $productPrimaryKey, $product_ids]; 65 | $filter['numPerPage'] = $numPerPage; 66 | $filter['pageNum'] = 1; 67 | $coll = Yii::$service->product->coll($filter); 68 | if (is_array($coll['coll']) && !empty($coll['coll'])) { 69 | foreach ($coll['coll'] as $one) { 70 | $one_name = $one['name']; 71 | $one_description = $one['description']; 72 | $one_short_description = $one['short_description']; 73 | if (!empty($this->searchLang) && is_array($this->searchLang)) { 74 | foreach ($this->searchLang as $langCode => $langName) { 75 | //echo $langCode; 76 | $one['_id'] = (string) $one['_id']; 77 | // yii2 elasticSearch bug问题进行的处理 78 | $one['m_id'] = (string) $one['_id']; 79 | $this->_searchModel::initLang($langCode); 80 | $elasticSearchModel = $this->_searchModel->findOne($one['_id']); 81 | if (!$elasticSearchModel['sku']) { 82 | $elasticSearchModel = new $this->_searchModelName(); 83 | $elasticSearchModel::initLang($langCode); 84 | } 85 | //$elasticSearchModel->_id = (string) $one['_id']; 86 | $one['name'] = Yii::$service->fecshoplang->getLangAttrVal($one_name, 'name', $langCode); 87 | $one['description'] = Yii::$service->fecshoplang->getLangAttrVal($one_description, 'description', $langCode); 88 | $one['short_description'] = Yii::$service->fecshoplang->getLangAttrVal($one_short_description, 'short_description', $langCode); 89 | $one['sync_updated_at'] = time(); 90 | $serialize = true; 91 | Yii::$service->helper->ar->save($elasticSearchModel, $one, $serialize); 92 | if ($errors = Yii::$service->helper->errors->get()) { 93 | // 报错。 94 | echo $errors; 95 | //return false; 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | //echo "XunSearch sync done ... \n"; 103 | 104 | return true; 105 | } 106 | 107 | // 废弃 108 | protected function actionDeleteNotActiveProduct($nowTimeStamp) 109 | { 110 | } 111 | 112 | /** 113 | * 删除在EsSearch的所有搜索数据, 114 | * 当您的产品有很多产品被删除了,但是在Es存在某些异常没有被删除 115 | * 您希望也被删除掉,那么,你可以通过这种方式批量删除掉产品 116 | * 然后重新跑一边同步脚本. 117 | */ 118 | protected function actionEsDeleteAllProduct() 119 | { 120 | if (!empty($this->searchLang) && is_array($this->searchLang)) { 121 | foreach ($this->searchLang as $langCode => $langName) { 122 | $this->_searchModel::initLang($langCode); 123 | $this->_searchModel::deleteIndex(); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * @property $select | Array , 搜索的字段 130 | * @property $where | Array ,搜索的条件 131 | * @property $pageNum | 页数 132 | * @property $numPerPage | 每页的产品数 133 | * @property $product_search_max_count | int ,最大搜索的个数,搜索引擎是没有分页概念的,只有一次查出来所有结果,因此需要限制搜索的最大数 134 | * @property $filterAttr | Array,聚合的字段 135 | * 得到搜索的产品列表. 136 | */ 137 | protected function actionGetSearchProductColl($select, $where, $pageNum, $numPerPage, $product_search_max_count, $filterAttr) 138 | { 139 | $collection = $this->fullTearchText($select, $where, $pageNum, $numPerPage, $product_search_max_count, $filterAttr); 140 | $collection['coll'] = Yii::$service->category->product->convertToCategoryInfo($collection['coll']); 141 | 142 | return $collection; 143 | } 144 | /** 145 | * 全文索引,参数这里不一一介绍,和函数 actionGetSearchProductColl的参数是一样的 146 | */ 147 | protected function fullTearchText($select, $where, $pageNum, $numPerPage, $product_search_max_count, $filter_attrs) 148 | { 149 | $lang = Yii::$service->store->currentLangCode; 150 | $this->_searchModel->initLang($lang); 151 | $searchText = $where['$text']['$search']; 152 | $productM = Yii::$service->product->getBySku($searchText); 153 | $productIds = []; 154 | // 如果通过sku直接可以查询到,那么,代表数据的是sku,直接返回数据即可。 155 | if ($productM) { 156 | $productIds[] = $productM['_id']; 157 | } else { 158 | // 如果不是sku,那么根据下面的步骤进行查询 159 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html 160 | // https://www.elastic.co/guide/cn/elasticsearch/guide/current/multi-query-strings.html#prioritising-clauses 161 | $query_arr = []; 162 | // 组织where条件。 163 | if (is_array($where) && !empty($where)) { 164 | if (isset($where['$text']['$search']) && $where['$text']['$search']) { 165 | $query_arr['bool']['must'] = [ 166 | // https://www.elastic.co/guide/en/elasticsearch/guide/current/multi-match-query.html 167 | // https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html 168 | [ 169 | 'multi_match' => [ 170 | 'query' => $where['$text']['$search'], 171 | 'type' => $this->type, //default best_fields, see: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types 172 | 'fields' => [ "name^4", "description^2" ], // ^后面是这个属性的权重。 see: https://www.elastic.co/guide/en/elasticsearch/guide/current/multi-match-query.html#_boosting_individual_fields 173 | 'operator' => 'and', 174 | 'tie_breaker' => 0.3 , 175 | //"minimum_should_match" => "0%", 176 | ], 177 | ] 178 | ]; 179 | } else { 180 | return []; 181 | } 182 | $queryMust = []; 183 | foreach ($where as $k => $v) { 184 | if ($k != '$text') { 185 | // 数组类型,代表的是范围类型,譬如价格,下面是进行的一系列的字符转换。 186 | if (is_array($v)) { 187 | $ar = []; 188 | foreach($v as $k1 => $v1) { 189 | $k1 = str_replace('$','',$k1); 190 | $ar[$k1] = $v1; 191 | } 192 | $queryMust[] = [ 193 | 'range' => [$k => $ar] 194 | ]; 195 | } else { 196 | $queryMust[] = [ 197 | 'term' => [$k => $v] 198 | ]; 199 | } 200 | } 201 | } 202 | if (!empty($queryMust)) { 203 | $query_arr['bool']['filter'] = $queryMust; 204 | } 205 | } 206 | $searchQuery = $this->_searchModel->find()->asArray()->query($query_arr); 207 | // 设置最大查询数 208 | $size = $product_search_max_count; // 5000; 209 | // 设置aggregate部分 210 | foreach ($filter_attrs as $filter_attr) { 211 | $type = 'terms'; 212 | $options = [ 213 | 'field' => $filter_attr, 214 | 'size' => $size, 215 | ]; 216 | $searchQuery->addAgg($filter_attr, $type, $options); 217 | } 218 | // 得到查询结果 219 | $search_data = $searchQuery->createCommand()->search(); 220 | // 根据上面的查询结果,得到过滤的部分 - aggregate 部分,。 221 | $agg_data = $search_data['aggregations']; 222 | if (is_array($agg_data)) { 223 | foreach ($agg_data as $f_attr => $filter) { 224 | $arr = []; 225 | $buckets = $filter['buckets']; 226 | if (is_array($buckets)) { 227 | foreach ($buckets as $o) { 228 | $k = $o['key']; 229 | $count = $o['doc_count']; 230 | // $arr[$k] = $count; 231 | $arr[] = [ 232 | '_id' => $k, 233 | 'count' => $count, 234 | ]; 235 | } 236 | } 237 | $this->filter_values[$f_attr] = $arr; 238 | } 239 | } 240 | // 根据上面的查询结果,得到产品数据部分 241 | $productData = []; 242 | if (is_array($search_data['hits']['hits'])) { 243 | foreach ($search_data['hits']['hits'] as $one) { 244 | $_id = $one['_id']; 245 | $productOne = $one['_source']; 246 | $productOne['_id'] = $_id; 247 | $productData[] = $productOne; 248 | } 249 | } 250 | $data = []; 251 | // 产品相同spu的产品,只显示一个。 252 | foreach ($productData as $one) { 253 | if (!isset($data[$one['spu']])) { 254 | $data[$one['spu']] = $one; 255 | //$data['_id'] = $one->getPrimaryKey(); 256 | } 257 | } 258 | $count = count($data); 259 | $offset = ($pageNum - 1) * $numPerPage; 260 | $limit = $numPerPage; 261 | $productIds = []; 262 | foreach ($data as $d) { 263 | $productIds[] = new \MongoDB\BSON\ObjectId($d['_id']); 264 | } 265 | // 最终得到产品id的数组。 266 | $productIds = array_slice($productIds, $offset, $limit); 267 | } 268 | // 根据上面查询得到的product_ids数组,去mongodb中查询产品。 269 | if (!empty($productIds)) { 270 | $query = $this->_productModel->find()->asArray() 271 | ->select($select) 272 | ->where(['_id'=> ['$in'=>$productIds]]); 273 | $data = $query->all(); 274 | /** 275 | * 下面的代码的作用:将结果按照上面in查询的顺序进行数组的排序,使结果和上面的搜索结果排序一致(_id)。 276 | */ 277 | $s_data = []; 278 | foreach ($data as $one) { 279 | $_id = (string) $one['_id']; 280 | if($_id){ 281 | $s_data[$_id] = $one; 282 | } 283 | } 284 | $return_data = []; 285 | foreach ($productIds as $product_id) { 286 | $pid = (string) $product_id; 287 | if (isset($s_data[$pid]) && $s_data[$pid]) { 288 | $return_data[] = $s_data[$pid]; 289 | } 290 | } 291 | return [ 292 | 'coll' => $return_data, 293 | 'count'=> $count, 294 | ]; 295 | } 296 | } 297 | /** 298 | * @property $filter_attr | string , 聚合的属性字段 299 | * @property $where | Array , 查询条件 300 | * @return Array, 得到搜索的sku列表侧栏的过滤.example: 301 | * [ 302 | * [ '_id' => $k, 'count' => $v,], 303 | * [ '_id' => $k, 'count' => $v,], 304 | * [ '_id' => $k, 'count' => $v,], 305 | * ] 306 | * 下面的$this->filter_values,该类变量在上面的查询中已经被初始化,这里直接调用即可。 307 | */ 308 | protected function actionGetFrontSearchFilter($filter_attr, $where) 309 | { 310 | if (isset($this->filter_values[$filter_attr])) { 311 | return $this->filter_values[$filter_attr]; 312 | } else { 313 | return []; 314 | } 315 | } 316 | 317 | /** 318 | * @property $product_id | String ,产品id 319 | * 通过product_id删除搜索数据. 320 | */ 321 | protected function actionRemoveByProductId($product_id) 322 | { 323 | if (is_object($product_id)) { 324 | $product_id = (string) $product_id; 325 | $model = $this->_searchModel->findOne($product_id); 326 | if($model){ 327 | $model->delete(); 328 | } 329 | } 330 | } 331 | /** 332 | * 更新ElasticSearch product部分的Mapping 333 | * 关于elasticSearch的mapping,参看:https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html 334 | */ 335 | public function updateMapping(){ 336 | if (!empty($this->searchLang) && is_array($this->searchLang)) { 337 | foreach ($this->searchLang as $langCode => $langName) { 338 | $this->_searchModel::initLang($langCode); 339 | $this->_searchModel->updateMapping(); 340 | } 341 | } 342 | 343 | 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /yii2-elasticsearch/ActiveRecord.php: -------------------------------------------------------------------------------- 1 | 53 | * @since 2.0 54 | */ 55 | class ActiveRecord extends BaseActiveRecord 56 | { 57 | private $_id; 58 | private $_score; 59 | private $_version; 60 | private $_highlight; 61 | private $_explanation; 62 | 63 | /** 64 | * Returns the database connection used by this AR class. 65 | * By default, the "elasticsearch" application component is used as the database connection. 66 | * You may override this method if you want to use a different database connection. 67 | * @return Connection the database connection used by this AR class. 68 | */ 69 | public static function getDb() 70 | { 71 | return \Yii::$app->get('elasticsearch'); 72 | } 73 | 74 | /** 75 | * @inheritdoc 76 | * @return ActiveQuery the newly created [[ActiveQuery]] instance. 77 | */ 78 | public static function find() 79 | { 80 | return Yii::createObject(ActiveQuery::className(), [get_called_class()]); 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | */ 86 | public static function findOne($condition) 87 | { 88 | $query = static::find(); 89 | if (is_array($condition)) { 90 | return $query->andWhere($condition)->one(); 91 | } else { 92 | return static::get($condition); 93 | } 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | */ 99 | public static function findAll($condition) 100 | { 101 | $query = static::find(); 102 | if (ArrayHelper::isAssociative($condition)) { 103 | return $query->andWhere($condition)->all(); 104 | } else { 105 | return static::mget((array) $condition); 106 | } 107 | } 108 | 109 | /** 110 | * Gets a record by its primary key. 111 | * 112 | * @param mixed $primaryKey the primaryKey value 113 | * @param array $options options given in this parameter are passed to elasticsearch 114 | * as request URI parameters. 115 | * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) 116 | * for more details on these options. 117 | * @return static|null The record instance or null if it was not found. 118 | */ 119 | public static function get($primaryKey, $options = []) 120 | { 121 | if ($primaryKey === null) { 122 | return null; 123 | } 124 | $command = static::getDb()->createCommand(); 125 | $result = $command->get(static::index(), static::type(), $primaryKey, $options); 126 | if ($result['found']) { 127 | $model = static::instantiate($result); 128 | static::populateRecord($model, $result); 129 | $model->afterFind(); 130 | 131 | return $model; 132 | } 133 | 134 | return null; 135 | } 136 | 137 | /** 138 | * Gets a list of records by its primary keys. 139 | * 140 | * @param array $primaryKeys an array of primaryKey values 141 | * @param array $options options given in this parameter are passed to elasticsearch 142 | * as request URI parameters. 143 | * 144 | * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) 145 | * for more details on these options. 146 | * @return array The record instances, or empty array if nothing was found 147 | */ 148 | public static function mget(array $primaryKeys, $options = []) 149 | { 150 | if (empty($primaryKeys)) { 151 | return []; 152 | } 153 | if (count($primaryKeys) === 1) { 154 | $model = static::get(reset($primaryKeys)); 155 | return $model === null ? [] : [$model]; 156 | } 157 | 158 | $command = static::getDb()->createCommand(); 159 | $result = $command->mget(static::index(), static::type(), $primaryKeys, $options); 160 | $models = []; 161 | foreach ($result['docs'] as $doc) { 162 | if ($doc['found']) { 163 | $model = static::instantiate($doc); 164 | static::populateRecord($model, $doc); 165 | $model->afterFind(); 166 | $models[] = $model; 167 | } 168 | } 169 | 170 | return $models; 171 | } 172 | 173 | // TODO add more like this feature http://www.elastic.co/guide/en/elasticsearch/reference/current/search-more-like-this.html 174 | 175 | // TODO add percolate functionality http://www.elastic.co/guide/en/elasticsearch/reference/current/search-percolate.html 176 | 177 | // TODO implement copy and move as pk change is not possible 178 | 179 | /** 180 | * @return float returns the score of this record when it was retrieved via a [[find()]] query. 181 | */ 182 | public function getScore() 183 | { 184 | return $this->_score; 185 | } 186 | 187 | /** 188 | * @return array|null A list of arrays with highlighted excerpts indexed by field names. 189 | */ 190 | public function getHighlight() 191 | { 192 | return $this->_highlight; 193 | } 194 | 195 | /** 196 | * @return array|null An explanation for each hit on how its score was computed. 197 | * @since 2.0.5 198 | */ 199 | public function getExplanation() 200 | { 201 | return $this->_explanation; 202 | } 203 | 204 | /** 205 | * Sets the primary key 206 | * @param mixed $value 207 | * @throws \yii\base\InvalidCallException when record is not new 208 | */ 209 | public function setPrimaryKey($value) 210 | { 211 | $pk = static::primaryKey()[0]; 212 | if ($this->getIsNewRecord() || $pk != '_id') { 213 | $this->$pk = $value; 214 | } else { 215 | throw new InvalidCallException('Changing the primaryKey of an already saved record is not allowed.'); 216 | } 217 | } 218 | 219 | /** 220 | * @inheritdoc 221 | */ 222 | public function getPrimaryKey($asArray = false) 223 | { 224 | $pk = static::primaryKey()[0]; 225 | if ($asArray) { 226 | return [$pk => $this->$pk]; 227 | } else { 228 | return $this->$pk; 229 | } 230 | } 231 | 232 | /** 233 | * @inheritdoc 234 | */ 235 | public function getOldPrimaryKey($asArray = false) 236 | { 237 | $pk = static::primaryKey()[0]; 238 | if ($this->getIsNewRecord()) { 239 | $id = null; 240 | } elseif ($pk == '_id') { 241 | $id = $this->_id; 242 | } else { 243 | $id = $this->getOldAttribute($pk); 244 | } 245 | if ($asArray) { 246 | return [$pk => $id]; 247 | } else { 248 | return $id; 249 | } 250 | } 251 | 252 | /** 253 | * This method defines the attribute that uniquely identifies a record. 254 | * 255 | * The primaryKey for elasticsearch documents is the `_id` field by default. This field is not part of the 256 | * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]]. 257 | * 258 | * You may override this method to define the primary key name when you have defined 259 | * [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) 260 | * for the `_id` field so that it is part of the `_source` and thus part of the [[attributes()|attributes]]. 261 | * 262 | * Note that elasticsearch only supports _one_ attribute to be the primary key. However to match the signature 263 | * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a 264 | * single string. 265 | * 266 | * @return string[] array of primary key attributes. Only the first element of the array will be used. 267 | */ 268 | public static function primaryKey() 269 | { 270 | return ['_id']; 271 | } 272 | 273 | /** 274 | * Returns the list of all attribute names of the model. 275 | * 276 | * This method must be overridden by child classes to define available attributes. 277 | * 278 | * Attributes are names of fields of the corresponding elasticsearch document. 279 | * The primaryKey for elasticsearch documents is the `_id` field by default which is not part of the attributes. 280 | * You may define [path mapping](http://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) 281 | * for the `_id` field so that it is part of the `_source` fields and thus becomes part of the attributes. 282 | * 283 | * @return string[] list of attribute names. 284 | * @throws \yii\base\InvalidConfigException if not overridden in a child class. 285 | */ 286 | public function attributes() 287 | { 288 | throw new InvalidConfigException('The attributes() method of elasticsearch ActiveRecord has to be implemented by child classes.'); 289 | } 290 | 291 | /** 292 | * A list of attributes that should be treated as array valued when retrieved through [[ActiveQuery::fields]]. 293 | * 294 | * If not listed by this method, attributes retrieved through [[ActiveQuery::fields]] will converted to a scalar value 295 | * when the result array contains only one value. 296 | * 297 | * @return string[] list of attribute names. Must be a subset of [[attributes()]]. 298 | */ 299 | public function arrayAttributes() 300 | { 301 | return []; 302 | } 303 | 304 | /** 305 | * @return string the name of the index this record is stored in. 306 | */ 307 | public static function index() 308 | { 309 | return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-')); 310 | } 311 | 312 | /** 313 | * @return string the name of the type of this record. 314 | */ 315 | public static function type() 316 | { 317 | return Inflector::camel2id(StringHelper::basename(get_called_class()), '-'); 318 | } 319 | 320 | /** 321 | * @inheritdoc 322 | * 323 | * @param ActiveRecord $record the record to be populated. In most cases this will be an instance 324 | * created by [[instantiate()]] beforehand. 325 | * @param array $row attribute values (name => value) 326 | */ 327 | public static function populateRecord($record, $row) 328 | { 329 | $attributes = []; 330 | if (isset($row['_source'])) { 331 | $attributes = $row['_source']; 332 | } 333 | if (isset($row['fields'])) { 334 | // reset fields in case it is scalar value 335 | $arrayAttributes = $record->arrayAttributes(); 336 | foreach($row['fields'] as $key => $value) { 337 | if (!isset($arrayAttributes[$key]) && count($value) == 1) { 338 | $row['fields'][$key] = reset($value); 339 | } 340 | } 341 | $attributes = array_merge($attributes, $row['fields']); 342 | } 343 | 344 | parent::populateRecord($record, $attributes); 345 | 346 | $pk = static::primaryKey()[0];//TODO should always set ID in case of fields are not returned 347 | if ($pk === '_id') { 348 | $record->_id = $row['_id']; 349 | } 350 | $record->_highlight = isset($row['highlight']) ? $row['highlight'] : null; 351 | $record->_score = isset($row['_score']) ? $row['_score'] : null; 352 | $record->_version = isset($row['_version']) ? $row['_version'] : null; // TODO version should always be available... 353 | $record->_explanation = isset($row['_explanation']) ? $row['_explanation'] : null; 354 | } 355 | 356 | /** 357 | * Creates an active record instance. 358 | * 359 | * This method is called together with [[populateRecord()]] by [[ActiveQuery]]. 360 | * It is not meant to be used for creating new records directly. 361 | * 362 | * You may override this method if the instance being created 363 | * depends on the row data to be populated into the record. 364 | * For example, by creating a record based on the value of a column, 365 | * you may implement the so-called single-table inheritance mapping. 366 | * @param array $row row data to be populated into the record. 367 | * This array consists of the following keys: 368 | * - `_source`: refers to the attributes of the record. 369 | * - `_type`: the type this record is stored in. 370 | * - `_index`: the index this record is stored in. 371 | * @return static the newly created active record 372 | */ 373 | public static function instantiate($row) 374 | { 375 | return new static; 376 | } 377 | 378 | /** 379 | * Inserts a document into the associated index using the attribute values of this record. 380 | * 381 | * This method performs the following steps in order: 382 | * 383 | * 1. call [[beforeValidate()]] when `$runValidation` is true. If validation 384 | * fails, it will skip the rest of the steps; 385 | * 2. call [[afterValidate()]] when `$runValidation` is true. 386 | * 3. call [[beforeSave()]]. If the method returns false, it will skip the 387 | * rest of the steps; 388 | * 4. insert the record into database. If this fails, it will skip the rest of the steps; 389 | * 5. call [[afterSave()]]; 390 | * 391 | * In the above step 1, 2, 3 and 5, events [[EVENT_BEFORE_VALIDATE]], 392 | * [[EVENT_BEFORE_INSERT]], [[EVENT_AFTER_INSERT]] and [[EVENT_AFTER_VALIDATE]] 393 | * will be raised by the corresponding methods. 394 | * 395 | * Only the [[dirtyAttributes|changed attribute values]] will be inserted into database. 396 | * 397 | * If the [[primaryKey|primary key]] is not set (null) during insertion, 398 | * it will be populated with a 399 | * [randomly generated value](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#_automatic_id_generation) 400 | * after insertion. 401 | * 402 | * For example, to insert a customer record: 403 | * 404 | * ~~~ 405 | * $customer = new Customer; 406 | * $customer->name = $name; 407 | * $customer->email = $email; 408 | * $customer->insert(); 409 | * ~~~ 410 | * 411 | * @param boolean $runValidation whether to perform validation before saving the record. 412 | * If the validation fails, the record will not be inserted into the database. 413 | * @param array $attributes list of attributes that need to be saved. Defaults to null, 414 | * meaning all attributes will be saved. 415 | * @param array $options options given in this parameter are passed to elasticsearch 416 | * as request URI parameters. These are among others: 417 | * 418 | * - `routing` define shard placement of this record. 419 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation 420 | * 421 | * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html) 422 | * for more details on these options. 423 | * 424 | * By default the `op_type` is set to `create` if model primary key is present. 425 | * @return boolean whether the attributes are valid and the record is inserted successfully. 426 | */ 427 | public function insert($runValidation = true, $attributes = null, $options = [ ]) 428 | { 429 | if ($runValidation && !$this->validate($attributes)) { 430 | return false; 431 | } 432 | if (!$this->beforeSave(true)) { 433 | return false; 434 | } 435 | $values = $this->getDirtyAttributes($attributes); 436 | 437 | if ($this->getPrimaryKey() !== null) { 438 | $options['op_type'] = isset($options['op_type']) ? $options['op_type'] : 'create'; 439 | } 440 | Yii::$app->params['es_pro_id'] = $values['m_id']; 441 | //unset($values['_id']); 442 | $response = static::getDb()->createCommand()->insert( 443 | static::index(), 444 | static::type(), 445 | $values, 446 | $this->getPrimaryKey(), 447 | $options 448 | ); 449 | 450 | $pk = static::primaryKey()[0]; 451 | $this->$pk = $response['_id']; 452 | if ($pk != '_id') { 453 | $values[$pk] = $response['_id']; 454 | } 455 | $this->_version = $response['_version']; 456 | $this->_score = null; 457 | 458 | $changedAttributes = array_fill_keys(array_keys($values), null); 459 | $this->setOldAttributes($values); 460 | $this->afterSave(true, $changedAttributes); 461 | 462 | return true; 463 | } 464 | 465 | /** 466 | * @inheritdoc 467 | * 468 | * @param boolean $runValidation whether to perform validation before saving the record. 469 | * If the validation fails, the record will not be inserted into the database. 470 | * @param array $attributeNames list of attribute names that need to be saved. Defaults to null, 471 | * meaning all attributes that are loaded from DB will be saved. 472 | * @param array $options options given in this parameter are passed to elasticsearch 473 | * as request URI parameters. These are among others: 474 | * 475 | * - `routing` define shard placement of this record. 476 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation 477 | * - `timeout` timeout waiting for a shard to become available. 478 | * - `replication` the replication type for the delete/index operation (sync or async). 479 | * - `consistency` the write consistency of the index/delete operation. 480 | * - `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. 481 | * - `detect_noop` this parameter will become part of the request body and will prevent the index from getting updated when nothing has changed. 482 | * 483 | * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html#_parameters_3) 484 | * for more details on these options. 485 | * 486 | * The following parameters are Yii specific: 487 | * 488 | * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it 489 | * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]]. 490 | * See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html) for details. 491 | * 492 | * Make sure the record has been fetched with a [[version]] before. This is only the case 493 | * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly. 494 | * 495 | * @return integer|boolean the number of rows affected, or false if validation fails 496 | * or [[beforeSave()]] stops the updating process. 497 | * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated. 498 | * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled. 499 | * @throws Exception in case update failed. 500 | */ 501 | public function update($runValidation = true, $attributeNames = null, $options = []) 502 | { 503 | if ($runValidation && !$this->validate($attributeNames)) { 504 | return false; 505 | } 506 | return $this->updateInternal($attributeNames, $options); 507 | } 508 | 509 | /** 510 | * @see update() 511 | * @param array $attributes attributes to update 512 | * @param array $options options given in this parameter are passed to elasticsearch 513 | * as request URI parameters. See [[update()]] for details. 514 | * @return integer|false the number of rows affected, or false if [[beforeSave()]] stops the updating process. 515 | * @throws StaleObjectException if optimistic locking is enabled and the data being updated is outdated. 516 | * @throws InvalidParamException if no [[version]] is available and optimistic locking is enabled. 517 | * @throws Exception in case update failed. 518 | */ 519 | protected function updateInternal($attributes = null, $options = []) 520 | { 521 | if (!$this->beforeSave(false)) { 522 | return false; 523 | } 524 | $values = $this->getDirtyAttributes($attributes); 525 | if (empty($values)) { 526 | $this->afterSave(false, $values); 527 | return 0; 528 | } 529 | 530 | if (isset($options['optimistic_locking']) && $options['optimistic_locking']) { 531 | if ($this->_version === null) { 532 | 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.'); 533 | } 534 | $options['version'] = $this->_version; 535 | unset($options['optimistic_locking']); 536 | } 537 | 538 | try { 539 | $result = static::getDb()->createCommand()->update( 540 | static::index(), 541 | static::type(), 542 | $this->getOldPrimaryKey(false), 543 | $values, 544 | $options 545 | ); 546 | } catch(Exception $e) { 547 | // HTTP 409 is the response in case of failed optimistic locking 548 | // http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html 549 | if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) { 550 | throw new StaleObjectException('The object being updated is outdated.', $e->errorInfo, $e->getCode(), $e); 551 | } 552 | throw $e; 553 | } 554 | 555 | if (is_array($result) && isset($result['_version'])) { 556 | $this->_version = $result['_version']; 557 | } 558 | 559 | $changedAttributes = []; 560 | foreach ($values as $name => $value) { 561 | $changedAttributes[$name] = $this->getOldAttribute($name); 562 | $this->setOldAttribute($name, $value); 563 | } 564 | $this->afterSave(false, $changedAttributes); 565 | 566 | if ($result === false) { 567 | return 0; 568 | } else { 569 | return 1; 570 | } 571 | } 572 | 573 | /** 574 | * Performs a quick and highly efficient scroll/scan query to get the list of primary keys that 575 | * satisfy the given condition. If condition is a list of primary keys 576 | * (e.g.: `['_id' => ['1', '2', '3']]`), the query is not performed for performance considerations. 577 | * @param array $condition please refer to [[ActiveQuery::where()]] on how to specify this parameter 578 | * @return array primary keys that correspond to given conditions 579 | * @see updateAll() 580 | * @see updateAllCounters() 581 | * @see deleteAll() 582 | * @since 2.0.4 583 | */ 584 | protected static function primaryKeysByCondition($condition) 585 | { 586 | $pkName = static::primaryKey()[0]; 587 | if (count($condition) == 1 && isset($condition[$pkName])) { 588 | $primaryKeys = (array)$condition[$pkName]; 589 | } else { 590 | //fetch only document metadata (no fields), 1000 documents per shard 591 | $query = static::find()->where($condition)->asArray()->source(false)->limit(1000); 592 | $primaryKeys = []; 593 | foreach ($query->each('1m') as $document) { 594 | $primaryKeys[] = $document['_id']; 595 | } 596 | } 597 | return $primaryKeys; 598 | } 599 | 600 | /** 601 | * Updates all records whos primary keys are given. 602 | * For example, to change the status to be 1 for all customers whose status is 2: 603 | * 604 | * ~~~ 605 | * Customer::updateAll(['status' => 1], ['status' => 2]); 606 | * ~~~ 607 | * 608 | * @param array $attributes attribute values (name-value pairs) to be saved into the table 609 | * @param array $condition the conditions that will be passed to the `where()` method when building the query. 610 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. 611 | * @see [[ActiveRecord::primaryKeysByCondition()]] 612 | * @return integer the number of rows updated 613 | * @throws Exception on error. 614 | */ 615 | public static function updateAll($attributes, $condition = []) 616 | { 617 | $primaryKeys = static::primaryKeysByCondition($condition); 618 | if (empty($primaryKeys)) { 619 | return 0; 620 | } 621 | 622 | $bulkCommand = static::getDb()->createBulkCommand([ 623 | "index" => static::index(), 624 | "type" => static::type(), 625 | ]); 626 | foreach ($primaryKeys as $pk) { 627 | $bulkCommand->addAction(["update" => ["_id" => $pk]], ["doc" => $attributes]); 628 | } 629 | $response = $bulkCommand->execute(); 630 | 631 | $n = 0; 632 | $errors = []; 633 | foreach ($response['items'] as $item) { 634 | if (isset($item['update']['status']) && $item['update']['status'] == 200) { 635 | $n++; 636 | } else { 637 | $errors[] = $item['update']; 638 | } 639 | } 640 | if (!empty($errors) || isset($response['errors']) && $response['errors']) { 641 | throw new Exception(__METHOD__ . ' failed updating records.', $errors); 642 | } 643 | 644 | return $n; 645 | } 646 | 647 | /** 648 | * Updates all matching records using the provided counter changes and conditions. 649 | * For example, to add 1 to age of all customers whose status is 2, 650 | * 651 | * ~~~ 652 | * Customer::updateAllCounters(['age' => 1], ['status' => 2]); 653 | * ~~~ 654 | * 655 | * @param array $counters the counters to be updated (attribute name => increment value). 656 | * Use negative values if you want to decrement the counters. 657 | * @param array $condition the conditions that will be passed to the `where()` method when building the query. 658 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. 659 | * @see [[ActiveRecord::primaryKeysByCondition()]] 660 | * @return integer the number of rows updated 661 | * @throws Exception on error. 662 | */ 663 | public static function updateAllCounters($counters, $condition = []) 664 | { 665 | $primaryKeys = static::primaryKeysByCondition($condition); 666 | if (empty($primaryKeys) || empty($counters)) { 667 | return 0; 668 | } 669 | 670 | $bulkCommand = static::getDb()->createBulkCommand([ 671 | "index" => static::index(), 672 | "type" => static::type(), 673 | ]); 674 | foreach ($primaryKeys as $pk) { 675 | $script = ''; 676 | foreach ($counters as $counter => $value) { 677 | $script .= "ctx._source.{$counter} += params.{$counter};\n"; 678 | } 679 | $bulkCommand->addAction(["update" => ["_id" => $pk]], [ 680 | 'script' => [ 681 | 'inline' => $script, 682 | 'params' => $counters, 683 | 'lang' => 'painless', 684 | ], 685 | ]); 686 | } 687 | $response = $bulkCommand->execute(); 688 | 689 | $n = 0; 690 | $errors = []; 691 | foreach ($response['items'] as $item) { 692 | if (isset($item['update']['status']) && $item['update']['status'] == 200) { 693 | $n++; 694 | } else { 695 | $errors[] = $item['update']; 696 | } 697 | } 698 | if (!empty($errors) || isset($response['errors']) && $response['errors']) { 699 | throw new Exception(__METHOD__ . ' failed updating records counters.', $errors); 700 | } 701 | 702 | return $n; 703 | } 704 | 705 | /** 706 | * @inheritdoc 707 | * 708 | * @param array $options options given in this parameter are passed to elasticsearch 709 | * as request URI parameters. These are among others: 710 | * 711 | * - `routing` define shard placement of this record. 712 | * - `parent` by giving the primaryKey of another record this defines a parent-child relation 713 | * - `timeout` timeout waiting for a shard to become available. 714 | * - `replication` the replication type for the delete/index operation (sync or async). 715 | * - `consistency` the write consistency of the index/delete operation. 716 | * - `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. 717 | * 718 | * Please refer to the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html) 719 | * for more details on these options. 720 | * 721 | * The following parameters are Yii specific: 722 | * 723 | * - `optimistic_locking` set this to `true` to enable optimistic locking, avoid updating when the record has changed since it 724 | * has been loaded from the database. Yii will set the `version` parameter to the value stored in [[version]]. 725 | * See the [elasticsearch documentation](http://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html#delete-versioning) for details. 726 | * 727 | * Make sure the record has been fetched with a [[version]] before. This is only the case 728 | * for records fetched via [[get()]] and [[mget()]] by default. For normal queries, the `_version` field has to be fetched explicitly. 729 | * 730 | * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. 731 | * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. 732 | * @throws StaleObjectException if optimistic locking is enabled and the data being deleted is outdated. 733 | * @throws Exception in case delete failed. 734 | */ 735 | public function delete($options = []) 736 | { 737 | if (!$this->beforeDelete()) { 738 | return false; 739 | } 740 | if (isset($options['optimistic_locking']) && $options['optimistic_locking']) { 741 | if ($this->_version === null) { 742 | 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.'); 743 | } 744 | $options['version'] = $this->_version; 745 | unset($options['optimistic_locking']); 746 | } 747 | 748 | try { 749 | $result = static::getDb()->createCommand()->delete( 750 | static::index(), 751 | static::type(), 752 | $this->getOldPrimaryKey(false), 753 | $options 754 | ); 755 | } catch(Exception $e) { 756 | // HTTP 409 is the response in case of failed optimistic locking 757 | // http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html 758 | if (isset($e->errorInfo['responseCode']) && $e->errorInfo['responseCode'] == 409) { 759 | throw new StaleObjectException('The object being deleted is outdated.', $e->errorInfo, $e->getCode(), $e); 760 | } 761 | throw $e; 762 | } 763 | 764 | $this->setOldAttributes(null); 765 | 766 | $this->afterDelete(); 767 | 768 | if ($result === false) { 769 | return 0; 770 | } else { 771 | return 1; 772 | } 773 | } 774 | 775 | /** 776 | * Deletes rows in the table using the provided conditions. 777 | * WARNING: If you do not specify any condition, this method will delete ALL rows in the table. 778 | * 779 | * For example, to delete all customers whose status is 3: 780 | * 781 | * ~~~ 782 | * Customer::deleteAll(['status' => 3]); 783 | * ~~~ 784 | * 785 | * @param array $condition the conditions that will be passed to the `where()` method when building the query. 786 | * Please refer to [[ActiveQuery::where()]] on how to specify this parameter. 787 | * @see [[ActiveRecord::primaryKeysByCondition()]] 788 | * @return integer the number of rows deleted 789 | * @throws Exception on error. 790 | */ 791 | public static function deleteAll($condition = []) 792 | { 793 | $primaryKeys = static::primaryKeysByCondition($condition); 794 | if (empty($primaryKeys)) { 795 | return 0; 796 | } 797 | 798 | $bulkCommand = static::getDb()->createBulkCommand([ 799 | "index" => static::index(), 800 | "type" => static::type(), 801 | ]); 802 | foreach ($primaryKeys as $pk) { 803 | $bulkCommand->addDeleteAction($pk); 804 | } 805 | $response = $bulkCommand->execute(); 806 | 807 | $n = 0; 808 | $errors = []; 809 | foreach ($response['items'] as $item) { 810 | if (isset($item['delete']['status']) && $item['delete']['status'] == 200) { 811 | if (isset($item['delete']['found']) && $item['delete']['found']) { 812 | $n++; 813 | } 814 | } else { 815 | $errors[] = $item['delete']; 816 | } 817 | } 818 | if (!empty($errors) || isset($response['errors']) && $response['errors']) { 819 | throw new Exception(__METHOD__ . ' failed deleting records.', $errors); 820 | } 821 | 822 | return $n; 823 | } 824 | 825 | /** 826 | * This method has no effect in Elasticsearch ActiveRecord. 827 | * 828 | * Elasticsearch ActiveRecord uses [native Optimistic locking](http://www.elastic.co/guide/en/elasticsearch/guide/current/optimistic-concurrency-control.html). 829 | * See [[update()]] for more details. 830 | */ 831 | public function optimisticLock() 832 | { 833 | return null; 834 | } 835 | 836 | /** 837 | * Destroys the relationship in current model. 838 | * 839 | * This method is not supported by elasticsearch. 840 | */ 841 | public function unlinkAll($name, $delete = false) 842 | { 843 | throw new NotSupportedException('unlinkAll() is not supported by elasticsearch, use unlink() instead.'); 844 | } 845 | } 846 | -------------------------------------------------------------------------------- /yii2-elasticsearch/Command.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fecshop/yii2_fecshop_elasticsearch/5adbff5e0c336f17d685131758c50f44d7b6fdbe/yii2-elasticsearch/Command.php -------------------------------------------------------------------------------- /yii2-elasticsearch/Connection.php: -------------------------------------------------------------------------------- 1 | 24 | * @since 2.0 25 | */ 26 | class Connection extends Component 27 | { 28 | /** 29 | * @event Event an event that is triggered after a DB connection is established 30 | */ 31 | const EVENT_AFTER_OPEN = 'afterOpen'; 32 | 33 | /** 34 | * @var boolean whether to autodetect available cluster nodes on [[open()]] 35 | */ 36 | public $autodetectCluster = true; 37 | /** 38 | * @var array The elasticsearch cluster nodes to connect to. 39 | * 40 | * This is populated with the result of a cluster nodes request when [[autodetectCluster]] is true. 41 | * 42 | * Additional special options: 43 | * 44 | * - `auth`: overrides [[auth]] property. For example: 45 | * 46 | * ```php 47 | * [ 48 | * 'http_address' => 'inet[/127.0.0.1:9200]', 49 | * 'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Overrides the `auth` property of the class with specific login and password 50 | * //'auth' => ['username' => 'yiiuser', 'password' => 'yiipw'], // Disabled auth regardless of `auth` property of the class 51 | * ] 52 | * ``` 53 | * 54 | * - `protocol`: explicitly sets the protocol for the current node (useful when manually defining a HTTPS cluster) 55 | * 56 | * @see http://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html#cluster-nodes-info 57 | */ 58 | public $nodes = [ 59 | ['http_address' => 'inet[/127.0.0.1:9200]'], 60 | ]; 61 | /** 62 | * @var string the active node. Key of one of the [[nodes]]. Will be randomly selected on [[open()]]. 63 | */ 64 | public $activeNode; 65 | /** 66 | * @var array Authentication data used to connect to the ElasticSearch node. 67 | * 68 | * Array elements: 69 | * 70 | * - `username`: the username for authentication. 71 | * - `password`: the password for authentication. 72 | * 73 | * Array either MUST contain both username and password on not contain any authentication credentials. 74 | * @see http://www.elasticsearch.org/guide/en/elasticsearch/client/php-api/current/_configuration.html#_example_configuring_http_basic_auth 75 | */ 76 | public $auth = []; 77 | /** 78 | * Elasticsearch has no knowledge of protocol used to access its nodes. Specifically, cluster autodetection request 79 | * returns node hosts and ports, but not the protocols to access them. Therefore we need to specify a default protocol here, 80 | * which can be overridden for specific nodes in the [[nodes]] property. 81 | * If [[autodetectCluster]] is true, all nodes received from cluster will be set to use the protocol defined by [[defaultProtocol]] 82 | * @var string Default protocol to connect to nodes 83 | * @since 2.0.5 84 | */ 85 | public $defaultProtocol = 'http'; 86 | /** 87 | * @var float timeout to use for connecting to an elasticsearch node. 88 | * This value will be used to configure the curl `CURLOPT_CONNECTTIMEOUT` option. 89 | * If not set, no explicit timeout will be set for curl. 90 | */ 91 | public $connectionTimeout = null; 92 | /** 93 | * @var float timeout to use when reading the response from an elasticsearch node. 94 | * This value will be used to configure the curl `CURLOPT_TIMEOUT` option. 95 | * If not set, no explicit timeout will be set for curl. 96 | */ 97 | public $dataTimeout = null; 98 | 99 | /** 100 | * @var resource the curl instance returned by [curl_init()](http://php.net/manual/en/function.curl-init.php). 101 | */ 102 | private $_curl; 103 | 104 | 105 | public function init() 106 | { 107 | foreach ($this->nodes as &$node) { 108 | if (!isset($node['http_address'])) { 109 | throw new InvalidConfigException('Elasticsearch node needs at least a http_address configured.'); 110 | } 111 | if (!isset($node['protocol'])) { 112 | $node['protocol'] = $this->defaultProtocol; 113 | } 114 | if (!in_array($node['protocol'], ['http', 'https'])) { 115 | throw new InvalidConfigException('Valid node protocol settings are "http" and "https".'); 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Closes the connection when this component is being serialized. 122 | * @return array 123 | */ 124 | public function __sleep() 125 | { 126 | $this->close(); 127 | 128 | return array_keys(get_object_vars($this)); 129 | } 130 | 131 | /** 132 | * Returns a value indicating whether the DB connection is established. 133 | * @return boolean whether the DB connection is established 134 | */ 135 | public function getIsActive() 136 | { 137 | return $this->activeNode !== null; 138 | } 139 | 140 | /** 141 | * Establishes a DB connection. 142 | * It does nothing if a DB connection has already been established. 143 | * @throws Exception if connection fails 144 | */ 145 | public function open() 146 | { 147 | if ($this->activeNode !== null) { 148 | return; 149 | } 150 | if (empty($this->nodes)) { 151 | throw new InvalidConfigException('elasticsearch needs at least one node to operate.'); 152 | } 153 | $this->_curl = curl_init(); 154 | if ($this->autodetectCluster) { 155 | $this->populateNodes(); 156 | } 157 | $this->selectActiveNode(); 158 | Yii::trace('Opening connection to elasticsearch. Nodes in cluster: ' . count($this->nodes) 159 | . ', active node: ' . $this->nodes[$this->activeNode]['http_address'], __CLASS__); 160 | $this->initConnection(); 161 | } 162 | 163 | /** 164 | * Populates [[nodes]] with the result of a cluster nodes request. 165 | * @throws Exception if no active node(s) found 166 | * @since 2.0.4 167 | */ 168 | protected function populateNodes() 169 | { 170 | $node = reset($this->nodes); 171 | $host = $node['http_address']; 172 | $protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol; 173 | if (strncmp($host, 'inet[/', 6) === 0) { 174 | $host = substr($host, 6, -1); 175 | } 176 | $response = $this->httpRequest('GET', "$protocol://$host/_nodes/_all/http"); 177 | if (!empty($response['nodes'])) { 178 | $nodes = $response['nodes']; 179 | } else { 180 | $nodes = []; 181 | } 182 | 183 | foreach ($nodes as $key => &$node) { 184 | // Make sure that nodes have an 'http_address' property, which is not the case if you're using AWS 185 | // Elasticsearch service (at least as of Oct., 2015). - TO BE VERIFIED 186 | // Temporary workaround - simply ignore all invalid nodes 187 | if (!isset($node['http']['publish_address'])) { 188 | unset($nodes[$key]); 189 | } 190 | $node['http_address'] = $node['http']['publish_address']; 191 | 192 | //Protocol is not a standard ES node property, so we add it manually 193 | $node['protocol'] = $this->defaultProtocol; 194 | } 195 | 196 | if (!empty($nodes)) { 197 | $this->nodes = array_values($nodes); 198 | } else { 199 | curl_close($this->_curl); 200 | throw new Exception('Cluster autodetection did not find any active nodes.'); 201 | } 202 | } 203 | 204 | /** 205 | * select active node randomly 206 | */ 207 | protected function selectActiveNode() 208 | { 209 | $keys = array_keys($this->nodes); 210 | $this->activeNode = $keys[rand(0, count($keys) - 1)]; 211 | } 212 | 213 | /** 214 | * Closes the currently active DB connection. 215 | * It does nothing if the connection is already closed. 216 | */ 217 | public function close() 218 | { 219 | if ($this->activeNode === null) { 220 | return; 221 | } 222 | Yii::trace('Closing connection to elasticsearch. Active node was: ' 223 | . $this->nodes[$this->activeNode]['http']['publish_address'], __CLASS__); 224 | $this->activeNode = null; 225 | if ($this->_curl) { 226 | curl_close($this->_curl); 227 | $this->_curl = null; 228 | } 229 | } 230 | 231 | /** 232 | * Initializes the DB connection. 233 | * This method is invoked right after the DB connection is established. 234 | * The default implementation triggers an [[EVENT_AFTER_OPEN]] event. 235 | */ 236 | protected function initConnection() 237 | { 238 | $this->trigger(self::EVENT_AFTER_OPEN); 239 | } 240 | 241 | /** 242 | * Returns the name of the DB driver for the current [[dsn]]. 243 | * @return string name of the DB driver 244 | */ 245 | public function getDriverName() 246 | { 247 | return 'elasticsearch'; 248 | } 249 | 250 | /** 251 | * Creates a command for execution. 252 | * @param array $config the configuration for the Command class 253 | * @return Command the DB command 254 | */ 255 | public function createCommand($config = []) 256 | { 257 | $this->open(); 258 | $config['db'] = $this; 259 | $command = new Command($config); 260 | 261 | return $command; 262 | } 263 | 264 | /** 265 | * Creates a bulk command for execution. 266 | * @param array $config the configuration for the [[BulkCommand]] class 267 | * @return BulkCommand the DB command 268 | * @since 2.0.5 269 | */ 270 | public function createBulkCommand($config = []) 271 | { 272 | $this->open(); 273 | $config['db'] = $this; 274 | $command = new BulkCommand($config); 275 | 276 | return $command; 277 | } 278 | 279 | /** 280 | * Creates new query builder instance 281 | * @return QueryBuilder 282 | */ 283 | public function getQueryBuilder() 284 | { 285 | return new QueryBuilder($this); 286 | } 287 | 288 | /** 289 | * Performs GET HTTP request 290 | * 291 | * @param string|array $url URL 292 | * @param array $options URL options 293 | * @param string $body request body 294 | * @param boolean $raw if response body contains JSON and should be decoded 295 | * @return mixed response 296 | * @throws Exception 297 | * @throws InvalidConfigException 298 | */ 299 | public function get($url, $options = [], $body = null, $raw = false) 300 | { 301 | $this->open(); 302 | return $this->httpRequest('GET', $this->createUrl($url, $options), $body, $raw); 303 | } 304 | 305 | /** 306 | * Performs HEAD HTTP request 307 | * 308 | * @param string|array $url URL 309 | * @param array $options URL options 310 | * @param string $body request body 311 | * @return mixed response 312 | * @throws Exception 313 | * @throws InvalidConfigException 314 | */ 315 | public function head($url, $options = [], $body = null) 316 | { 317 | $this->open(); 318 | return $this->httpRequest('HEAD', $this->createUrl($url, $options), $body); 319 | } 320 | 321 | /** 322 | * Performs POST HTTP request 323 | * 324 | * @param string|array $url URL 325 | * @param array $options URL options 326 | * @param string $body request body 327 | * @param boolean $raw if response body contains JSON and should be decoded 328 | * @return mixed response 329 | * @throws Exception 330 | * @throws InvalidConfigException 331 | */ 332 | public function post($url, $options = [], $body = null, $raw = false) 333 | { 334 | $this->open(); 335 | return $this->httpRequest('POST', $this->createUrl($url, $options), $body, $raw); 336 | } 337 | 338 | 339 | public function postInsert($url, $options = [], $body = null, $raw = false) 340 | { 341 | $this->open(); 342 | return $this->httpRequest('POST', $this->createUrlInsert($url, $options), $body, $raw); 343 | } 344 | /** 345 | * Creates URL 346 | * 347 | * @param string|array $path path 348 | * @param array $options URL options 349 | * @return array 350 | */ 351 | private function createUrlInsert($path, $options = []) 352 | { 353 | if (!is_string($path)) { 354 | $url = implode('/', array_map(function ($a) { 355 | return urlencode(is_array($a) ? implode(',', $a) : $a); 356 | }, $path)); 357 | $es_pro_id = Yii::$app->params['es_pro_id']; 358 | $url .= '/'.$es_pro_id.'?op_type=create'; 359 | //if (!empty($options)) { 360 | // $url .= '?' . http_build_query($options); 361 | // } 362 | } else { 363 | $url = $path; 364 | if (!empty($options)) { 365 | $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options); 366 | } 367 | } 368 | 369 | $node = $this->nodes[$this->activeNode]; 370 | $protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol; 371 | $host = $node['http_address']; 372 | 373 | return [$protocol, $host, $url]; 374 | } 375 | 376 | /** 377 | * Performs PUT HTTP request 378 | * 379 | * @param string|array $url URL 380 | * @param array $options URL options 381 | * @param string $body request body 382 | * @param boolean $raw if response body contains JSON and should be decoded 383 | * @return mixed response 384 | * @throws Exception 385 | * @throws InvalidConfigException 386 | */ 387 | public function put($url, $options = [], $body = null, $raw = false) 388 | { 389 | $this->open(); 390 | return $this->httpRequest('PUT', $this->createUrl($url, $options), $body, $raw); 391 | } 392 | 393 | /** 394 | * Performs DELETE HTTP request 395 | * 396 | * @param string|array $url URL 397 | * @param array $options URL options 398 | * @param string $body request body 399 | * @param boolean $raw if response body contains JSON and should be decoded 400 | * @return mixed response 401 | * @throws Exception 402 | * @throws InvalidConfigException 403 | */ 404 | public function delete($url, $options = [], $body = null, $raw = false) 405 | { 406 | $this->open(); 407 | return $this->httpRequest('DELETE', $this->createUrl($url, $options), $body, $raw); 408 | } 409 | 410 | /** 411 | * Creates URL 412 | * 413 | * @param string|array $path path 414 | * @param array $options URL options 415 | * @return array 416 | */ 417 | private function createUrl($path, $options = []) 418 | { 419 | if (!is_string($path)) { 420 | $url = implode('/', array_map(function ($a) { 421 | return urlencode(is_array($a) ? implode(',', $a) : $a); 422 | }, $path)); 423 | if (!empty($options)) { 424 | $url .= '?' . http_build_query($options); 425 | } 426 | } else { 427 | $url = $path; 428 | if (!empty($options)) { 429 | $url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($options); 430 | } 431 | } 432 | 433 | $node = $this->nodes[$this->activeNode]; 434 | $protocol = isset($node['protocol']) ? $node['protocol'] : $this->defaultProtocol; 435 | $host = $node['http_address']; 436 | 437 | return [$protocol, $host, $url]; 438 | } 439 | 440 | /** 441 | * Performs HTTP request 442 | * 443 | * @param string $method method name 444 | * @param string $url URL 445 | * @param string $requestBody request body 446 | * @param boolean $raw if response body contains JSON and should be decoded 447 | * @return mixed if request failed 448 | * @throws Exception if request failed 449 | * @throws InvalidConfigException 450 | */ 451 | protected function httpRequest($method, $url, $requestBody = null, $raw = false) 452 | { 453 | $method = strtoupper($method); 454 | 455 | // response body and headers 456 | $headers = []; 457 | $headersFinished = false; 458 | $body = ''; 459 | 460 | $options = [ 461 | CURLOPT_USERAGENT => 'Yii Framework ' . Yii::getVersion() . ' ' . __CLASS__, 462 | CURLOPT_RETURNTRANSFER => false, 463 | CURLOPT_HEADER => false, 464 | // http://www.php.net/manual/en/function.curl-setopt.php#82418 465 | CURLOPT_HTTPHEADER => [ 466 | 'Expect:', 467 | 'Content-Type: application/json', 468 | ], 469 | 470 | CURLOPT_WRITEFUNCTION => function ($curl, $data) use (&$body) { 471 | $body .= $data; 472 | return mb_strlen($data, '8bit'); 473 | }, 474 | CURLOPT_HEADERFUNCTION => function ($curl, $data) use (&$headers, &$headersFinished) { 475 | if ($data === '') { 476 | $headersFinished = true; 477 | } elseif ($headersFinished) { 478 | $headersFinished = false; 479 | } 480 | if (!$headersFinished && ($pos = strpos($data, ':')) !== false) { 481 | $headers[strtolower(substr($data, 0, $pos))] = trim(substr($data, $pos + 1)); 482 | } 483 | return mb_strlen($data, '8bit'); 484 | }, 485 | CURLOPT_CUSTOMREQUEST => $method, 486 | CURLOPT_FORBID_REUSE => false, 487 | ]; 488 | 489 | if (!empty($this->auth) || isset($this->nodes[$this->activeNode]['auth']) && $this->nodes[$this->activeNode]['auth'] !== false) { 490 | $auth = isset($this->nodes[$this->activeNode]['auth']) ? $this->nodes[$this->activeNode]['auth'] : $this->auth; 491 | if (empty($auth['username'])) { 492 | throw new InvalidConfigException('Username is required to use authentication'); 493 | } 494 | if (empty($auth['password'])) { 495 | throw new InvalidConfigException('Password is required to use authentication'); 496 | } 497 | 498 | $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; 499 | $options[CURLOPT_USERPWD] = $auth['username'] . ':' . $auth['password']; 500 | } 501 | 502 | if ($this->connectionTimeout !== null) { 503 | $options[CURLOPT_CONNECTTIMEOUT] = $this->connectionTimeout; 504 | } 505 | if ($this->dataTimeout !== null) { 506 | $options[CURLOPT_TIMEOUT] = $this->dataTimeout; 507 | } 508 | if ($requestBody !== null) { 509 | $options[CURLOPT_POSTFIELDS] = $requestBody; 510 | } 511 | if ($method == 'HEAD') { 512 | $options[CURLOPT_NOBODY] = true; 513 | unset($options[CURLOPT_WRITEFUNCTION]); 514 | } else { 515 | $options[CURLOPT_NOBODY] = false; 516 | } 517 | 518 | if (is_array($url)) { 519 | list($protocol, $host, $q) = $url; 520 | if (strncmp($host, 'inet[', 5) == 0) { 521 | $host = substr($host, 5, -1); 522 | if (($pos = strpos($host, '/')) !== false) { 523 | $host = substr($host, $pos + 1); 524 | } 525 | } 526 | $profile = "$method $q#$requestBody"; 527 | $url = "$protocol://$host/$q"; 528 | } else { 529 | $profile = false; 530 | } 531 | 532 | Yii::trace("Sending request to elasticsearch node: $method $url\n$requestBody", __METHOD__); 533 | if ($profile !== false) { 534 | Yii::beginProfile($profile, __METHOD__); 535 | } 536 | 537 | $this->resetCurlHandle(); 538 | curl_setopt($this->_curl, CURLOPT_URL, $url); 539 | curl_setopt_array($this->_curl, $options); 540 | if (curl_exec($this->_curl) === false) { 541 | throw new Exception('Elasticsearch request failed: ' . curl_errno($this->_curl) . ' - ' . curl_error($this->_curl), [ 542 | 'requestMethod' => $method, 543 | 'requestUrl' => $url, 544 | 'requestBody' => $requestBody, 545 | 'responseHeaders' => $headers, 546 | 'responseBody' => $this->decodeErrorBody($body), 547 | ]); 548 | } 549 | 550 | $responseCode = curl_getinfo($this->_curl, CURLINFO_HTTP_CODE); 551 | 552 | if ($profile !== false) { 553 | Yii::endProfile($profile, __METHOD__); 554 | } 555 | 556 | if ($responseCode >= 200 && $responseCode < 300) { 557 | if ($method === 'HEAD') { 558 | return true; 559 | } else { 560 | if (isset($headers['content-length']) && ($len = mb_strlen($body, '8bit')) < $headers['content-length']) { 561 | throw new Exception("Incomplete data received from elasticsearch: $len < {$headers['content-length']}", [ 562 | 'requestMethod' => $method, 563 | 'requestUrl' => $url, 564 | 'requestBody' => $requestBody, 565 | 'responseCode' => $responseCode, 566 | 'responseHeaders' => $headers, 567 | 'responseBody' => $body, 568 | ]); 569 | } 570 | if (isset($headers['content-type']) && (!strncmp($headers['content-type'], 'application/json', 16) || !strncmp($headers['content-type'], 'text/plain', 10))) { 571 | return $raw ? $body : Json::decode($body); 572 | } 573 | throw new Exception('Unsupported data received from elasticsearch: ' . $headers['content-type'], [ 574 | 'requestMethod' => $method, 575 | 'requestUrl' => $url, 576 | 'requestBody' => $requestBody, 577 | 'responseCode' => $responseCode, 578 | 'responseHeaders' => $headers, 579 | 'responseBody' => $this->decodeErrorBody($body), 580 | ]); 581 | } 582 | } elseif ($responseCode == 404) { 583 | return false; 584 | } else { 585 | throw new Exception("Elasticsearch request failed with code $responseCode. Response body:\n{$body}", [ 586 | 'requestMethod' => $method, 587 | 'requestUrl' => $url, 588 | 'requestBody' => $requestBody, 589 | 'responseCode' => $responseCode, 590 | 'responseHeaders' => $headers, 591 | 'responseBody' => $this->decodeErrorBody($body), 592 | ]); 593 | } 594 | } 595 | 596 | private function resetCurlHandle() 597 | { 598 | // these functions do not get reset by curl automatically 599 | static $unsetValues = [ 600 | CURLOPT_HEADERFUNCTION => null, 601 | CURLOPT_WRITEFUNCTION => null, 602 | CURLOPT_READFUNCTION => null, 603 | CURLOPT_PROGRESSFUNCTION => null, 604 | CURLOPT_POSTFIELDS => null, 605 | ]; 606 | curl_setopt_array($this->_curl, $unsetValues); 607 | if (function_exists('curl_reset')) { // since PHP 5.5.0 608 | curl_reset($this->_curl); 609 | } 610 | } 611 | 612 | /** 613 | * Try to decode error information if it is valid json, return it if not. 614 | * @param $body 615 | * @return mixed 616 | */ 617 | protected function decodeErrorBody($body) 618 | { 619 | try { 620 | $decoded = Json::decode($body); 621 | if (isset($decoded['error']) && !is_array($decoded['error'])) { 622 | $decoded['error'] = preg_replace('/\b\w+?Exception\[/', "\\0\n ", $decoded['error']); 623 | } 624 | return $decoded; 625 | } catch(InvalidParamException $e) { 626 | return $body; 627 | } 628 | } 629 | 630 | public function getNodeInfo() 631 | { 632 | return $this->get([]); 633 | } 634 | 635 | public function getClusterState() 636 | { 637 | return $this->get(['_cluster', 'state']); 638 | } 639 | } 640 | -------------------------------------------------------------------------------- /yii2-elasticsearch/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fecshop/yii2_fecshop_elasticsearch/5adbff5e0c336f17d685131758c50f44d7b6fdbe/yii2-elasticsearch/README.md --------------------------------------------------------------------------------