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

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 <[email protected]>
 * @author Insekticid <[email protected]>
 */
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