spatie/searchindex
Opinionated Laravel package to index and search objects via a unified API. Supports Elasticsearch and Algolia, with simple upsert and query methods for any model implementing the Searchable interface.
Installation
composer require spatie/searchindex
Publish the config file (if needed):
php artisan vendor:publish --provider="Spatie\SearchIndex\SearchIndexServiceProvider"
Configure the Driver
Edit config/searchindex.php to specify either elasticsearch or algolia as the default driver. Configure credentials and connection details (e.g., Elasticsearch host, Algolia app ID/secret).
Implement the Searchable Interface
For a model (e.g., Product), implement the required methods:
use Spatie\SearchIndex\Searchable;
class Product implements Searchable
{
public function getSearchableData()
{
return [
'title' => $this->title,
'description' => $this->description,
'price' => $this->price,
'tags' => $this->tags,
];
}
public function getSearchableId()
{
return $this->id;
}
}
First Indexing
Use the SearchIndex facade to index a model:
use Spatie\SearchIndex\Facades\SearchIndex;
$product = Product::find(1);
SearchIndex::upsertToIndex($product);
First Search Retrieve results from the search index:
$results = SearchIndex::getResults('laptop');
// $results is a collection of indexed models
Indexing Models
SearchIndex::upsertToIndex($model) for single models or loop through collections:
Product::all()->each(fn ($product) => SearchIndex::upsertToIndex($product));
IndexProductsJob) to avoid timeouts:
dispatch(new IndexProductsJob($products));
class IndexProductsJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable;
public function handle()
{
foreach ($this->products as $product) {
SearchIndex::upsertToIndex($product);
}
}
}
Searching
$results = SearchIndex::getResults('query', ['limit' => 10]);
$results = SearchIndex::getResults('query', [
'filters' => ['price' => ['min' => 100]],
'limit' => 5,
]);
getResultsWithFacets for Algolia or custom Elasticsearch aggregations.Deleting from Index
SearchIndex::deleteFromIndex($product);
SearchIndex::clearIndex();
Customizing Search Behavior
getSearchableData() dynamically:
public function getSearchableData()
{
return array_merge(parent::getSearchableData(), [
'is_featured' => $this->isFeatured,
]);
}
$client = SearchIndex::getClient();
$response = $client->search([...]); // Algolia/Elasticsearch raw query
Event-Based Indexing
Listen to model events (e.g., saved, deleted) to auto-index:
Product::saved(function ($product) {
SearchIndex::upsertToIndex($product);
});
Soft Deletes
Handle soft-deleted models by checking deleted_at in getSearchableData():
public function getSearchableData()
{
return $this->deleted_at ? [] : [...];
}
Testing
Use SearchIndex::fake() in tests to mock the search index:
public function test_search()
{
SearchIndex::fake();
SearchIndex::shouldReceive('getResults')
->once()
->andReturn([$fakeProduct]);
// Test your search logic
}
Multi-Tenant Indexes
Scope the index by tenant in getSearchableId():
public function getSearchableId()
{
return "tenant_{$this->tenant->id}_product_{$this->id}";
}
Outdated Package
scout or laravel-elasticsearch).Elasticsearch Version Mismatch
elasticsearch.php config for deprecated settings (e.g., type → doc_type).Algolia Rate Limits
Data Serialization
getSearchableData() or use toArray():
public function getSearchableData()
{
return $this->toArray();
}
Case Sensitivity
typoTolerance:
$results = SearchIndex::getResults('query', [
'typoTolerance' => 'min',
]);
Check Raw Queries Enable logging for the underlying client:
// config/searchindex.php
'log_queries' => env('SEARCH_INDEX_LOG_QUERIES', false),
Or inspect the client directly:
$client = SearchIndex::getClient();
$client->getLogger()->setLevel(\Monolog\Logger::DEBUG);
Validate Indexed Data Use the driver’s API to inspect the index:
curl -XGET 'http://localhost:9200/your_index/_search?pretty'
Handle Missing Fields
If a field is missing during search, use null_as_default in filters:
$results = SearchIndex::getResults('query', [
'filters' => ['category' => ['exists' => true]],
]);
Custom Drivers Extend the package by creating a custom driver (e.g., for OpenSearch or Meilisearch):
class CustomDriver implements DriverContract
{
public function upsert($model)
{
// Custom logic
}
public function search($query, array $options)
{
// Custom logic
}
}
Register it in SearchIndexServiceProvider.
Modify Search Results
Override the SearchIndex facade to transform results:
class CustomSearchIndex extends Facade
{
protected static function getFacadeAccessor()
{
return 'searchIndex';
}
public static function getResults($query, array $options = [])
{
$results = parent::getResults($query, $options);
return $results->map(fn ($model) => $model->load('relationship'));
}
}
Add Synonyms or Stop Words Configure the driver to include synonyms (Elasticsearch) or custom stop words:
// config/searchindex.php (Elasticsearch)
'elasticsearch' => [
'settings' => [
'analysis' => [
'filter' => [
'english_stop' => ['type' => 'stop', 'stopwords' => '_english_'],
],
'analyzer' => [
'custom_analyzer' => [
'tokenizer' => 'standard',
'filter' => ['english_stop', 'lowercase'],
],
],
],
],
];
Batch Processing For large indices, implement chunked processing:
Product::chunk(100, function ($products) {
foreach
How can I help you explore Laravel packages today?