告别复杂搜索逻辑:Laravel Searchable 一站式模型检索解决方案
你是否还在为 Laravel 项目中的多模型搜索功能编写重复代码?是否遇到过需要同时检索用户、文章、评论等多种资源的场景?是否希望用最少的代码实现高效、灵活且可扩展的搜索系统?本文将带你深入探索 Laravel Searchable 开源项目,通过 7 个实战步骤,彻底解决多源数据检索难题,让搜索功能开发效率提升 300%。
读完本文你将掌握:
- 5 分钟快速集成多模型搜索的实现方法
- 3 种高级搜索场景(精确匹配/关联查询/自定义数据源)的落地技巧
- 性能优化的 4 个关键策略
- 从 0 到 1 构建企业级搜索系统的完整流程
项目概述:什么是 Laravel Searchable?
Laravel Searchable 是由 Spatie 团队开发的一款专注于多源数据检索的开源工具包(MIT 许可证),它允许开发者通过统一接口实现对 Eloquent 模型、外部 API、数据库视图等多种数据源的联合搜索。其核心优势在于:
| 特性 | 传统搜索实现 | Laravel Searchable |
|---|---|---|
| 多模型支持 | 需要手动编写联合查询 | 一行代码注册模型 |
| 搜索逻辑复用 | 重复编码条件判断 | 统一 SearchAspect 抽象 |
| 结果格式化 | 需手动转换结构 | 标准化 SearchResult 对象 |
| 扩展性 | 硬编码扩展困难 | 自定义 SearchAspect 机制 |
| 性能控制 | 复杂的手动分页 | 内置结果数量限制 |
该项目目前稳定版本为 1.12.0,兼容 Laravel 6.0+ 所有版本,在 Packagist 上累计下载量已突破 1,000,000 次,是 Laravel 生态中最受欢迎的搜索解决方案之一。
环境准备与安装
系统要求
- PHP 7.4+(推荐 8.0+)
- Laravel 6.0+
- Composer 2.0+
快速安装
通过 Composer 安装包:
composer require spatie/laravel-searchable
如需手动指定版本(例如在 composer.json 中锁定版本):
composer require spatie/laravel-searchable:^1.12
国内用户可使用 GitCode 镜像加速克隆:
git clone https://gitcode.com/gh_mirrors/la/laravel-searchable.git
核心概念与架构设计
在开始实战前,我们需要理解三个核心抽象概念,这是掌握该工具包的关键:
- Search 类:搜索调度中心,负责注册搜索源、执行搜索请求、聚合结果集
- SearchAspect 抽象:搜索源适配器,定义了搜索能力的标准接口,其子类包括:
- ModelSearchAspect:Eloquent 模型搜索适配器
- 自定义 SearchAspect:用于外部 API、CSV 文件等非模型数据源
- Searchable 接口:模型需实现的契约,提供结果格式化能力
- SearchResult 类:标准化结果对象,统一封装标题、URL、详情等展示属性
这种架构设计带来了极致的灵活性——无论是搜索 Eloquent 模型还是第三方服务,都能通过相同的 search() 方法获取格式化结果。
实战步骤:从基础到高级
步骤 1:基础模型搜索实现(5 分钟入门)
目标:实现对 User 模型和 Post 模型的基础搜索功能,支持按名称/标题模糊匹配。
1.1 模型准备
让需要搜索的模型实现 Searchable 接口:
// app/Models/User.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;
class User extends Model implements Searchable
{
// 实现接口方法,定义搜索结果格式
public function getSearchResult(): SearchResult
{
$url = route('users.show', $this->id); // 结果页面URL
return new SearchResult(
$this, // 原始模型实例
$this->name, // 显示标题
$url // 跳转链接
);
}
}
对 Post 模型执行相同操作:
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;
class Post extends Model implements Searchable
{
public function getSearchResult(): SearchResult
{
return new SearchResult(
$this,
$this->title,
route('posts.show', $this->slug)
);
}
}
1.2 执行搜索
在控制器中创建搜索实例并注册模型:
// app/Http/Controllers/SearchController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\User;
use Illuminate\Http\Request;
use Spatie\Searchable\Search;
class SearchController extends Controller
{
public function __invoke(Request $request)
{
$searchTerm = $request->input('query');
$searchResults = (new Search())
// 注册User模型,搜索name字段
->registerModel(User::class, 'name')
// 注册Post模型,搜索title和excerpt字段
->registerModel(Post::class, ['title', 'excerpt'])
->search($searchTerm);
return view('search.results', compact('searchResults', 'searchTerm'));
}
}
1.3 结果展示
在 Blade 模板中遍历结果集:
{{-- resources/views/search/results.blade.php --}}
<h1>搜索结果: "{{ $searchTerm }}"</h1>
<p>共找到 {{ $searchResults->count() }} 条匹配结果</p>
@foreach($searchResults->groupByType() as $type => $results)
<div class="search-group">
<h2>{{ $type }}</h2>
<ul>
@foreach($results as $result)
<li>
<a href="{{ $result->url }}">{{ $result->title }}</a>
</li>
@endforeach
</ul>
</div>
@endforeach
关键点:groupByType() 方法会按模型类型(默认是表名)对结果分组,便于分类展示。可通过在模型中定义 $searchableType 属性自定义分组名称:
class Post extends Model implements Searchable
{
public $searchableType = '文章'; // 分组显示为"文章"而非默认的"posts"
// ...
}
步骤 2:高级模型搜索配置
目标:实现精确匹配、应用查询作用域、关联预加载等高级搜索需求。
2.1 混合搜索模式(模糊+精确匹配)
通过闭包注册模型,实现姓名模糊匹配+邮箱精确匹配:
$searchResults = (new Search())
->registerModel(User::class, function (ModelSearchAspect $aspect) {
$aspect
->addSearchableAttribute('name') // 模糊匹配姓名
->addExactSearchableAttribute('email') // 精确匹配邮箱
->where('active', true) // 仅搜索活跃用户
->with('roles'); // 预加载角色关联
})
->search($searchTerm);
addSearchableAttribute() 和 addExactSearchableAttribute() 的区别在于底层 SQL:
- 模糊匹配:
LOWER(name) LIKE '%searchterm%' - 精确匹配:
email = 'searchterm'
2.2 复杂查询条件
利用查询构建器方法添加任意条件,例如搜索发布日期在近30天且评论数大于10的文章:
->registerModel(Post::class, function (ModelSearchAspect $aspect) {
$aspect
->addSearchableAttribute('title')
->addSearchableAttribute('content')
->where('published_at', '>=', now()->subDays(30))
->has('comments', '>', 10)
->orderBy('published_at', 'desc');
})
所有 Eloquent 查询构建器方法(where/has/orderBy等)均可在闭包中调用,极大增强了搜索条件的灵活性。
步骤 3:自定义搜索源(非模型数据)
目标:实现对外部 API 数据源的搜索支持,展示如何扩展 SearchAspect。
3.1 创建自定义 SearchAspect
假设需要搜索外部订单系统 API,创建 OrderSearchAspect:
// app/Search/OrderSearchAspect.php
namespace App\Search;
use Spatie\Searchable\SearchAspect;
use Illuminate\Support\Collection;
use App\Services\OrderApiService;
class OrderSearchAspect extends SearchAspect
{
protected $limit = 20; // 默认结果数量
public function getType(): string
{
return '订单'; // 结果分组名称
}
public function getResults(string $term): Collection
{
// 调用外部API获取搜索结果
$rawResults = OrderApiService::search($term, $this->limit);
// 转换为SearchResult对象集合
return collect($rawResults)->map(function ($order) {
return new \Spatie\Searchable\SearchResult(
$order, // 原始数据对象
"订单 #{$order->id}", // 标题
route('admin.orders.show', $order->id) // URL
);
});
}
}
3.2 注册自定义搜索源
use App\Search\OrderSearchAspect;
$searchResults = (new Search())
->registerModel(User::class, 'name')
->registerModel(Post::class, 'title')
->registerAspect(OrderSearchAspect::class) // 注册自定义搜索源
->search($searchTerm);
现在搜索结果将包含用户、文章和订单三种类型,尽管订单数据来自外部 API,但结果格式与模型搜索完全一致。
步骤 4:结果集处理与展示优化
目标:掌握结果过滤、排序、分页等高级操作。
4.1 限制单源结果数量
全局限制每个搜索源返回的最大结果数(防止某个源返回过多结果淹没其他源):
$searchResults = (new Search())
->registerModel(User::class, 'name')
->registerModel(Post::class, 'title')
->limitAspectResults(15) // 每个源最多返回15条结果
->search($searchTerm);
4.2 结果排序与去重
SearchResultCollection 提供了多种集合操作方法:
// 按相关性(匹配度)排序(需模型实现相关度计算)
$sortedResults = $searchResults->sortByRelevance();
// 按创建时间倒序排列
$timeSorted = $searchResults->sortBy(function ($result) {
return $result->model->created_at;
}, SORT_REGULAR, true);
// 去重(基于URL)
$uniqueResults = $searchResults->unique(function ($result) {
return $result->url;
});
4.3 自定义结果详情展示
扩展 SearchResult 包含更多展示信息,例如文章摘要和阅读时间:
// 在Post模型中
public function getSearchResult(): SearchResult
{
return new \Spatie\Searchable\SearchResult(
$this,
$this->title,
route('posts.show', $this->slug),
[
'excerpt' => Str::limit($this->content, 150),
'read_time' => (int)strlen($this->content) / 200, // 估算阅读时间
'author' => $this->author->name
]
);
}
// 在视图中访问详情
@foreach($results as $result)
<li>
<a href="{{ $result->url }}">{{ $result->title }}</a>
<p>{{ $result->details['excerpt'] }}</p>
<small>阅读时间: {{ $result->details['read_time'] }} 分钟</small>
</li>
@endforeach
性能优化策略
当数据量增长到 10 万+ 级别时,基础实现可能面临性能瓶颈,以下是经过生产环境验证的优化方案:
策略 1:索引优化
为所有搜索字段添加数据库索引:
// 迁移文件中
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->index('name');
$table->index('email');
});
}
对于 MySQL,可使用全文索引替代 LIKE 查询(需 Laravel 8+):
// 在ModelSearchAspect中自定义查询
$query->whereRaw(
"MATCH(title, content) AGAINST(? IN BOOLEAN MODE)",
[$term]
);
策略 2:查询优化
- 延迟加载无关字段:使用
select()只查询必要字段 - 合理使用缓存:对热门搜索词结果进行缓存
- 避免 N+1 查询:通过
with()预加载所有关联
->registerModel(Post::class, function (ModelSearchAspect $aspect) {
$aspect
->addSearchableAttribute('title')
->select('id', 'title', 'slug', 'published_at') // 仅加载必要字段
->with('author:id,name') // 仅加载作者ID和名称
->cacheFor(now()->hour()); // 缓存1小时(需安装laravel-cache扩展)
})
策略 3:异步搜索与队列
对于超大数据集或慢查询,可将搜索任务放入队列:
// 控制器中
dispatch(new PerformSearchJob($searchTerm))->onQueue('search');
// 队列任务中
class PerformSearchJob implements ShouldQueue
{
public function handle()
{
$results = (new Search())->registerModel(...)->search($this->term);
cache()->put("search:{$this->term}", $results, now()->day());
}
}
策略 4:分阶段搜索
实现"即时结果+完整结果"双阶段搜索:
- 优先返回缓存结果或快速查询结果(100ms内)
- 后台异步执行完整搜索并更新结果
// 前端代码示例
axios.get('/search?term=' + term)
.then(response => {
// 显示即时结果
renderResults(response.data.instant_results);
// 轮询获取完整结果
const poll = setInterval(() => {
axios.get('/search/status?term=' + term)
.then(res => {
if (res.data.complete) {
renderResults(res.data.full_results);
clearInterval(poll);
}
});
}, 1000);
});
常见问题与解决方案
Q1:搜索结果包含 HTML 标签或特殊字符?
A:在 getSearchResult() 中使用 strip_tags() 和 htmlspecialchars() 清理:
public function getSearchResult(): SearchResult
{
return new SearchResult(
$this,
strip_tags($this->title),
$this->url
);
}
Q2:如何实现多语言搜索?
A:结合 Laravel Translatable 包,搜索当前语言字段:
->addSearchableAttribute(app()->getLocale() . '_title')
Q3:如何处理中文/日文等非拉丁语系搜索?
A:需配合数据库特定功能:
- MySQL:使用
ngram解析器(MySQL 5.7+) - PostgreSQL:使用
pg_bigm扩展 - 推荐方案:集成 Elasticsearch 作为搜索引擎
Q4:如何记录搜索日志用于分析?
A:通过装饰器模式包装 Search 类:
class LoggableSearch extends Search
{
public function search(string $query): SearchResultCollection
{
// 记录搜索日志
SearchLog::create(['term' => $query, 'user_id' => auth()->id()]);
return parent::search($query);
}
}
// 使用
(new LoggableSearch())->registerModel(...)->search($term);
企业级扩展方案
对于中大型项目,可基于 Laravel Searchable 构建更强大的搜索系统:
扩展点 1:集成 Elasticsearch
创建 ElasticSearchAspect,将复杂搜索委托给 Elasticsearch:
class ElasticSearchAspect extends SearchAspect
{
public function getResults(string $term): Collection
{
$results = Elasticsearch::search([
'index' => 'posts',
'body' => [
'query' => [
'multi_match' => [
'query' => $term,
'fields' => ['title^3', 'content'], // 标题权重更高
'fuzziness' => 'AUTO'
]
]
]
]);
return collect($results['hits']['hits'])->map(function ($hit) {
$source = $hit['_source'];
return new SearchResult(
(object)$source,
$source['title'],
route('posts.show', $source['slug'])
);
});
}
}
扩展点 2:搜索分析与推荐
基于搜索日志实现热门搜索词推荐和拼写纠错:
// 热门搜索词
public function getTrendingTerms()
{
return SearchLog::select('term', DB::raw('count(*) as total'))
->where('created_at', '>=', now()->subWeek())
->groupBy('term')
->orderBy('total', 'desc')
->limit(10)
->pluck('term');
}
项目贡献与社区支持
Laravel Searchable 作为活跃维护的开源项目,欢迎开发者参与贡献:
- 贡献指南:提交 PR 前请阅读 CONTRIBUTING.md
- 问题反馈:通过 GitHub Issues 提交 bug 报告(需包含复现步骤)
- 社区资源:
- 官方文档:spatie.be/docs/laravel-searchable
- 讨论组:GitHub Discussions
- 中文社区:Laravel China 论坛搜索 "laravel-searchable"
总结与展望
Laravel Searchable 通过优雅的抽象设计,将原本需要数百行代码的多源搜索功能简化为几行注册代码,其核心价值在于:
- 标准化:统一不同数据源的搜索接口和结果格式
- 灵活性:通过闭包配置和自定义 Aspect 支持任意搜索逻辑
- 可扩展性:从简单模型搜索到企业级搜索引擎的平滑过渡
随着 Laravel 生态的发展,我们可以期待未来版本可能引入的特性:
- 内置全文搜索支持
- 搜索结果高亮
- 分面搜索(Faceted Search)
- 与 Laravel Scout 的深度集成
掌握 Laravel Searchable 不仅能解决当前的搜索功能开发难题,更能帮助开发者理解"面向接口编程"和"开闭原则"在实际项目中的应用。现在就将它集成到你的项目中,体验高效搜索开发的乐趣吧!
行动步骤:
- 克隆项目到本地:
git clone https://gitcode.com/gh_mirrors/la/laravel-searchable.git - 参考本文步骤实现基础搜索功能
- 尝试创建一个自定义 SearchAspect(例如搜索本地 Markdown 文件)
- 在评论区分享你的使用心得或扩展方案
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



