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