This project demonstrates how to add search with suggestions using Laravel Scout and Alpine.js.
How It Works
For this example, a database
scout driver is used.
// ... SCOUT_DRIVER=database
The Searchable
trait must be added to the Eloquent models from which search you will want to do. A good practice would be to add searchable fields to the toSearchableArray()
method as an array in the model.
app/Models/Category.php:
use Laravel\Scout\Searchable;use Illuminate\Database\Eloquent\Model; class Category extends Model{ use Searchable; // ... public function toSearchableArray(): array { return [ 'id' => $this->id, 'name' => $this->name, ]; }}
app/Models/Product.php:
use Laravel\Scout\Searchable;use Illuminate\Database\Eloquent\Model; class Product extends Model{ use Searchable; // ... public function toSearchableArray(): array { return [ 'id' => $this->id, 'title' => $this->title, 'code' => $this->code, ]; }}
A Blade component is used to add a search input. In the Blade component, Alpine.js makes a call to the /search
endpoint. The response from a search call is saved to a results
variable.
If categories or products exist in the result, they are looped through separately to show a result.
<div class="flex items-center min-w-96"> <div x-data="{ query: '', results: { categories: [], products: [] }, isLoading: false, resetSearch() { this.query = ''; this.results = { categories: [], products: [] }; }, async search() { if (this.query.length < 2) { this.results = { categories: [], products: [] }; return; } this.isLoading = true; try { const url = new URL(@js(route('search'))); url.searchParams.set('query', this.query); const response = await fetch(url); this.results = await response.json(); } catch (error) { console.error('Search failed:', error); } this.isLoading = false; } }" @click.away="resetSearch()" @keydown.escape.window="resetSearch()" class="relative w-full" > <x-text-input type="text" x-model="query" @input.debounce.300ms="search()" placeholder="Search..." class="w-full" /> <div x-show="query.length >= 2" x-transition class="absolute z-50 mt-1 w-full rounded border bg-white text-sm shadow-lg"> <template x-if="results.categories?.length"> <div class="p-2"> <template x-for="category in results.categories" :key="category.id"> <a :href="`/categories/${category.slug}`" class="block border-b p-2 last:border-b-0 hover:bg-gray-100" x-text="category.name"> </a> </template> </div> </template> <template x-if="results.products?.length"> <div class="p-2"> <h3 class="mb-2 font-bold">Products found</h3> <template x-for="product in results.products" :key="product.id"> <a :href="`/products/${product.slug}`" class="flex items-center justify-between p-2 hover:bg-gray-100"> <span x-text="product.title"></span> <span class="text-gray-600" x-text="`$${product.price}`"></span> </a> </template> </div> </template> <div x-show="isLoading" class="p-4 text-center"> Loading... </div> </div> </div></div>
The /search
endpoint goes to the SearchController
.
routes/web.php:
use App\Http\Controllers\SearchController; Route::get('search', SearchController::class)->name('search');
In the SearchController
, a search is done for a given query. The search()
method on a model comes from a Laravel Scout. The returned result is a JSON.
app/Http/Controllers/SearchController.php:
use App\Models\Product;use App\Models\Category;use Illuminate\Http\Request;use Illuminate\Http\JsonResponse; class SearchController extends Controller{ public function __invoke(Request $request): JsonResponse { $categories = Category::search($request->input('query')) ->take(5) ->get(); $products = Product::search($request->input('query')) ->take(10) ->get(); return response()->json([ 'categories' => $categories, 'products' => $products, ]); }}