Laravel
Search & Filtering
Laravel provides multiple tools for searching and filtering records — from raw Eloquent where/like queries to full-text search via Laravel Scout with Meilisearch or Algolia backends.
Basic Where/Like Search: Use Eloquent's
where + LIKE for simple keyword search. Wrap keywords with % for partial matching.app/Http/Controllers/ProductController.php
PHP
use App\Models\Product;
use Illuminate\Http\Request;
public function index(Request $request)
{
$query = Product::query();
// Single field search
if ($search = $request->input('search')) {
$query->where('name', 'LIKE', "%{$search}%");
}
// Multi-field search (search across columns)
if ($search = $request->input('search')) {
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%{$search}%")
->orWhere('description', 'LIKE', "%{$search}%")
->orWhere('sku', 'LIKE', "%{$search}%");
});
}
$products = $query->paginate(15)->withQueryString();
return view('products.index', compact('products'));
}
Install Laravel Scout: Scout adds full-text search to Eloquent models via a driver-based API (Algolia, Meilisearch, database, etc.).
terminal
BASH
composer require laravel/scout
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
# Install Meilisearch driver (recommended for self-hosting)
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle
# Or Algolia driver
composer require algolia/algoliasearch-client-php
.env (Scout config)
ENV
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=your-meilisearch-key
# Or for Algolia
SCOUT_DRIVER=algolia
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SECRET=your-secret
Searchable Trait: Add the
Searchable trait to your model and define toSearchableArray() to control which fields are indexed.app/Models/Product.php
PHP
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Product extends Model
{
use Searchable;
// Define the searchable index name
public function searchableAs(): string
{
return 'products_index';
}
// Define what data is indexed
public function toSearchableArray(): array
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'sku' => $this->sku,
'price' => $this->price,
'category' => $this->category->name ?? null,
];
}
}
terminal – import existing records
BASH
# Import all existing model records into the search index
php artisan scout:import "App\Models\Product"
# Flush all records from the index
php artisan scout:flush "App\Models\Product"
app/Http/Controllers/ProductController.php (Scout search)
PHP
// Full-text search using Scout
$products = Product::search($request->input('search', ''))
->paginate(15);
// Scout search with Eloquent constraints
$products = Product::search($request->input('search'))
->query(fn ($q) => $q->with('category')->where('is_active', true))
->paginate(15);
Query Scopes for Filtering: Encapsulate filter logic in Eloquent local scopes to keep controllers clean and reusable.
app/Models/Product.php (scopes)
PHP
use Illuminate\Database\Eloquent\Builder;
// Filter by category
public function scopeInCategory(Builder $query, ?string $category): Builder
{
return $query->when(
$category,
fn ($q) => $q->whereHas('category', fn ($c) => $c->where('slug', $category))
);
}
// Filter by price range
public function scopePriceBetween(Builder $query, ?float $min, ?float $max): Builder
{
return $query
->when($min, fn ($q) => $q->where('price', '>=', $min))
->when($max, fn ($q) => $q->where('price', '<=', $max));
}
// Filter only active products
public function scopeActive(Builder $query): Builder
{
return $query->where('is_active', true);
}
app/Http/Controllers/ProductController.php (using scopes)
PHP
public function index(Request $request)
{
$products = Product::query()
->active()
->inCategory($request->input('category'))
->priceBetween($request->input('min_price'), $request->input('max_price'))
->when($request->input('search'), function ($q, $search) {
$q->where('name', 'LIKE', "%{$search}%");
})
->paginate(15)
->withQueryString();
return view('products.index', compact('products'));
}
Sorting: Allow the user to sort results by a whitelist of allowed columns to prevent SQL injection. Use
orderBy with the validated column.app/Http/Controllers/ProductController.php (sorting)
PHP
public function index(Request $request)
{
$allowedSorts = ['name', 'price', 'created_at', 'updated_at'];
$sortBy = in_array($request->input('sort'), $allowedSorts)
? $request->input('sort')
: 'created_at';
$sortDir = $request->input('direction', 'desc') === 'asc' ? 'asc' : 'desc';
$products = Product::query()
->active()
->orderBy($sortBy, $sortDir)
->paginate(15)
->withQueryString();
return view('products.index', compact('products', 'sortBy', 'sortDir'));
}
Pagination with Query String Retention: Use
withQueryString() so filters and sort params persist across paginated pages. Render links in Blade with Bootstrap.resources/views/products/index.blade.php
HTML
<form method="GET" action="{{ route('products.index') }}">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search products...">
<select name="category">
<option value="">All Categories</option>
@foreach($categories as $cat)
<option value="{{ $cat->slug }}" {{ request('category') == $cat->slug ? 'selected' : '' }}>
{{ $cat->name }}
</option>
@endforeach
</select>
<input type="number" name="min_price" value="{{ request('min_price') }}" placeholder="Min price">
<input type="number" name="max_price" value="{{ request('max_price') }}" placeholder="Max price">
<button type="submit">Filter</button>
</form>
@foreach($products as $product)
<div>{{ $product->name }} - ${{ $product->price }}</div>
@endforeach
{{-- Pagination links retain all query params --}}
{{ $products->links() }}
app/Providers/AppServiceProvider.php (Bootstrap pagination)
PHP
use Illuminate\Pagination\Paginator;
public function boot(): void
{
// Use Bootstrap 5 pagination view
Paginator::useBootstrapFive();
}