Introducing Laravel Scout

Posted on July 29, 2016 | By Matt Stauffer


Warning: This post is over a year old. I don't always update old posts with new information, so some of this information may be out of date.

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