Laravel Scout: Create A Single API Endpoint for Searching Purpose Only
Tags: laravel, scout, api, search
Published: 2022-09-21 14:02:46

Today, I had a request, to improve how searching should work in the application which my team currently working on. 

The searching mechanism that implemented at the moment, is kind of slow - I can say performance issue.

Then I have a thought, can we make an API, a single API that can do all the searching in your database - I mean any targeted table like users, posts, orders, etc.

So I look in to [Laravel Scout](https://laravel.com/docs/9.x/scout) and install as usual. 

The only thing I configure is the driver - I use database driver, and the searchable keys.

**TLDR;**

```
composer require laravel/scout
```

Then update `.env`

```
SCOUT_DRIVER=database
```

Setup the `\App\Models\User`:


```php

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Scout\Searchable;

class User extends Authenticatable implements MustVerifyEmail
{
    use HasApiTokens;
    use HasProfilePhoto;
    use Searchable;

    /**
     * Get the indexable data array for the model.
     *
     * @return array
     */
    public function toSearchableArray()
    {
        return [
            'name' => $this->name,
            'email' => $this->email,
        ];
    }
}
```

So, above are how I configured the Laravel Scout. 

The important part is the API endpoint. The API endpoint should be usable to any kind of searching - in case I want to search for notifications, for posts, for orders, etc. I want everything to be at one single API endpoint - `/search`.

So here how I did at first:


```php

<?php

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')
    ->get('/search', function (Request $request) {
        return User::search($request->search)->first();
    });
```

But this only allow to search for one model and first record found only.

Imagine I have a `\App\Models\Post` model, configured similar to `\App\Models\User`. How can I make the API endpoint more dynamic?

```php
<?php

use App\Models\Post;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')
    ->get('/search', function (Request $request) {
        return match($request->type) {
            'user' => User::search($request->search)->first(),
            'post' => Post::search($request->search)->first(),
        }
    });
```

But it's kind of, hard to maintain in future, too many hardcoded in this route. 

So let's refactor a bit, we going to pull out the `match` part, to somewhere else - a helper. 

```php

<?php

use Laravel\Scout\Searchable;

if (! function_exists('search')) {
    function search(string $type, string $keyword, bool $paginate = false)
    {
        abort_if(
            ! class_exists($type), 
            "Class $type not exists."
        );

        throw_if(
            ! in_array(
                Searchable::class, 
                class_uses_recursive($type)
            )
        );

        $class = $type;

        $query = $class::search($keyword);

        return $paginate
            ? $query->paginate()
            : $query->first();
    }
}
```

The `abort_if()` just to ensured the class we going to use exists.

The `throw_if()`, we are strictly accept ONLY Laravel Scout `Searchable` trait. 

So your new route will be:

```php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->get('/search', function (Request $request) {
    return search(
        $request->type, 
        $request->search, 
        $request->query('paginate', false)
    );
});
```

BUT! We missing one more part. How do we know if that type, is belongs to which model? There's multiple way doing this, a simple mapping will do between type and model.

How I, I make use of Laravel Enum by Spatie, and my route will be like the following:

```php
<?php

use App\Enums\SearchType;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->get('/search', function (Request $request) {
    abort_if(
        empty($request->type) || empty(SearchType::tryFrom($request->type)),
        404,
        'Unknown search type'
    );

    abort_if(
        empty($request->search),
        404,
        'Please provide search keyword'
    );

    return search(SearchType::tryFrom($request->type), $request->search, $request->query('paginate', false));
});
```

Where the `SearchType`:

```php
<?php

namespace App\Enums;

use Spatie\Enum\Laravel\Enum;

/**
 * @method static self user()
 * @method static self profile()
 */
class SearchType extends Enum
{
    public static function values(): array
    {
        return [
            'user' => \App\Models\User::class,
            'profile' => \Profile\Models\Profile::class,
        ];
    }

    protected static function labels(): array
    {
        return [
            'user' => __('User'),
            'profile' => __('Profile'),
        ];
    }
}
```

And I need to update my `search()` helper, to rely on this new enum as well:

```php
<?php

use App\Enums\SearchType;
use Laravel\Scout\Searchable;

if (! function_exists('search')) {
    function search(SearchType $type, string $keyword, bool $paginate = false)
    {
        throw_if(
            ! in_array(Searchable::class, class_uses_recursive($type->value))
        );

        $class = $type->value;

        $query = $class::search($keyword);

        return $paginate
            ? $query->paginate()
            : $query->first();
    }
}
```

So now, everything in place, I can simply later add `Laravel\Scout\Searchable` trait to any model and update my `App\Enums\SearchType` to accept new type of searching.

So the usage:

```bash
curl --request GET \
  --url 'http://127.0.0.1:8000/api/search?type=profile&search=00000000' \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer WD7wV13lCSp1XqesClDsND4IP92OPiMwC9soWuS6'
```

And the response will be like:

```json
{
	"uuid": "82dea65f-38bd-4ef6-a4cd-c3bffa3b3b2f",
	"profile_no": "00000000",
	"name": "Superadmin",
	"created_at": "2022-09-15T05:01:20.000000Z",
	"updated_at": "2022-09-15T05:01:20.000000Z"
}
```

I hope this give some insight, how you can write better code for your application.

By the end of the day, I have a better solution for my team for provide and maintain the Search API, and for the performance issue - let the team switch to this approach and give it a try. 

> Don't forget to make use of the [API Resource](https://laravel.com/docs/9.x/eloquent-resources#main-content). 
> You can change to Agnolia if you want to, but let's keep it for database.
> Database driver only support MySQL and PostgreSQL at the moment.