XunSearch.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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\xunsearch\Search as XunSearchModel;
  12. use fecshop\services\Service;
  13. use Yii;
  14. /**
  15. * Search XunSearch Service.
  16. * @author Terry Zhao <2358269014@qq.com>
  17. * @since 1.0
  18. */
  19. class XunSearch extends Service implements SearchInterface
  20. {
  21. public $searchIndexConfig;
  22. //public $searchLang;
  23. public $fuzzy = false;
  24. public $synonyms = false;
  25. //protected $_productModelName = '\fecshop\models\mongodb\Product';
  26. //protected $_productModel;
  27. protected $_searchModelName = '\fecshop\models\xunsearch\Search';
  28. protected $_searchModel;
  29. public function init()
  30. {
  31. parent::init();
  32. //list($this->_productModelName, $this->_productModel) = \Yii::mapGet($this->_productModelName);
  33. list($this->_searchModelName, $this->_searchModel) = \Yii::mapGet($this->_searchModelName);
  34. }
  35. /**
  36. * 初始化xunSearch索引.
  37. */
  38. protected function actionInitFullSearchIndex()
  39. {
  40. }
  41. protected function getProductSelectData()
  42. {
  43. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  44. //echo $productPrimaryKey;exit;
  45. return [
  46. $productPrimaryKey,
  47. 'name',
  48. 'spu',
  49. 'sku',
  50. 'score',
  51. 'status',
  52. 'is_in_stock',
  53. 'url_key',
  54. 'price',
  55. 'cost_price',
  56. 'special_price',
  57. 'special_from',
  58. 'special_to',
  59. 'final_price', // 算出来的最终价格。这个通过脚本赋值。
  60. 'image',
  61. 'short_description',
  62. 'description',
  63. 'created_at',
  64. ];
  65. }
  66. protected $_searchLangCode;
  67. protected function getActiveLangCode()
  68. {
  69. if (!$this->_searchLangCode) {
  70. $langArr = Yii::$app->store->get('mutil_lang');
  71. foreach ($langArr as $one) {
  72. if ($one['search_engine'] == 'xunSearch') {
  73. $this->_searchLangCode[] = $one['lang_code'];
  74. }
  75. }
  76. }
  77. return $this->_searchLangCode;
  78. }
  79. /**
  80. * 将产品信息同步到xunSearch引擎中.
  81. */
  82. protected function actionSyncProductInfo($product_ids, $numPerPage)
  83. {
  84. if (is_array($product_ids) && !empty($product_ids)) {
  85. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  86. $xunSearchModel = new $this->_searchModelName();
  87. $filter['select'] = $this->getProductSelectData();
  88. $filter['asArray'] = true;
  89. $filter['where'][] = ['in', $productPrimaryKey, $product_ids];
  90. $filter['numPerPage'] = $numPerPage;
  91. $filter['pageNum'] = 1;
  92. $coll = Yii::$service->product->coll($filter);
  93. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  94. if (is_array($coll['coll']) && !empty($coll['coll'])) {
  95. foreach ($coll['coll'] as $one) {
  96. $one['_id'] = $one[$productPrimaryKey];
  97. $one['status'] = (int)$one['status'];
  98. $one['score'] = (int)$one['score'];
  99. $one['is_in_stock'] = (int)$one['is_in_stock'];
  100. $one['created_at'] = (int)$one['created_at'];
  101. $one['price'] = (float)$one['price'];
  102. $one['cost_price'] = (float)$one['cost_price'];
  103. $one['special_price'] = (float)$one['special_price'];
  104. $one['special_from'] = (int)$one['special_from'];
  105. $one['special_to'] = (int)$one['special_to'];
  106. $one['final_price'] = (float)$one['final_price'];
  107. //unset($one[$productPrimaryKey]);
  108. $one_name = $one['name'];
  109. $one_description = $one['description'];
  110. $one_short_description = $one['short_description'];
  111. $searchLangCode = $this->getActiveLangCode();
  112. if (!empty($searchLangCode) && is_array($searchLangCode)) {
  113. foreach ($searchLangCode as $langCode) {
  114. //echo $langCode;
  115. $xunSearchModel = new $this->_searchModelName();
  116. $xunSearchModel->_id = (string) $one[$productPrimaryKey];
  117. $one['name'] = Yii::$service->fecshoplang->getLangAttrVal($one_name, 'name', $langCode);
  118. $one['description'] = Yii::$service->fecshoplang->getLangAttrVal($one_description, 'description', $langCode);
  119. $one['short_description'] = Yii::$service->fecshoplang->getLangAttrVal($one_short_description, 'short_description', $langCode);
  120. $one['sync_updated_at'] = time();
  121. //echo $one['name']."\n";
  122. $serialize = true;
  123. Yii::$service->helper->ar->save($xunSearchModel, $one, $serialize);
  124. if ($errors = Yii::$service->helper->errors->get()) {
  125. // 报错。
  126. var_dump($errors);
  127. //return false;
  128. }
  129. }
  130. }
  131. }
  132. }
  133. }
  134. //echo "XunSearch sync done ... \n";
  135. return true;
  136. }
  137. protected function actionDeleteNotActiveProduct($nowTimeStamp)
  138. {
  139. }
  140. /**
  141. * 删除在xunSearch的所有搜索数据,
  142. * 当您的产品有很多产品被删除了,但是在xunsearch 存在某些异常没有被删除
  143. * 您希望也被删除掉,那么,你可以通过这种方式批量删除掉产品
  144. * 然后重新跑一边同步脚本.
  145. */
  146. protected function actionXunDeleteAllProduct($numPerPage, $i)
  147. {
  148. //var_dump($index);
  149. $dbName = $this->_searchModel->projectName();
  150. // 删除索引
  151. Yii::$app->xunsearch->getDatabase($dbName)->getIndex()->clean();
  152. //$index = Yii::$app->xunsearch->getDatabase($dbName)->index;
  153. echo "begin delete Xun Search Date \n";
  154. $nowTimeStamp = (int) $nowTimeStamp;
  155. $XunSearchData = $this->_searchModel->find()
  156. ->limit($numPerPage)
  157. ->offset(($i - 1) * $numPerPage)
  158. ->all();
  159. foreach ($XunSearchData as $one) {
  160. $one->delete();
  161. }
  162. }
  163. /**
  164. * 得到搜索的产品列表.
  165. */
  166. protected function actionGetSearchProductColl($select, $where, $pageNum, $numPerPage, $product_search_max_count)
  167. {
  168. $collection = $this->fullTearchText($select, $where, $pageNum, $numPerPage, $product_search_max_count);
  169. $collection['coll'] = Yii::$service->category->product->convertToCategoryInfo($collection['coll']);
  170. //var_dump($collection);
  171. //exit;
  172. return $collection;
  173. }
  174. protected function fullTearchText($select, $where, $pageNum, $numPerPage, $product_search_max_count)
  175. {
  176. $enableStatus = Yii::$service->product->getEnableStatus();
  177. $searchText = $where['$text']['$search'];
  178. $productM = Yii::$service->product->getBySku($searchText);
  179. $productIds = [];
  180. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  181. if ($productM && $enableStatus == $productM['status']) {
  182. $productIds[] = $productM[$productPrimaryKey];
  183. } else {
  184. if (!isset($where['status'])) {
  185. $where['status'] = Yii::$service->product->getEnableStatus();
  186. }
  187. $XunSearchQuery = $this->_searchModel->find()->asArray();
  188. $XunSearchQuery->fuzzy($this->fuzzy);
  189. $XunSearchQuery->synonyms($this->synonyms);
  190. if (is_array($where) && !empty($where)) {
  191. if (isset($where['$text']['$search']) && $where['$text']['$search']) {
  192. $XunSearchQuery->where($where['$text']['$search']);
  193. } else {
  194. return [];
  195. }
  196. foreach ($where as $k => $v) {
  197. if ($k === '$text') {
  198. continue;
  199. }
  200. if (is_array($v)) {
  201. // 范围查询,类似 [ 'price' => [ '$gte'=> 100, '$lte' => 150 ] ] 的这种情况
  202. // 如果$k的值为`price`, 改为 `final_price`
  203. $k !== 'price' || $k = 'final_price';
  204. // 得到范围查询的开始和结束值,如果范围查询只有开始,譬如 x < 3, 那么范围的结束用空字符串'', 不能使用null,使用null会跑出异常, 详细参看
  205. // @vendor/hightman/xunsearch/wrapper/yii2-ext/QueryBuilder.php 281行函数。
  206. $rangBegin = isset($v['$gte']) ? $v['$gte'] : (isset($v['$gt']) ? $v['$gt'] : '');
  207. $rangEnd = isset($v['$lte']) ? $v['$lte'] : (isset($v['$lt']) ? $v['$lt'] : '');
  208. // 关于xunsearch的查询,参看:https://github.com/hightman/xs-sdk-php#%E6%A3%80%E7%B4%A2%E5%AF%B9%E8%B1%A1
  209. $XunSearchQuery->andWhere(['BETWEEN', $k, $rangBegin, $rangEnd]);
  210. } else {
  211. $XunSearchQuery->andWhere([$k => $v]);
  212. }
  213. }
  214. }
  215. $XunSearchQuery->orderBy(['score' => SORT_DESC]);
  216. $XunSearchQuery->limit($product_search_max_count);
  217. $XunSearchQuery->offset(0);
  218. $search_data = $XunSearchQuery->all();
  219. /**
  220. * 在搜索页面, spu相同的sku,是否只显示其中score高的sku,其他的sku隐藏
  221. * 如果设置为true,那么在搜索结果页面,spu相同,sku不同的产品,只会显示score最高的那个产品
  222. * 如果设置为false,那么在搜索结果页面,所有的sku都显示。
  223. * 这里做设置的好处,譬如服装,一个spu的不同颜色尺码可能几十个产品,都显示出来会占用很多的位置,对于这种产品您可以选择设置true
  224. * 这个针对的京东模式的产品
  225. */
  226. $data = [];
  227. if (Yii::$service->search->productSpuShowOnlyOneSku) {
  228. foreach ($search_data as $one) {
  229. if (!isset($data[$one['spu']])) {
  230. $data[$one['spu']] = $one;
  231. }
  232. }
  233. } else {
  234. $data = $search_data;
  235. }
  236. $count = count($data);
  237. $offset = ($pageNum - 1) * $numPerPage;
  238. $limit = $numPerPage;
  239. $productIds = [];
  240. foreach ($data as $d) {
  241. if ($productPrimaryKey == '_id') {
  242. if (strlen($d['_id']) == 24) {
  243. $productIds[] = new \MongoDB\BSON\ObjectId($d['_id']);
  244. }
  245. } else {
  246. $productIds[] = $d['_id'];
  247. }
  248. }
  249. $productIds = array_slice($productIds, $offset, $limit);
  250. }
  251. $productPrimaryKey = Yii::$service->product->getPrimaryKey();
  252. if (!empty($productIds)) {
  253. //
  254. foreach ($select as $sk => $se) {
  255. if ($se == 'product_id') {
  256. unset($select[$sk]);
  257. }
  258. }
  259. $select[] = $productPrimaryKey;
  260. $filter = [
  261. 'select' => $select,
  262. 'where' => [
  263. [ 'in', $productPrimaryKey, $productIds]
  264. ],
  265. ];
  266. $collData = Yii::$service->product->coll($filter);
  267. $data = $collData['coll'];
  268. /**
  269. * 下面的代码的作用:将结果按照上面in查询的顺序进行数组的排序,使结果和上面的搜索结果排序一致(_id)。
  270. */
  271. //var_dump($data);exit;
  272. $s_data = [];
  273. foreach ($data as $one) {
  274. if ($one[$productPrimaryKey]) {
  275. $_id = (string) $one[$productPrimaryKey];
  276. $s_data[$_id] = $one;
  277. }
  278. }
  279. $return_data = [];
  280. foreach ($productIds as $product_id) {
  281. $pid = (string) $product_id;
  282. if (isset($s_data[$pid]) && $s_data[$pid]) {
  283. $return_data[] = $s_data[$pid];
  284. }
  285. }
  286. return [
  287. 'coll' => $return_data,
  288. 'count'=> $count,
  289. ];
  290. }
  291. }
  292. /**
  293. * 得到搜索的sku列表侧栏的过滤.
  294. */
  295. protected function actionGetFrontSearchFilter($filter_attr, $where)
  296. {
  297. //var_dump($where);
  298. $dbName = $this->_searchModel->projectName();
  299. $_search = Yii::$app->xunsearch->getDatabase($dbName)->getSearch();
  300. $text = isset($where['$text']['$search']) ? $where['$text']['$search'] : '';
  301. if (!$text) {
  302. return [];
  303. }
  304. $sh = '';
  305. foreach ($where as $k => $v) {
  306. if ($k != '$text') {
  307. if (!$sh) {
  308. $sh = ' AND '.$k.':'.$v;
  309. } else {
  310. $sh .= ' AND '.$k.':'.$v;
  311. }
  312. }
  313. }
  314. //echo $sh;
  315. $docs = $_search->setQuery($text.$sh)
  316. ->setFacets([$filter_attr])
  317. ->setFuzzy($this->fuzzy)
  318. ->setAutoSynonyms($this->synonyms)
  319. ->search();
  320. $filter_attr_counts = $_search->getFacets($filter_attr);
  321. $count_arr = [];
  322. if (is_array($filter_attr_counts) && !empty($filter_attr_counts)) {
  323. foreach ($filter_attr_counts as $k => $v) {
  324. $count_arr[] = [
  325. '_id' => $k,
  326. 'count' => $v,
  327. ];
  328. }
  329. }
  330. return $count_arr;
  331. }
  332. /**
  333. * 通过product_id删除搜索数据.
  334. */
  335. protected function actionRemoveByProductId($product_id)
  336. {
  337. if (is_object($product_id)) {
  338. $product_id = (string) $product_id;
  339. $model = $this->_searchModel->findOne($product_id);
  340. if ($model) {
  341. $model->delete();
  342. }
  343. }
  344. }
  345. }