Advanced indexing with FOSElasticaBundle (ModelToElasticaTransformer)

Share Button

Lire la version française

The XML configuration for FOSElasticaBundle is powerful, but in some cases this is not enough.
In order to solve this, FOS allows you to override the transformation from Doctrine Object to an ElasticSearch Document : ModelToElasticSearchTransformer.

When using the ModelToElasticaTransformer

In most cases, your problems may be solved by creating one or more getters (read Indexing and simple search with Elasticsearch and Symfony.
If your problem is too complex, this article explains how to create your own Model To Elastica Transformer.
As the case we present here is not really relevant in the context of our blog sandbow project,
we haven’t posted the sources on the Github repository.

Note : Whereas the post Elasticsearch and Symfony : hydrating objects explains how to custom the transformation of an Elasticsearch document into a Doctrine object, this post present the reverse process : transform one (or more) Doctrine objects into an Elasticsearch document.

The first thing to do is to create a new service :

    <! -- services.xml -->
        <service id="obtao.transformers.model.article" class="Obtao\BlogBundle\Transformer\ArticleToElasticaTransformer" />

Creating the Transformer

Then we can create the class. For this transformer we choose to flat the article object to an associative array.
It’s also possible to extends ModelToElasticaAutoTranformer : call the parent and use the Document set method.

<?php

namespace Obtao\BlogBundle\Transformer;

use Elastica\Document;
use FOS\ElasticaBundle\Transformer\ModelToElasticaTransformerInterface;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class ArticleToElasticaTransformer implements ModelToElasticaTransformerInterface
{
    /**
     * Transforms an article into an elastica object having the required keys
     *
     * @param article $article the object to convert
     * @param array  $fields the keys we want to have in the returned array
     *
     * @return Document
     **/
    public function transform($article, array $fields)
    {

        $identifier = $article->getId();

        $authors = array();

        // flatten the nested "authors"
        // This is an example for this post. Actually, we could (and should)
        // create a "flattenAuthor" property in the Elastisearch configuration
        // and create the method getFlattenAuthor in the Article.php class with
        // the following implementation
        foreach($article->getAuthors() as $author){
            $nestedAuthor = array(
                'firstname' => $author->getFirstname(),
                'surname' => $author->getSurname(),
                'id' => $author->getId(),
            );
            $authors[] = $nestedAuthor;
        }

        $publishedAt = $article->getPublishedAt();
        $publishedAt = $publishedAt ? $publishedAt->format('c'):null;

        $values = array(
            'id' => $article->getId(),
            'category' => $article->getCategory()->__toString(),
            'createdAt' => $article->getCreatedAt()->format('c'),
            'content' => $article->getContent(),
            'publishedAt' => $publishedAt,
            'published' => $article->isPublished(),
            'tags' => $article->getTags(),
            'title' => $article->getTitle(),
            'authors' => $authors, 
        );

        //Create a document to index
        $document = new Document($identifier,$values);
        $document->setParent($article->getCategory()->getId());

        return $document;
    }
}

The last step is to declare the Transformer in the FOSElastica configuration.

#config/fos_elastica.yml

fos_elastica:
    indexes:
        obtao_blog:
            types:
                article:
                    persistence:
                        model_to_elastica_transformer:
                            service: obtao.transformers.model.article

When reindexing, the Transformer will be called and your object will always be transformed thanks to this logic.

Share Button

8 thoughts on “Advanced indexing with FOSElasticaBundle (ModelToElasticaTransformer)

  1. Bonjour,
    première visite sur ce site je pense que je vais le bookmark :) Très bon travail pour vos tutoriaux clairs et précis.

    Je m’attaque au bundle elastica sur mon projet et j’ai un problème que je pensais résoudre avec un datatransformer comme dans votre article : obtao.com/blog/2014/05/advanced-indexing-with-elasticsearch-foselasticabundle/

    Mon problème est le suivant :
    J’ai des Articles liés à des Catégories en ManyToMany.
    Dans ces Articles, j’ai un arbre de Contenu imbriqués. (Article, Catégories et Contenu étant mes entités en base)
    Je souhaite faire une recherche avec un seul champ de recherche, qui devra trouver les meilleurs Articles en fonction de leur arbre de contenus, leurs titres et leurs catégories.

    Du coup en suivant votre article, j’ai créé un model_to_elastica_transformer qui retourne un objet Elastica\Document que j’utilise pour remplir elasticsearch, puis un elastica_to_model_transformer pour faire le lien dans l’autre sens pour l’affichage.
    J’ai la config.yml suivante :
    ““
    fos_elastica:
    clients:
    default: { host: localhost, port: 9200 }
    serializer:
    callback_class: FOS\ElasticaBundle\Serializer\Callback
    serializer: serializer
    indexes:
    article:
    client: default
    types:
    ElasticaDocument:
    persistence:
    driver: orm
    model: MyBundle\Entity\Article
    elastica_to_model_transformer:
    service: mybundle.elasticatoarticle.transformer
    finder: ~
    listener: ~
    provider: ~
    model_to_elastica_transformer:
    service: mybundle.articletoelastica.transformer
    mappings:
    articleId
    articleTitle
    categoryId
    categoryTitle
    contenus:
    type: “nested”
    ““
    Le transformer crée bien mon Elastica\Document, avec ses deux champs ID et un array contenant l’arbre des contenus “applati”.
    Mais dans la base d’elasticasearch, chaque ligne contient en réalité :
    – l’Elastica\Document simplifié que je souhaite,
    – puis mon article complet avec toutes ses dépendences !
    Je pense que le problème viens de la config “persistence: { driver: orm, model: MyBundle\Entity\Article }” mais je n’arrive pas à faire tourner le bundle sans, et je n’ai pas envie de persister mon Elastica\Document dans une entité, ça enlève toute l’utilité du dataTransformer !

    Est-ce que j’aurais mal compris ou mal réalisé une étape ?
    Encore merci pour vos tutoriaux,
    G. XOLIN

    • Bonjour,

      Avant d’utiliser le ModelToElasticaTransformer, êtes-vous sûr de ne pas pouvoir réaliser la même chose avec un simple getter qui retournerait le contenu de l’arbre? L’utilisation d’un ModelToElasticaTransformer est très spécifique, et pour le moment, nous avons toujours réussi à nous en tirer autrement.

      • J’avais oublié ce commentaire, au cas ou d’autres auraient eu le même problème :
        j’ai tout simplement enlevé ma ligne de conf :

        serializer:
        callback_class: FOS\ElasticaBundle\Serializer\Callback

        car elle prennait le devant sur tout le reste :)
        G.XOLIN

  2. Hi there, thanks for the tutorials. I’ m using the bundle (v3.1.3) with Symfony 2 and I have models for Professors and Students. I populate the index normally and when I run searches, I get the serialized representation of the objects as given by the orm. It turns you I would like to use just a subset of that information. I thought that your transformer class would be suitable for the job, but when I adapted your code to mine example, I get warnings from the bundle requiring that my Student and Professor model classes to implement things like setType and setIndex. Am I doing something wrong ? Does this tutorial use version 3.1 ?

    • Hi, The tutorial was made with the ~3.0 version of the FOSElasticaBundle. But I’ve updated the dependencies just now to use the last version (3.1.3 when I’m writting this), and it still works here.
      I don’t know what you may have missed but you can have a look on this repository to compare our code with your and maybe find a clue : https://github.com/obtao/blog-sandbox
      Please tell me if you fixed it or not.

  3. Bonjour,

    Excellent Tuto.

    Je l’ai suivi et j’arrive à bien gérer mon indexation lors du fos:populate.

    En revanche, quand je sauvegarde mes objets doctrine, l’indexation Elastica ne se met plus à jour en utilisant un custom model to Elastica Transformer.
    Dois-je aussi customiser le listener ? Ou ai-je oublier une conf ?

    Mon mapping :

    product:
    mappings:
    id: ~
    lac_product_supplier_product_code: { type: string, boost: 3, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_stock_type: { type: string, boost: 3, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_product_title: { type: string, boost: 10, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_web_material: { type: string, boost: 8, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_finish_group: { type: string, boost: 8, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_style: { type: string, boost: 8, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_supplier: { type: string, boost: 9, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_designation: { type: string, boost: 10, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer}
    lac_product_season: { type: string, boost: 7, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer }
    finished: { type: bool, boost: 7, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer }
    enabled: { type: bool, boost: 7, search_analyzer: custom_search_analyzer, index_analyzer: custom_index_analyzer }

    persistence:
    elastica_to_model_transformer:
    query_builder_method: elasticsearchQueryBuilder
    model_to_elastica_transformer:
    service: lac.product.elastica_transformer
    driver: orm
    model: Lac\Bundle\AkeneoBridgeBundle\Entity\AkeneoProduct
    repository: Lac\Bundle\SearchBundle\Repository\ProductElasticsearchRepository
    provider:
    batch_size: 100
    listener: ~
    finder: ~

    Merci beaucoup

    • Bonjour,

      Est-ce que le service est bien enregistré? (app/console container:debug | grep lac.product.elastica_transformer)
      Et si oui, est-ce que la méthode tranform() est atteinte? (via un die() ou quelque chose du même genre)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Protected by WP Anti Spam