Integrations / Frameworks / Laravel / Multiple Models in One Index

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:

$
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();
    }
}

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
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
17
18
19
{
   "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.

Did you find this page helpful?