I've used Symfony2 for a large project at my work for the first time and I loved it. I used Elasticsearch for much of the frontend display and it was a little tough for a beginner like me, due to lack of documentation and well, straightforward examples. I use FOSElasticaBundle to communicate with my backend Elasticsearch. Unfortunately, Elastica (The PHP API for Elasticsearch server) has many features that I need, but no way to access them in FOSElasticaBundle.

Today I will show you how you can use the Elastica object in FOSElasticaBundle without comprimising the awesome ability FOSElasticaBundle has to transform your Elasticsearch results into Doctrine entities.

Versions:
Symfony 2.3
FOSElasticaBundle 3.0.x
Elasticsearch Server 1.4.4
(Versions are guideline, it might work in higher or lower versions)

You Will Need:
All of the above (software)
A bundle in your Symfony2 project
Moderate knowledge of Elastica

We need to make several new files in our bundle. We will use Symfony2's Dependency Injection to override our FOSElasticaBundle service so that we may use our own methods. When creating a file, please also create the correct folder structure (Acme/AcmeBundle is obviously your bundle)!

Create file: Acme/AcmeBundle/DependencyInjection/Compiler/OverrideServiceCompilerPass.php

File Contents:

namespace Acme\AcmeBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class OverrideServiceCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition('fos_elastica.finder.search');
        $definition->setClass('Acme\AcmeBundle\Finder\TransformedFinder');
    }
}

In order to use this compiler pass, we need to register it in our bundle's base controller. This tells symfony to override FOSUserBundle's finder service class with ours.

Edit File: Acme/AcmeBundle/AcmeBundle.php

We need to add these use statements & method inside the AcmeBundle class:

//... under namespace:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Events\EventsBundle\DependencyInjection\Compiler\OverrideServiceCompilerPass;

//... in class:
public function build(ContainerBuilder $container)
{
    parent::build($container);
    $container->addCompilerPass(new OverrideServiceCompilerPass());
}   

Now we just need to create our custom extension of the finder class (TransformedFinder) and we are home free! Since we are just extending the class and using our own methods rather than completely overriding it, we might not have to worry so much about future updates breaking our application. It is always good practice to look if this file has changed after updating though.

Create File: Acme/AcmeBundle/Finder/TransformedFinder.php

File Contents:

namespace Acme\AcmeBundle\Finder;

use FOS\ElasticaBundle\Finder\TransformedFinder as BaseFinder;
use FOS\ElasticaBundle\Paginator\TransformedPaginatorAdapter;
use FOS\ElasticaBundle\Paginator\FantaPaginatorAdapter;
use Pagerfanta\Pagerfanta;
use Elastica\Query;

class TransformedFinder extends BaseFinder
{
   
    public function findSuggest($query, $limit = null, $options = array())
    {
        $resultsOriginal = $this->searchSuggest($query, $limit, $options);
        $resultsTransformed = $resultsOriginal->getResults();
        $resultSet = array();
        $resultSet['suggestion'] = $resultsSuggest->getSuggests();
        $resultSet['results'] = $this->transformer->transform($results);
        return $resultSet;
    }
    
    protected function searchSuggest($query, $limit = null, $options = array())
    {
        $queryObject = Query::create($query);
        if (null !== $limit) {
            $queryObject->setSize($limit);
        }
        $results = $this->searchable->search($queryObject, $options);
        return $results;
    }  

    public function findPaginatedSuggest($query, $options = array())
    {
        $queryObject = Query::create($query);
        $paginatorAdapter = $this->createPaginatorAdapter($queryObject, $options);
        $results = array();
        $results['pager'] = new Pagerfanta(new FantaPaginatorAdapter($paginatorAdapter));
        $results['suggestion'] = $this->searchSuggest($query)->getSuggests();
        return $results;
    } 
}

As you can see, we have made our own methods for find and findPaginated, so they now are findSuggest and findPaginatedSuggest! They also return arrays instead of your results as Doctrine objects. Don't fret though, as your Doctrine objects (or pager, whichever you need) is under the ['results'] or ['pager'] keys. Your shiney new suggestions are under key ['suggestion'].

Now that you have this all set up, it's time to use it in your application!

//Your container. Must match your config!
$finder = $this->container->get('fos_elastica.finder.search');

//Start suggesting.
$suggest = new \Elastica\Suggest();
    //Start Phrase Suggest (There are other options such as Term, refer to API)
    $suggestPhrase = new \Elastica\Suggest\Phrase('search', 'event');
    $suggestPhrase ->setText('mispalled search');
    $suggestPhrase ->setMaxErrors('3');
$suggest->addSuggestion($suggestPhrase );

//Start Elastica query
$searchQuery = new \Elastica\Query();
$boolQuery = new \Elastica\Query\Bool();

//Search a query string
$queryString = new \Elastica\Query\QueryString('mispalled search');
$boolQuery->addMust($queryString);      

//Wrap up the query and get ready to send to Elasticsearch
$searchQuery->setSuggest($suggest); 
$searchQuery->setQuery($boolQuery);

//We are using our custom function to return an array $result
$results = $finder->findSuggest($searchQuery);

//explanation of results
$suggestions = $results['suggestion'];
$doctrineResults = $results['results'];

Once you get your array of goodies, you can do further things with it, like create "Did you mean {{corrected result}}?" or just go straight to the result and give the user to go back to their mispelling if you prefere.

You can adapt this solution into anything that FOSElasticaBundle doesn't support, for example: Aggrigations. FOSElasticaBundle supports Facets, but those are already depriciated in favor of Aggrigations. I'm sure the FOS team will get Aggrigations in there soon enough, but if you just can't wait, try this instead!