How to Search across multiple ElasticSearch Indexes with Symfony FOS\ElasticaBundle

ElasticSearch v6.0 deprecated multiple types in one index. You can read more here: Removal of mapping types

How to deal with this breaking change?

Create MultiIndex.php file in your Symfony App project


<?php

declare(strict_types=1);

/*
 * Created by Exploit.cz <insekticid AT exploit.cz>
 */

namespace App\Search\Elastica;

use Elastica\Exception\InvalidException;
use Elastica\Index;
use Elastica\ResultSet\BuilderInterface;
use Elastica\Search;

class MultiIndex extends Index
{
    /**
     * Array of indices.
     *
     * @var array
     */
    protected $_indices = [];

    /**
     * Adds a index to the list.
     *
     * @param \Elastica\Index|string $index Index object or string
     *
     * @throws \Elastica\Exception\InvalidException
     *
     * @return $this
     */
    public function addIndex($index)
    {
        if ($index instanceof Index) {
            $index = $index->getName();
        }

        if (!is_scalar($index)) {
            throw new InvalidException('Invalid param type');
        }

        $this->_indices[] = (string) $index;

        return $this;
    }

    /**
     * Add array of indices at once.
     *
     * @param array $indices
     *
     * @return $this
     */
    public function addIndices(array $indices = [])
    {
        foreach ($indices as $index) {
            $this->addIndex($index);
        }

        return $this;
    }

    /**
     * Return array of indices.
     *
     * @return array List of index names
     */
    public function getIndices()
    {
        return $this->_indices;
    }

    /**
     * @param string|array|\Elastica\Query $query
     * @param int|array                    $options
     * @param BuilderInterface             $builder
     *
     * @return Search
     */
    public function createSearch($query = '', $options = null, BuilderInterface $builder = null)
    {
        $search = new Search($this->getClient(), $builder);
        //$search->addIndex($this);
        $search->addIndices($this->getIndices());
        $search->setOptionsAndQuery($options, $query);

        return $search;
    }
}

Create App\Search\Transformer\ElasticaToModelTransformerCollection.php (removed from FosElasticaBundle)

<?php

namespace App\Search\Transformer;

use FOS\ElasticaBundle\HybridResult;
use FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface;

/**
 * Holds a collection of transformers for an index wide transformation.
 *
 * @author Tim Nagel <tim@nagel.com.au>
 * @author Insekticid <insekticid+fos@exploit.cz>
 */
class ElasticaToModelTransformerCollection implements ElasticaToModelTransformerInterface
{
    /**
     * @var ElasticaToModelTransformerInterface[]
     */
    protected $transformers = [];

    /**
     * @param ElasticaToModelTransformerInterface[] $transformers
     */
    public function __construct(array $transformers)
    {
        $this->transformers = $transformers;
    }

    /**
     * {@inheritdoc}
     */
    public function getObjectClass(): string
    {
        return implode(',', array_map(function (ElasticaToModelTransformerInterface $transformer) {
            return $transformer->getObjectClass();
        }, $this->transformers));
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentifierField(): string
    {
        return array_map(function (ElasticaToModelTransformerInterface $transformer) {
            return $transformer->getIdentifierField();
        }, $this->transformers)[0];
    }

    /**
     * {@inheritdoc}
     */
    public function transform(array $elasticaObjects)
    {
        $sorted = [];
        foreach ($elasticaObjects as $object) {
            $sorted[$object->getIndex()][] = $object;
        }

        $transformed = [];
        foreach ($sorted as $type => $objects) {
            $transformedObjects = $this->transformers[$type]->transform($objects);
            $identifierGetter = 'get'.ucfirst($this->transformers[$type]->getIdentifierField());
            $transformed[$type] = array_combine(
                array_map(
                    function ($o) use ($identifierGetter) {
                        return $o->$identifierGetter();
                    },
                    $transformedObjects
                ),
                $transformedObjects
            );
        }

        $result = [];
        foreach ($elasticaObjects as $object) {
            if (array_key_exists((string) $object->getId(), $transformed[$object->getIndex()])) {
                $result[] = $transformed[$object->getIndex()][(string) $object->getId()];
            }
        }

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function hybridTransform(array $elasticaObjects)
    {
        $objects = $this->transform($elasticaObjects);

        $result = [];
        for ($i = 0, $j = count($elasticaObjects); $i < $j; ++$i) {
            if (!isset($objects[$i])) {
                continue;
            }
            $result[] = new HybridResult($elasticaObjects[$i], $objects[$i]);
        }

        return $result;
    }
}


Refactor fos_elastica.yaml and move all types from the same index to your newly created index (separate it).

Example:

  • before: index:recipes, types: [recipe, recipes]
  • after:
    • index: recipe, type: recipe
    • index: recipes, type: recipes

Now add MultiIndex service into services.yaml file in config directory and change recipe and recipes to your newly created index names.

    App\Search\Elastica\MultiIndex:
        arguments:
            $name: 'recipe'
        calls:
            - [ addIndices, [['@fos_elastica.index.recipe', '@fos_elastica.index.recipes']]]

    Elastica\SearchableInterface: '@App\Search\Elastica\MultiIndex'

    #FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerCollection:
    App\Search\Transformer\ElasticaToModelTransformerCollection:
        arguments:
            - {
                recipe: '@fos_elastica.elastica_to_model_transformer.recipe',
                recipes: '@fos_elastica.elastica_to_model_transformer.recipes'
              }

    FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerInterface: '@FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerCollection'

    FOS\ElasticaBundle\Finder\TransformedFinder: ~
        #arguments:
        #    - '@App\Search\Elastica\MultiIndex'
        #    - '@FOS\ElasticaBundle\Transformer\ElasticaToModelTransformerCollection'

    FOS\ElasticaBundle\Finder\PaginatedFinderInterface: '@FOS\ElasticaBundle\Finder\TransformedFinder'

Now create SearchRepository.php

<?php

declare(strict_types=1);

use Elastica\Query\Match;
use FOS\ElasticaBundle\Repository;

class SearchRepository extends Repository
{
    public function search(string $searchTerm, int $page = 1, int $limit = 48) : ?array
    {
        if ($searchTerm) {
            $fieldQuery = new Match();
            $fieldQuery->setFieldQuery('name', $searchTerm);
            $items     = $this->findPaginated($fieldQuery);
            $items->setMaxPerPage($limit);
            $items->setCurrentPage($page);

            return $items;
        }

        return null;
    }
}

Now you can use this repository in your Controller

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use App\Repository\SearchRepository;

class SearchController extends AbstractController
{
    /**
     * @var SearchRepository
     */
    protected $searchRepository;

    public function __construct(SearchRepository $searchRepository)
    {
        $this->searchRepository = $searchRepository;
    }
}

Github issues Multiple index paginated search #1521, Multi type search to multi index search #1385