jeroen-g/explorer
Laravel-friendly wrapper for Explorer, a lightweight full-text search engine. Index and search your Eloquent models with simple configuration, fast queries, and flexible ranking/filters. Ideal for adding on-site search without running Elasticsearch or Algolia.
Installation:
composer require jeroen-g/explorer
php artisan vendor:publish --tag=explorer.config
Update config/explorer.php with your Elasticsearch connection details.
Model Integration:
Add Searchable trait and implement Explored interface (or use config-based mapping):
use Laravel\Scout\Searchable;
use JeroenG\Explorer\Application\Explored;
class Post extends Model implements Explored {
use Searchable;
public function mappableAs(): array {
return ['title' => 'text', 'published' => 'boolean'];
}
}
First Search:
$results = Post::search('query')->get();
search() with a query string.take(20) or use Laravel's paginate().orderBy('field', 'direction') (e.g., orderBy('published_at', 'desc')).Indexing:
// Auto-index on save (Scout default)
Post::create(['title' => 'Test']);
// Manual update
Post::all()->each->updateSearchData();
Query Composition:
$results = Post::search('query')
->must(new Matching('title', 'query'))
->filter(new Term('published', true))
->get();
Aggregations:
$results = Post::search()
->aggregation('categories', new TermsAggregation('category'))
->raw();
toSearchableArray() for dynamic fields.Nested queries and dot notation for nested fields (e.g., author.name).SyntaxInterface for Elasticsearch-specific queries (e.g., GeoDistance).public function search(Request $request) {
$query = Post::search($request->q)
->filter(new Term('status', 'published'))
->orderBy('created_at', 'desc')
->take(50);
if ($request->has('category')) {
$query->must(new Matching('category', $request->category));
}
return $query->get();
}
Mapping Conflicts:
mappableAs() or config to avoid runtime errors.id as keyword).Nested Queries:
author.name) requires proper nested mapping in mappableAs().nested in the mapping.Pagination Limits:
size: 10. Use take() to increase limits (e.g., take(100)).take() values may impact performance.Alias Mismatches:
read, write) require Aliased interface or config enablement.Aliased or add model to indexes in config.->raw() to inspect Elasticsearch payloads.FakeResponse (see testing.md).
$this->instance(ElasticClientFactory::class, ElasticClientFactory::fake($fakeResponse));
->field('title', 'id') to reduce payload size (though Elasticsearch still processes full docs).prune_old_aliases to avoid bloated history indices.Custom Syntax:
// Example: Add a `Range` query
class Range implements SyntaxInterface {
public function build(): array {
return ['range' => ['price' => ['gte' => 100]]];
}
}
Query Properties:
// Example: Add `timeout` to queries
class Timeout implements QueryProperty {
public function __construct(private int $ms) {}
public function build(): array { return ['timeout' => $this->ms]; }
}
Testing:
FakeResponse for unit tests (mock Elasticsearch responses).tests/Support/Elastic/Responses/.searchableAs() if needed.mappableAs() and config-based mapping for the same model.published/active flags are mapped as boolean to avoid scoring issues.| Error | Cause | Solution |
|---|---|---|
MappingException |
Missing/invalid field mapping | Define all fields in mappableAs() or config. |
No hits |
Query syntax mismatch | Use ->raw() to debug the query payload. |
AliasNotFoundException |
Alias not configured | Implement Aliased or update config. |
| Slow queries | Unoptimized aggregations | Limit aggregation fields or use size: 0. |
How can I help you explore Laravel packages today?