Symfony2: elasticsearch custom repositories

This is the fourth in a series of posts about using elasticsearch with Symfony2:

  1. Symfony2: Integrating elasticsearch
  2. Symfony2: improving elasticsearch results
  3. Symfony2: elasticsearch custom analyzers
  4. Symfony2: elasticsearch custom repositories

A new feature of the FOQElasticaSearch bundle is the provision of repositories similar to those for Doctrine queries. The first advantage of this is that instead of having to use the finder service specific to a particular entity you can use the same manager service for all mapped entities and use the entity class name to get a repository to run queries against. So using the query we have built up in the previous posts:

/**
* @Route("/sites/search/", name="site_search")
* @Method({ "head", "get" })
* @Template
*/
public function searchAction(Request $request)
{
    $sm = $this->get('foq_elastica.finder.manager');
    $searchTerm = $request->query->get('search');
    
    $nameQuery = new \Elastica_Query_Text();
    $nameQuery->setFieldQuery('name', $searchTerm);
    $nameQuery->setFieldParam('name', 'analyzer', 'snowball');

    $keywordsQuery = new \Elastica_Query_Text();
    $keywordsQuery->setFieldQuery('keywords', $searchTerm);
    $keywordsQuery->setFieldParam('keywords', 'analyzer', 'snowball');

    $urlQuery = new \Elastica_Query_Text();
    $urlQuery->setFieldQuery('url', $searchTerm);
    $urlQuery->setFieldParam('url', 'analyzer', 'url_analyzer');


    $boolQuery = new \Elastica_Query_Bool();
    $boolQuery->addShould($nameQuery);
    $boolQuery->addShould($keywordsQuery);
    $boolQuery->addShould($urlQuery);

    $sites = $sm->getRepository('ExampleBundle:Site')->find($boolQuery);
    return array('sites' => $sites);
}

Note that the use of the short syntax ExampleBundle:Site is only available in master, if you are using the branch compatible with 2.0.x Symfony releases then you will need to use the fully qualified class name e.g. LimeThinking\ExampleBundle\Entity\Site.

A much bigger advantage of using this functionality is that we can create a custom repository to encapsulate our query which will clean up our controller method and make it easy to reuse elsewhere. So our custom repository looks like this:

<?php

namespace LimeThinking\ExampleBundle\SearchRepository;

use FOQ\ElasticaBundle\Repository;

class SiteRepository extends Repository
{

    public function findByNameKeywordsOrUrl($searchTerm)
    {
        $nameQuery = new \Elastica_Query_Text();
        $nameQuery->setFieldQuery('name', $searchTerm);
        $nameQuery->setFieldParam('name', 'analyzer', 'snowball');

        $keywordsQuery = new \Elastica_Query_Text();
        $keywordsQuery->setFieldQuery('keywords', $searchTerm);
        $keywordsQuery->setFieldParam('keywords', 'analyzer', 'snowball');

        $urlQuery = new \Elastica_Query_Text();
        $urlQuery->setFieldQuery('url', $searchTerm);
        $urlQuery->setFieldParam('url', 'analyzer', 'url_analyzer');

        $boolQuery = new \Elastica_Query_Bool();
        $boolQuery->addShould($nameQuery);
        $boolQuery->addShould($keywordsQuery);
        $boolQuery->addShould($urlQuery);

        return $this->find($boolQuery);
    }


}

The custom repository must extend the base repository in order to be able to use the relevant finder service, which is automatically injected in. We also need to specify the custom repository class. In master this can be done using an annotation:

<?php

namespace LimeThinking\ExampleBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOQ\ElasticaBundle\Configuration\Search;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\Table(name="site")
 * @ORM\HasLifecycleCallbacks()
 * @Search(repositoryClass="LimeThinking\ExampleBundle\SearchRepository\SiteRepository")
 */

class Site
{
//--
}

For the 2.0 branch we need to specify it in our config, which now looks like this:

foq_elastica:
    clients:
        default: { host: localhost, port: 9200 }
    indexes:
        bookmarks:
            //--
            types:
                site:
                    mappings:
                        name: { analyzer: snowball }
                        keywords: { analyzer: snowball }
                        url: { analyzer: url_analyzer }
                    doctrine:
                        driver: orm
                        model: LimeThinking\ExampleBundle\Entity\Site
                        repository: LimeThinking\ExampleBundle\SearchRepository\SiteRepository
                        provider:
                        listener:
                        finder:

Our controller method can now be simplified to this:

/**
* @Route("/sites/search/", name="site_search")
* @Method({ "head", "get" })
* @Template
*/
public function searchAction(Request $request)
{
    $sm = $this->get('foq_elastica.finder.manager');
    $searchTerm = $request->query->get('search');

    $sites = $sm->getRepository('ExampleBundle:Site')
                ->findByNameKeywordsOrUrl($searchTerm);
    return array('sites' => $sites);
}

The controller method is now much cleaner with the implementation of the query moved to the repository, the query can now also easily be reused elsewhere.