aternovtsii/search-bundle
Laravel search bundle that adds a reusable, configurable search layer for your app. Provides easy integration for searching across multiple models/resources with a simple API, sensible defaults, and room to extend matching, filtering, and result formatting.
Install the Package Add the package via Composer in a Laravel 10+ project (Symfony 8 compatible):
composer require aternovtsii/search-bundle
Note: If using Laravel <10, pin Symfony dependencies to avoid conflicts (e.g., symfony/dependency-injection:^7.4).
Configure the Bundle Publish the bundle’s configuration:
php artisan vendor:publish --provider="ATernovtsii\SearchBundle\SearchBundle" --tag="config"
Edit config/search.php to define:
enabled: true (default).client: elasticsearch or opensearch.index_name: Your Elastic/OpenSearch index name.hosts: Array of search engine URLs (e.g., ['http://localhost:9200']).default_locale: en (or your app’s default).Enable Search for an Entity
Annotate a Doctrine entity (e.g., Post) with searchable fields:
use ATernovtsii\SearchBundle\Annotation\Searchable;
use ATernovtsii\SearchBundle\Annotation\Field;
/**
* @Searchable(index="posts")
*/
class Post
{
/**
* @Field(type="text")
*/
private string $title;
/**
* @Field(type="date")
*/
private \DateTimeInterface $publishedAt;
/**
* @Field(type="relation", targetEntity="User", field="username")
*/
private User $author;
}
First Search Query
Inject the SearchService into a controller or command:
use ATernovtsii\SearchBundle\Service\SearchService;
class PostSearchController extends Controller
{
public function __construct(private SearchService $searchService) {}
public function index()
{
$results = $this->searchService->search('Post', 'query=laravel');
return view('posts.index', compact('results'));
}
}
Reindex Data Run the reindex command to populate your search engine:
php artisan search:reindex Post
Optional: Enable event-based reindexing in config/search.php:
'auto_reindex' => true,
Pattern: Use dependency injection for SearchService to decouple search logic from business logic.
class ProductController extends Controller
{
public function search(Request $request, SearchService $search)
{
$query = $request->input('q');
$results = $search->search('Product', [
'query' => $query,
'sort' => ['price' => 'asc'],
'limit' => 20,
]);
return response()->json($results);
}
}
Pattern: Extend field types for custom logic (e.g., nested objects, computed fields).
use ATernovtsii\SearchBundle\Annotation\Field;
use ATernovtsii\SearchBundle\Field\FieldInterface;
class CustomField implements FieldInterface
{
public function getType(): string { return 'custom'; }
public function getValue($entity): mixed
{
return $entity->getComputedValue();
}
}
/**
* @Field(type="custom", customClass="App\Search\CustomField")
*/
private string $computedField;
Pattern: Leverage Laravel’s pagination helpers with search results.
use Illuminate\Pagination\LengthAwarePaginator;
$results = $this->searchService->search('Article', [
'query' => 'tutorial',
'from' => $request->input('page') * 10,
'size' => 10,
]);
$paginated = new LengthAwarePaginator(
$results['hits'],
$results['total'],
10,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
Pattern: Trigger reindexing on entity updates via Doctrine events.
// config/search.php
'auto_reindex' => [
'enabled' => true,
'events' => ['prePersist', 'preUpdate', 'preRemove'],
],
Manual override: Disable for specific entities:
/**
* @Searchable(autoReindex=false)
*/
class Newsletter
{
// ...
}
Pattern: Search across translated fields using the locale parameter.
$results = $search->search('Product', [
'query' => 'shirt',
'locale' => 'fr', // French translations
]);
Service Container Binding
Override the default SearchService binding in AppServiceProvider:
$this->app->bind(SearchService::class, function ($app) {
return new CustomSearchService($app->get('search.client'));
});
Artisan Command Aliases Create Laravel-style commands for search management:
php artisan search:reindex Post
php artisan search:analyze Post
Blade Directives Extend Blade to render search forms:
// app/Providers/BladeServiceProvider.php
Blade::directive('searchForm', function ($expression) {
return "<?php echo ATernovtsii\SearchBundle\Blade::searchForm($expression); ?>";
});
Usage:
{!! searchForm('Post', ['fields' => ['title', 'description']]) !!}
API Resource Integration Format search results as API resources:
use ATernovtsii\SearchBundle\Resource\SearchResource;
return SearchResource::collection($results['hits']);
Queue-Based Reindexing Offload reindexing to queues for large datasets:
// config/search.php
'queue_reindex' => true,
// In a command:
$this->searchService->queueReindex('Product');
Symfony Version Conflicts
symfony/dependency-injection, http-client, or event-dispatcher.composer.json:
"extra": {
"laravel": {
"dont-discover": ["vendor/aternovtsii/search-bundle"]
}
},
"require": {
"symfony/dependency-injection": "8.0.0",
"symfony/http-client": "8.0.0"
}
Doctrine Event Overhead
auto_reindex) triggers on every prePersist/preUpdate, which can slow down bulk operations.$this->searchService->reindex('Product', ['batch_size' => 50]);
Field Type Mismatches
FieldFloat for non-numeric values).if (!is_numeric($entity->getPrice())) {
throw new \InvalidArgumentException('Price must be numeric');
}
Index Template Conflicts
config/search.php:
'template' => [
'generate' => false,
],
Then manually define the index mapping.Translation Loading Bug
$subSelects are misconfigured (fixed in v0.1.5).default_locale is set and verify the locale parameter in queries.Max Result Window
max_result_window limit (default: 10,000).config/search.php:
'client' => [
'max_result_window' => 5000
How can I help you explore Laravel packages today?