Laravel多模型全局搜索改造:单模型搜索正常,多模型实现遇阻
搞定Laravel多模型全局搜索的正确姿势
嘿,我懂你现在的困扰——单模型搜得好好的,一搞跨表全局搜索就卡壳了。先别急,咱们一步步来解决这个问题。
首先,先给你提个小提醒:你现在的单模型scopeSearch其实藏着个小坑——whereHas和orWhere混用的时候,逻辑优先级会出问题。比如如果之后你加个where('status', 1)这样的条件,orWhere会把它给绕过去。所以先把单模型的scope调整一下,用外层where把所有搜索条件包起来:
public function scopeSearch(Builder $query, $search) { $query->where(function ($q) use ($search) { $q->whereHas('translations', function ($subQ) use ($search) { $subQ->where('value', 'like', '%' . $search . '%'); }) ->orWhere('title', 'LIKE', '%' . $search . '%') ->orWhere('sub_body', 'like', '%' . $search . '%'); }); }
好了,单模型的基础打牢了,接下来搞全局搜索。核心思路就是:分别搜索每个模型,然后把结果合并、格式化。
方案一:原生Eloquent实现(无需第三方包)
你可以写一个全局辅助函数或者专门的搜索服务类,来统一处理多模型搜索。比如这样:
1. 创建全局搜索方法
use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Str; use App\Models\Post; // 替换成你的实际模型 use App\Models\Product; // 另一个需要搜索的模型 function globalSearch(string $searchTerm): Collection { $combinedResults = new Collection(); // 搜索第一个模型:Post $posts = Post::search($searchTerm)->get(); if ($posts->isNotEmpty()) { $formattedPosts = $posts->map(function ($post) { // 优先取内容摘要,没有就用翻译内容 $summary = $post->sub_body ?? $post->translations->first()?->value; return [ 'model_type' => '文章', // 给用户看的友好类型名称 'title' => $post->title, 'link' => route('posts.show', $post), // 生成详情页链接 'summary' => Str::limit($summary, 100), // 截取100字以内的摘要 'created_at' => $post->created_at ]; }); $combinedResults = $combinedResults->merge($formattedPosts); } // 搜索第二个模型:Product $products = Product::search($searchTerm)->get(); if ($products->isNotEmpty()) { $formattedProducts = $products->map(function ($product) { return [ 'model_type' => '商品', 'title' => $product->name, 'link' => route('products.show', $product), 'summary' => Str::limit($product->description, 100), 'created_at' => $product->created_at ]; }); $combinedResults = $combinedResults->merge($formattedProducts); } // 按创建时间倒序排序,把最新的结果放前面 return $combinedResults->sortByDesc('created_at')->values(); }
2. 在控制器里调用
public function handleGlobalSearch(Request $request) { $searchTerm = $request->input('search'); $results = globalSearch($searchTerm); return view('search.global-results', compact('results')); }
方案二:用Laravel Scout简化(适合需要更强大搜索的场景)
如果你的项目需要更高效的搜索(比如分词、权重排序),可以用Laravel官方的Scout扩展包,它支持多模型搜索,还能对接Meilisearch、Algolia这些搜索引擎。
1. 安装并配置Scout
首先安装包:
composer require laravel/scout # 比如安装Meilisearch驱动(轻量开源,适合中小项目) composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle
然后在每个需要搜索的模型里引入Searchable trait,定义搜索字段:
use Laravel\Scout\Searchable; class Post extends Model { use Searchable; // 定义要索引的字段,把需要搜索的内容都放进来 public function toSearchableArray() { return [ 'id' => $this->id, 'title' => $this->title, 'sub_body' => $this->sub_body, 'translations' => $this->translations->pluck('value')->join(' '), ]; } }
2. 全局搜索调用
use App\Models\Post; use App\Models\Product; public function handleGlobalSearch(Request $request) { $searchTerm = $request->input('search'); // 搜索多个模型并合并结果 $results = Post::search($searchTerm)->get() ->merge(Product::search($searchTerm)->get()); return view('search.global-results', compact('results')); }
关于分页的小提示
如果要给全局搜索结果做分页,直接用paginate()会有问题(因为多个模型的分页没法直接合并)。这时候可以手动创建LengthAwarePaginator来处理:
use Illuminate\Pagination\LengthAwarePaginator; function globalSearchWithPagination(string $searchTerm, int $page = 1, int $perPage = 10): LengthAwarePaginator { $combinedResults = globalSearch($searchTerm); // 调用前面的全局搜索方法 $offset = ($page - 1) * $perPage; $paginatedResults = $combinedResults->slice($offset, $perPage)->values(); return new LengthAwarePaginator( $paginatedResults, $combinedResults->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); }
这样就能实现全局搜索的分页功能了。
内容的提问来源于stack exchange,提问作者Ali Özen
相关产品推荐
相关产品推荐

