MeiliSearch.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // |
  4. // +----------------------------------------------------------------------
  5. // |
  6. // +----------------------------------------------------------------------
  7. // |
  8. // +----------------------------------------------------------------------
  9. // |
  10. // +----------------------------------------------------------------------
  11. // +----------------------------------------------------------------------
  12. // | CMS全文搜索类(Meilisearch版本)
  13. // +----------------------------------------------------------------------
  14. namespace addons\cms\library;
  15. use addons\meilisearch\library\Meilisearch as MeilisearchLib;
  16. use think\facade\Db;
  17. class MeiliSearch
  18. {
  19. public static function config()
  20. {
  21. $data = [
  22. [
  23. 'name' => self::getProjectName(),
  24. 'title' => 'cms内容管理',
  25. 'primary_key' => 'pid',
  26. 'ranking_rules' => 'words,sort,typo,proximity,attribute,exactness',
  27. 'max_total_hits' => 1000,
  28. 'fields' => [
  29. [
  30. 'name' => 'pid',
  31. 'title' => '主键',
  32. 'is_displayed' => 0,
  33. 'is_searchable' => 0,
  34. 'is_filterable' => 0,
  35. 'is_sortable' => 0,
  36. 'listorder' => 99,
  37. ],
  38. [
  39. 'name' => 'id',
  40. 'title' => '文档ID',
  41. 'is_displayed' => 1,
  42. 'is_searchable' => 0,
  43. 'is_filterable' => 0,
  44. 'is_sortable' => 0,
  45. 'listorder' => 98,
  46. ],
  47. [
  48. 'name' => 'catid',
  49. 'title' => '栏目ID',
  50. 'is_displayed' => 1,
  51. 'is_searchable' => 0,
  52. 'is_filterable' => 1,
  53. 'is_sortable' => 0,
  54. 'listorder' => 97,
  55. ],
  56. [
  57. 'name' => 'modelid',
  58. 'title' => '模型id',
  59. 'is_displayed' => 0,
  60. 'is_searchable' => 0,
  61. 'is_filterable' => 1,
  62. 'is_sortable' => 0,
  63. 'listorder' => 96,
  64. ],
  65. [
  66. 'name' => 'title',
  67. 'title' => '标题',
  68. 'is_displayed' => 1,
  69. 'is_searchable' => 1,
  70. 'is_filterable' => 0,
  71. 'is_sortable' => 0,
  72. 'listorder' => 95,
  73. ],
  74. [
  75. 'name' => 'content',
  76. 'title' => '内容',
  77. 'is_displayed' => 1,
  78. 'is_searchable' => 1,
  79. 'is_filterable' => 0,
  80. 'is_sortable' => 0,
  81. 'listorder' => 94,
  82. ],
  83. [
  84. 'name' => 'publish_time',
  85. 'title' => '发布时间',
  86. 'is_displayed' => 1,
  87. 'is_searchable' => 0,
  88. 'is_filterable' => 1,
  89. 'is_sortable' => 1,
  90. 'listorder' => 93,
  91. ],
  92. [
  93. 'name' => 'url',
  94. 'title' => '网址',
  95. 'is_displayed' => 1,
  96. 'is_searchable' => 0,
  97. 'is_filterable' => 0,
  98. 'is_sortable' => 0,
  99. 'listorder' => 92,
  100. ],
  101. [
  102. 'name' => 'thumb',
  103. 'title' => '缩略图',
  104. 'is_displayed' => 1,
  105. 'is_searchable' => 0,
  106. 'is_filterable' => 0,
  107. 'is_sortable' => 0,
  108. 'listorder' => 91,
  109. ],
  110. ],
  111. ],
  112. ];
  113. return $data;
  114. }
  115. /**
  116. * 重置搜索索引数据库
  117. */
  118. public static function reset()
  119. {
  120. $models = array_values(cms_cache('Model'));
  121. $category = Db::name('cms_category')->where('type', 2)->column('modelid', 'id');
  122. $search = self::getSearchInstance();
  123. $totalCount = 0;
  124. if (count($models) > 0) {
  125. foreach ($models as $k => $v) {
  126. if ($v['type'] == 1) {
  127. // 独立表
  128. Db::name($v['tablename'])->where('status', 1)->chunk(100, function ($list) use ($category, $search, &$totalCount) {
  129. $documents = [];
  130. foreach ($list as $val) {
  131. $documents[] = self::buildDocument(($category[$val['catid']] ?? 0), $val['catid'], $val['id'], $val);
  132. $totalCount++;
  133. }
  134. // 批量添加
  135. $search->add($documents);
  136. });
  137. } elseif ($v['type'] == 2) {
  138. // 主附表
  139. Db::view($v['tablename'], '*')
  140. ->where('status', 1)
  141. ->view($v['tablename'] . '_data', '*', $v['tablename'] . '.id=' . $v['tablename'] . '_data' . '.did', 'LEFT')
  142. ->chunk(100, function ($list) use ($category, $search, &$totalCount) {
  143. $documents = [];
  144. foreach ($list as $val) {
  145. $documents[] = self::buildDocument(($category[$val['catid']] ?? 0), $val['catid'], $val['id'], $val, $val);
  146. $totalCount++;
  147. }
  148. // 批量添加
  149. $search->add($documents);
  150. });
  151. }
  152. }
  153. }
  154. return ['count' => $totalCount];
  155. }
  156. /**
  157. * 构建文档数据
  158. */
  159. protected static function buildDocument($modelid, $catid, $id, $data, $dataExt = [])
  160. {
  161. $content = '';
  162. if (isset($dataExt['content'])) {
  163. $content = strip_tags(htmlspecialchars_decode($dataExt['content']));
  164. // 限制内容长度,避免文档过大
  165. $content = mb_substr($content, 0, 10000, 'UTF-8');
  166. }
  167. return [
  168. 'pid' => self::generateDocumentId($catid, $id),
  169. 'id' => (int) $id,
  170. 'catid' => (int) $catid,
  171. 'modelid' => (int) $modelid,
  172. 'title' => $data['title'] ?? '',
  173. 'content' => $content,
  174. 'publish_time' => isset($data['publish_time']) ? (int) $data['publish_time'] : 0,
  175. 'url' => buildContentUrl($catid, $id),
  176. 'thumb' => $data['thumb'] ?? '',
  177. ];
  178. }
  179. /**
  180. * 生成文档ID
  181. */
  182. protected static function generateDocumentId($catid, $id)
  183. {
  184. return "{$catid}_{$id}";
  185. }
  186. /**
  187. * 解析文档ID
  188. */
  189. protected static function parseDocumentId($documentId)
  190. {
  191. if (strpos($documentId, '_') !== false) {
  192. list($catid, $id) = explode('_', $documentId, 2);
  193. return ['catid' => $catid, 'id' => $id];
  194. }
  195. return null;
  196. }
  197. /**
  198. * 新增/更新索引
  199. */
  200. public static function update($modelid, $catid, $id, $data, $dataExt = [])
  201. {
  202. try {
  203. $document = self::buildDocument($modelid, $catid, $id, $data, $dataExt);
  204. $search = self::getSearchInstance();
  205. return $search->add($document);
  206. } catch (\Exception $e) {
  207. return false;
  208. }
  209. }
  210. /**
  211. * 删除索引
  212. */
  213. public static function del($catid, $id)
  214. {
  215. try {
  216. $documentId = self::generateDocumentId($catid, $id);
  217. $search = self::getSearchInstance();
  218. return $search->del($documentId);
  219. } catch (\Exception $e) {
  220. return false;
  221. }
  222. }
  223. /**
  224. * 获取搜索结果
  225. */
  226. public static function search($page = 1, $pagesize = 20, $order = '', $query = [], $pagequery = [])
  227. {
  228. $search = self::getSearchInstance();
  229. // 构建搜索参数
  230. $searchParams = array_merge([
  231. 'q' => '',
  232. 'filter' => '',
  233. 'sort' => [],
  234. ], $query);
  235. // 执行搜索
  236. return $search->search($page, $pagesize, $order, $searchParams, $pagequery);
  237. }
  238. /**
  239. * 获取搜索实例
  240. */
  241. protected static function getSearchInstance()
  242. {
  243. $projectName = self::getProjectName();
  244. return MeilisearchLib::instance($projectName);
  245. }
  246. // 获取项目名
  247. public static function getProjectName()
  248. {
  249. $config = get_addon_config('cms');
  250. return $config['meilisearch_project_name'] ?? 'cms';
  251. }
  252. }