David Nguyen
David Nguyen

Follow

David Nguyen

Follow
A practical guide to search Eloquent relationships using Laravel Scout Database Driver

A practical guide to search Eloquent relationships using Laravel Scout Database Driver

Laravel Scout is a tool for adding full-text search functionality to your Laravel applications. It uses drivers like Algolia, Meilisearch, SQL, etc

David Nguyen's photo
David Nguyen
·Dec 31, 2022·

6 min read

Play this article

Laravel Scout is a tool for adding full-text search functionality to your Laravel applications. It uses drivers like Algolia, Meilisearch, SQL databases, and others to search through your Eloquent models. The package is highly customizable, making it a versatile option for implementing search capabilities in your application.

Let's examine how we can search for Eloquent Model relationships using the Laravel Scout with the Database driver. Remember, the Laravel Scout driver currently supports MySQL and PostgreSQL.

Let’s start by creating a new Laravel application. For this purpose, you can check the Laravel documentation.

Create Models and Migrations

We will make use of two Eloquent models:

  • Post

  • Category

Create Category Model

Run the following command to create the Category model:

php artisan make:model Category -m

This command will create a new Category model class in the app\Models directory and generate a migration file for creating the categories table in the database.

Open the migration file in the database/migrations directory and add the necessary columns for the categories table. For example:

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

Create Post Model

Run the following command to create the Post model:

php artisan make:model Post -m

The command will create a new Post model class in the app\Models directory and generate a migration file for creating the posts table in the database.

Open the migration file in the database/migrations directory and add the necessary columns for the posts table. For example:

public function up()
{
     Schema::create('posts', function (Blueprint $table) {
         $table->id();
         $table->string('title');
         $table->text('body');
         $table->foreignId('category_id')->constrained();
         $table->timestamps();
     });
}

Apply the Migrations

Assuming you have configured the database for the application, run the following command to apply the migrations and create the posts and categories tables in the database:

php artisan migrate

Generate dummy data using a Database Seeder

Let’s create a database seeder to generate some dummy records for posts and categories tables.

First, create a new seeder class by running the following artisan command:

php artisan make:seeder PostsCategoriesTableSeeder

Open the PostsCategoriesTableSeeder class located in the database/seeds directory and update the run method with the following code:

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class PostsCategoriesTableSeeder extends Seeder
{
    use WithoutModelEvents;

    public function run()
    {
        // Generate 5 categories
        $data = [
            ['name' => 'News'],
            ['name' => 'Sports'],
            ['name' => 'Entertainment'],
            ['name' => 'Technology'],
            ['name' => 'Business'],
        ];

        DB::table('categories')->insert($data);

        // Generate 1000 posts
        for ($i = 1; $i <= 1000; $i++) {
            DB::table('posts')->insert([
                'title' => "Post $i",
                'body' => "Content for post $i",
                'category_id' => rand(1, 5)
            ]);
        }
    }
}

This seeder class generates five dummy categories and 1000 dummy posts. The name of each category is generated using a concatenation of a string and an automatically-incrementing id value, and the title, body, and category_id of each post are generated using a similar method. The category_id is chosen randomly from the list of categories.

To run the seeder, add the following line to the run method of the DatabaseSeeder class located in the database/seeds directory:

$this->call(PostsCategoriesTableSeeder::class);

Run the seeder by executing the following artisan command:

php artisan db:seed

That's all there is to it! Now, you have a new Laravel application with both Post and Category models with seeded dummy data.

Configure the Application to use Laravel Scout

To configure a Laravel application to use Laravel Scout with the Database Driver, you will need to follow these steps:

First, install the Laravel Scout package:

composer require laravel/scout

Next, publish the Scout configuration file:

php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"

Now in the .env file, set the SCOUT_DRIVER option to database

SCOUT_DRIVER=database

Next, add the Laravel\Scout\Searchable trait to your model(s) to make them searchable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;

class Post extends Model
{
    use HasFactory;
    use Searchable;
}

After that, define the category() relationship on the Post model as follows:

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

Finally, define the toSearchableArray() method on your model to specify which fields should be indexed by Laravel Scout:

public function toSearchableArray()
{
    return [
        'title' => '',
        'body' => '',
        'categories.name' => '',
    ];
}

To use Laravel Scout to perform full-text search on your models, you need to define a toSearchableArray() method on your model. This method should return an array of data that Laravel Scout should index.

The toSearchableArray() method should include relevant data to the search query. For example, if you are searching for posts that belong to a particular category, you might include the name field of the Category model in the toSearchableArray() method.

That's it! You should now be able to use Laravel Scout to perform full-text searches on your models using the database driver.

To start, create a new SearchController to test queries.

Create Controller

Run the following command:

php artisan make:controller SearchController --invokable

This will create a new SearchController class in the app/Http/Controllers directory. The controller class will contain a single __invoke() method called when the controller is invoked.

Add the Route

To use the SearchController in your application, you need to define a route for it in the routes/web.php file:

Route::get('/search', SearchController::class);

This route will map a GET request to the /search URL to the __invoke() method of the SearchController.

Write a Search Query

Open the SearchController class and add our first query using Laravel Scout.

<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class SearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $posts = Post::search(trim($request->get('search')) ?? '')
            ->query(function ($query) {
                $query->join('categories', 'posts.category_id', 'categories.id')
                    ->select(['posts.id', 'posts.title', 'posts.body', 'categories.name as category'])
                    ->orderBy('posts.id', 'DESC');
            })
            ->get();

        return response()->json(data: $posts, status: 200);
    }
}

The Post::search() method initiates a search query on the posts database table. This method returns an instance of a Builder class. This means that before actually executing the query by the DatabaseEngine you can apply several functions such as where(), whereIn(), onlyTrashed(), orderBy(), take(), paginate() and others to filter down your results further.

The trim($request->get('search')) ?? '' input parameter retrieves the query string search from the HTTP request and trims any leading or trailing whitespace or an empty string if the search query string is not present.

The query() function allows you to customize the search query by defining additional constraints or modifying the SELECT clause of the query. In this case, the query uses a JOIN clause to join with the categories table to search not only the posts table columns but also the name column of the categories table. Moreover, I am explicitly selecting what columns to return by prefixing the columns with the database name and adding an ORDER BY clause by specifying the full column name, including the database table. The query fails without prefixing the database table name.

Finally, the get() method is used to execute the search query and retrieve the matching posts from the database.

Let’s run the application in the browser and provide a search query string with the value of "technology" to return all posts with the category "technology."

The first part of the results.

The beginning of the results

The last part of the results.

The end of the results

Let’s run a query against the database to retrieve posts with the category "Technology" and compare.

Database results for the same query.

Database results for the same query

As you can see, both return the same result!

As you can see, the query() function on the search() method is a powerful tool for searching through relationships in your Laravel Scout database driver.

You can download a copy of the source code here at GitHub.

 
Share this