Full-text search is crucial for allowing users to navigate content-rich websites. In this post, I'll show you how to implement full-text search for a Laravel app. In fact, we'll use the Laravel Scout library, which makes implementation of full-text search easy and fun.
What exactly is the Laravel Scout? The official documentation sums it up like this:
Laravel Scout provides a simple, driver-based solution for adding full-text search to your Eloquent models. Using model observers, Scout will automatically keep your search indexes in sync with your Eloquent records.
Basically, Laravel Scout is a library which manages manipulation of the index whenever there's a change in the model data. The place where the data will be indexed depends on the driver which you've configured with the Scout library.
As of now, the Laravel Scout library supports Algolia, a cloud-based search engine API, and that's what we'll use in this article to demonstrate the full-text search implementation.
We'll start by installing the Scout and Algolia server libraries, and as we move on we'll go through a real-world example to demonstrate how you could index and search your data.
Server Configurations
In this section, we're going to install the dependencies that are required in order to make the Scout library work with Laravel. After installation, we'll need to go through quite a bit of configuration so that Laravel can detect the Scout library.
Let's go ahead and install the Scout library using Composer.
1 |
$composer require laravel/scout
|
That's pretty much it as far as the Scout library installation is concerned. Now that we've installed the Scout library, let's make sure that Laravel knows about it.
Working with Laravel, you're probably aware of the concept of a service provider, which allows you to configure services in your application. Thus, whenever you want to enable a new service in your Laravel application, you just need to add an associated service provider entry in config/app.php.
If you're not familiar with Laravel service providers yet, I would strongly recommend that you do yourself a favor and go through this introductory article that explains the basics of service providers in Laravel.
In our case, we just need to add the ScoutServiceProvider
provider to the list of service providers in config/app.php, as shown in the following snippet.
1 |
...
|
2 |
...
|
3 |
'providers' => [ |
4 |
/*
|
5 |
* Laravel Framework Service Providers...
|
6 |
*/
|
7 |
Illuminate\Auth\AuthServiceProvider::class, |
8 |
Illuminate\Broadcasting\BroadcastServiceProvider::class, |
9 |
Illuminate\Bus\BusServiceProvider::class, |
10 |
Illuminate\Cache\CacheServiceProvider::class, |
11 |
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, |
12 |
Illuminate\Cookie\CookieServiceProvider::class, |
13 |
Illuminate\Database\DatabaseServiceProvider::class, |
14 |
Illuminate\Encryption\EncryptionServiceProvider::class, |
15 |
Illuminate\Filesystem\FilesystemServiceProvider::class, |
16 |
Illuminate\Foundation\Providers\FoundationServiceProvider::class, |
17 |
Illuminate\Hashing\HashServiceProvider::class, |
18 |
Illuminate\Mail\MailServiceProvider::class, |
19 |
Illuminate\Notifications\NotificationServiceProvider::class, |
20 |
Illuminate\Pagination\PaginationServiceProvider::class, |
21 |
Illuminate\Pipeline\PipelineServiceProvider::class, |
22 |
Illuminate\Queue\QueueServiceProvider::class, |
23 |
Illuminate\Redis\RedisServiceProvider::class, |
24 |
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, |
25 |
Illuminate\Session\SessionServiceProvider::class, |
26 |
Illuminate\Translation\TranslationServiceProvider::class, |
27 |
Illuminate\Validation\ValidationServiceProvider::class, |
28 |
Illuminate\View\ViewServiceProvider::class, |
29 |
|
30 |
/*
|
31 |
* Package Service Providers...
|
32 |
*/
|
33 |
|
34 |
/*
|
35 |
* Application Service Providers...
|
36 |
*/
|
37 |
App\Providers\AppServiceProvider::class, |
38 |
App\Providers\AuthServiceProvider::class, |
39 |
// App\Providers\BroadcastServiceProvider::class,
|
40 |
App\Providers\EventServiceProvider::class, |
41 |
App\Providers\RouteServiceProvider::class, |
42 |
Laravel\Scout\ScoutServiceProvider::class, |
43 |
],
|
44 |
...
|
45 |
...
|
Now, Laravel is aware of the ScoutServiceProvider
service provider. The Scout library comes with a configuration file which allows us to set API credentials.
Let's go ahead and publish the assets provided by the Scout library using the following command.
1 |
$ php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider" |
2 |
Copied File [/vendor/laravel/scout/config/scout.php] To [/config/scout.php] |
3 |
Publishing complete. |
As you can see, it has copied the vendor/laravel/scout/config/scout.php file to config/scout.php.
Next, go ahead and create an account with Algolia as we'll need API credentials in the first place. Once you have the API information, let's go ahead and configure the necessary settings in the config/scout.php file, as shown in the following snippet.
1 |
<?php
|
2 |
|
3 |
return [ |
4 |
|
5 |
/*
|
6 |
|--------------------------------------------------------------------------
|
7 |
| Default Search Engine
|
8 |
|--------------------------------------------------------------------------
|
9 |
|
|
10 |
| This option controls the default search connection that gets used while
|
11 |
| using Laravel Scout. This connection is used when syncing all models
|
12 |
| to the search service. You should adjust this based on your needs.
|
13 |
|
|
14 |
| Supported: "algolia", "null"
|
15 |
|
|
16 |
*/
|
17 |
|
18 |
'driver' => env('SCOUT_DRIVER', 'algolia'), |
19 |
|
20 |
/*
|
21 |
|--------------------------------------------------------------------------
|
22 |
| Index Prefix
|
23 |
|--------------------------------------------------------------------------
|
24 |
|
|
25 |
| Here you may specify a prefix that will be applied to all search index
|
26 |
| names used by Scout. This prefix may be useful if you have multiple
|
27 |
| "tenants" or applications sharing the same search infrastructure.
|
28 |
|
|
29 |
*/
|
30 |
|
31 |
'prefix' => env('SCOUT_PREFIX', ''), |
32 |
|
33 |
/*
|
34 |
|--------------------------------------------------------------------------
|
35 |
| Queue Data Syncing
|
36 |
|--------------------------------------------------------------------------
|
37 |
|
|
38 |
| This option allows you to control if the operations that sync your data
|
39 |
| with your search engines are queued. When this is set to "true" then
|
40 |
| all automatic data syncing will get queued for better performance.
|
41 |
|
|
42 |
*/
|
43 |
|
44 |
'queue' => env('SCOUT_QUEUE', false), |
45 |
|
46 |
/*
|
47 |
|--------------------------------------------------------------------------
|
48 |
| Database Transactions
|
49 |
|--------------------------------------------------------------------------
|
50 |
|
|
51 |
| This configuration option determines if your data will only be synced
|
52 |
| with your search indexes after every open database transaction has
|
53 |
| been committed, thus preventing any discarded data from syncing.
|
54 |
|
|
55 |
*/
|
56 |
|
57 |
'after_commit' => false, |
58 |
|
59 |
/*
|
60 |
|--------------------------------------------------------------------------
|
61 |
| Chunk Sizes
|
62 |
|--------------------------------------------------------------------------
|
63 |
|
|
64 |
| These options allow you to control the maximum chunk size when you are
|
65 |
| mass importing data into the search engine. This allows you to fine
|
66 |
| tune each of these chunk sizes based on the power of the servers.
|
67 |
|
|
68 |
*/
|
69 |
|
70 |
'chunk' => [ |
71 |
'searchable' => 500, |
72 |
'unsearchable' => 500, |
73 |
],
|
74 |
|
75 |
/*
|
76 |
|--------------------------------------------------------------------------
|
77 |
| Soft Deletes
|
78 |
|--------------------------------------------------------------------------
|
79 |
|
|
80 |
| This option allows you to control whether to keep soft deleted records in
|
81 |
| the search indexes. Maintaining soft deleted records can be useful
|
82 |
| if your application still needs to search for the records later.
|
83 |
|
|
84 |
*/
|
85 |
|
86 |
'soft_delete' => false, |
87 |
|
88 |
/*
|
89 |
|--------------------------------------------------------------------------
|
90 |
| Identify User
|
91 |
|--------------------------------------------------------------------------
|
92 |
|
|
93 |
| This option allows you to control whether to notify the search engine
|
94 |
| of the user performing the search. This is sometimes useful if the
|
95 |
| engine supports any analytics based on this application's users.
|
96 |
|
|
97 |
| Supported engines: "algolia"
|
98 |
|
|
99 |
*/
|
100 |
|
101 |
'identify' => env('SCOUT_IDENTIFY', false), |
102 |
|
103 |
/*
|
104 |
|--------------------------------------------------------------------------
|
105 |
| Algolia Configuration
|
106 |
|--------------------------------------------------------------------------
|
107 |
|
|
108 |
| Here you may configure your Algolia settings. Algolia is a cloud hosted
|
109 |
| search engine which works great with Scout out of the box. Just plug
|
110 |
| in your application ID and admin API key to get started searching.
|
111 |
|
|
112 |
*/
|
113 |
|
114 |
'algolia' => [ |
115 |
'id' => env('ALGOLIA_APP_ID', 'XXXXXX'), |
116 |
'secret' => env('ALGOLIA_SECRET', 'XXXXXXXXXXXXXXXXXXXXXXXX'), |
117 |
],
|
118 |
|
119 |
];
|
Note that we've set the value of SCOUT_DRIVER
to algolia
driver. Thus, it's required that you configure the necessary settings for the Algolia driver at the end of the file. Basically, you just need to set the id
and secret
that you've got from the Algolia account.
As you can see, we're fetching values from environment variables. So let's make sure that we set the following variables in the .env file properly.
1 |
... |
2 |
... |
3 |
ALGOLIA_APP_ID=XXXXXX |
4 |
ALGOLIA_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXX |
5 |
... |
6 |
... |
Finally, we need to install the Algolia PHP SDK, which will be used to interact with Algolia using APIs. Let's install it using Composer, as shown in the following snippet.
1 |
$composer require algolia/algoliasearch-client-php
|
And with that, we've installed all the dependencies that are necessary in order to post and index data to the Algolia service.
How to Make Models Indexable and Searchable
In the previous section, we did all the hard work to set up the Scout and Algolia libraries so that we could index and search data using the Algolia search service.
In this section, we'll go through an example to demonstrate how you could index the existing data and retrieve search results from Algolia. I assume that you have a default Post
model in your application.
The first thing that we'll need to do is to add the Laravel\Scout\Searchable
trait to the Post
model. That makes the Post
model searchable; Laravel synchronizes post records with the Algolia index every time the post record is added, updated, or deleted.
1 |
<?php
|
2 |
namespace App; |
3 |
|
4 |
use Illuminate\Database\Eloquent\Model; |
5 |
use Laravel\Scout\Searchable; |
6 |
|
7 |
class Post extends Model |
8 |
{
|
9 |
use Searchable; |
10 |
|
11 |
...
|
12 |
...
|
13 |
}
|
With that, the Post
model is search-friendly!
Next, we would like to configure the fields that should get indexed in the first place. Of course, you don't want to index all the fields of your model in Algolia to keep it effective and lightweight. In fact, more often than not, you won't need it.
You can add the toSearchableArray
in the model class to configure the fields that'll be indexed.
1 |
/**
|
2 |
* Get the indexable data array for the model.
|
3 |
*
|
4 |
* @return array
|
5 |
*/
|
6 |
public function toSearchableArray() |
7 |
{
|
8 |
$array = $this->toArray(); |
9 |
|
10 |
return array('id' => $array['id'],'name' => $array['name']); |
11 |
}
|
Now, we're ready to import and index existing Post
records into Algolia. In fact, the Scout library makes this easy by providing the following artisan command.
1 |
$php artisan scout:import "App\Post" |
That should import all the records of the Post
model in a single go! They are indexed as soon as they're imported, so we're ready to query records already. Go ahead and explore the Algolia dashboard to see the imported records and other utilities.
How It Works Altogether
In this section, we'll create an example which demonstrates how to perform search and CRUD operations that are synced in real time with the Algolia index.
Go ahead and create the app/Http/Controllers/SearchController.php file with the following contents.
1 |
<?php
|
2 |
namespace App\Http\Controllers; |
3 |
|
4 |
use App\Http\Controllers\Controller; |
5 |
use App\Post; |
6 |
|
7 |
class SearchController extends Controller |
8 |
{
|
9 |
public function query() |
10 |
{
|
11 |
// queries to Algolia search index and returns matched records as Eloquent Models
|
12 |
$posts = Post::search('title')->get(); |
13 |
|
14 |
// do the usual stuff here
|
15 |
foreach ($posts as $post) { |
16 |
// ...
|
17 |
}
|
18 |
}
|
19 |
|
20 |
public function add() |
21 |
{
|
22 |
// this post should be indexed at Algolia right away!
|
23 |
$post = new Post; |
24 |
$post->setAttribute('name', 'Another Post'); |
25 |
$post->setAttribute('user_id', '1'); |
26 |
$post->save(); |
27 |
}
|
28 |
|
29 |
public function delete() |
30 |
{
|
31 |
// this post should be removed from the index at Algolia right away!
|
32 |
$post = Post::find(1); |
33 |
$post->delete(); |
34 |
}
|
35 |
}
|
Of course, we need to add the associated routes as well.
1 |
Route::get('search/query', 'SearchController@query'); |
2 |
Route::get('search/add', 'SearchController@add'); |
3 |
Route::get('search/delete', 'SearchController@delete'); |
Let's go through the query
method to see how to perform a search in Algolia.
1 |
public function query() |
2 |
{
|
3 |
// queries to Algolia search index and returns matched records as Eloquent Models
|
4 |
$posts = Post::search('title')->get(); |
5 |
|
6 |
// do the usual stuff here
|
7 |
foreach ($posts as $post) { |
8 |
// ...
|
9 |
}
|
10 |
}
|
Recall that we made the Post
model searchable by adding the Searchable
trait. Thus, the Post
model can use the search
method to retrieve records from the Algolia index. In the above example, we're trying to fetch records that match the title
keyword.
Next, there's the add
method, which imitates the workflow of adding a new post record.
1 |
public function add() |
2 |
{
|
3 |
// this post should be indexed at Algolia right away!
|
4 |
$post = new Post; |
5 |
$post->setAttribute('name', 'Another Post'); |
6 |
$post->setAttribute('user_id', '1'); |
7 |
$post->save(); |
8 |
}
|
There's nothing fancy in the above code; it just creates a new post record using the Post
model. But the Post
model implements the Searchable
trait, so Laravel does some extra work this time around by indexing the newly created record in Algolia. So as you can see, the indexing is done in real time.
Finally, there's the delete
method. Let's go through it as well.
1 |
public function delete() |
2 |
{
|
3 |
// this post should be removed from the index at Algolia right away!
|
4 |
$post = Post::find(1); |
5 |
$post->delete(); |
6 |
}
|
As you would have expected, the record is deleted right away from the Algolia index as soon as it's deleted from the database.
Basically, there's no extra effort required from your side if you want to make existing models searchable. Everything is handled by the Scout library using model observers.
How to Make a Custom Search Engine/Driver
By default, the Scout library supports the Algolia
and MeiliSearch
drivers. Additionally, you can also use the database
driver for a lightweight database. On the other hand, if you want to implement your own custom engine, Scout allows you to achieve that. You just need to write your custom engine and register it with Scout!
Your custom engine class may look like this:
1 |
<?php
|
2 |
|
3 |
namespace App\Engines; |
4 |
|
5 |
use Laravel\Scout\Builder; |
6 |
use Laravel\Scout\Engines\Engine; |
7 |
|
8 |
class CustomScoutEngine extends Engine { |
9 |
public function update($models) {} |
10 |
public function delete($models) {} |
11 |
public function search(Builder $builder) {} |
12 |
public function paginate(Builder $builder, $perPage, $page) {} |
13 |
public function mapIds($results) {} |
14 |
public function map(Builder $builder, $results, $model) {} |
15 |
public function getTotalCount($results) {} |
16 |
public function flush($model) {} |
17 |
}
|
Of course, you need to implement abstract methods as per your requirements.
Once you've implemented your custom engine class, you just need to register it. You can do it with the help of the boot
method of the service provider, as shown in the following snippet.
1 |
use App\Engines\CustomScoutEngine; |
2 |
use Laravel\Scout\EngineManager; |
3 |
|
4 |
/**
|
5 |
* Bootstrap any application services.
|
6 |
*
|
7 |
* @return void
|
8 |
*/
|
9 |
public function boot() |
10 |
{
|
11 |
resolve(EngineManager::class)->extend('custom_scout_engine', function () { |
12 |
return new CustomScoutEngine; |
13 |
});
|
14 |
}
|
And finally, after your custom engine is registered, you can use it in the config/scout.php file.
1 |
'driver' => 'custom_scout_engine', |
Conclusion
Today, we discussed how you can implement full-text search in Laravel using the Laravel Scout library. In the process, we went through the necessary installations and a real-world example to demonstrate it.