ElasticLens
The convenience of Scout. The full power of Elasticsearch. Complete control of your models.
// Starts just like Scout - add a trait, search your models.User::search('loves espressos');// Except you're not limited to basic text search.User::viaIndex() ->searchPhrase('ice bathing') ->where('status', 'active') ->whereNestedObject('logs', function ($query) { $query->where('logs.country', 'Norway'); }) ->orderByDesc('created_at') ->paginate(10);// Geo queries. Fuzzy matching. Regex. Aggregations. On your Laravel models.User::viaIndex() ->filterGeoPoint('home.location', '5km', [40.7128, -74.0060]) ->orderByGeo('home.location', [40.7128, -74.0060]) ->get();Scout gives you a search box behind a black box. ElasticLens gives you a search engine you can open up.
Every index is a real Eloquent model you can query, inspect, and control directly. You define the field mappings. You define the Elasticsearch schema. You see exactly what’s indexed and how. No magic, no guessing, no driver abstractions between you and your data.
The Query Boundary
Section titled “The Query Boundary”viaIndex() is the gateway. Everything you chain after it is running against Elasticsearch’s full query builder. Results come back as your Laravel models.
User::viaIndex() // Gateway: enter Elasticsearch scope ->searchTerm('david') // ← full-text search ->where('state', 'active') // ← familiar Eloquent syntax ->whereNestedObject('logs', // ← search across embedded relations fn ($q) => $q->where('logs.country', 'Norway') ) ->orderByDesc('created_at') // ← ordering, pagination, aggregations ->paginate(10); // Returns: Paginator of User modelsEverything between viaIndex() and get()/paginate() is the Elasticsearch query builder - the same one from the Laravel-Elasticsearch package. Full-text search, geo queries, fuzzy matching, regex, nested objects, aggregations, highlighting - all of it.
Embed Relationships Into Your Index
Section titled “Embed Relationships Into Your Index”This is where it gets interesting. Flatten your relational data into Elasticsearch and search across it as nested objects.
Define what gets embedded in your index model:
class IndexedUser extends IndexModel{ protected $baseModel = User::class;
public function fieldMap(): IndexBuilder { return IndexBuilder::map(User::class, function (IndexField $field) { $field->text('name'); $field->text('email'); $field->embedsMany('profiles', Profile::class)->embedMap(function ($field) { $field->text('bio'); $field->array('tags'); }); $field->embedsBelongTo('company', Company::class)->embedMap(function ($field) { $field->text('name'); $field->text('industry'); }); $field->embedsMany('logs', UserLog::class, null, null, function ($query) { $query->orderBy('created_at', 'desc')->limit(10); })->embedMap(function ($field) { $field->text('action'); }); }); }}Then search across all of it:
User::viaIndex()->whereNestedObject('profiles', function ($query) { $query->where('profiles.bio', 'like', '%elasticsearch%');})->get();The related models are observed too. Update a Profile and the parent IndexedUser rebuilds automatically.
Features
Section titled “Features”- Auto-sync - Every create, update, and delete on your base model is reflected in the index automatically
- Embedded relations - Flatten hasMany, hasOne, belongsTo into searchable nested objects
- Full-text search - Term, phrase, fuzzy, regex, geo, nested object queries - all returning your Laravel models
- Index migrations - Define your Elasticsearch mapping with a Blueprint, just like database migrations
- Conditional indexing - Control which records get indexed with
excludeIndex() - Soft delete support - Configure whether soft-deleted records keep their index
- CLI tools - Status, health, build, migrate, and make commands
Requirements
Section titled “Requirements”| Version | |
|---|---|
| PHP | 8.2+ |
| Laravel | 10 / 11 / 12 |
| Elasticsearch | 8.x |