Introducing Laravel Scout

(This is part of a series of posts on New Features in Laravel 5.3.)

  1. Introducing Laravel Echo: An In-Depth Walk-Through
  2. The new $loop variable in Laravel 5.3
  3. Customizing additional parameters in FirstOrCreate in Laravel 5.3
  4. The new cache() global helper in Laravel 5.3
  5. New JSON-column where() and update() syntax in Laravel 5.3
  6. Advanced operations with Collection::where in Laravel 5.3
  7. Image dimension validation rules in Laravel 5.3
  8. Customizing pagination templates in Laravel 5.3
  9. 5.3 feature announcement notes from Laracon
  10. Routing changes in Laravel 5.3
  11. Introducing Laravel Scout
  12. Introducing Laravel Passport
  13. Introducing Mailables in Laravel 5.3
  14. Directory structure changes in Laravel 5.3
  15. The new Notification system in Laravel 5.3
  16. Update to queue workers in Laravel 5.3
  17. Using Vue in Laravel 5.3, with the Vue bootstrap and sample component
  18. Defining console commands via closure in Laravel 5.3

Search tools ElasticSearch and Algolia have gained a lot of popularity in the Laravel community in the last few years as powerful tools for indexing and searching your data. Ben Corlett did a fantastic job introducing ElasticSearch at Laracon Eu 2014, and I wrote a pull request to Laravel.com introducing ElasticSearch-based indexing for the docs in 2015. But before my PR was merged, the folks at Algolia took it and updated it to instead use Algolia (faster and with a better UI!), and that's what you'll see today if you search the Laravel docs.

If you take a look at my pull request or theirs, you'll see that it's not a small task to integrate fulltext search into your site. Algolia has since released a free product called Algolia DocSearch that makes it easy to add an Algolia search widget to documentation pages. But for anything else, you're still stuck writing the integration yourself—that is, until now.

Introducing Laravel Scout #

Scout is a driver-based fulltext search solution for Eloquent. Scout makes it easy to index and search the contents of your Eloquent models; currently it works with Algolia and ElasticSearch, but Taylor's asked for community contributions to other fulltext search services.

Scout is a separate Laravel package, like Cashier, that you'll need to pull in with Composer. We'll be adding traits to our models that indicate to Scout that it should listen to the events fired when instances of those models are modified and update the search index in response.

Take a look at this syntax for fulltext search, for finding any Review with the word Llew in it:

Review::search('Llew')->get();
Review::search('Llew')->paginate(20);
Review::search('Llew')->where('account_id', 2)->get();

All that with very little configuration. That's a beautiful thing.

Installing Scout #

First, pull in the package (once it's live, and on a Laravel 5.3 app):

composer require laravel/scout

Next, add the Scout service provider (Laravel\Scout\ScoutServiceProvider::class) to the providers section of config/app.php.

We'll want to set up our Scout configuration. Run php artisan vendor:publish and paste your Algolia credentials in config/scout.php.

Finally, assuming you're using Algolia, install the Algolia SDK:

composer require algolia/algoliasearch-client-php

Marking your model for indexing #

Now, go to your model (we'll use Review, for a book review, for this example). Import the Laravel\Scout\Searchable trait. You can define which properties are searchable using the toSearchableArray() method (it defaults to mirroring toArray()), and define the name of the model's index using the searchableAs() method (it defaults to the table name).

Once we've done this, you can go check out your Algolia index page on their web site; when you add, update, or delete Review records, you'll see your Algolia index update. Just like that.

Searching your index #

We took a look at this already, but here's a refresh of how to search:

// Get all records from the Review that match the term "Llew"
Review::search('Llew')->get();

// Get all records from the Review that match the term "Llew",
// limited to 20 per page and reading the ?page query parameter,
// just like Eloquent pagination
Review::search('Llew')->paginate(20);

// Get all records from the Review that match the term "Llew"
// and have an account_id field set to 2
Review::search('Llew')->where('account_id', 2)->get();

What comes back from these searches? A Collection of Eloquent models, re-hydrated from your database. The IDs are stored in Algolia, which returns a list of matched IDs, and then Scout pulls the database records for those and returns them as Eloquent objects.

You don't have full access to the complexity of SQL where commands, but it handles a solid basic framework for comparison checks like you can see in the code samples above.

Queues #

You can probably guess that we're now making HTTP requests to Algolia on every request that modifies any database records. That can make things slow down very quickly, so you may find yourself wanting to queue these operations—which, thankfully, is simple.

In config/scout.php, set queue to true so that these updates are set to be indexed asynchronously. We're now looking at "eventual consistency"; your database records will receive the updates immediately, and the updates to your search indexes will be queued and updated as fast as your queue worker allows.

Special cases #

Let's cover some special cases.

Perform operations without indexing #

What if you want to perform a set of operations and avoid triggering the indexing in response? Just wrap them in the withoutSyncingToSearch() method on your model:

Review::withoutSyncingToSearch(function () {
    // make a bunch of reviews, e.g.
    factory(Review::class, 10)->create();
});

Manually trigger indexing via code #

Let's say you're now ready to perform the indexes, now that some bulk operation has been successfully performed. How?

Just add searchable() to the end of any Eloquent query and it will index all of the records that were found in that query.

Review::all()->searchable();

You can also choose to scope the query to only those you want to index, but it's worth noting that the indexing will insert new records and update old records, so it's not bad to let it run over some records that may be indexed already.

This will also work on a relationship:

$user->reviews()->searchable();

You can also un-index any records with the same sort of query chaining, but just using unsearchable() instead:

Review::where('sucky', true)->unsearchable();

Manually trigger indexing via CLI #

There's an Artisan command for that.™

php artisan scout:import App\\Review

That'll chunk all of the Review models and index them all.

Conclusion #

That's it! With almost no work, you now have complete full-text search running on your Eloquent models.


Comments? I'm @stauffermatt on Twitter


Tags: laravel | laravel 5.3 | laravel scout

Matt Stauffer headshot

Hi, I'm Matt Stauffer.

I'm partner & technical director at Tighten Co.

You can find me on Twitter at @stauffermatt


Like what you're reading?

I wrote an entire 450+ page book for O'Reilly: Laravel: Up and Running.

You can order the eBook or print book today.