#19 FilamentPHP応用

グローバル検索のカスタマイズ——検索対象・表示形式の細かな制御

グローバル検索とは

Filamentのグローバル検索はパネル上部の検索バーから、複数のResourceにまたがって一括検索できる機能です。Cmd+K(macOS)またはCtrl+K(Windows)で素早くアクセスでき、検索結果をクリックすると該当レコードの編集ページに遷移します。

基本設定:$recordTitleAttribute$globallySearchable

class PostResource extends Resource
{
    // グローバル検索の対象にする(デフォルト: null = 無効)
    protected static ?string $recordTitleAttribute = 'title';

    // 検索の対象にするフィールド(複数可)
    public static function getGloballySearchableAttributes(): array
    {
        return ['title', 'body', 'slug', 'author.name'];
    }
}

$recordTitleAttributeに指定したカラムが検索結果のメインタイトルとして表示されます。

getGlobalSearchResultTitle() でタイトルをカスタマイズ

動的にタイトルを生成したい場合:

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

getGlobalSearchResultDetails() で詳細情報を追加

検索結果の各アイテムにサブ情報を追加できます。

public static function getGlobalSearchResultDetails(Model $record): array
{
    return [
        '著者'       => $record->author->name,
        'カテゴリ'   => $record->category->name,
        '公開日'     => $record->published_at?->format('Y/m/d') ?? '未公開',
        'ステータス' => match($record->status) {
            'published' => '公開中',
            'draft'     => '下書き',
            'archived'  => 'アーカイブ',
            default     => $record->status,
        },
    ];
}

getGlobalSearchResultUrl() で遷移先をカスタマイズ

デフォルトは編集ページへの遷移ですが、別のページに遷移させることもできます。

public static function getGlobalSearchResultUrl(Model $record): string
{
    // 詳細(View)ページがある場合はそちらに遷移
    return PostResource::getUrl('view', ['record' => $record]);

    // 外部URLにも遷移できる
    // return route('posts.show', $record->slug);
}

カスタム検索クエリ

Eloquentのデフォルト検索ロジック(LIKE %keyword%)をカスタマイズします。

public static function getGlobalSearchResults(string $search): Collection
{
    return Post::query()
        ->where(function (Builder $query) use ($search): void {
            $query->where('title', 'like', "%{$search}%")
                  ->orWhere('body', 'like', "%{$search}%")
                  ->orWhereHas('author', fn ($q) =>
                      $q->where('name', 'like', "%{$search}%")
                  )
                  ->orWhereHas('tags', fn ($q) =>
                      $q->where('name', 'like', "%{$search}%")
                  );
        })
        ->with(['author', 'category', 'tags'])
        ->limit(10)  // 検索結果の上限
        ->get()
        ->map(fn (Post $record): GlobalSearchResult => GlobalSearchResult::make(
            title: "[{$record->category->name}] {$record->title}",
            url: static::getGlobalSearchResultUrl($record),
            details: [
                '著者' => $record->author->name,
                '公開日' => $record->published_at?->format('Y/m/d') ?? '未公開',
            ],
        ));
}

getNavigationGroup() でグループ分け

検索結果はResourceのナビゲーショングループで自動的にグループ分けされます。

class PostResource extends Resource
{
    protected static ?string $navigationGroup = 'コンテンツ管理';
    // 検索結果は「コンテンツ管理」グループとして表示される
}

getGlobalSearchResultIcon() でアイコンを設定

public static function getGlobalSearchResultIcon(Model $record): string
{
    return match($record->status) {
        'published' => 'heroicon-o-globe-alt',
        'draft'     => 'heroicon-o-pencil-square',
        default     => 'heroicon-o-document-text',
    };
}

検索結果のキャッシュ

グローバル検索は入力のたびにリクエストが発生します。重いクエリの場合はキャッシュを活用します。

public static function getGlobalSearchResults(string $search): Collection
{
    $cacheKey = "global_search_posts_{$search}_" . auth()->id();

    return cache()->remember($cacheKey, 30, function () use ($search): Collection {
        return Post::query()
            ->where('title', 'like', "%{$search}%")
            ->with(['author', 'category'])
            ->limit(10)
            ->get()
            ->map(fn (Post $record) => GlobalSearchResult::make(
                title: $record->title,
                url: static::getGlobalSearchResultUrl($record),
            ));
    });
}

テナント環境での検索スコープ

マルチテナンシーが有効な場合、グローバル検索も自動的にテナントスコープが適用されます。ただしgetGlobalSearchResults()をオーバーライドする場合は自分でスコープを追加する必要があります。

public static function getGlobalSearchResults(string $search): Collection
{
    $tenantId = Filament::getTenant()?->id;

    return Post::query()
        ->when($tenantId, fn ($q) => $q->where('team_id', $tenantId))
        ->where('title', 'like', "%{$search}%")
        ->limit(10)
        ->get()
        ->map(fn (Post $record) => GlobalSearchResult::make(
            title: $record->title,
            url: static::getGlobalSearchResultUrl($record),
        ));
}

パネル全体の検索設定

PanelProviderでグローバル検索の動作を設定できます。

// app/Providers/Filament/AdminPanelProvider.php

->globalSearchKeyBindings(['command+k', 'ctrl+k'])
->globalSearchDebounce('500ms')   // デバウンス時間
->globalSearchFieldSuffix(fn (): ?string =>
    auth()->user()->name . ' のワークスペース'
)

コツ・注意点・ハマりポイント

コツ: getGloballySearchableAttributes()でリレーションのカラム(author.nameなど)を指定した場合、そのリレーションのEager Loadingが必要です。getGlobalSearchResults()内でwith()を付けるか、getEloquentQuery()でデフォルトのwithを設定してください。

注意点: グローバル検索はキーワード入力のたびにdebounce後にリクエストが送られます。デフォルトのdebounceは短めなので、重いクエリの場合は->globalSearchDebounce('500ms')で延ばすことを検討してください。

ハマりポイント: getGlobalSearchResults()をオーバーライドするとgetGloballySearchableAttributes()の設定は無視されます。カスタムクエリを使う場合はgetGlobalSearchResults()の中ですべての検索ロジックを実装してください。

まとめ

FilamentのグローバルSearchは$recordTitleAttributeとGloballySearchableAttributesの設定だけで動作しますが、getGlobalSearchResultDetails()getGlobalSearchResults()をカスタマイズすることで、検索結果の表示形式や検索ロジックを細かく制御できます。テナント環境での注意点を押さえれば、本格的なマルチリソース検索が実現できます。