Multiple models in one index
An aggregator is a clean way to implement site-wide search among multiple models. In other words, it allows you to have multiple models in the one index. Behind the scenes, the aggregator listens for updates in your models to automatically keep everything in sync.
Creating an aggregator
To create a new aggregator, use the scout:make-aggregator
Artisan command. This command creates a new aggregator class in the app/Search
directory:
1
php artisan scout:make-aggregator News
Don’t worry if this directory doesn’t exist in your application. The command automatically creates it if necessary.
After generating your aggregator, you should fill in the $models
property of the class to identify the models to aggregate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Search;
use Algolia\ScoutExtended\Searchable\Aggregator;
class News extends Aggregator
{
/**
* The names of the models that should be aggregated.
*
* @var string[]
*/
protected $models = [
\App\Event::class,
\App\Article::class,
];
}
To register an Aggregator, use the bootSearchable
method on the aggregator you wish to register.
For this, you should use the boot
method of one of your service providers.
The following example registers the aggregator in the App\AppServiceProvider
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace App\Providers;
use App\Search\News;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
News::bootSearchable();
}
}
When you use a model in multiple aggregators, by default Scout Extended registers one observer per model per aggregator.
This can result in twice the amount of indexing jobs when you update a model. To ensure a single observer gets registered you have to boot all your aggregators using the Aggregator::bootSearchables
method, passing your aggregator classes as argument:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
namespace App\Providers;
use App\Search\Article;
use App\Search\PremiumArticle;
use Algolia\ScoutExtended\Searchable\Aggregator;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Aggregator::bootSearchables([
Article::class,
PremiumArticle::class,
]);
}
}
Conditionally sync an aggregator
For Searchable
models, you can use the shouldBeSearchable
method to conditionally change which results you want to index.
For aggregators, you can define a shouldBeSearchable
method that calls the shouldBeSearchable
method of the aggregated model.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
namespace App\Search;
use Algolia\ScoutExtended\Searchable\Aggregator;
use Laravel\Scout\Searchable;
class News extends Aggregator
{
/**
* The names of the models that should be aggregated.
*
* @var string[]
*/
protected $models = [
\App\Event::class,
\App\Article::class,
];
public function shouldBeSearchable()
{
// Check if the class uses the Searchable trait before calling shouldBeSearchable
if (array_key_exists(Searchable::class, class_uses($this->model))) {
return $this->model->shouldBeSearchable();
}
}
}
You can change the logic of your aggregator’s shouldBeSearchable
method with any condition that doesn’t depend on its grouped models.
Searching an aggregator
An aggregator is a standard Searchable
class, and, as usual, you may begin searching models on the aggregator using the search
method:
1
2
3
4
$models = App\Search\News::search('Star Trek')->get();
echo get_class($models[0]); // "App\Article"
echo get_class($models[1]); // "App\Comment"
Be careful, the $models
array may contain different types of models instances.
If you want to get the raw results from Algolia, use the raw
method.
Be aware that each result may contain a different structure when using this method:
1
$results = App\Search\News::search('Star Trek')->raw();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"hits":[
{
"id":1,
"title": "Article title",
"slug": "article-title",
"content": "Article content",
"objectID":"App\\Article::1",
},
{
"id": 1,
"content": "Comment content",
"objectID": "App\\Comment::1",
},
]
}
To ensure that each result have a similar structure, you may need to implement the method toSearchableArray
on the
each Searchable
classes or directly on the aggregator class.
Eager loading relationships
Sometimes you may wish to eager load a relationship of an aggregated model.
To eager load relationships, you need to use the property $relations
of your aggregator class. For example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class News extends Aggregator
{
/**
* The names of the models that should be aggregated.
*
* @var string[]
*/
protected $models = [
\App\Event::class,
\App\Article::class,
];
/**
* Map of model relations to load.
*
* @var string[]
*/
protected $relations = [
\App\Event::class => ['user'],
\App\Article::class => ['photo', 'user'],
];
}
Disabling syncing per model
By default, the aggregator creates one aggregated index (in this case, “news”). But, Laravel Scout also still indexes the Article
and Event
models in their own “articles” and “events” indices.
To stop this behavior, you must tell your Laravel app to turn off indexing for these models globally. In your AppServiceProvider
, add the following:
1
2
Laravel\Scout\ModelObserver::disableSyncingFor(Article::class);
Laravel\Scout\ModelObserver::disableSyncingFor(Event::class);
You can’t use the shouldBeSearchable
method in this case because the method doesn’t know about the destination index.
The scout:import command
doesn’t take calls to disableSearchSyncing
into account. Disabling search syncing only applies to updates to models made by the application, and not to bulk import operations through the scout:import
command.