Installation:
composer require ajcastro/searchable
Publish the config (if needed):
php artisan vendor:publish --provider="AjCastro\Searchable\SearchableServiceProvider"
Define Searchable Model:
Add the Searchable trait to your Eloquent model and configure $searchable:
use AjCastro\Searchable\Searchable;
class Post extends Model
{
use Searchable;
protected $searchable = [
'columns' => [
'posts.title',
'posts.body',
'author_full_name' => 'CONCAT(authors.first_name, " ", authors.last_name)'
],
'joins' => [
'authors' => ['authors.id', 'posts.author_id']
]
];
}
First Search Query:
$results = Post::search('query')->paginate(10);
For a searchable API endpoint (e.g., /api/posts):
use AjCastro\Searchable\Searchable;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(Request $request)
{
$search = $request->input('search', '');
$perPage = $request->input('per_page', 10);
$query = Post::search($search)->paginate($perPage);
return response()->json($query);
}
}
Define Searchable Columns:
CONCAT()).authors.first_name).'columns' => [
'posts.title',
'posts.body',
'custom_field' => 'UPPER(posts.title) AS custom_field'
]
Handle Joins:
$searchable['joins'] to ensure columns are available.posts and authors tables.'joins' => [
'authors' => ['authors.id', 'posts.author_id']
]
API Request Handling:
$request->search).$query = Post::search($request->search)
->orderBy($request->sort_by ?? 'created_at')
->paginate($request->per_page ?? 10);
Dynamic Search Fields: Dynamically add/remove searchable columns based on user roles or settings:
$searchableColumns = ['posts.title'];
if (auth()->user()->is_admin) {
$searchableColumns[] = 'posts.body';
}
Post::search($query, $searchableColumns)->get();
Combining with Scopes:
Use the search method alongside custom scopes:
Post::search($query)
->published()
->with('author')
->get();
Full-Text Search Optimization:
FULLTEXT index hints for MySQL:'columns' => [
'FULLTEXT(posts.title)',
'FULLTEXT(posts.body)'
]
Search in Relationships: Search across related models by defining joins and columns:
'columns' => [
'comments.body' // Requires a join to comments table
],
'joins' => [
'comments' => ['comments.post_id', 'posts.id']
]
MySQL-Only Limitation:
Case Sensitivity:
LIKE is case-insensitive, but derived columns (e.g., UPPER()) may behave unexpectedly.LOWER() for consistent case-insensitive searches:'columns' => [
'LOWER(posts.title)'
]
Reserved Keywords:
order, group, or join may cause SQL syntax errors.'columns' => [
'posts.`order`' => 'posts.order'
]
Performance with Large Datasets:
FULLTEXT indexes for better performance:ALTER TABLE posts ADD FULLTEXT(title, body);
Join Ambiguity:
created_at), explicitly qualify them:'columns' => [
'posts.created_at',
'authors.created_at'
]
Inspect Generated SQL: Use Laravel’s query logging to debug:
DB::enableQueryLog();
Post::search('test')->toSql();
dd(DB::getQueryLog());
Validate Join Syntax: Ensure join definitions match your database schema:
'joins' => [
'authors' => ['authors.id', 'posts.author_id'] // Correct
// 'authors' => ['posts.id', 'authors.post_id'] // Incorrect
]
Handle Empty Searches:
The package may return all records if $search is empty. Add a guard:
if (empty($request->search)) {
return Post::paginate($request->per_page);
}
Custom Search Logic:
Override the search method in your model:
public function scopeCustomSearch($query, $searchTerm)
{
return $query->where('status', 'active')
->search($searchTerm);
}
Add Search to Global Scopes: Attach a global scope for app-wide search:
class SearchableScope implements Scope
{
public function apply(Builder $builder, Model $model, $search)
{
return $builder->search($search);
}
}
Extend with Full-Text Search: Combine with Laravel Scout or Algolia for hybrid search:
Post::search($query)->orWhereHas('scout', function ($q) use ($query) {
$q->where('body', 'like', "%{$query}%");
});
Localization Support: For multilingual apps, add localized columns:
'columns' => [
'posts.title',
'posts.title_translated' => 'posts.title'
],
// Dynamically switch based on locale
Default Search Behavior:
LIKE with % wildcards by default.Post::where(function ($query) use ($search) {
foreach ($this->searchable['columns'] as $column) {
$query->orWhere($column, 'like', "%{$search}%");
}
});
Boolean Search (AND/OR):
OR by default.AND logic, split the query:$terms = explode(' ', $search);
$query = Post::query();
foreach ($terms as $term) {
$query->orWhere(function ($q) use ($term) {
$q->search($term);
});
}
How can I help you explore Laravel packages today?