Full-Text Search with Laravel Scout
Laravel Scout provides a simple, driver-based solution for adding full-text search to your Eloquent models. Scout supports multiple search engines including Algolia, Meilisearch, and custom drivers. In this lesson, we'll explore how to implement powerful search functionality in your Laravel applications.
Installing and Configuring Scout
First, install Scout and choose your preferred search engine driver.
# Install Scout
composer require laravel/scout
# Publish configuration
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
# Install Algolia driver
composer require algolia/algoliasearch-client-php
# Or install Meilisearch driver
composer require meilisearch/meilisearch-php http-interop/http-factory-guzzle
# Configure in .env
SCOUT_DRIVER=algolia
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SECRET=your-secret-key
# Or for Meilisearch
SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://127.0.0.1:7700
MEILISEARCH_KEY=your-master-key
Note: Meilisearch is an excellent open-source alternative to Algolia. You can run it locally via Docker: docker run -d -p 7700:7700 getmeili/meilisearch:latest
Making Models Searchable
Add the Searchable trait to any model you want to make searchable.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
class Post extends Model
{
use Searchable;
/**
* Get the indexable data array for the model.
*/
public function toSearchableArray()
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'excerpt' => $this->excerpt,
'author' => $this->author->name,
'category' => $this->category->name,
'tags' => $this->tags->pluck('name')->toArray(),
'published_at' => $this->published_at->timestamp,
];
}
/**
* Get the index name for the model.
*/
public function searchableAs()
{
return 'posts_index';
}
/**
* Determine if the model should be searchable.
*/
public function shouldBeSearchable()
{
return $this->is_published;
}
}
// Product model with custom indexing
class Product extends Model
{
use Searchable;
public function toSearchableArray()
{
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'price' => $this->price,
'stock' => $this->stock,
'category' => $this->category->name,
'brand' => $this->brand,
'sku' => $this->sku,
'rating' => $this->averageRating(),
'is_available' => $this->stock > 0,
];
}
public function shouldBeSearchable()
{
return $this->status === 'active' && $this->stock > 0;
}
}
Indexing Data
Scout automatically indexes models when they're created or updated. You can also manually control indexing.
# Import all existing records into the search index
php artisan scout:import "App\Models\Post"
# Flush all records from the index
php artisan scout:flush "App\Models\Post"
# Delete and re-import (useful for schema changes)
php artisan scout:flush "App\Models\Post"
php artisan scout:import "App\Models\Post"
// Manually index a model
$post = Post::find(1);
$post->searchable();
// Index multiple models
$posts = Post::where('is_published', true)->get();
$posts->searchable();
// Remove from index
$post->unsearchable();
// Pause indexing during batch operations
Post::withoutSyncingToSearch(function () {
Post::where('spam', true)->delete();
});
// Conditionally pause syncing
if ($this->app->environment('testing')) {
Model::disableSearchSyncing();
}
Tip: Use withoutSyncingToSearch when performing bulk operations to avoid making hundreds of API calls to your search service. Re-import after bulk operations are complete.
Basic Search Queries
Perform searches using Scout's intuitive query builder.
// Simple search
$posts = Post::search('Laravel tutorial')->get();
// Paginated search
$posts = Post::search('Vue.js')->paginate(15);
// Search with custom page size
$posts = Post::search('PHP')->paginate(20);
// Get specific page
$posts = Post::search('JavaScript')->paginate(15, 'page', 2);
// Search and filter by attributes
$posts = Post::search('Laravel')
->where('is_published', true)
->where('category_id', 5)
->get();
// Search with query callback for additional constraints
$posts = Post::search('tutorial')->query(function ($builder) {
$builder->where('views', '>', 1000);
})->get();
// Get total results count
$count = Post::search('Laravel')->count();
// Search with custom order
$posts = Post::search('programming')
->orderBy('published_at', 'desc')
->get();
// Search in controller
public function search(Request $request)
{
$query = $request->input('q');
$posts = Post::search($query)
->where('is_published', 1)
->query(fn($builder) => $builder->with(['author', 'category']))
->paginate(15);
return view('posts.search', [
'posts' => $posts,
'query' => $query,
]);
}
Advanced Search with Filtering
Implement complex search scenarios with multiple filters and faceted search.
// Product search with multiple filters
public function searchProducts(Request $request)
{
$search = Product::search($request->input('query', ''));
// Filter by category
if ($request->has('category')) {
$search->where('category_id', $request->category);
}
// Filter by price range
if ($request->has('min_price')) {
$search->where('price', '>=', $request->min_price);
}
if ($request->has('max_price')) {
$search->where('price', '<=', $request->max_price);
}
// Filter by brand
if ($request->has('brands')) {
$search->whereIn('brand', $request->brands);
}
// Filter by availability
if ($request->boolean('in_stock')) {
$search->where('is_available', true);
}
// Sort options
$sortBy = $request->input('sort', 'relevance');
switch ($sortBy) {
case 'price_low':
$search->orderBy('price', 'asc');
break;
case 'price_high':
$search->orderBy('price', 'desc');
break;
case 'rating':
$search->orderBy('rating', 'desc');
break;
// 'relevance' is default Scout ordering
}
return $search->paginate(24);
}
// Advanced search with facets (Algolia)
use Algolia\AlgoliaSearch\SearchIndex;
public function searchWithFacets($query)
{
$index = app(SearchIndex::class, ['indexName' => 'products_index']);
$results = $index->search($query, [
'facets' => ['category', 'brand', 'color'],
'filters' => 'price > 10 AND price < 1000',
'attributesToRetrieve' => ['name', 'price', 'image'],
'hitsPerPage' => 20,
]);
return [
'hits' => $results['hits'],
'facets' => $results['facets'],
'total' => $results['nbHits'],
];
}
Warning: The where method in Scout only supports basic equality checks. For complex filtering (range queries, OR conditions), use the query callback or the search engine's native API.
Custom Search Engines
Create custom Scout engines to integrate with any search service or implement custom logic.
namespace App\Scout\Engines;
use Laravel\Scout\Engines\Engine;
use Laravel\Scout\Builder;
class CustomSearchEngine extends Engine
{
public function update($models)
{
// Index the given models
$models->each(function ($model) {
// Your custom indexing logic
$data = $model->toSearchableArray();
// Send to your search service
Http::post('https://your-search-api.com/index', [
'id' => $model->getScoutKey(),
'data' => $data,
]);
});
}
public function delete($models)
{
// Remove the given models from the index
$models->each(function ($model) {
Http::delete("https://your-search-api.com/index/{$model->getScoutKey()}");
});
}
public function search(Builder $builder)
{
return $this->performSearch($builder, [
'filters' => $this->filters($builder),
'limit' => $builder->limit,
]);
}
public function paginate(Builder $builder, $perPage, $page)
{
return $this->performSearch($builder, [
'filters' => $this->filters($builder),
'limit' => $perPage,
'offset' => ($page - 1) * $perPage,
]);
}
protected function performSearch(Builder $builder, array $options = [])
{
$response = Http::post('https://your-search-api.com/search', [
'query' => $builder->query,
'index' => $builder->index ?: $builder->model->searchableAs(),
'options' => $options,
]);
return $response->json();
}
public function mapIds($results)
{
return collect($results['hits'])->pluck('id')->values();
}
public function map(Builder $builder, $results, $model)
{
if (count($results['hits']) === 0) {
return $model->newCollection();
}
$objectIds = $this->mapIds($results);
$models = $model->getScoutModelsByIds(
$builder, $objectIds
)->keyBy(function ($model) {
return $model->getScoutKey();
});
return collect($results['hits'])->map(function ($hit) use ($models) {
$key = $hit['id'];
return $models[$key] ?? null;
})->filter()->values();
}
public function getTotalCount($results)
{
return $results['total'] ?? 0;
}
public function flush($model)
{
Http::delete("https://your-search-api.com/index/{$model->searchableAs()}");
}
protected function filters(Builder $builder)
{
return collect($builder->wheres)->map(function ($value, $key) {
return "{$key}={$value}";
})->values()->all();
}
}
// Register the engine in a service provider
use App\Scout\Engines\CustomSearchEngine;
public function boot()
{
resolve(EngineManager::class)->extend('custom', function () {
return new CustomSearchEngine();
});
}
// Use in config/scout.php
'driver' => env('SCOUT_DRIVER', 'custom'),
Search Result Highlighting
Implement search term highlighting to improve user experience.
// In your model
use Illuminate\Support\Str;
public function getHighlightedTitleAttribute()
{
$query = request()->input('query', '');
if (empty($query)) {
return $this->title;
}
return $this->highlightText($this->title, $query);
}
public function getHighlightedContentAttribute()
{
$query = request()->input('query', '');
if (empty($query)) {
return Str::limit($this->content, 200);
}
$highlighted = $this->highlightText($this->content, $query);
return Str::limit($highlighted, 200);
}
protected function highlightText($text, $query)
{
$terms = explode(' ', $query);
foreach ($terms as $term) {
if (strlen($term) < 3) continue;
$pattern = '/(' . preg_quote($term, '/') . ')/i';
$text = preg_replace(
$pattern,
'<mark class="highlight">$1</mark>',
$text
);
}
return $text;
}
// In your view
<div class="search-result">
<h3>{!! $post->highlighted_title !!}</h3>
<p>{!! $post->highlighted_content !!}</p>
</div>
// CSS for highlighting
<style>
.highlight {
background-color: yellow;
font-weight: bold;
padding: 2px 4px;
}
</style>
Exercise 1: Create a multi-model search feature that searches across Posts, Products, and Users simultaneously. Display results grouped by model type with a tab interface to switch between result types.
Exercise 2: Build an advanced product search with faceted filtering. Include filters for category, price range, brand, rating, and availability. Display the number of results for each filter option and allow users to combine multiple filters.
Exercise 3: Implement a search analytics system that tracks popular search queries, queries with no results, and user click-through rates on search results. Create an admin dashboard to view these analytics and identify content gaps.