Advanced Laravel

Full-Text Search with Scout

18 min Lesson 13 of 40

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.