Symfony Search Bundle
This package will help you get your data indexed in a dedicated Search Engine
New package
You're looking at the new major version of this package. If your looking for the previous one, it was moved to the 2.x
branch.
Table of Contents
- Compatibility
- Install
- Configuration
- Search
- Index entities
- Normalizers
- Engine
- Using the Algolia Client (Advanced)
- Tests
Compatibility
This package is compatible with Symfony 3.4 and higher.
If your app runs an older version, you can use the previous version, available on the 2.x branch.
Install
With composer
composer require algolia/search-bundle
Configuration
The following configuration assume you are using Symfony/demo project.
algolia_search:
prefix: demoapp_
indices:
- name: posts
class: App\Entity\Post
- name: comments
class: App\Entity\Comment
enable_serializer_groups: true
Credentials
You will also need to provide Algolia App ID and Admin API key. By default they are loaded from env variables ALGOLIA_APP_ID
and ALGOLIA_API_KEY
.
If you don't use env variable, you can set them in your parameters.yml
.
parameters:
env(ALGOLIA_APP_ID): K7MLRQH1JG
env(ALGOLIA_API_KEY): 0d7036b75416ad0c811f30536134b313
Search
In this example we'll search for posts.
$em = $this->getDoctrine()->getManager();
$indexManager = $this->get('search.index_manager');
$posts = $indexManager->search('query', Post::class, $em);
Note that this method will return an array of entities retrieved by Doctrine object manager (data are pulled from the database).
If you want to get the raw result from Algolia, use the rawSearch
method.
$indexManager = $this->get('search.index_manager');
$posts = $indexManager->rawSearch('query', Post::class);
Pagination
To get a specific page, define the page
(and nbResults
if you want).
$em = $this->getDoctrine()->getManager();
$indexManager = $this->get('search.index_manager');
$posts = $indexManager->search('query', Post::class, $em, 2);
// Or
$posts = $indexManager->search('query', Post::class, $em, 2, 100);
Count
$indexManager = $this->get('search.index_manager');
$posts = $indexManager->count('query', Post::class);
Advanced search
Pass anything you want in the parameters
array. You can pass it in any search-related method.
$indexManager = $this->get('search.index_manager');
$posts = $indexManager->count('query', Post::class, 0, 10, ['filters' => 'comment_count>10']);
Index entities
Automatically
The bundle will listen to postPersist
and preRemove
doctrine events to keep your data in sync. You have nothing to do.
Manually
If you want to update a post manually, you can get the IndexManager
from the container and call the index
method manually.
$em = $this->getDoctrine()->getManager();
$indexManager = $this->get('search.index_manager');
$post = $em->getRepository(Post::class)->findBy(['author' => 1]);
$indexManager->index($post, $em);
Normalizers
By default all entities are converted to an array with the built-in (Symfony Normalizers)[https://symfony.com/doc/current/components/serializer.html#normalizers] (GetSetMethodNormalizer, DateTimeNormalizer, ObjectNormalizer...) which should be enough for simple use case, but we encourage you to write your own Normalizer to have more control on what you send to Algolia or to simply avoid (circular references)[https://symfony.com/doc/current/components/serializer.html#handling-circular-references].
Symfony will use the first one to support your entity or format.
Note that the normalizer is called with searchableArray format.
Custom Normalizers
Using a dedicated normalizer
You can create a custom normalizer for any entity. The following snippet shows a simple CommentNormalizer. Normalizer must implement Symfony\Component\Serializer\Normalizer\NormalizerInterface
interface.
<?php
namespace App\Serializer\Normalizers;
use App\Entity\Comment;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class CommentNormalizer implements NormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
/**
* Normalizes an Comment into a set of arrays/scalars.
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'post_id' => $object->getPost()->getId(),
'content' => $object->getContent(),
'createdAt' => $this->serializer->normalize($object->getCreatedAt(), $format, $context),
'author' => $this->serializer->normalize($object->getAuthor(), $format, $context),
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Comment;
}
}
<?php
namespace App\Serializer\Normalizers;
use App\Entity\User;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class UserNormalizer implements NormalizerInterface
{
/**
* Normalizes an Comment into a set of arrays/scalars.
*/
public function normalize($object, $format = null, array $context = array())
{
return [
'username' => $object->getUsername(),
'id' => $object->getAuthor()->getFullName(),
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof User;
}
}
Don't forget to create the new services for your newly created Normalizers
(if you don't rely on autowiring). You can find an example on the
(Symfony documentation)[http://symfony.com/doc/current/serializer.html#adding-normalizers-and-encoders].
In our case, it will be:
<service id="comment_normalizer" class="App\Serializer\Normalizer\CommentNormalizer" public="false">
<tag name="serializer.normalizer" />
</service>
<service id="comment_normalizer" class="App\Serializer\Normalizer\UserNormalizer" public="false">
<tag name="serializer.normalizer" />
</service>
normalize
method in entity
Using To define the normalize
method in the entity class.
- Implement
Symfony\Component\Serializer\Normalizer\NormalizableInterface
- Define
normalize
method
Example
<?php
public function normalize(NormalizerInterface $normalizer, $format = null, array $context = array()): array
{
return [
'title' => $this->getTitle(),
'content' => $this->getContent(),
'author' => $this->getAuthor()->getFullName(),
];
}
Algolia\SearchBundle
only
Create Normalizer for Sometimes, you want to create a specific Normalizer
just to send data to Algolia. But what happens if you have 2 normalizers for the same class ?
Well you can add a condition so your Normalizer
will only be called when Indexing through Algolia.
When you create your Normalizers
, you can add an additionnal check for the format :
<?php
use Algolia\SearchBundle\Searchable;
class UserNormalizer implements NormalizerInterface
{
...
public function supportsNormalization($data, $format = null)
{
return $data instanceof User && $format == Searchable::NORMALIZATION_FORMAT;
}
}
Though it's not really encouraged to add this kind of logic inside the normalize method, you could also add/remove some specific fields on your entity for Algolia this way:
<?php
use Algolia\SearchBundle\Searchable;
class UserNormalizer implements NormalizerInterface
{
public function normalize($object, $format = null, array $context = array())
{
if ($format == Searchable::NORMALIZATION_FORMAT) {
// 'id' of user will be provided only in Algolia usage
return ['id' => $object->getId()];
}
return [
'username' => $object->getUsername(),
'email' => $object->getEmail(),
];
}
...
}
Using normalizer groups
You can also rely on (@Group
annotation)[https://symfony.com/doc/current/components/serializer.html].
The name of the group is searchable
.
You have to explicitly enable this feature on your configuration:
algolia_search:
prefix: demoapp_
indices:
- name: posts
class: App\Entity\Post
- name: comments
class: App\Entity\Comment
enable_serializer_groups: true
In the example below, $comment
, $author
and $createdAt
data will be sent to Algolia.
<?php
namespace App\Entity;
use Symfony\Component\Serializer\Annotation\Groups;
class Comment
{
public $createdAt;
public $reviewer;
/**
* @Groups({"searchable"})
*/
public $comment;
/**
* @Groups({"searchable"})
*/
public $author;
/**
* @Groups({"searchable"})
*/
public function getCreatedAt()
{
return $this->createdAt;
}
}
Engine
NullEngine
The The package ships with a NullEngine. This engine implements the EngineInterface
and return an empty array, zero or null depending on the method.
You can use it for your test for instance but also in dev environment.
Using another engine
Let's say you want to use the NullEngine
in your dev environment. You can override the service
definition in your config/dev/serices.yaml
this way:
services:
search.engine:
class: Algolia\SearchBundle\Engine\NullEngine
Or in XML
<services>
<service id="search.engine" class="Algolia\SearchBundle\Engine\NullEngine" />
</services>
This is also how you can use a custom engine, to handle another search engine or extend Algolia' default engine.
Using the Algolia Client (Advanced)
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
in not public in the container, but you can easily expose it.
In the service file of your project, config/serices.yaml
in a typical Symfony4 app,
you can alias it and make it public with the following code:
services:
algolia.client:
alias: algolia_client
public: true
Or in XML
<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.
class TestController extends Controller
{
public function testAction()
{
$algoliaClient = $this->get('algolia.client');
var_dump($algoliaClient->listIndexes());
$indexManager = $this->get('search.index_manager');
$index = $algoliaClient->initIndex(
$indexManager->getFullIndexName(Post::class)
);
var_dump($index->listApiKeys());
die;
}
}
Tests
The tests require ALGOLIA_APP_ID
and ALGOLIA_API_KEY
to be defined in the environment variables.
ALGOLIA_APP_ID=XXXXXXXXXX ALGOLIA_API_KEY=4b31300d70d70b75811f413366ad0c ./vendor/bin/phpunit
AlgoliaSyncEngine
About 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 autoloaded during in the tests, if you will to use it in your
project, you can copy it into your app and modify the search.engine
service definition.