Integrations / Frameworks / Symfony / Advanced
Feb. 11, 2019

Aggregators - multiple entities types in the one index

An Aggregator is a clean way to implement site-wide search amongst multiple entities. In other words, it allows you to have multiple entities types in the one index.

Defining Aggregators

To create a new aggregator, add a new class to your entities directory, App\Entity in this example, with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Algolia\SearchBundle\Entity\Aggregator;

/**
 * @ORM\Entity
 */
class News extends Aggregator
{
    /**
     * Returns the entities class names that should be aggregated.
     *
     * @return string[]
     */
    public static function getEntities()
    {
        return [
            Post::class,
            Comment::class,
        ];
    }
}

Remember, the method getEntities should return the entities classes names that should be aggregated.

Finally, add the new aggregator class name into the algolia configuration file algolia_search.yml:

1
2
3
- indices:
    - name: news
      class: App\Entity\News

Searching

An aggregator is a standard entity class, and, as usual, you may begin searching entities on the aggregator using the search on your Index Manager service:

1
2
3
4
5
6
7
$indexManager->index($post, $objectManager);
$indexManager->index($comment, $objectManager);

$results = $indexManager->search('query', News::class, $objectManager);

// $results[0] contains a \App\Entity\Post.
// $results[1] contain a \App\Entity\Comment.

Be careful, the $result array may contain different types of entities instances.

If you want to get the raw results from Algolia, use the rawSearch method. However, this time, each result may contain a different structure:

1
$results = $indexManager->rawSearch('', News::class);
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\\Entity\\Article::1",
            ...
        },
        {
            "id": 1,
            "content": "Comment content",
            "objectID": "App\\Entity\\Comment::1",
            ...
        },
        ...
    ]
}

To ensure that each result has a similar structure, you may need to implement the method normalize on each entity or override it on the aggregator class.

Indexing conditionally

Conditional indexing on Aggregators works just like a normal entity, using the index_if key. You may want to use $this->entity to have access to the current entity being aggregated. Here is an example using the method approach:

1
2
3
4
- indices:
    - name: news
      class: App\Entity\News
      index_if: isPublished
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * @ORM\Entity
 */
class News extends Aggregator
{
    // ...

    public function isPublished()
    {
        // Index only published articles.
        if ($this->entity instanceof Article) {
            return $this->entity->isPublished();
        }

        // If is not an article, index anyway.
        return true;
    }
}

For more information on conditional indexing, check out the documentation.

Using Algolia Client

In some cases, you may want to access the Algolia client directly to perform advanced operations (like manage API keys, manage indices and such).

By default, the AlgoliaSearch\Client is not public in the container, but you can easily expose it. In the service file of your project, config/services.yaml in a typical Symfony 4 app, you can alias it and make it public with the following code:

1
2
3
4
services:
    algolia.client:
        alias: algolia_client
        public: true

Or in XML

1
2
3
<services>
    <service id="algolia.client" alias="algolia_client" public="true" />
</services>

Example

Here is an example of how to use the client after your registered it publicly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TestController extends Controller
{

    protected $indexManager;

    public function __construct(IndexManagerInterface $indexingManager)
    {
        $this->indexManager = $indexingManager;
    }

    public function testAction()
    {
        $algoliaClient = $this->get('algolia.client');
        var_dump($algoliaClient->listIndexes());

        $index = $algoliaClient->initIndex(
            $this->indexManager->getFullIndexName(Post::class)
        );

        var_dump($index->listApiKeys());

        die;
    }
}

Other engines

Everything related to Algolia is contained in the AlgoliaEngine class, hence it’s easy to use the bundle with another search engine. It also allows you to write your own engine if you want to do things differently.

Using another Engine

Considering that you have this AnotherEngine class implementing the EngineInterface, and you want to use it, you can override the service search.engine definition in your config/services.yaml this way:

1
2
3
services:
    search.engine:
        class: Algolia\SearchBundle\Engine\AnotherEngine

Or in XML

1
2
3
<services>
    <service id="search.engine" class="Algolia\SearchBundle\Engine\AnotherEngine" />
</services>

About the NullEngine

The package ships with a \Algolia\SearchBundle\Engine\NullEngine engine class. This engine implements the EngineInterface interface and returns an empty array, zero or null depending on the methods. This is a great way to make sure everything works, without having to call Algolia.

You can use it for your tests but also in a dev environment.

About the AlgoliaSyncEngine

In Algolia, all indexing operations are asynchronous. The API will return a taskID and you can check if this task is completed or not, via another API endpoint.

For test purposes, we use the AlgoliaSyncEngine. It will always wait for task to be completed before returning. This engine is only auto-loaded during the tests. If you use it in your project, you can copy it into your app and modify the search.engine service definition.

Did you find this page helpful?