config('blog.search_engine', 'like'), 'host' => config('blog.es_host', 'http://127.0.0.1:9200'), 'index' => config('blog.es_index', 'ywxapp_blog'), ]; } /** * 前台搜索:status=1 + audit_status=2 通过的文章 * @return \think\Paginator */ public static function search(string $kw, int $page = 1, int $size = 12) { $kw = trim($kw); if ($kw === '') { return BlogArticle::with(['user', 'category']) ->where('status', 1)->where('audit_status', 2) ->order('create_at', 'desc')->paginate($size, false, ['page' => $page]); } $cfg = self::conf(); if ($cfg['engine'] === 'es') { try { EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content', 'summary']); $ids = EsSearch::search($cfg['host'], $cfg['index'], $kw, ['title', 'content', 'summary'], 200); if (!empty($ids)) { $list = BlogArticle::with(['user', 'category']) ->where('status', 1) ->where('audit_status', 2) ->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(); return new \think\Paginator($list, $size, $page); } } catch (\RuntimeException $e) { // 降级 LIKE } } return BlogArticle::with(['user', 'category']) ->where('title|content|summary', 'like', '%' . $kw . '%') ->where('status', 1) ->where('audit_status', 2) ->order('create_at', 'desc') ->paginate($size, false, ['page' => $page]); } /** * 后台/会员中心文章搜索(按 uid 可选限定) */ public static function searchAdmin(string $kw, int $page = 1, int $size = 15, int $uid = 0) { $kw = trim($kw); $query = BlogArticle::with(['user', 'category']); if ($uid > 0) { $query->where('uid', $uid); } if ($kw === '') { return $query->order('create_at', 'desc')->paginate($size, false, ['page' => $page]); } $cfg = self::conf(); if ($cfg['engine'] === 'es') { try { EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content', 'summary']); $ids = EsSearch::search($cfg['host'], $cfg['index'], $kw, ['title', 'content', 'summary'], 200); if (!empty($ids)) { $q = BlogArticle::with(['user', 'category'])->whereIn('id', $ids); if ($uid > 0) { $q->where('uid', $uid); } $list = $q->select()->all(); $order = array_flip($ids); $list = collect($list)->sortBy(fn($m) => $order[$m->id] ?? PHP_INT_MAX)->values(); return new \think\Paginator($list, $size, $page); } } catch (\RuntimeException $e) { } } if ($uid > 0) { $query->where('uid', $uid); } return $query->where('title|content', 'like', '%' . $kw . '%') ->order('create_at', 'desc') ->paginate($size, false, ['page' => $page]); } /** * 同步单条到 ES */ public static function sync(int $articleId): void { $cfg = self::conf(); if ($cfg['engine'] !== 'es') { return; } try { $a = BlogArticle::find($articleId); if (!$a) { return; } EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content', 'summary']); if ((int) $a->status !== 1 || (int) $a->audit_status !== 2) { EsSearch::deleteDoc($cfg['host'], $cfg['index'], $articleId); return; } EsSearch::indexDoc($cfg['host'], $cfg['index'], $articleId, [ 'title' => $a->title, 'content' => $a->content, 'summary' => $a->summary, ]); } catch (\RuntimeException $e) { } } public static function remove(int $articleId): void { $cfg = self::conf(); if ($cfg['engine'] !== 'es') { return; } try { EsSearch::deleteDoc($cfg['host'], $cfg['index'], $articleId); } catch (\RuntimeException $e) { } } /** * 相关文章推荐:优先 ES more_like_this,未启用 ES 时降级为「同分类 + 标签重叠」 * @return \addon\blog\model\BlogArticle[] */ public static function related(int $articleId, int $limit = 6): array { $article = BlogArticle::find($articleId); if (!$article) { return []; } $cfg = self::conf(); if ($cfg['engine'] === 'es') { try { EsSearch::ensureIndex($cfg['host'], $cfg['index'], ['title', 'content', 'summary']); $ids = EsSearch::moreLikeThis( $cfg['host'], $cfg['index'], $article->title . "\n" . $article->summary . "\n" . $article->content, ['title', 'content', 'summary'], $articleId, $limit ); if (!empty($ids)) { return BlogArticle::with(['user', 'category']) ->where('status', 1) ->where('audit_status', 2) ->whereIn('id', $ids) ->select() ->all(); } } catch (\RuntimeException $e) { // 降级 } } // 降级:同分类最新文章(或标签重叠) $tagIds = $article->tags()->column('id'); if (!empty($tagIds)) { $rel = BlogArticle::with(['user', 'category']) ->alias('a') ->join('blog_article_tag t', 't.article_id = a.id') ->where('a.id', '<>', $articleId) ->where('a.status', 1) ->where('a.audit_status', 2) ->whereIn('t.tag_id', $tagIds) ->order('a.create_at', 'desc') ->limit($limit) ->select() ->all(); if ($rel->count() >= $limit) { return $rel->all(); } } return BlogArticle::with(['user', 'category']) ->where('id', '<>', $articleId) ->where('status', 1) ->where('audit_status', 2) ->where('cid', $article->cid) ->order('create_at', 'desc') ->limit($limit) ->select() ->all(); } /** * 附近文章(GEO/LBS):Haversine 球面距离过滤,零外部依赖 * @param float $lat 纬度 * @param float $lng 经度 * @param float $radiusKm 半径(千米),默认 10 * @param int $limit 返回数量 * @return \addon\blog\model\BlogArticle[] */ 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; $R = 6371; return BlogArticle::with(['user', 'category']) ->where('status', 1) ->where('audit_status', 2) ->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(); } }