Search

Filament Global Search: Customization and Performance

Filament Global Search: Customization and Performance

Key takeaways

  • ✓ Global search turns on per resource once you set $recordTitleAttribute. Every enabled resource is queried on every search.
  • ✓ Make results useful with getGloballySearchableAttributes(), getGlobalSearchResultDetails(), getGlobalSearchResultActions() and a Cmd+K shortcut.
  • ✓ Keep it fast: eager load in getGlobalSearchEloquentQuery(), lower $globalSearchResultsLimit (default 50), turn off term splitting on big tables, and use the opt-in mode added in Filament 5.4.
  • ✓ LIKE '%term%' can’t use normal indexes. For large datasets, plug in a Scout-backed custom GlobalSearchProvider.

The search box in Filament’s top bar is one of the most-used features in any panel, and one of the most expensive by accident. Out of the box, it runs a LIKE query against every searchable resource each time someone pauses typing. On a demo database, that’s instant. On 3 million orders, it’s a slow query log. This guide makes global search both useful and fast.

How global search works

  1. The user types. After the debounce (500 ms by default), Livewire sends the query.
  2. For each resource with a $recordTitleAttribute, Filament builds a query from getGlobalSearchEloquentQuery() and applies WHERE col LIKE %term% across the searchable attributes.
  3. Results are grouped by resource, up to $globalSearchResultsLimit per resource (50 by default).

So cost ≈ (number of searchable resources) × (columns searched) × (how unindexable %term% is). Keep that in mind for the rest of this guide.

Step 1: Enable it on a resource

class CustomerResource extends Resource
{
    protected static ?string $recordTitleAttribute = 'name';
}

That’s the whole setup. Results link to the record’s edit page (or its view page if it has one) when the user is authorised.

Step 2: Search the right columns

public static function getGloballySearchableAttributes(): array
{
    return ['name', 'email', 'company.name'];
}

Dot notation searches relationships with a whereHas, which is convenient and costs more. Only add relationship columns that people genuinely search.

Step 3: Make results informative

A list of bare names makes users guess. Add context:

use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

public static function getGlobalSearchResultTitle(Model $record): string | Htmlable
{
    return "{$record->name} ({$record->account_number})";
}

public static function getGlobalSearchResultDetails(Model $record): array
{
    return [
        'Company' => $record->company?->name,
        'Plan' => $record->plan?->getLabel(),
    ];
}

public static function getGlobalSearchEloquentQuery(): Builder
{
    return parent::getGlobalSearchEloquentQuery()->with(['company']);
}

The eager load isn’t optional. Without ->with('company'), getGlobalSearchResultDetails() runs one query per result. With the default limit of 50, one search becomes 51 queries for this resource alone.

Step 4: Actions straight from the results

Users can act without opening the record first:

use Filament\Actions\Action;

public static function getGlobalSearchResultActions(Model $record): array
{
    return [
        Action::make('edit')
            ->url(static::getUrl('edit', ['record' => $record])),
        Action::make('invoices')
            ->url(InvoiceResource::getUrl('index', ['filters' => ['customer' => ['value' => $record->id]]])),
    ];
}

Result actions can open URLs (in a new tab if you like) or dispatch Livewire events.

Step 5: Panel-level settings

public function panel(Panel $panel): Panel
{
    return $panel
        ->globalSearchKeyBindings(['command+k', 'ctrl+k'])
        ->globalSearchFieldKeyBindingSuffix()   // shows "⌘K" in the field
        ->globalSearchDebounce('750ms');        // fewer requests while typing
}

A longer debounce is the cheapest performance win there is: 750 ms instead of 500 ms often halves the number of requests from fast typists. Turn global search off completely for a panel with ->globalSearch(false).

Performance: seven fixes that matter

1. Opt resources in, not out (Filament 5.4+)

Filament v5.4 added an opt-in mode. Instead of every resource with a title attribute being searchable, you choose explicitly:

// Panel provider
->globalSearchResourceOptIn()

// Only on the resources that should be searched
protected static bool $isGloballySearchable = true;

Twelve searchable resources nobody searches means twelve wasted queries per keystroke. Opt-in mode gets you to three or four.

2. Lower the per-resource limit

protected static int $globalSearchResultsLimit = 8;

Nobody scrolls past eight customers in a dropdown. They refine the query.

3. Turn off term splitting on large tables

By default, “jane smith” is split into terms, and each is matched against each column, producing a growing chain of OR/AND conditions. On large tables, search the phrase instead:

protected static ?bool $shouldSplitGlobalSearchTerms = false;

4. Index what you can

A leading wildcard (LIKE '%smith%') can’t use a B-tree index. Some options:

  • Exact-match columns such as order numbers, SKUs and emails: offer a separate, fast path. Many teams override getGlobalSearchEloquentQuery() to use where('number', $term) when the term looks like an order number.
  • MySQL/MariaDB: add a FULLTEXT index and search with whereFullText() in a custom provider (see below).
  • PostgreSQL: a pg_trgm GIN index makes ILIKE '%term%' indexable:

    DB::statement('CREATE EXTENSION IF NOT EXISTS pg_trgm');
    DB::statement('CREATE INDEX customers_name_trgm ON customers USING gin (name gin_trgm_ops)');

5. Case sensitivity on PostgreSQL

PostgreSQL’s LIKE is case-sensitive, and Filament handles this for you. If you know your collation is case-insensitive (typical on MySQL), you can skip the extra LOWER() work. Filament’s global search docs cover the options for forcing or disabling case-insensitive matching per resource.

6. Don’t let tenancy become a full-table scan

In multi-tenant panels, every search is also filtered by team_id. A composite index that starts with team_id (for example (team_id, name)) keeps the planner working on one tenant’s rows instead of the whole table.

7. Measure, then decide

Use Laravel Debugbar, Telescope or Nightwatch to see the queries one search produces. If a single keystroke produces 30+ queries or anything over ~100 ms, apply the fixes above in order.

Going further: a Scout-backed global search provider

For large or text-heavy datasets, hand search to a real search engine (Meilisearch, Typesense, Algolia or the database driver) through Laravel Scout. Filament lets you replace the whole global search implementation with your own provider:

namespace App\Filament\GlobalSearch;

use App\Filament\Resources\Customers\CustomerResource;
use App\Filament\Resources\Orders\OrderResource;
use App\Models\Customer;
use App\Models\Order;
use Filament\GlobalSearch\GlobalSearchResult;
use Filament\GlobalSearch\GlobalSearchResults;
use Filament\GlobalSearch\Providers\Contracts\GlobalSearchProvider;

class ScoutGlobalSearchProvider implements GlobalSearchProvider
{
    public function getResults(string $query): ?GlobalSearchResults
    {
        $results = GlobalSearchResults::make();

        $customers = Customer::search($query)->take(8)->get();
        $results->category('Customers', $customers->map(fn (Customer $c) => new GlobalSearchResult(
            title: $c->name,
            url: CustomerResource::getUrl('edit', ['record' => $c]),
            details: ['Email' => $c->email],
        )));

        $orders = Order::search($query)->take(8)->get();
        $results->category('Orders', $orders->map(fn (Order $o) => new GlobalSearchResult(
            title: "Order #{$o->number}",
            url: OrderResource::getUrl('view', ['record' => $o]),
            details: ['Total' => '£' . number_format($o->total, 2)],
        )));

        return $results;
    }
}
// Panel provider
->globalSearch(ScoutGlobalSearchProvider::class)

Check the exact constructor signature of GlobalSearchResult in vendor/filament/filament/src/GlobalSearch for your installed version before copying this. Because you control the provider, you also control authorisation. Filter results by policy (auth()->user()->can('view', $record)) and by tenant, since your search engine doesn’t know about either.

Deciding what should be searchable

Not every resource belongs in global search. Ask one question for each: would someone type a name or number from this model into a search box to jump to it?

ResourceSearchable?Why
Customers, users, companiesYesPeople search by name and email
Orders, invoices, ticketsYes, by numberExact identifiers from emails and phone calls
ProductsYes, by SKU and nameCommon lookups
Settings, roles, tax ratesNoFew rows, reached through navigation
Logs, activity, eventsNoHigh volume, low jump value, and expensive
Pivot or detail models (order items)NoSearch the parent instead

In practice, three to five searchable resources cover nearly every real search. That’s also the point where global search stays fast without further work.

Authorisation and search results

Filament only shows results that link to pages the user can access: the resource must pass viewAny(), and each result’s URL respects view() or update(). Two things to watch:

  • Details can leak data. getGlobalSearchResultDetails() renders whatever you return, so don’t show a customer’s lifetime value to a role that can’t see revenue. Check the permission inside the method.
  • Custom providers bypass Filament’s checks. In a Scout provider (below), you’re responsible for filtering results by policy and tenant.

Table search vs global search

They’re separate features. Table search (->searchable() on columns) has its own performance profile. If a list page’s search box is slow, see our 8 fixes for slow Filament tables.

FAQ

How do I enable global search in Filament?

Set protected static ?string $recordTitleAttribute = 'name'; on a resource. Customise the searched columns with getGloballySearchableAttributes().

Why is Filament global search slow?

Each search queries every searchable resource with LIKE '%term%' on several columns, often with relationship lookups and per-result N+1 queries. Eager load, reduce the result limit, opt resources in, disable term splitting and add suitable indexes, or use Scout.

How do I add a keyboard shortcut for global search?

Call ->globalSearchKeyBindings(['command+k', 'ctrl+k']) on the panel. ->globalSearchFieldKeyBindingSuffix() shows the shortcut in the field.

Can I use Laravel Scout with Filament global search?

Yes. Implement Filament’s GlobalSearchProvider interface, run Scout queries inside getResults(), and register it with ->globalSearch(YourProvider::class).

Sources and further reading

Related on The Web Tier: Multi-tenant SaaS with Filament

TWT Staff

TWT Staff

Writes about Programming, tech news, discuss programming topics for web developers (and Web designers), and talks about SEO tools and techniques

Your experience on this site will be improved by allowing cookies Cookie Policy