spatie/laravel-site-search
Crawl and index your Laravel site for fast full-text search—like a private Google. Highly customizable crawling and indexing, with concurrent requests. Uses SQLite FTS5 by default (no external services), or Meilisearch for advanced features.
Installation
composer require spatie/laravel-site-search
php artisan vendor:publish --provider="Spatie\SiteSearch\SiteSearchServiceProvider"
php artisan migrate
Create an Index
php artisan site-search:create-index
main) and your site's base URL (e.g., https://example.com).Crawl and Index
php artisan site-search:crawl
Search
use Spatie\SiteSearch\Search;
$results = Search::onIndex('main')
->query('laravel')
->get();
DefaultSearchProfile and DefaultIndexer for immediate results.@foreach($results->hits as $hit)
<div>
<a href="{{ $hit->url }}">{{ $hit->title() }}</a>
<p>{!! $hit->highlightedSnippet() !!}</p>
</div>
@endforeach
Define Indexes
site-search:create-index to define multiple indexes (e.g., blog, products).SiteSearchConfig model (stored in site_search_configs table).Customize Crawling
DefaultSearchProfile to filter URLs or modify crawler behavior:
namespace App\Profiles;
use Spatie\SiteSearch\Profiles\DefaultSearchProfile;
class CustomProfile extends DefaultSearchProfile {
public function shouldCrawl(string $url): bool {
return str_contains($url, 'blog') && parent::shouldCrawl($url);
}
}
config/site-search.php:
'profiles' => [
'blog' => \App\Profiles\CustomProfile::class,
],
Extract Custom Content
Indexer for specialized content (e.g., e-commerce products):
namespace App\Indexers;
use Spatie\SiteSearch\Indexers\Indexer;
use Carbon\Carbon;
class ProductIndexer implements Indexer {
public function __construct(public $response) {}
public function pageTitle(): ?string {
return $this->response->data['title'] ?? null;
}
public function entries(): array {
return [
['text' => $this->response->data['description']],
['text' => $this->response->data['features']],
];
}
public function dateModified(): ?Carbon {
return Carbon::parse($this->response->data['updated_at']);
}
}
public function useIndexer(string $url, CrawlResponse $response): ?Indexer {
return new ProductIndexer($response);
}
Search with Filters
Search facade to filter results:
$results = Search::onIndex('products')
->query('laptop')
->filter('price', '<=', 1000)
->get();
Automate Crawling
app/Console/Kernel.php):
protected function schedule(Schedule $schedule) {
$schedule->command('site-search:crawl')->daily();
}
public function search(Request $request) {
$results = Search::onIndex('main')
->query($request->input('q'))
->get();
return response()->json($results);
}
use Spatie\SiteSearch\SiteSearchConfig;
$config = SiteSearchConfig::create([
'name' => 'dynamic-index',
'url' => 'https://dynamic-site.com',
'profile_class' => \App\Profiles\CustomProfile::class,
]);
config/site-search.php:
'drivers' => [
'meilisearch' => [
'host' => env('MEILISEARCH_HOST', 'http://localhost:7700'),
],
],
Crawl Limits
max_pages, max_depth) may exclude critical pages.config/site-search.php or override in configureCrawler():
public function configureCrawler(Crawler $crawler) {
$crawler->setMaxPages(1000);
$crawler->setMaxDepth(5);
}
Dynamic Content
Rate Limiting
429 Too Many Requests.configureCrawler():
$crawler->setDelayBetweenRequests(1000); // 1 second
Database Driver Quirks
UNICODE and TOKENIZE configurations for non-English content.FULLTEXT with +, -, ~) requires careful query construction.tsvector weights must be explicitly set for ranking.Indexer Conflicts
ArrayDriver first:
'default_driver' => \Spatie\SiteSearch\Drivers\ArrayDriver::class,
ArrayDriver Logging
'default_driver' => \Spatie\SiteSearch\Drivers\ArrayDriver::class,
tail -f storage/logs/laravel.log
Synchronous Crawls
php artisan site-search:crawl --sync
Failed URLs
php artisan site-search:list --failed
php artisan site-search:retry --url="https://example.com/page"
Custom Drivers
Spatie\SiteSearch\Drivers\Driver for alternative storage (e.g., Elasticsearch):
namespace App\Drivers;
use Spatie\SiteSearch\Drivers\Driver;
class ElasticsearchDriver implements Driver {
// Implement index(), search(), etc.
}
Pre/Post-Crawl Hooks
// In EventServiceProvider
protected $listen = [
'site-search.crawling' => [\App\Listeners\LogCrawlStart::class],
'site-search.crawled' => [\App\Listeners\NotifyCrawlEnd::class],
];
Highlighting Customization
namespace App\Drivers;
use Spatie\SiteSearch\Drivers\DatabaseDriver;
class CustomDatabaseDriver extends DatabaseDriver {
protected function generateHighlightedSnippet(string $text, string $query): string {
// Custom logic
}
}
Robots.txt Overrides
robots.txt checks in a profile:
public function configureCrawler(Crawler $crawler) {
$crawler->ignoreRobotsTxt();
}
Batch Crawling
--batch-size to control queue load:
php artisan site-search:crawl --batch-size=50
Selective Indexing
shouldIndex():
public function shouldIndex(string $url, CrawlResponse $response): bool {
return !str_contains($url, 'admin') && parent::shouldIndex($url, $response);
}
Database Optimization
FTS5 is configured with `How can I help you explore Laravel packages today?