MongoSearch.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. /*
  3. * FecShop file.
  4. *
  5. * @link http://www.fecshop.com/
  6. * @copyright Copyright (c) 2016 FecShop Software LLC
  7. * @license http://www.fecshop.com/license/
  8. */
  9. namespace fecshop\services\search;
  10. //use fecshop\models\mongodb\Product;
  11. //use fecshop\models\mongodb\Search;
  12. use fecshop\services\Service;
  13. use Yii;
  14. /**
  15. * Search MongoSearch Service
  16. * @author Terry Zhao <2358269014@qq.com>
  17. * @since 1.0
  18. */
  19. class MongoSearch extends Service implements SearchInterface
  20. {
  21. public $searchIndexConfig;
  22. public $searchLang;
  23. public $enable;
  24. protected $_productModelName = '\fecshop\models\mongodb\Product';
  25. protected $_productModel;
  26. protected $_searchModelName = '\fecshop\models\mongodb\Search';
  27. protected $_searchModel;
  28. public function init()
  29. {
  30. parent::init();
  31. list($this->_productModelName, $this->_productModel) = \Yii::mapGet($this->_productModelName);
  32. list($this->_searchModelName, $this->_searchModel) = \Yii::mapGet($this->_searchModelName);
  33. $sModel = $this->_searchModel;
  34. /**
  35. * 初始化search model 的属性,将需要过滤的属性添加到search model的类属性中。
  36. * $searchModel = new $this->_searchModelName;
  37. * $searchModel->attributes();
  38. * 上面的获取的属性,就会有下面添加的属性了。
  39. * 将产品同步到搜索表的时候,就会把这些字段也添加进去.
  40. */
  41. $filterAttr = Yii::$service->search->filterAttr;
  42. if (is_array($filterAttr) && !empty($filterAttr)) {
  43. $sModel::$_filterColumns = $filterAttr;
  44. }
  45. }
  46. /**
  47. * 创建索引.
  48. */
  49. protected function actionInitFullSearchIndex()
  50. {
  51. $sModel = $this->_searchModel;
  52. $config1 = [];
  53. $config2 = [];
  54. //var_dump($this->searchIndexConfig);exit;
  55. if (is_array($this->searchIndexConfig) && (!empty($this->searchIndexConfig))) {
  56. foreach ($this->searchIndexConfig as $column => $weight) {
  57. $config1[$column] = 'text';
  58. $config2['weights'][$column] = (int) $weight;
  59. }
  60. }
  61. //$langCodes = Yii::$service->fecshoplang->allLangCode;
  62. if (!empty($this->searchLang) && is_array($this->searchLang)) {
  63. foreach ($this->searchLang as $langCode => $mongoSearchLangName) {
  64. /*
  65. * 如果语言不存在,譬如中文,mongodb的fullSearch是不支持中文的,
  66. * 这种情况是不能搜索的。
  67. * 能够进行搜索的语言列表:https://docs.mongodb.com/manual/reference/text-search-languages/#text-search-languages
  68. */
  69. if ($mongoSearchLangName) {
  70. $sModel::$_lang = $langCode;
  71. $searchModel = new $this->_searchModelName();
  72. $colltionM = $searchModel->getCollection();
  73. $config2['default_language'] = $mongoSearchLangName;
  74. $colltionM->createIndex($config1, $config2);
  75. }
  76. }
  77. }
  78. /*
  79. $searchModel->getCollection()->ensureIndex(
  80. [
  81. 'name' => 'text',
  82. 'description' => 'text',
  83. ],
  84. [
  85. 'weights' => [
  86. 'name' => 10,
  87. 'description' => 5,
  88. ],
  89. 'default_language'=>$store,
  90. ]
  91. );
  92. */
  93. }
  94. /**
  95. * @param $product_ids | Array ,里面的子项是MongoId类型。
  96. * 将产品表的数据同步到各个语言对应的搜索表中。
  97. */
  98. protected function actionSyncProductInfo($product_ids, $numPerPage)
  99. {
  100. $sModel = $this->_searchModel;
  101. if (is_array($product_ids) && !empty($product_ids)) {
  102. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  103. $searchModel = new $this->_searchModelName();
  104. $filter['select'] = $searchModel->attributes();
  105. $filter['asArray'] = true;
  106. $filter['where'][] = ['in', $productPrimaryKey, $product_ids];
  107. $filter['numPerPage'] = $numPerPage;
  108. $filter['pageNum'] = 1;
  109. $coll = Yii::$service->product->coll($filter);
  110. if (is_array($coll['coll']) && !empty($coll['coll'])) {
  111. foreach ($coll['coll'] as $one) {
  112. $one['product_id'] = $one['_id'];
  113. unset($one['_id']);
  114. //$langCodes = Yii::$service->fecshoplang->allLangCode;
  115. //if(!empty($langCodes) && is_array($langCodes)){
  116. // foreach($langCodes as $langCodeInfo){
  117. $one_name = $one['name'];
  118. $one_description = $one['description'];
  119. $one_short_description = $one['short_description'];
  120. if (!empty($this->searchLang) && is_array($this->searchLang)) {
  121. foreach ($this->searchLang as $langCode => $mongoSearchLangName) {
  122. $sModel::$_lang = $langCode;
  123. $searchModel = $this->_searchModel->findOne(['product_id' => $one['product_id']]);
  124. if (!$searchModel['product_id']) {
  125. $searchModel = new $this->_searchModelName();
  126. }
  127. $one['name'] = Yii::$service->fecshoplang->getLangAttrVal($one_name, 'name', $langCode);
  128. $one['description'] = Yii::$service->fecshoplang->getLangAttrVal($one_description, 'description', $langCode);
  129. $one['short_description'] = Yii::$service->fecshoplang->getLangAttrVal($one_short_description, 'short_description', $langCode);
  130. $one['sync_updated_at'] = time();
  131. Yii::$service->helper->ar->save($searchModel, $one);
  132. if ($errors = Yii::$service->helper->errors->get()) {
  133. // 报错。
  134. echo $errors;
  135. //return false;
  136. }
  137. }
  138. }
  139. }
  140. }
  141. }
  142. //echo "MongoSearch sync done ... \n";
  143. return true;
  144. }
  145. /**
  146. * @param $nowTimeStamp | int
  147. * 批量更新过程中,被更新的产品都会更新字段sync_updated_at
  148. * 删除xunSearch引擎中sync_updated_at小于$nowTimeStamp的字段.
  149. */
  150. protected function actionDeleteNotActiveProduct($nowTimeStamp)
  151. {
  152. $sModel = $this->_searchModel;
  153. echo "begin delete Mongodb Search Date \n";
  154. //$langCodes = Yii::$service->fecshoplang->allLangCode;
  155. //if(!empty($langCodes) && is_array($langCodes)){
  156. // foreach($langCodes as $langCodeInfo){
  157. if (!empty($this->searchLang) && is_array($this->searchLang)) {
  158. foreach ($this->searchLang as $langCode => $mongoSearchLangName) {
  159. $sModel::$_lang = $langCode;
  160. // 更新时间方式删除。
  161. $this->_searchModel->deleteAll([
  162. '<', 'sync_updated_at', (int) $nowTimeStamp,
  163. ]);
  164. // 不存在更新时间的直接删除掉。
  165. $this->_searchModel->deleteAll([
  166. 'sync_updated_at' => [
  167. '?exists' => false,
  168. ],
  169. ]);
  170. }
  171. }
  172. }
  173. protected function actionRemoveByProductId($product_id)
  174. {
  175. $sModel = $this->_searchModel;
  176. if (!empty($this->searchLang) && is_array($this->searchLang)) {
  177. foreach ($this->searchLang as $langCode => $mongoSearchLangName) {
  178. $sModel::$_lang = $langCode;
  179. $this->_searchModel->deleteAll([
  180. '_id' => $product_id,
  181. ]);
  182. }
  183. }
  184. return true;
  185. }
  186. /**
  187. * @param $select | Array
  188. * @param $where | Array
  189. * @param $pageNum | Int
  190. * @param $numPerPage | Array
  191. * @param $product_search_max_count | Int , 搜索结果最大产品数。
  192. * 对于上面的参数和以前的$filter类似,大致和下面的类似
  193. * [
  194. * 'category_id' => 1,
  195. * 'pageNum' => 2,
  196. * 'numPerPage' => 50,
  197. * 'orderBy' => 'name',
  198. * 'where' => [
  199. * ['>','price',11],
  200. * ['<','price',22],
  201. * ],
  202. * 'select' => ['xx','yy'],
  203. * 'group' => '$spu',
  204. * ]
  205. * 得到搜索的产品列表.
  206. */
  207. protected function actionGetSearchProductColl($select, $where, $pageNum, $numPerPage, $product_search_max_count)
  208. {
  209. // 先进行sku搜索,如果有结果,说明是针对sku的搜索
  210. $enableStatus = Yii::$service->product->getEnableStatus();
  211. $searchText = $where['$text']['$search'];
  212. $productM = Yii::$service->product->getBySku($searchText);
  213. if ($productM && $enableStatus == $productM['status']) {
  214. $collection['coll'][] = $productM;
  215. $collection['count'] = 1;
  216. } else {
  217. $filter = [
  218. 'pageNum' => $pageNum,
  219. 'numPerPage' => $numPerPage,
  220. 'where' => $where,
  221. 'product_search_max_count' => $product_search_max_count,
  222. 'select' => $select,
  223. ];
  224. //var_dump($filter);exit;
  225. $collection = $this->fullTearchText($filter);
  226. }
  227. $collection['coll'] = Yii::$service->category->product->convertToCategoryInfo($collection['coll']);
  228. //var_dump($collection);
  229. return $collection;
  230. }
  231. /**
  232. * 全文搜索
  233. * $filter Example:
  234. * $filter = [
  235. * 'pageNum' => $this->getPageNum(),
  236. * 'numPerPage' => $this->getNumPerPage(),
  237. * 'where' => $this->_where,
  238. * 'product_search_max_count' => Yii::$app->controller->module->params['product_search_max_count'],
  239. * 'select' => $select,
  240. * ];
  241. * 因为mongodb的搜索涉及到计算量,因此产品过多的情况下,要设置 product_search_max_count的值。减轻服务器负担
  242. * 因为对客户来说,前10页的产品已经足矣,后面的不需要看了,限定一下产品个数,减轻服务器的压力。
  243. * 多个spu,取score最高的那个一个显示。
  244. * 按照搜索的匹配度来进行排序,没有其他排序方式.
  245. */
  246. protected function fullTearchText($filter)
  247. {
  248. $sModel = $this->_searchModel;
  249. $where = $filter['where'];
  250. if (!isset($where['status'])) {
  251. $where['status'] = Yii::$service->product->getEnableStatus();
  252. }
  253. $product_search_max_count = $filter['product_search_max_count'] ? $filter['product_search_max_count'] : 1000;
  254. $select = $filter['select'];
  255. $pageNum = $filter['pageNum'];
  256. $numPerPage = $filter['numPerPage'];
  257. $orderBy = $filter['orderBy'];
  258. //
  259. /*
  260. * 说明:1.'search_score'=>['$meta'=>"textScore" ,这个是text搜索为了排序,
  261. * 详细参看:https://docs.mongodb.com/manual/core/text-search-operators/
  262. * 2. sort排序:search_score是全文搜索匹配后的得分,score是product表的一个字段,这个字段可以通过销售量或者其他作为参考设置。
  263. */
  264. $sModel::$_lang = Yii::$service->store->currentLangCode;
  265. //$search_data = $this->_searchModel->getCollection();
  266. //$mongodb = Yii::$app->mongodb;
  267. //$search_data = $mongodb->getCollection('full_search_product_en')
  268. $search_data = $this->_searchModel->getCollection()->find(
  269. $where,
  270. ['search_score'=>['$meta'=>'textScore'], 'id' => 1, 'spu'=> 1, 'score' => 1,'product_id' => 1],
  271. [
  272. 'sort' => ['search_score'=> ['$meta'=> 'textScore'], 'score' => -1],
  273. 'limit'=> $product_search_max_count,
  274. ]
  275. );
  276. /**
  277. * 在搜索页面, spu相同的sku,是否只显示其中score高的sku,其他的sku隐藏
  278. * 如果设置为true,那么在搜索结果页面,spu相同,sku不同的产品,只会显示score最高的那个产品
  279. * 如果设置为false,那么在搜索结果页面,所有的sku都显示。
  280. * 这里做设置的好处,譬如服装,一个spu的不同颜色尺码可能几十个产品,都显示出来会占用很多的位置,对于这种产品您可以选择设置true
  281. * 这个针对的京东模式的产品
  282. */
  283. $data = [];
  284. if (Yii::$service->search->productSpuShowOnlyOneSku) {
  285. foreach ($search_data as $one) {
  286. if (!isset($data[$one['spu']])) {
  287. $data[$one['spu']] = $one;
  288. }
  289. }
  290. } else {
  291. $data = $search_data;
  292. }
  293. $count = count($data);
  294. $offset = ($pageNum - 1) * $numPerPage;
  295. $limit = $numPerPage;
  296. $productIds = [];
  297. foreach ($data as $d) {
  298. $productIds[] = $d['product_id'];
  299. }
  300. $productIds = array_slice($productIds, $offset, $limit);
  301. if (!empty($productIds)) {
  302. $query = $this->_productModel->find()->asArray()
  303. ->select($select)
  304. ->where(['_id'=> ['$in'=>$productIds]]);
  305. $data = $query->all();
  306. /**
  307. * 下面的代码的作用:将结果按照上面in查询的顺序进行数组的排序,使结果和上面的搜索结果排序一致(_id)。
  308. */
  309. $s_data = [];
  310. foreach ($data as $one) {
  311. if ($one['_id']) {
  312. $_id = (string) $one['_id'];
  313. $s_data[$_id] = $one;
  314. }
  315. }
  316. $return_data = [];
  317. foreach ($productIds as $product_id) {
  318. $pid = (string) $product_id;
  319. if (isset($s_data[$pid]) && $s_data[$pid]) {
  320. $return_data[] = $s_data[$pid];
  321. }
  322. }
  323. return [
  324. 'coll' => $return_data,
  325. 'count'=> $count,
  326. ];
  327. }
  328. }
  329. /**
  330. * @param $filter_attr | String 需要进行统计的字段名称
  331. * @propertuy $where | Array 搜索条件。这个需要些mongodb的搜索条件。
  332. * 得到的是个属性,以及对应的个数。
  333. * 这个功能是用于前端分类侧栏进行属性过滤。
  334. */
  335. protected function actionGetFrontSearchFilter($filter_attr, $where)
  336. {
  337. if (empty($where)) {
  338. return [];
  339. }
  340. $group['_id'] = '$'.$filter_attr;
  341. $group['count'] = ['$sum'=> 1];
  342. $project = [$filter_attr => 1];
  343. $pipelines = [
  344. [
  345. '$match' => $where,
  346. ],
  347. [
  348. '$project' => $project,
  349. ],
  350. [
  351. '$group' => $group,
  352. ],
  353. ];
  354. $sModel = $this->_searchModel;
  355. $sModel::$_lang = Yii::$service->store->currentLangCode;
  356. $filter_data = $this->_searchModel->getCollection()->aggregate($pipelines);
  357. return $filter_data;
  358. }
  359. }