In this example we are going to use aggregation feature of elasticsearch. This example has many-to-many relationship between Product and Shop tables. We will search products by their names using name field. The "aggregation" section in search result will use name field of shop table.


Database


ProductID   |ProductName        |ShopID     |ShopName
1 |Product1 |1 |Shop1
1 |Product1 |3 |Shop3
1 |Product1 |4 |Shop4
2 |Product2 |1 |Shop1
2 |Product2 |2 |Shop2
3 |Product3 |1 |Shop1
3 |Product3 |3 |Shop3
4 |Product4 |1 |Shop1
4 |Product4 |2 |Shop2
4 |Product4 |4 |Shop4
5 |Product5 |1 |Shop1
5 |Product5 |3 |Shop3
6 |Product6 |1 |Shop1
6 |Product6 |2 |Shop2
7 |Product Product1 |4 |Shop4

Elastica bundle


Setup


You need to install friendsofsymfony/elastica-bundle and enable it in AppKernel.php file.


Config.yml


We will make only product.name as full-text searchable field because our application will only search by product name.


fos_elastica:
clients:
default: { host: 127.0.0.1, port: 9200 }
indexes:
product_index:
client: default
index_name: product_%kernel.environment%
types:
product:
mappings:
id:
type: integer
index: not_analyzed
name:
type: string
analyzer: english
shops:
type: object
properties:
id:
type: integer
index: not_analyzed
name:
type: string
index: not_analyzed
persistence:
driver: orm
model: AppBundle\Entity\Product
finder: ~
provider: ~
listener: ~

Elasticsearch index


Current state


As you can see the index doesn't exists yet.


$ curl 127.0.0.1:9200/_cat/indices?v
health status index pri rep docs.count docs.deleted store.size pri.store.size

Populate


$ bin/console fos:elastica:populate --env=test
Resetting product_index
7/7 [============================] 100%
Populating product_index/product
Refreshing product_index
Refreshing product_index

New state


As you can see the index has been created.


$ curl 127.0.0.1:9200/_cat/indices?v
health status index pri rep docs.count docs.deleted store.size pri.store.size
yellow open product_test 5 1 7 0 16.2kb 16.2kb

Content


$ curl -XGET 127.0.0.1:9200/product_test/_search?pretty=1
{
...
"hits" : {
"total" : 7,
"hits" : [ {
...
"_source":{"id":4,"name":"Product4","shops":[{"id":1,"name":"Shop1"},{"id":2,"name":"Shop2"},{"id":4,"name":"Shop4"}]}
}, {
"_source":{"id":5,"name":"Product5","shops":[{"id":1,"name":"Shop1"},{"id":3,"name":"Shop3"}]}
}, {
"_source":{"id":1,"name":"Product1","shops":[{"id":1,"name":"Shop1"},{"id":3,"name":"Shop3"},{"id":4,"name":"Shop4"}]}
}, {
"_source":{"id":6,"name":"Product6","shops":[{"id":1,"name":"Shop1"},{"id":2,"name":"Shop2"}]}
}, {
"_source":{"id":2,"name":"Product2","shops":[{"id":1,"name":"Shop1"},{"id":2,"name":"Shop2"}]}
}, {
"_source":{"id":7,"name":"Product Product1","shops":[{"id":4,"name":"Shop4"}]}
}, {
"_source":{"id":3,"name":"Product3","shops":[{"id":1,"name":"Shop1"},{"id":3,"name":"Shop3"}]}
} ]
}
}

ProductController


namespace AppBundle\Controller;

use AppBundle\Service\ProductService;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;

/**
* @Route("/products", service="app.controller.product")
*/
class ProductController
{
private $productService;

public function __construct(
ProductService $productService
) {
$this->productService = $productService;
}

/**
* @param Request $request
*
* @Method({"GET"})
* @Route("/search")
* @Template
*
* @return array
*/
public function searchAction(Request $request)
{
$name = $request->query->get('name');

return ['result' => $this->productService->search($name)];
}
}

services:
app.controller.product:
class: AppBundle\Controller\ProductController
arguments:
- "@app.service.product"

ProductService


namespace AppBundle\Service;

use AppBundle\Factory\ModelFactory;
use Elastica\Query;
use Elastica\Type;

class ProductService
{
private $modelFactory;
private $productType;

public function __construct(
ModelFactory $modelFactory,
Type $productType
) {
$this->modelFactory = $modelFactory;
$this->productType = $productType;
}

public function search($name)
{
$query['query']['match']['name']['query'] = $name;
$query['aggs']['shops']['terms']['field'] = 'shops.name';

$result = $this->productType->search(new Query($query));

return $this->modelFactory->createProductSearchResult($result);
}
}

services:
app.service.product:
class: AppBundle\Service\ProductService
arguments:
- "@app.factory.model"
- "@fos_elastica.index.product_index.product"

ModelFactory


namespace AppBundle\Factory;

use AppBundle\Model\Product\Search\Result as ProductSearchResultModel;
use AppBundle\Model\Product\Search\Product as ProductSearchModel;
use AppBundle\Model\Product\Shop as ShopSearchModel;
use Elastica\ResultSet;

class ModelFactory
{
public function createProductSearchResult(ResultSet $resultSet)
{
$resultModel = new ProductSearchResultModel();

if ($resultSet->getTotalHits() < 1) {
return $resultModel;
}

$resultModel->total = $resultSet->getTotalHits();
$resultModel->aggregations = array_column($resultSet->getAggregations()['shops']['buckets'], 'doc_count', 'key');

/** @var \Elastica\Result $item */
foreach ($resultSet->getResults() as $item) {
$data = $item->getData();

$productSearchModel = new ProductSearchModel();
$productSearchModel->id = $data['id'];
$productSearchModel->name = $data['name'];

foreach ($data['shops'] as $shop) {
$shopSearchModel = new ShopSearchModel();
$shopSearchModel->id = $shop['id'];
$shopSearchModel->name = $shop['name'];

$productSearchModel->shops[] = $shopSearchModel;
}

$resultModel->products[] = $productSearchModel;
}

return $resultModel;
}
}

services:
app.factory.model:
class: AppBundle\Factory\ModelFactory

Result


namespace AppBundle\Model\Product\Search;

class Result
{
/**
* @var int
*/
public $total;

/**
* @var array
*/
public $aggregations;

/**
* @var Product[]
*/
public $products;
}

Product


namespace AppBundle\Model\Product\Search;

use AppBundle\Model\Product\Shop;

class Product
{
/**
* @var int
*/
public $id;

/**
* @var string
*/
public $name;

/**
* @var Shop[]
*/
public $shops;
}

Shop


namespace AppBundle\Model\Product;

class Shop
{
/**
* @var int
*/
public $id;

/**
* @var string
*/
public $name;
}

Search.html.twig


{% extends '::base.html.twig' %}

{% block body %}
{{ dump(result) }}
{% endblock %}

Test


If you go to http://product.dev/app_test.php/products/search?name=Product1 address to search for products named Product1, the result will look like below. If you compare the data in aggregations section to the actual result, you will see that it is a match.


AppBundle\Model\Product\Search\Result Object
(
[total] => 2
[aggregations] => Array
(
[Shop4] => 2
[Shop1] => 1
[Shop3] => 1
)
[products] => Array
(
[0] => AppBundle\Model\Product\Search\Product Object
(
[id] => 1
[name] => Product1
[shops] => Array
(
[0] => AppBundle\Model\Product\Shop Object
(
[id] => 1
[name] => Shop1
)
[1] => AppBundle\Model\Product\Shop Object
(
[id] => 3
[name] => Shop3
)
[2] => AppBundle\Model\Product\Shop Object
(
[id] => 4
[name] => Shop4
)
)
)
[1] => AppBundle\Model\Product\Search\Product Object
(
[id] => 7
[name] => Product Product1
[shops] => Array
(
[0] => AppBundle\Model\Product\Shop Object
(
[id] => 4
[name] => Shop4
)
)
)
)
)