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();
}