PHPackages                             mozex/laravel-searchable - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. mozex/laravel-searchable

ActiveLibrary[Database &amp; ORM](/categories/database)

mozex/laravel-searchable
========================

Advanced multi-column, relation, and morph search for Laravel Eloquent models

1.1.1(3w ago)31.2kMITPHPPHP ^8.2.0CI passing

Since Apr 16Pushed 1mo agoCompare

[ Source](https://github.com/mozex/laravel-searchable)[ Packagist](https://packagist.org/packages/mozex/laravel-searchable)[ Docs](https://github.com/mozex/laravel-searchable)[ GitHub Sponsors](https://github.com/mozex)[ RSS](/packages/mozex-laravel-searchable/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (6)Dependencies (28)Versions (7)Used By (0)

Laravel Searchable
==================

[](#laravel-searchable)

[![Latest Version on Packagist](https://camo.githubusercontent.com/859b1d89b94cdc457d0022320ffca454379946386135e533d0d52e479159e8b0/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6d6f7a65782f6c61726176656c2d73656172636861626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/mozex/laravel-searchable)[![GitHub Checks Action Status](https://camo.githubusercontent.com/92a356d2f22ed48bd39f65b94c3a8983f7c426ee24022ff6ffb3c7bc900df168/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6d6f7a65782f6c61726176656c2d73656172636861626c652f636865636b732e796d6c3f6272616e63683d6d61696e266c6162656c3d636865636b73267374796c653d666c61742d737175617265)](https://github.com/mozex/laravel-searchable/actions?query=workflow%3AChecks+branch%3Amain)[![Docs](https://camo.githubusercontent.com/6bae6cab4895bc9b3768db4e7f44e2640da7b03da9635f43f34a1ea306f23e0e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f646f63732d6d6f7a65782e6465762d3130423938313f7374796c653d666c61742d737175617265)](https://mozex.dev/docs/laravel-searchable/v1)[![License](https://camo.githubusercontent.com/86ccab3dc837c7210e0fcc912480cc1bbba18c4857f71005b6011ee6136e7b60/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6d6f7a65782f6c61726176656c2d73656172636861626c653f7374796c653d666c61742d737175617265)](https://packagist.org/packages/mozex/laravel-searchable)[![Total Downloads](https://camo.githubusercontent.com/aaaf6395f2dc27b49fb2706d343ac1fc7909e17eb45174aed8d33582eaf3474f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f6d6f7a65782f6c61726176656c2d73656172636861626c652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/mozex/laravel-searchable)

Add a `Searchable` trait to any Eloquent model and search across multiple columns, regular relations, polymorphic relations, and even cross-database relations with a single `->search()` call. Works alongside Laravel Scout. Ships with optional Filament integration for table search and global search.

> **[Read the full documentation at mozex.dev](https://mozex.dev/docs/laravel-searchable/v1)**: searchable docs, version requirements, detailed changelog, and more.

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Search Types](#search-types)
    - [Direct Columns](#direct-columns)
    - [Relation Columns](#relation-columns)
    - [Morph Relations](#morph-relations)
    - [Cross-Database Relations](#cross-database-relations)
- [Case Sensitivity](#case-sensitivity)
- [Column Filtering](#column-filtering)
- [Relevance Ordering](#relevance-ordering)
- [Performance](#performance)
- [Filament Integration](#filament-integration)
    - [Ranked Table Search](#ranked-table-search)
    - [Global Search](#global-search)
- [Handling Conflicts](#handling-conflicts)
    - [Laravel Scout](#laravel-scout)
    - [Existing `search` Methods](#existing-search-methods)

Support This Project
--------------------

[](#support-this-project)

I maintain this package along with [several other open-source PHP packages](https://mozex.dev/docs) used by thousands of developers every day.

If my packages save you time or help your business, consider [**sponsoring my work on GitHub Sponsors**](https://github.com/sponsors/mozex). Your support lets me keep these packages updated, respond to issues quickly, and ship new features.

Business sponsors get logo placement in package READMEs. [**See sponsorship tiers →**](https://github.com/sponsors/mozex)

Installation
------------

[](#installation)

> **Requires [PHP 8.2+](https://php.net/releases/)** - see [all version requirements](https://mozex.dev/docs/laravel-searchable/v1/requirements)

```
composer require mozex/laravel-searchable
```

That's it. No config files to publish, no migrations to run.

Basic Usage
-----------

[](#basic-usage)

Add the `Searchable` trait to your model and define which columns should be searchable. You can mix direct columns, relation columns, and morph relations in the same array:

```
use Mozex\Searchable\Searchable;

class Comment extends Model
{
    use Searchable;

    public function searchableColumns(): array
    {
        return [
            'body',                          // direct column
            'author.name',                   // BelongsTo relation
            'tags.name',                     // HasMany relation
            'commentable:post.title',        // morph relation
            'commentable:video.name',        // another morph type
        ];
    }

    public function author(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
```

Then search:

```
// Shortest form, searches all configured columns
Comment::search('laravel')->get();

// Chain with other query constraints
Comment::query()
    ->where('published', true)
    ->search($request->input('q'))
    ->paginate();
```

The search wraps all its conditions in a `WHERE (... OR ...)` group, so it plays nicely with any existing query constraints.

Search Types
------------

[](#search-types)

Dot is for regular relations (`author.name`), colon is for morph relations where you have to name the target type because the package can't infer it (`commentable:post.title`).

### Direct Columns

[](#direct-columns)

Plain column names on the model's own table:

```
public function searchableColumns(): array
{
    return ['title', 'body', 'slug'];
}
```

### Relation Columns

[](#relation-columns)

Use dot notation to search through BelongsTo and HasMany relations:

```
public function searchableColumns(): array
{
    return [
        'title',
        'author.name',      // BelongsTo
        'author.email',     // BelongsTo, different column
        'comments.body',    // HasMany
        'tags.name',        // BelongsToMany / HasMany
    ];
}
```

### Morph Relations

[](#morph-relations)

For polymorphic relations, use `relation:morphType.column` notation. The morph type needs to match your morph map alias:

```
// In a ServiceProvider:
Relation::morphMap([
    'post' => Post::class,
    'video' => Video::class,
]);
```

```
class Comment extends Model
{
    use Searchable;

    public function searchableColumns(): array
    {
        return [
            'body',
            'commentable:post.title',        // search Post's title
            'commentable:video.name',         // search Video's name
            'commentable:post.author.name',   // nested: Post -> Author -> name
        ];
    }

    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}
```

Nested relations inside morph targets work too. `commentable:post.author.name` first resolves the morph to a Post, then follows the `author` relation on Post to search the author's name.

You need the morph type. A `MorphTo` written in plain dot notation (`commentable.title`, no type) can't resolve to a single model, so the package skips that column rather than guessing. Always name the type with the colon syntax.

### Cross-Database Relations

[](#cross-database-relations)

If a BelongsTo relation points to a model on a different database connection, the package picks this up on its own. Since cross-database JOINs aren't possible, it runs a separate query on the external connection, fetches matching IDs (capped at 50 by default), and uses `whereIn` on the foreign key.

Morph relations to external connections work the same way.

The cap keeps the resulting `IN (...)` clause from getting unmanageably large when a search term hits a lot of rows on the external side. If 50 isn't the right number for your data, pass `externalLimit`:

```
Post::search('term', externalLimit: 200)->get();
```

The same parameter works on `applySearch()` and on the Filament `advancedSearchable()` macro.

Case Sensitivity
----------------

[](#case-sensitivity)

Searches are case-insensitive by default. `Comment::search('LARAVEL')` matches rows containing `laravel`, `Laravel`, or `LARAVEL` without any extra flag or argument.

This comes from Laravel's `whereLike` helper, which the package uses for every search type (direct, relation, morph, and external). Actual behavior follows your database:

- **MySQL / MariaDB**: case-insensitive on the usual `_ci` collations like `utf8mb4_unicode_ci` and `utf8mb4_0900_ai_ci`. A column on a `_bin` or `_cs` collation matches case-sensitively.
- **PostgreSQL**: always case-insensitive. `whereLike` compiles to `ILIKE`.
- **SQLite**: case-insensitive for ASCII only. `Café` and `café` won't match each other under the default `LIKE`.

The package doesn't expose a flag to flip this. If you need case-sensitive matching on a normally case-insensitive column, change the column's collation at the database level.

Column Filtering
----------------

[](#column-filtering)

You can override or adjust which columns are searched per-query:

```
// Search only specific columns (ignores searchableColumns)
Post::search('term', in: ['title', 'body'])->get();

// Add extra columns on top of searchableColumns
Post::search('term', include: ['slug'])->get();

// Exclude specific columns from searchableColumns
Post::search('term', except: ['author.name'])->get();

// Combine them
Post::search('term', include: ['slug'], except: ['body'])->get();
```

All three parameters accept a string or an array.

Relevance Ordering
------------------

[](#relevance-ordering)

Results come back ranked by how well they match, not by `id`. The order of your `searchableColumns()` array sets the priority: a hit in the first column outranks a hit that only shows up in a later one.

Say a `Course` searches `['title', 'description']`. Search "laravel", and a course with "Laravel" in its title sorts above one that only mentions it in the description, even when the description course was created first. Title is column 0, so it wins. This is the whole point. Before, both matched equally and you had to scroll to find the one you meant.

Within a single column, closer matches rank higher. An exact value beats a prefix, which beats a buried substring. Searching "laravel" against three titles:

- `Laravel` ranks first (exact match)
- `Laravel Guide` ranks next (starts with the term)
- `Best Laravel Tips` ranks last (term sits in the middle)

The same grading runs on relation and morph columns. `author.name` is scored right after `title`, `commentable:post.title` in its own array position, and so on down the list. A HasMany relation is scored by its best-matching child, so an author with a post titled exactly "Laravel" sorts above one with "A Laravel Tutorial".

Cross-database relations and two-hop morph columns like `commentable:post.author.name` get a simpler matched-or-not score instead of the exact/prefix/substring grading, because those can't be ranked inside a single SQL statement. Column priority still holds for them.

Ordering is on by default. Turn it off with `orderByRelevance: false`:

```
Post::search('laravel', orderByRelevance: false)->get();
```

The same parameter works on `applySearch()`.

Add your own `orderBy()` before `search()` and yours stays the primary sort, with relevance as the tiebreaker:

```
// created_at drives the order, relevance only breaks ties
Post::query()->orderByDesc('created_at')->search('laravel')->get();
```

Call `orderBy()` after `search()` and the roles flip: relevance leads, your column breaks ties.

In Filament tables this is automatic while searching. See [Ranked Table Search](#ranked-table-search).

Performance
-----------

[](#performance)

This package compiles to `LIKE '%term%'`. The leading wildcard defeats B-tree indexes, so every row in the searched column gets scanned, and each relation or morph column adds a correlated `EXISTS` subquery on top. On small-to-mid tables this is fine; into the millions of rows, or once a search hits many relations, switch to Laravel Scout with Meilisearch, Typesense, or Algolia. Indexing the searched columns themselves won't help, but indexing the columns you also filter on (e.g., `tenant_id`, `status`) lets the database prune rows before the `LIKE` runs. On Postgres, a `pg_trgm` GIN index is the one thing that genuinely speeds up `LIKE '%term%'` while staying in SQL.

Relevance ordering adds a scoring expression to the `ORDER BY` for each searchable column, and for relation and morph columns that's another correlated subquery. It only runs over rows that already passed the `WHERE`, so the cost tracks the number of matches, not the table size. If you don't need ranked results, `orderByRelevance: false` skips all of it.

Filament Integration
--------------------

[](#filament-integration)

When Filament is installed, the package registers an `advancedSearchable()` macro on `TextColumn`. Add it to one column in your table, and it'll search across all your model's configured searchable columns:

```
use Filament\Tables\Columns\TextColumn;

TextColumn::make('title')
    ->advancedSearchable()
    ->sortable(),
```

You can pass the same `in`, `include`, `except` parameters:

```
TextColumn::make('title')
    ->advancedSearchable(except: ['author.name'])
    ->sortable(),
```

### Ranked Table Search

[](#ranked-table-search)

Tables rank by relevance automatically while searching. There's nothing to add to your tables. When the package boots with Filament present, it registers a global table query scope, so any table whose model uses the `Searchable` trait floats the best matches to the top the moment someone types in the search box, using the same column-priority and exact/prefix/substring rules from [Relevance Ordering](#relevance-ordering).

It's deliberately careful about not stepping on your existing sorts:

- **Not searching?** It does nothing. Your table's own `defaultSort`, however complex, runs exactly as before.
- **Searching?** Relevance leads, and your `defaultSort` becomes the tiebreaker behind it. Your sort still runs; it just settles ties among equally relevant rows.
- **Clicked a column header to sort?** Relevance gets out of the way completely and the chosen sort wins.

This works because Filament's search and sort are separate phases. The macro can't rank on its own (Filament runs the search callback inside a nested `WHERE`, and Eloquent throws away any `orderBy` added there), so the ranking rides on a query scope that runs before sorting instead.

If you'd rather wire ranking yourself, turn the automatic behavior off once, anywhere in a service provider:

```
use Mozex\Searchable\Filament\RelevanceSort;

RelevanceSort::$enabled = false;
```

Then apply it where you want, for example inside a `modifyQueryUsing` or a custom sort, reusing the same decision logic:

```
// $search and $sortColumn come from your table's livewire component
RelevanceSort::apply($query, $search, $sortColumn);
```

### Global Search

[](#global-search)

Global search ranks results on its own. The provider applies relevance ordering for you, so the most relevant hits land at the top of each resource's results with no extra wiring.

Register the provider on your panel:

```
use Mozex\Searchable\Filament\SearchableGlobalSearchProvider;

return $panel
    ->id('admin')
    ->path('admin')
    ->globalSearch(SearchableGlobalSearchProvider::class);
```

Then on each resource, define `getGloballySearchableAttributes()` to control which columns global search uses for that resource. Return all of the model's columns, or a subset:

```
class CourseResource extends Resource
{
    // Use everything the model declared as searchable
    public static function getGloballySearchableAttributes(): array
    {
        return (new Course)->searchableColumns();
    }
}

class PostResource extends Resource
{
    // Or limit global search to a subset, even though
    // the Post model has more columns in searchableColumns()
    public static function getGloballySearchableAttributes(): array
    {
        return ['title', 'author.name'];
    }
}
```

Each resource you want in global search needs to define `getGloballySearchableAttributes()`. Resources without it are excluded from global search entirely.

Resources whose models don't use the `Searchable` trait fall through to Filament's default global search behavior.

Handling Conflicts
------------------

[](#handling-conflicts)

### Laravel Scout

[](#laravel-scout)

Scout and this package both expose a `search()` method on your model. Scout's is a static method that hits its search engine; this package's is a query scope that runs SQL. Technically, different call paths, so they don't collide.

In practice, having two `search` entry points on the same model gets confusing fast. The cleaner approach is to alias this package's scope to a different name using PHP's trait aliasing, so each search path has its own clear name:

```
use Laravel\Scout\Searchable;
use Mozex\Searchable\Searchable as DatabaseSearchable;

class Lesson extends Model
{
    use DatabaseSearchable {
        scopeSearch as scopeDatabaseSearch;
    }
    use Searchable;

    public function searchableColumns(): array
    {
        return ['name', 'description'];
    }
}
```

Now `Lesson::search('term')` runs Scout's full-text search, and `Lesson::databaseSearch('term')` runs this package's database search. No ambiguity.

For the Filament macro, pass the renamed method:

```
TextColumn::make('name')->advancedSearchable(method: 'databaseSearch')
```

### Existing `search` Methods

[](#existing-search-methods)

Sometimes you can't reach this package's scope through `$query->search()` because something else already owns that name. Two common cases:

- **A custom Eloquent Builder defines its own `search()`** (Corcel's `PostBuilder` is the textbook example). `$query->search()` calls the Builder's method, not this package's scope.
- **A parent model you extend already declares `scopeSearch`** with a different signature. Adding our trait causes a fatal error because PHP enforces method signature compatibility between trait methods and inherited methods.

For both cases, use `applySearch()` to invoke the scope directly without going through the `search` name:

```
$query = Product::query();
$query->getModel()->applySearch($query, 'term');
$results = $query->get();
```

`applySearch` accepts the same parameters as the scope:

```
$query->getModel()->applySearch($query, 'term', in: ['title', 'body']);
$query->getModel()->applySearch($query, 'term', except: ['author.name']);
```

If a parent model's `scopeSearch` signature conflicts with this package's, alias our scope to a different name when adding the trait (the same pattern as the Scout case above):

```
use Mozex\Searchable\Searchable as DatabaseSearchable;

class Product extends VendorModel
{
    use DatabaseSearchable {
        scopeSearch as scopeDatabaseSearch;
    }
}
```

For the Builder case specifically, you can also override the Builder's `search()` to delegate back to `applySearch`, so the rest of your codebase keeps calling `$query->search()`:

```
class ProductBuilder extends \Corcel\Model\Builder\PostBuilder
{
    public function search($term = false, ...$args): self
    {
        $query = Product::query();

        (new Product)->applySearch($query, $term, ...$args);

        return $query;
    }
}
```

Resources
---------

[](#resources)

Visit the [documentation site](https://mozex.dev/docs/laravel-searchable/v1) for searchable docs auto-updated from this repository.

- **[AI Integration](https://mozex.dev/docs/laravel-searchable/v1/ai-integration)**: Use this package with AI coding assistants via Context7 and Laravel Boost
- **[Requirements](https://mozex.dev/docs/laravel-searchable/v1/requirements)**: PHP, Laravel, and dependency versions
- **[Changelog](https://mozex.dev/docs/laravel-searchable/v1/changelog)**: Release history with linked pull requests and diffs
- **[Contributing](https://mozex.dev/docs/laravel-searchable/v1/contributing)**: Development setup, code quality, and PR guidelines
- **[Questions &amp; Issues](https://mozex.dev/docs/laravel-searchable/v1/questions-and-issues)**: Bug reports, feature requests, and help
- **[Security](mailto:hello@mozex.dev)**: Report vulnerabilities directly via email

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

47

—

FairBetter than 93% of packages

Maintenance94

Actively maintained with recent releases

Popularity24

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 96.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~15 days

Total

6

Last Release

25d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/f4d33a9cb8263eab6fa251e57fc763c5c6977701e5cafa0edc538c4651bb9780?d=identicon)[mozex](/maintainers/mozex)

---

Top Contributors

[![mozex](https://avatars.githubusercontent.com/u/18025667?v=4)](https://github.com/mozex "mozex (53 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

database-searcheloquentfilamentglobal-searchlaravelmorph-searchmulti-column-searchphprelation-searchsearchsearchablesearchlaraveleloquentsearchablemulti-columnfilamentmozexrelation\_searchmorph-search

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/mozex-laravel-searchable/health.svg)

```
[![Health](https://phpackages.com/badges/mozex-laravel-searchable/health.svg)](https://phpackages.com/packages/mozex-laravel-searchable)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[spatie/laravel-health

Monitor the health of a Laravel application

87912.0M177](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.0k3.2M246](/packages/laravel-ai)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[masterix21/laravel-licensing

Laravel licensing package with polymorphic assignment to any model, activation keys, expirations/renewals, and seat control via LicenseUsage. Supports offline verification with public-key–signed tokens, a CLI to generate/rotate/revoke keys, and an extensible architecture via config and contracts.

1613.3k4](/packages/masterix21-laravel-licensing)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
