New Package Laravel-Searchable: Easily Search in Multiple Models

Spatie team is still on fire with new packages. This week they released another one called Laravel Searchable, created mainly by AlexVanderbist. I've tried it myself and can show you a demo, along with my opinion.

What is Laravel Searchable

Spatie's package makes searching in models an easy task, without external dependencies.

The main advantage, as I've tested it, is ability to perform mega-search in all project database, specifying more than one model to search in.

Here's an example search code from Controller:

$searchResults = (new Search())
   ->registerModel(User::class, 'name')
   ->registerModel(BlogPost::class, 'title')
   ->perform('john');

Looks pretty simple and readable, right?

You would say there's no need for another "search" package when we have Laravel Scout, Algolia, ElasticSearch and others, right? Here's Freek Van der Herten's official take on it:

  1. Main difference with Scout is that this one has no external dependencies.
  2. laravel-searchable does not try to replace Scout. Both packages have their place. Make your own decision what you need in your project!

Example mini-project: preparation

To test the package, I've created a fresh Laravel 5.7 project (the code will be available on GitHub - link at the end of article) with two database tables: categories and companies:

Schema::create('categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->timestamps();
});

Schema::create('companies', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->nullable();
    $table->unsignedInteger('category_id');
    $table->foreign('category_id')->references('id')->on('categories');
    $table->timestamps();
});

Also, seeded some data for both tables, used make:auth to generate a simple Bootstrap template, and ended up with this list of companies:

The code is really simple, here's HomeController:

public function index()
{
    $companies = Company::with('category')->get();
    return view('home', compact('companies'));
}

Now, let's say we want to search both Categories and Companies from one text field. This is where the package will help us.

Notice a Search bar in top-right corner? Here's HTML code for it:

<form action="{{ route('search') }}" method="POST">
    @csrf
    <input type="text" name="query" />
    <input type="submit" class="btn btn-sm btn-primary" value="Search" />
</form>

Meanwhile, in routes/web.php, we have the homepage, search results, and pages for individual category/company:

Route::get('/', 'HomeController@index')->name('home');
Route::post('/search', 'HomeController@search')->name('search');
Route::get('/companies/{company}', 'CompanyController@show')->name('companies.show');
Route::get('/categories/{category}', 'CategoryController@show')->name('categories.show');

So, "all" we need to do now, is implement HomeController@search method. Now, step-by-step.


Example mini-project: using Laravel Searchable

Step 1. Install the package.

composer require spatie/laravel-searchable

That's it, no more steps required at installation.

Step 2. Prepare the Models to be Searchable

This is where we need to do some manual work, in both app/Category.php and app/Company.php.

This is how Category model should look:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;

class Category extends Model implements Searchable
{

    protected $fillable = ['name'];

    public function getSearchResult(): SearchResult
    {
        $url = route('categories.show', $this->id);

        return new SearchResult(
            $this,
            $this->name,
            $url
         );
    }

}

Let's break it down, what changes have we made?

  • Using Spatie\Searchable\Searchable and then implements Searchable;
  • Using Spatie\Searchable\SearchResult and implementing method getSearchResult() which should return SearchResult object;
  • Within that SearchResult() object we need to specify three parameters: where are we searching (model itself - $this), what is the returned column for title (it will be displayed in results - so category name, $this->name), and what URL the result should link to (we're building that with route() helper)

Really really similar transformations happen in app/Company.php:

namespace App;

use Illuminate\Database\Eloquent\Model;
use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;

class Company extends Model implements Searchable
{

    protected $fillable = ['name', 'category_id'];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function getSearchResult(): SearchResult
    {
        $url = route('companies.show', $this->id);

        return new SearchResult(
            $this,
            $this->name,
            $url
        );
    }

}

Step 3. Performing the search from Controller

Here's the code for our HomeController method:

namespace App\Http\Controllers;

use App\Category;
use App\Company;
use Illuminate\Http\Request;
use Spatie\Searchable\Search;

class HomeController extends Controller
{

    // ... other methods

    public function search(Request $request)
    {
        $searchResults = (new Search())
            ->registerModel(Company::class, 'name')
            ->registerModel(Category::class, 'name')
            ->perform($request->input('query'));

        return view('search', compact('searchResults'));
    }

}

As you can see, we're creating a Search() object and registering TWO models with the fields that we need to search for. So that 'name' could be different for each model, it's just a coincidence that it's the same here.

Step 4. Viewing the Results: Grouped by Model

Here I've taken the example from the official documentation with a few small tweaks. Here's our resources/views/search.blade.php:

... Some main Blade code here ...

<div class="card">
    <div class="card-header"><b>{{ $searchResults->count() }} results found for "{{ request('query') }}"</b></div>

    <div class="card-body">

        @foreach($searchResults->groupByType() as $type => $modelSearchResults)
            <h2>{{ ucfirst($type) }}</h2>

            @foreach($modelSearchResults as $searchResult)
                <ul>
                    <li><a href="{{ $searchResult->url }}">{{ $searchResult->title }}</a></li>
                </ul>
            @endforeach
        @endforeach

    </div>
</div>

... Some more main Blade template code ...

And here's how it looks:

As you can see, only one Company is returned but not the Category. Now, let's try a query with both results present:

Here's how $searchResults structure looks like if we do dd():


We've covered a basic usage, but there are a few more customizable things in Laravel Searchable. You can read them in the official documentation.

As promised, here's the link to Github repository of this demo-project.

What do you think? Will you use the package?

No comments or questions yet...

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 58 courses (1056 lessons, total 44 h 09 min)
  • 78 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials