MongoSearch.php 18 KB

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