Users expect instant, relevant search results. Laravel Scout makes full-text search trivial by abstracting Algolia and other search drivers. At ZIRA Software, Scout-powered search has transformed clunky SQL LIKE queries into lightning-fast, typo-tolerant searches that users love.
Why Laravel Scout?
Problems with SQL search:
// Slow, inflexible
$posts = Post::where('title', 'like', '%laravel%')
->orWhere('content', 'like', '%laravel%')
->get();
Issues:
- Slow on large datasets
- No relevance ranking
- No typo tolerance
- Case-sensitive
- Can't search relationships
- No highlighting
- Poor user experience
Scout advantages:
- Lightning-fast searches
- Typo tolerance
- Relevance ranking
- Faceted search
- Geolocation search
- Real-time indexing
- Multi-model search
- Highlighted results
Installation
composer require laravel/scout
Publish configuration:
php artisan vendor:publish --provider="Laravel\Scout\ScoutServiceProvider"
config/scout.php:
<?php
return [
'driver' => env('SCOUT_DRIVER', 'algolia'),
'prefix' => env('SCOUT_PREFIX', ''),
'queue' => env('SCOUT_QUEUE', false),
'chunk' => [
'searchable' => 500,
'unsearchable' => 500,
],
'soft_delete' => false,
'algolia' => [
'id' => env('ALGOLIA_APP_ID', ''),
'secret' => env('ALGOLIA_SECRET', ''),
],
];
Setup Algolia
Install Algolia SDK:
composer require algolia/algoliasearch-client-php
.env:
SCOUT_DRIVER=algolia
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SECRET=your-admin-api-key
Get credentials from algolia.com (free tier available).
Make Model Searchable
app/Post.php:
<?php namespace App;
use Laravel\Scout\Searchable;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use Searchable;
/**
* Get the indexable data array for the model.
*/
public function toSearchableArray()
{
$array = $this->toArray();
// Customize the data sent to Algolia
return [
'id' => $this->id,
'title' => $this->title,
'content' => strip_tags($this->content),
'author' => $this->author->name,
'published_at' => $this->published_at->timestamp,
'tags' => $this->tags->pluck('name')->toArray(),
];
}
/**
* Get the index name for the model.
*/
public function searchableAs()
{
return 'posts_index';
}
}
Import Existing Records
php artisan scout:import "App\Post"
This indexes all existing posts.
Automatic Indexing
Scout automatically indexes:
- When creating records
- When updating records
- When deleting records
// Automatically indexed
$post = Post::create([
'title' => 'Laravel Scout Tutorial',
'content' => 'Learn how to implement search...',
]);
// Automatically updated in index
$post->update(['title' => 'Updated Title']);
// Automatically removed from index
$post->delete();
Basic Search
use App\Post;
// Simple search
$posts = Post::search('laravel')->get();
// Paginated results
$posts = Post::search('laravel')->paginate(15);
// Get specific columns
$posts = Post::search('laravel')->get(['id', 'title']);
// Get IDs only
$ids = Post::search('laravel')->keys();
// Count results
$count = Post::search('laravel')->count();
Search Controller
app/Http/Controllers/SearchController.php:
<?php namespace App\Http\Controllers;
use App\Post;
use Illuminate\Http\Request;
class SearchController extends Controller
{
public function posts(Request $request)
{
$query = $request->input('q');
if (empty($query)) {
return view('search.posts', ['posts' => collect()]);
}
$posts = Post::search($query)
->paginate(15);
return view('search.posts', [
'posts' => $posts,
'query' => $query,
]);
}
}
routes/web.php:
Route::get('/search/posts', 'SearchController@posts');
Search Form
resources/views/layouts/app.blade.php:
<form action="{{ url('/search/posts') }}" method="GET">
<input type="text"
name="q"
placeholder="Search posts..."
value="{{ request('q') }}"
autocomplete="off">
<button type="submit">Search</button>
</form>
resources/views/search/posts.blade.php:
@extends('layouts.app')
@section('content')
<h1>Search Results for "{{ $query }}"</h1>
@if($posts->isEmpty())
<p>No results found.</p>
@else
<p>Found {{ $posts->total() }} results</p>
@foreach($posts as $post)
<article>
<h2>
<a href="{{ route('posts.show', $post->slug) }}">
{{ $post->title }}
</a>
</h2>
<p>{{ $post->excerpt }}</p>
<small>By {{ $post->author->name }} on {{ $post->published_at->format('M d, Y') }}</small>
</article>
@endforeach
{{ $posts->appends(['q' => $query])->links() }}
@endif
@endsection
Advanced Queries
Where clauses:
// Filter by author
$posts = Post::search('laravel')
->where('author_id', 1)
->get();
// Multiple filters
$posts = Post::search('laravel')
->where('published', true)
->where('featured', true)
->get();
Ordering:
// Order by attribute
$posts = Post::search('laravel')
->orderBy('published_at', 'desc')
->get();
// Multiple orderings
$posts = Post::search('laravel')
->orderBy('views', 'desc')
->orderBy('published_at', 'desc')
->get();
Limit results:
$posts = Post::search('laravel')
->take(5)
->get();
Conditional Indexing
Only index published posts:
public function shouldBeSearchable()
{
return $this->isPublished();
}
Manual Indexing Control
Disable auto-indexing:
Post::withoutSyncingToSearch(function () {
// Bulk operations without indexing
Post::where('draft', true)->delete();
});
Manually add to index:
$post->searchable();
Manually remove from index:
$post->unsearchable();
Multiple Models Search
use Illuminate\Support\Collection;
public function search(Request $request)
{
$query = $request->input('q');
$posts = Post::search($query)->take(5)->get();
$users = User::search($query)->take(5)->get();
$products = Product::search($query)->take(5)->get();
$results = collect([
'posts' => $posts,
'users' => $users,
'products' => $products,
]);
return view('search.all', compact('results', 'query'));
}
Algolia-Specific Features
Configure Index Settings
use Algolia\AlgoliaSearch\SearchClient;
public function boot()
{
$client = SearchClient::create(
config('scout.algolia.id'),
config('scout.algolia.secret')
);
$index = $client->initIndex('posts_index');
$index->setSettings([
'searchableAttributes' => [
'title',
'content',
'author',
'tags',
],
'customRanking' => [
'desc(views)',
'desc(published_at)',
],
'attributesForFaceting' => [
'filterOnly(author_id)',
'searchable(tags)',
],
'highlightPreTag' => '<mark>',
'highlightPostTag' => '</mark>',
]);
}
Faceted Search
// Search with facet filters
$posts = Post::search('laravel', function ($algolia, $query, $options) {
$options['facetFilters'] = ['tags:tutorial'];
return $algolia->search($query, $options);
})->get();
Geo Search
Model:
public function toSearchableArray()
{
return [
'id' => $this->id,
'name' => $this->name,
'_geoloc' => [
'lat' => $this->latitude,
'lng' => $this->longitude,
],
];
}
Search nearby:
$stores = Store::search('coffee', function ($algolia, $query, $options) {
$options['aroundLatLng'] = '40.71, -74.01'; // NYC
$options['aroundRadius'] = 5000; // 5km
return $algolia->search($query, $options);
})->get();
Typo Tolerance
Algolia handles typos automatically:
- "laravle" → finds "laravel"
- "pyton" → finds "python"
Synonyms
$index->saveSynonym('framework', [
'objectID' => 'framework',
'type' => 'synonym',
'synonyms' => ['framework', 'library', 'toolkit'],
]);
Now "framework" matches "library" and "toolkit".
Instant Search UI
Install Algolia JavaScript:
npm install algoliasearch instantsearch.js --save
resources/js/search.js:
import algoliasearch from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';
import { searchBox, hits, pagination } from 'instantsearch.js/es/widgets';
const searchClient = algoliasearch(
'YOUR_APP_ID',
'YOUR_SEARCH_API_KEY' // Use search-only key!
);
const search = instantsearch({
indexName: 'posts_index',
searchClient,
});
search.addWidgets([
searchBox({
container: '#searchbox',
placeholder: 'Search posts...',
}),
hits({
container: '#hits',
templates: {
item: `
<article>
<h3>
<a href="/posts/{{slug}}">
{{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}}
</a>
</h3>
<p>{{#helpers.snippet}}{ "attribute": "content" }{{/helpers.snippet}}</p>
<small>By {{author}} on {{published_at}}</small>
</article>
`,
},
}),
pagination({
container: '#pagination',
}),
]);
search.start();
HTML:
<div id="searchbox"></div>
<div id="hits"></div>
<div id="pagination"></div>
This creates instant-as-you-type search with no server requests!
Alternative Drivers
TNT Search (Database)
Install:
composer require teamtnt/laravel-scout-tntsearch-driver
config/scout.php:
'driver' => env('SCOUT_DRIVER', 'tntsearch'),
'tntsearch' => [
'storage' => storage_path('tntsearch'),
],
Pros:
- Free
- No external service
- Works offline
Cons:
- Slower than Algolia
- Limited features
- Resource-intensive
Elasticsearch
Install:
composer require matchish/laravel-scout-elasticsearch
config/scout.php:
'driver' => env('SCOUT_DRIVER', 'elasticsearch'),
'elasticsearch' => [
'hosts' => [
env('ELASTICSEARCH_HOST', 'localhost:9200'),
],
],
Pros:
- Powerful
- Self-hosted
- Advanced queries
Cons:
- Complex setup
- Resource-heavy
- Requires maintenance
Testing
tests/Feature/SearchTest.php:
<?php
use App\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
class SearchTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
// Use fake scout driver
config(['scout.driver' => 'collection']);
}
/** @test */
public function it_searches_posts()
{
factory(Post::class)->create(['title' => 'Laravel Scout Tutorial']);
factory(Post::class)->create(['title' => 'Vue.js Guide']);
$results = Post::search('scout')->get();
$this->assertCount(1, $results);
$this->assertEquals('Laravel Scout Tutorial', $results->first()->title);
}
/** @test */
public function it_returns_paginated_results()
{
factory(Post::class, 20)->create(['title' => 'Laravel Post']);
$response = $this->get('/search/posts?q=laravel');
$response->assertStatus(200);
$response->assertViewHas('posts');
}
}
Performance Tips
- Queue indexing for faster responses:
// config/scout.php
'queue' => true,
- Index only necessary fields:
public function toSearchableArray()
{
return [
'title' => $this->title,
'excerpt' => $this->excerpt,
// Don't index full content if not needed
];
}
-
Use search-only API key in frontend: Never expose admin key in JavaScript!
-
Implement caching for popular queries:
$posts = Cache::remember("search.{$query}", 60, function () use ($query) {
return Post::search($query)->get();
});
Monitoring
Track search analytics in Algolia dashboard:
- Top searches
- No-result searches
- Click-through rates
- Conversion rates
Use insights to improve:
- Add synonyms for common misspellings
- Adjust ranking
- Identify missing content
Best Practices
- Index selectively - Don't index everything
- Update index strategically - Use queues for bulk operations
- Implement autocomplete - Instant Search makes this easy
- Handle no results - Suggest alternatives or relaxed searches
- Track analytics - Learn what users search for
- Customize relevance - Adjust ranking for your use case
- Use appropriate driver - Algolia for speed, TNT for simplicity
Conclusion
Laravel Scout transforms search from complex SQL queries to simple, powerful full-text search. Whether using Algolia's lightning-fast cloud search or self-hosted solutions, Scout provides a consistent, elegant API. At ZIRA Software, Scout-powered search has dramatically improved user experience across client applications.
Start with basic search, add instant-as-you-type UI, then leverage advanced features like faceting and geo-search as needed.
Need powerful search for your Laravel application? Contact ZIRA Software to discuss implementing Scout with Algolia, Elasticsearch, or custom search solutions.