config('forum.search_engine', 'like'), 'host' => config('forum.es_host', 'http://127.0.0.1:9200'), 'index' => config('forum.es_index', 'ywxapp_forum'), ]; } /** * 前台搜索:仅返回 status=1 的帖子 * @return \think\Paginator */ public static function search(string $kw, int $page = 1, int $size = 20) { $kw = trim($kw); if ($kw === '') { return ForumTopic::where('status', 1)->order('id desc')->paginate($size, false, ['page' => $page]); } $cfg = self::conf(); if ($cfg['engine'] === 'es') { try { EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content']); $ids = EsSearch::search($cfg['host'], $cfg['index'], $kw, ['title', 'content'], 200); if (!empty($ids)) { // 保持相关度顺序 + 回表取完整行 $list = ForumTopic::with(['board']) ->where('status', 1) ->whereIn('id', $ids) ->select() ->all(); $order = array_flip($ids); $list = collect($list)->sortBy(function ($m) use ($order) { return $order[$m->id] ?? PHP_INT_MAX; })->values(); // 包装成分页结构(ES 结果集直接返回,分页信息从 ids 推算) $total = count($ids); $coll = $list; $paginator = new \think\Paginator($coll, $size, $page); return $paginator; } } catch (\RuntimeException $e) { // ES 不可用:降级 LIKE } } // 默认 LIKE 分支 return ForumTopic::with(['board']) ->where('status', 1) ->where('title', 'like', '%' . addslashes($kw) . '%') ->order('id desc') ->paginate($size, false, ['page' => $page]); } /** * 同步单条到 ES(发帖/编辑后调用) */ public static function sync(int $topicId): void { $cfg = self::conf(); if ($cfg['engine'] !== 'es') { return; } try { $t = ForumTopic::find($topicId); if (!$t) { return; } EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content']); if ((int) $t->status !== 1) { EsSearch::deleteDoc($cfg['host'], $cfg['index'], $topicId); return; } EsSearch::indexDoc($cfg['host'], $cfg['index'], $topicId, [ 'title' => $t->title, 'content' => $t->content, ]); } catch (\RuntimeException $e) { // 静默:索引失败不影响主流程 } } /** * 删除索引文档 */ public static function remove(int $topicId): void { $cfg = self::conf(); if ($cfg['engine'] !== 'es') { return; } try { EsSearch::deleteDoc($cfg['host'], $cfg['index'], $topicId); } catch (\RuntimeException $e) { } } /** * 相关帖子推荐:优先 ES more_like_this,未启用 ES 时降级为「同版块 + 标题关键词」相似度 * @return \addon\forum\model\ForumTopic[] */ public static function related(int $topicId, int $limit = 6): array { $topic = ForumTopic::find($topicId); if (!$topic) { return []; } $cfg = self::conf(); if ($cfg['engine'] === 'es') { try { EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content']); $ids = EsSearch::moreLikeThis( $cfg['host'], $cfg['index'], $topic->title . "\n" . $topic->content, ['title', 'content'], $topicId, $limit ); if (!empty($ids)) { return ForumTopic::with(['board']) ->where('status', 1) ->whereIn('id', $ids) ->select() ->all(); } } catch (\RuntimeException $e) { // 降级 } } // 降级:同版块 + 标题关键词重叠,按发布时间取最近 $kw = trim($topic->title); return ForumTopic::with(['board']) ->where('status', 1) ->where('id', '<>', $topicId) ->where('board_id', $topic->board_id) ->where('title', 'like', '%' . addslashes(mb_substr($kw, 0, 10)) . '%') ->order('id desc') ->limit($limit) ->select() ->all(); } /** * 附近帖子(GEO/LBS):Haversine 球面距离过滤,零外部依赖 * @param float $lat 纬度 * @param float $lng 经度 * @param float $radiusKm 半径(千米),默认 10 * @param int $limit 返回数量 * @return \addon\forum\model\ForumTopic[] */ public static function nearby(float $lat, float $lng, float $radiusKm = 10.0, int $limit = 20): array { if ($lat == 0.0 && $lng == 0.0) { return []; } $lat = (float) $lat; $lng = (float) $lng; // 地球半径 km $R = 6371; return ForumTopic::with(['board']) ->where('status', 1) ->where('lat', '<>', 0) ->where('lng', '<>', 0) ->fieldRaw("*, ({$R} * acos(cos(radians({$lat})) * cos(radians(lat)) * cos(radians(lng) - radians({$lng})) + sin(radians({$lat})) * sin(radians(lat)))) AS distance_km") ->having('distance_km <= ' . floatval($radiusKm)) ->order('distance_km', 'asc') ->limit($limit) ->select() ->all(); } }