Search.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | 搜索管理
  4. // +----------------------------------------------------------------------
  5. namespace addons\cms\controller;
  6. use addons\cms\library\FulltextSearch;
  7. use addons\cms\library\MeiliSearch;
  8. use addons\cms\model\Cms as CmsModel;
  9. use addons\cms\model\Models;
  10. use think\exception\ValidateException;
  11. use think\facade\Db;
  12. class Search extends Cmsbase
  13. {
  14. protected $noNeedLogin = ['*'];
  15. protected $noNeedRight = [];
  16. protected $cms_model = null;
  17. protected function initialize()
  18. {
  19. parent::initialize();
  20. $this->cms_model = new CmsModel;
  21. }
  22. public function index()
  23. {
  24. if ($this->cmsConfig['site_searchtype'] == 'xunsearch') {
  25. $info = get_addon_info('xunsearch');
  26. if (!$info || $info['status'] != 1) {
  27. return $this->error('请在后台插件管理中安装《迅搜搜索》并启用后再尝试');
  28. }
  29. return $this->xunsearch();
  30. }
  31. if ($this->cmsConfig['site_searchtype'] == 'meilisearch') {
  32. $info = get_addon_info('meilisearch');
  33. if (!$info || $info['status'] != 1) {
  34. return $this->error('请在后台插件管理中安装《Meilisearch》并启用后再尝试');
  35. }
  36. return $this->meilisearch();
  37. }
  38. $seo = seo('', cms_t('site_search', '站内搜索'));
  39. $modelid = $this->request->param('modelid/d', 0);
  40. $q = $this->request->param('q/s', '', 'trim,safe_replace,strip_tags,htmlspecialchars');
  41. $q = str_replace('%', '', $q);
  42. $time = $this->request->param('time/s', '');
  43. $scope = $this->request->param('scope/s', 'site');
  44. $tab = $this->request->param('tab/s', '');
  45. try {
  46. $this->validate([
  47. 'q' => $q,
  48. ], [
  49. 'q|标题关键词' => 'chsDash|max:25',
  50. ]);
  51. } catch (ValidateException $e) {
  52. $this->error($e->getMessage());
  53. }
  54. $begin = microtime(true);
  55. if ($time == 'day') {
  56. $search_time = time() - 86400;
  57. $sql_time = ' AND publish_time > ' . $search_time;
  58. } elseif ($time == 'week') {
  59. $search_time = time() - 604800;
  60. $sql_time = ' AND publish_time > ' . $search_time;
  61. } elseif ($time == 'month') {
  62. $search_time = time() - 2592000;
  63. $sql_time = ' AND publish_time > ' . $search_time;
  64. } elseif ($time == 'year') {
  65. $search_time = time() - 31536000;
  66. $sql_time = ' AND publish_time > ' . $search_time;
  67. } else {
  68. $search_time = 0;
  69. $sql_time = '';
  70. }
  71. $shistory = session('shistory');
  72. if (!$shistory) {
  73. $shistory = [];
  74. }
  75. if (!empty($q)) {
  76. array_unshift($shistory, $q);
  77. $shistory = array_slice(array_unique(array_filter($shistory)), 0, 10);
  78. session('shistory', $shistory);
  79. }
  80. $modellist = Models::where('status', 1)->where('module', 'cms')->column('*', 'id');
  81. if (!$modellist) {
  82. return $this->error('没有可搜索模型~');
  83. }
  84. $newsModelIds = [1];
  85. $productModelIds = [2];
  86. $helpModelIds = [9];
  87. $tabModelMap = [
  88. 'product' => $productModelIds,
  89. 'news' => $newsModelIds,
  90. 'help' => $helpModelIds,
  91. ];
  92. $isSupportSearch = $scope === 'support_faq';
  93. $defaultTab = in_array($tab, ['product', 'news', 'help'], true) ? $tab : ($isSupportSearch ? 'help' : 'product');
  94. $allowedModelIds = array_merge($newsModelIds, $productModelIds, $helpModelIds);
  95. $currentLang = function_exists('cms_current_lang') ? (string)cms_current_lang() : '';
  96. $currentPage = max(1, (int)$this->request->param('page/d', 1));
  97. $perPage = 10;
  98. $pages = '';
  99. $simplePages = [];
  100. $groupedResults = [
  101. 'product' => [],
  102. 'news' => [],
  103. 'help' => [],
  104. ];
  105. $groupCounts = [
  106. 'product' => 0,
  107. 'news' => 0,
  108. 'help' => 0,
  109. ];
  110. $buildSearchWhere = function ($modelKey, $modelMeta) use ($q, $currentLang, $sql_time) {
  111. $searchField = Db::name('cms_field')->where('modelid', $modelKey)->where('ifsystem', 1)->where('ifsearch', 1)->column('name');
  112. $tableName = $modelMeta['tablename'] ?? '';
  113. $extMatchedIds = [];
  114. $hasLangExtTable = false;
  115. if (!empty($tableName)) {
  116. $extTable = $tableName . '_data';
  117. try {
  118. $extFields = array_map('strtolower', array_keys(Db::name($extTable)->getFields()));
  119. if (in_array('did', $extFields, true) && in_array('lang', $extFields, true) && !empty($currentLang)) {
  120. $hasLangExtTable = true;
  121. $extSearchFields = array_values(array_intersect($searchField ?: [], $extFields));
  122. if (!in_array('title', $extSearchFields, true) && in_array('title', $extFields, true)) {
  123. $extSearchFields[] = 'title';
  124. }
  125. $extSearchFields = array_unique(array_filter($extSearchFields));
  126. if (!empty($extSearchFields)) {
  127. $dataWhereParts = [];
  128. foreach ($extSearchFields as $fieldName) {
  129. $dataWhereParts[] = "$fieldName like '%$q%'";
  130. }
  131. if (!empty($dataWhereParts)) {
  132. $dataWhere = '(' . implode(' OR ', $dataWhereParts) . ") AND lang='" . addslashes($currentLang) . "'";
  133. $extMatchedIds = Db::name($extTable)->whereRaw($dataWhere)->column('did');
  134. $extMatchedIds = array_map('intval', array_filter((array)$extMatchedIds));
  135. }
  136. }
  137. }
  138. } catch (\Throwable $e) {
  139. }
  140. }
  141. $whereParts = [];
  142. if ($hasLangExtTable && !empty($currentLang)) {
  143. if (!empty($extMatchedIds)) {
  144. $whereParts[] = 'id in (' . implode(',', $extMatchedIds) . ')';
  145. }
  146. } else {
  147. foreach ($searchField as $fieldName) {
  148. $whereParts[] = "$fieldName like '%$q%'";
  149. }
  150. if (!empty($extMatchedIds)) {
  151. $whereParts[] = 'id in (' . implode(',', $extMatchedIds) . ')';
  152. }
  153. }
  154. if (empty($whereParts)) {
  155. return '';
  156. }
  157. $where = '(' . implode(' OR ', $whereParts) . ')';
  158. $where .= " AND status='1' $sql_time";
  159. return $where;
  160. };
  161. if ($modelid) {
  162. if (!array_key_exists($modelid, $modellist) || !in_array((int)$modelid, $allowedModelIds, true)) {
  163. $this->error('模型错误~');
  164. }
  165. $where = $buildSearchWhere($modelid, $modellist[$modelid]);
  166. if ($where === '') {
  167. $this->error('没有设置搜索字段~');
  168. }
  169. $list = $this->cms_model->getList($modelid, $where, true, '*', 'listorder DESC,id DESC', $perPage, true, false, ['strict_lang' => true]);
  170. if (method_exists($list, 'items')) {
  171. $items = $list->items();
  172. foreach ($items as &$item) {
  173. if (!isset($item['modelid'])) {
  174. $item['modelid'] = (int)$modelid;
  175. }
  176. }
  177. unset($item);
  178. }
  179. $count = $list->total();
  180. $pages = $list->render();
  181. } else {
  182. foreach ($tabModelMap as $tabKey => $tabModelIds) {
  183. foreach ($tabModelIds as $tabModelId) {
  184. if (!array_key_exists($tabModelId, $modellist) || !in_array((int)$tabModelId, $allowedModelIds, true)) {
  185. continue;
  186. }
  187. $where = $buildSearchWhere($tabModelId, $modellist[$tabModelId]);
  188. if ($where === '') {
  189. continue;
  190. }
  191. $groupCounts[$tabKey] += (int)Db::name($modellist[$tabModelId]['tablename'])->whereRaw($where)->count();
  192. }
  193. }
  194. $activeModelIds = $tabModelMap[$defaultTab] ?? [];
  195. $activeModelId = 0;
  196. foreach ($activeModelIds as $candidateModelId) {
  197. if (array_key_exists($candidateModelId, $modellist) && in_array((int)$candidateModelId, $allowedModelIds, true)) {
  198. $activeModelId = (int)$candidateModelId;
  199. break;
  200. }
  201. }
  202. if ($activeModelId > 0) {
  203. $activeWhere = $buildSearchWhere($activeModelId, $modellist[$activeModelId]);
  204. if ($activeWhere !== '') {
  205. $list = $this->cms_model->getList($activeModelId, $activeWhere, true, '*', 'listorder DESC,id DESC', $perPage, true, false, ['strict_lang' => true]);
  206. if (method_exists($list, 'items')) {
  207. $items = $list->items();
  208. foreach ($items as &$item) {
  209. if (!isset($item['modelid'])) {
  210. $item['modelid'] = (int)$activeModelId;
  211. }
  212. }
  213. unset($item);
  214. }
  215. $count = $groupCounts[$defaultTab] ?? (method_exists($list, 'total') ? $list->total() : 0);
  216. $pages = $list->render();
  217. } else {
  218. $list = collect([]);
  219. $count = 0;
  220. }
  221. } else {
  222. $list = collect([]);
  223. $count = 0;
  224. }
  225. $sourceItems = method_exists($list, 'all') ? $list->all() : (is_array($list) ? $list : []);
  226. $groupedResults[$defaultTab] = $sourceItems;
  227. }
  228. if ($modelid) {
  229. $sourceItems = method_exists($list, 'all') ? $list->all() : (is_array($list) ? $list : []);
  230. foreach ($sourceItems as $item) {
  231. $itemModelId = (int)($item['modelid'] ?? 0);
  232. if (in_array($itemModelId, $productModelIds, true)) {
  233. $groupedResults['product'][] = $item;
  234. } elseif (in_array($itemModelId, $newsModelIds, true)) {
  235. $groupedResults['news'][] = $item;
  236. } elseif (in_array($itemModelId, $helpModelIds, true)) {
  237. $groupedResults['help'][] = $item;
  238. }
  239. }
  240. $groupCounts = [
  241. 'product' => count($groupedResults['product']),
  242. 'news' => count($groupedResults['news']),
  243. 'help' => count($groupedResults['help']),
  244. ];
  245. }
  246. $end = microtime(true);
  247. $this->assign([
  248. 'time' => $time,
  249. 'modelid' => $modelid,
  250. 'q' => $q,
  251. 'scope' => $scope,
  252. 'tab' => $tab,
  253. 'shistory' => $shistory,
  254. 'SEO' => $seo,
  255. 'list' => $list,
  256. 'count' => $count,
  257. 'modellist' => $modellist,
  258. 'search_time' => number_format(($end - $begin), 6),
  259. 'pages' => $pages,
  260. 'simplePages' => isset($simplePages) ? $simplePages : [],
  261. 'isSupportSearch' => $isSupportSearch,
  262. 'defaultTab' => $defaultTab,
  263. 'groupedResults' => $groupedResults,
  264. 'groupCounts' => $groupCounts,
  265. ]);
  266. if (!empty($q)) {
  267. return $this->fetch('/search_result');
  268. }
  269. return $this->fetch('/search');
  270. }
  271. public function xunsearch()
  272. {
  273. $seo = seo('', cms_t('site_search', '站内搜索'));
  274. $modelid = $this->request->param('modelid/d', 0);
  275. $q = $this->request->param('q/s', '', 'trim,safe_replace,strip_tags,htmlspecialchars');
  276. $q = str_replace('%', '', $q);
  277. $time = $this->request->param('time/s', '');
  278. $page = $this->request->get('page/d', 1);
  279. $pagesize = 5;
  280. $order = $this->request->get('order', '');
  281. $fulltext = $this->request->get('fulltext/d', '1');
  282. $fuzzy = $this->request->get('fuzzy/d', '0');
  283. $synonyms = $this->request->get('synonyms/d', '0');
  284. try {
  285. $this->validate([
  286. 'q' => $q,
  287. ], [
  288. 'q|标题关键词' => 'max:25',
  289. ]);
  290. } catch (ValidateException $e) {
  291. $this->error($e->getMessage());
  292. }
  293. $search = FulltextSearch::setQuery($q, $fulltext, $fuzzy, $synonyms);
  294. if ($modelid > 0) {
  295. $search->addQueryString("modelid:({$modelid})");
  296. }
  297. if ($time == 'day') {
  298. $search_time = time() - 86400;
  299. $search->addRange('publish_time', $search_time, time());
  300. } elseif ($time == 'week') {
  301. $search_time = time() - 604800;
  302. $search->addRange('publish_time', $search_time, time());
  303. } elseif ($time == 'month') {
  304. $search_time = time() - 2592000;
  305. $search->addRange('publish_time', $search_time, time());
  306. } elseif ($time == 'year') {
  307. $search_time = time() - 31536000;
  308. $search->addRange('publish_time', $search_time, time());
  309. }
  310. $modellist = Models::where('status', 1)->where('module', 'cms')->select()->toArray();
  311. if (!$modellist) {
  312. return $this->error('没有可搜索模型~');
  313. }
  314. $query = ['q' => $q, 'modelid' => $modelid];
  315. $result = FulltextSearch::search($page, $pagesize, $order, $query);
  316. $hot = FulltextSearch::hot();
  317. $this->assign([
  318. 'time' => $time,
  319. 'modelid' => $modelid,
  320. 'q' => $q,
  321. 'SEO' => $seo,
  322. 'list' => $result['list'],
  323. 'count' => $result['count'],
  324. 'total' => $result['total'],
  325. 'search_time' => $result['search_time'],
  326. 'pages' => $result['list'] ? $result['list']->render() : '',
  327. 'search' => $result['search'],
  328. 'corrected' => $result['corrected'],
  329. 'related' => $result['related'],
  330. 'hot' => $hot,
  331. 'modellist' => $modellist,
  332. ]);
  333. if (!empty($q)) {
  334. return $this->fetch('/xunsearch_result');
  335. }
  336. return $this->fetch('/search');
  337. }
  338. public function meilisearch()
  339. {
  340. $seo = seo('', cms_t('site_search', '站内搜索'));
  341. $modelid = $this->request->param('modelid/d', 0);
  342. $q = $this->request->param('q/s', '', 'trim,safe_replace,strip_tags,htmlspecialchars');
  343. $q = str_replace('%', '', $q);
  344. $time = $this->request->param('time/s', '');
  345. $page = $this->request->get('page/d', 1);
  346. $pagesize = 5;
  347. $sort = $this->request->get('sort/s', '');
  348. $order = $this->request->get('order/s', 'desc');
  349. try {
  350. $this->validate([
  351. 'q' => $q,
  352. ], [
  353. 'q|标题关键词' => 'max:25',
  354. ]);
  355. } catch (ValidateException $e) {
  356. $this->error($e->getMessage());
  357. }
  358. $query = $pagequery = ['q' => $q];
  359. $modellist = Models::where('status', 1)->where('module', 'cms')->column('*', 'id');
  360. if (!$modellist) {
  361. return $this->error('没有可搜索模型~');
  362. }
  363. if ($modelid > 0) {
  364. $pagequery['modelid'] = $modelid;
  365. if (isset($query['filter'])) {
  366. $query['filter'] .= " AND modelid = {$modelid}";
  367. } else {
  368. $query['filter'] = "modelid = {$modelid}";
  369. }
  370. }
  371. if ($time) {
  372. $timeRanges = [
  373. 'day' => time() - 86400,
  374. 'week' => time() - 604800,
  375. 'month' => time() - 2592000,
  376. 'year' => time() - 31536000,
  377. ];
  378. $pagequery['time'] = $time;
  379. if (isset($timeRanges[$time])) {
  380. $search_time = $timeRanges[$time];
  381. if (isset($query['filter'])) {
  382. $query['filter'] .= " AND publish_time >= {$search_time}";
  383. } else {
  384. $query['filter'] = "publish_time >= {$search_time}";
  385. }
  386. }
  387. }
  388. if ($sort) {
  389. $pagequery['sort'] = $sort;
  390. $pagequery['order'] = $order;
  391. }
  392. $query['attributesToHighlight'] = ['title', 'content'];
  393. $result = MeiliSearch::search($page, $pagesize, "{$sort},{$order}", $query, $pagequery);
  394. $this->assign([
  395. 'SEO' => $seo,
  396. 'q' => $q,
  397. 'modelid' => $modelid,
  398. 'time' => $time,
  399. 'count' => $result['count'],
  400. 'search_time' => $result['search_time'],
  401. 'list' => $result['list'],
  402. 'pages' => $result['list'] ? $result['list']->render() : '',
  403. 'modellist' => $modellist,
  404. 'sort' => $sort,
  405. 'order' => $order,
  406. ]);
  407. if (!empty($q)) {
  408. return $this->fetch('/meilisearch_result');
  409. }
  410. return $this->fetch('/search');
  411. }
  412. }