* @since 1.0 */ class MongoSearch extends Service implements SearchInterface { public $searchIndexConfig; public $searchLang; public $enable; protected $_productModelName = '\fecshop\models\mongodb\Product'; protected $_productModel; protected $_searchModelName = '\fecshop\models\mongodb\Search'; protected $_searchModel; public function init() { parent::init(); list($this->_productModelName, $this->_productModel) = \Yii::mapGet($this->_productModelName); list($this->_searchModelName, $this->_searchModel) = \Yii::mapGet($this->_searchModelName); $sModel = $this->_searchModel; /** * 初始化search model 的属性,将需要过滤的属性添加到search model的类属性中。 * $searchModel = new $this->_searchModelName; * $searchModel->attributes(); * 上面的获取的属性,就会有下面添加的属性了。 * 将产品同步到搜索表的时候,就会把这些字段也添加进去. */ $filterAttr = Yii::$service->search->filterAttr; if (is_array($filterAttr) && !empty($filterAttr)) { $sModel::$_filterColumns = $filterAttr; } } /** * 创建索引. */ protected function actionInitFullSearchIndex() { $sModel = $this->_searchModel; $config1 = []; $config2 = []; //var_dump($this->searchIndexConfig);exit; if (is_array($this->searchIndexConfig) && (!empty($this->searchIndexConfig))) { foreach ($this->searchIndexConfig as $column => $weight) { $config1[$column] = 'text'; $config2['weights'][$column] = (int) $weight; } } //$langCodes = Yii::$service->fecshoplang->allLangCode; if (!empty($this->searchLang) && is_array($this->searchLang)) { foreach ($this->searchLang as $langCode => $mongoSearchLangName) { /* * 如果语言不存在,譬如中文,mongodb的fullSearch是不支持中文的, * 这种情况是不能搜索的。 * 能够进行搜索的语言列表:https://docs.mongodb.com/manual/reference/text-search-languages/#text-search-languages */ if ($mongoSearchLangName) { $sModel::$_lang = $langCode; $searchModel = new $this->_searchModelName(); $colltionM = $searchModel->getCollection(); $config2['default_language'] = $mongoSearchLangName; $colltionM->createIndex($config1, $config2); } } } /* $searchModel->getCollection()->ensureIndex( [ 'name' => 'text', 'description' => 'text', ], [ 'weights' => [ 'name' => 10, 'description' => 5, ], 'default_language'=>$store, ] ); */ } /** * @param $product_ids | Array ,里面的子项是MongoId类型。 * 将产品表的数据同步到各个语言对应的搜索表中。 */ protected function actionSyncProductInfo($product_ids, $numPerPage) { $sModel = $this->_searchModel; if (is_array($product_ids) && !empty($product_ids)) { $productPrimaryKey = Yii::$service->product->getPrimaryKey(); $searchModel = new $this->_searchModelName(); $filter['select'] = $searchModel->attributes(); $filter['asArray'] = true; $filter['where'][] = ['in', $productPrimaryKey, $product_ids]; $filter['numPerPage'] = $numPerPage; $filter['pageNum'] = 1; $coll = Yii::$service->product->coll($filter); if (is_array($coll['coll']) && !empty($coll['coll'])) { foreach ($coll['coll'] as $one) { $one['product_id'] = $one['_id']; unset($one['_id']); //$langCodes = Yii::$service->fecshoplang->allLangCode; //if(!empty($langCodes) && is_array($langCodes)){ // foreach($langCodes as $langCodeInfo){ $one_name = $one['name']; $one_description = $one['description']; $one_short_description = $one['short_description']; if (!empty($this->searchLang) && is_array($this->searchLang)) { foreach ($this->searchLang as $langCode => $mongoSearchLangName) { $sModel::$_lang = $langCode; $searchModel = $this->_searchModel->findOne(['product_id' => $one['product_id']]); if (!$searchModel['product_id']) { $searchModel = new $this->_searchModelName(); } $one['name'] = Yii::$service->fecshoplang->getLangAttrVal($one_name, 'name', $langCode); $one['description'] = Yii::$service->fecshoplang->getLangAttrVal($one_description, 'description', $langCode); $one['short_description'] = Yii::$service->fecshoplang->getLangAttrVal($one_short_description, 'short_description', $langCode); $one['sync_updated_at'] = time(); Yii::$service->helper->ar->save($searchModel, $one); if ($errors = Yii::$service->helper->errors->get()) { // 报错。 echo $errors; //return false; } } } } } } //echo "MongoSearch sync done ... \n"; return true; } /** * @param $nowTimeStamp | int * 批量更新过程中,被更新的产品都会更新字段sync_updated_at * 删除xunSearch引擎中sync_updated_at小于$nowTimeStamp的字段. */ protected function actionDeleteNotActiveProduct($nowTimeStamp) { $sModel = $this->_searchModel; echo "begin delete Mongodb Search Date \n"; //$langCodes = Yii::$service->fecshoplang->allLangCode; //if(!empty($langCodes) && is_array($langCodes)){ // foreach($langCodes as $langCodeInfo){ if (!empty($this->searchLang) && is_array($this->searchLang)) { foreach ($this->searchLang as $langCode => $mongoSearchLangName) { $sModel::$_lang = $langCode; // 更新时间方式删除。 $this->_searchModel->deleteAll([ '<', 'sync_updated_at', (int) $nowTimeStamp, ]); // 不存在更新时间的直接删除掉。 $this->_searchModel->deleteAll([ 'sync_updated_at' => [ '?exists' => false, ], ]); } } } protected function actionRemoveByProductId($product_id) { $sModel = $this->_searchModel; if (!empty($this->searchLang) && is_array($this->searchLang)) { foreach ($this->searchLang as $langCode => $mongoSearchLangName) { $sModel::$_lang = $langCode; $this->_searchModel->deleteAll([ '_id' => $product_id, ]); } } return true; } /** * @param $select | Array * @param $where | Array * @param $pageNum | Int * @param $numPerPage | Array * @param $product_search_max_count | Int , 搜索结果最大产品数。 * 对于上面的参数和以前的$filter类似,大致和下面的类似 * [ * 'category_id' => 1, * 'pageNum' => 2, * 'numPerPage' => 50, * 'orderBy' => 'name', * 'where' => [ * ['>','price',11], * ['<','price',22], * ], * 'select' => ['xx','yy'], * 'group' => '$spu', * ] * 得到搜索的产品列表. */ protected function actionGetSearchProductColl($select, $where, $pageNum, $numPerPage, $product_search_max_count) { // 先进行sku搜索,如果有结果,说明是针对sku的搜索 $enableStatus = Yii::$service->product->getEnableStatus(); $searchText = $where['$text']['$search']; $productM = Yii::$service->product->getBySku($searchText); if ($productM && $enableStatus == $productM['status']) { $collection['coll'][] = $productM; $collection['count'] = 1; } else { $filter = [ 'pageNum' => $pageNum, 'numPerPage' => $numPerPage, 'where' => $where, 'product_search_max_count' => $product_search_max_count, 'select' => $select, ]; //var_dump($filter);exit; $collection = $this->fullTearchText($filter); } $collection['coll'] = Yii::$service->category->product->convertToCategoryInfo($collection['coll']); //var_dump($collection); return $collection; } /** * 全文搜索 * $filter Example: * $filter = [ * 'pageNum' => $this->getPageNum(), * 'numPerPage' => $this->getNumPerPage(), * 'where' => $this->_where, * 'product_search_max_count' => Yii::$app->controller->module->params['product_search_max_count'], * 'select' => $select, * ]; * 因为mongodb的搜索涉及到计算量,因此产品过多的情况下,要设置 product_search_max_count的值。减轻服务器负担 * 因为对客户来说,前10页的产品已经足矣,后面的不需要看了,限定一下产品个数,减轻服务器的压力。 * 多个spu,取score最高的那个一个显示。 * 按照搜索的匹配度来进行排序,没有其他排序方式. */ protected function fullTearchText($filter) { $sModel = $this->_searchModel; $where = $filter['where']; if (!isset($where['status'])) { $where['status'] = Yii::$service->product->getEnableStatus(); } $product_search_max_count = $filter['product_search_max_count'] ? $filter['product_search_max_count'] : 1000; $select = $filter['select']; $pageNum = $filter['pageNum']; $numPerPage = $filter['numPerPage']; $orderBy = $filter['orderBy']; // /* * 说明:1.'search_score'=>['$meta'=>"textScore" ,这个是text搜索为了排序, * 详细参看:https://docs.mongodb.com/manual/core/text-search-operators/ * 2. sort排序:search_score是全文搜索匹配后的得分,score是product表的一个字段,这个字段可以通过销售量或者其他作为参考设置。 */ $sModel::$_lang = Yii::$service->store->currentLangCode; //$search_data = $this->_searchModel->getCollection(); //$mongodb = Yii::$app->mongodb; //$search_data = $mongodb->getCollection('full_search_product_en') $search_data = $this->_searchModel->getCollection()->find( $where, ['search_score'=>['$meta'=>'textScore'], 'id' => 1, 'spu'=> 1, 'score' => 1,'product_id' => 1], [ 'sort' => ['search_score'=> ['$meta'=> 'textScore'], 'score' => -1], 'limit'=> $product_search_max_count, ] ); /** * 在搜索页面, spu相同的sku,是否只显示其中score高的sku,其他的sku隐藏 * 如果设置为true,那么在搜索结果页面,spu相同,sku不同的产品,只会显示score最高的那个产品 * 如果设置为false,那么在搜索结果页面,所有的sku都显示。 * 这里做设置的好处,譬如服装,一个spu的不同颜色尺码可能几十个产品,都显示出来会占用很多的位置,对于这种产品您可以选择设置true * 这个针对的京东模式的产品 */ $data = []; if (Yii::$service->search->productSpuShowOnlyOneSku) { foreach ($search_data as $one) { if (!isset($data[$one['spu']])) { $data[$one['spu']] = $one; } } } else { $data = $search_data; } $count = count($data); $offset = ($pageNum - 1) * $numPerPage; $limit = $numPerPage; $productIds = []; foreach ($data as $d) { $productIds[] = $d['product_id']; } $productIds = array_slice($productIds, $offset, $limit); if (!empty($productIds)) { $query = $this->_productModel->find()->asArray() ->select($select) ->where(['_id'=> ['$in'=>$productIds]]); $data = $query->all(); /** * 下面的代码的作用:将结果按照上面in查询的顺序进行数组的排序,使结果和上面的搜索结果排序一致(_id)。 */ $s_data = []; foreach ($data as $one) { if ($one['_id']) { $_id = (string) $one['_id']; $s_data[$_id] = $one; } } $return_data = []; foreach ($productIds as $product_id) { $pid = (string) $product_id; if (isset($s_data[$pid]) && $s_data[$pid]) { $return_data[] = $s_data[$pid]; } } return [ 'coll' => $return_data, 'count'=> $count, ]; } } /** * @param $filter_attr | String 需要进行统计的字段名称 * @propertuy $where | Array 搜索条件。这个需要些mongodb的搜索条件。 * 得到的是个属性,以及对应的个数。 * 这个功能是用于前端分类侧栏进行属性过滤。 */ protected function actionGetFrontSearchFilter($filter_attr, $where) { if (empty($where)) { return []; } $group['_id'] = '$'.$filter_attr; $group['count'] = ['$sum'=> 1]; $project = [$filter_attr => 1]; $pipelines = [ [ '$match' => $where, ], [ '$project' => $project, ], [ '$group' => $group, ], ]; $sModel = $this->_searchModel; $sModel::$_lang = Yii::$service->store->currentLangCode; $filter_data = $this->_searchModel->getCollection()->aggregate($pipelines); return $filter_data; } }