23/05/2017 - ELASTICSEARCH, SYMFONY
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.
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
You need to install friendsofsymfony/elastica-bundle
and enable it in AppKernel.php file.
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: ~
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
$ bin/console fos:elastica:populate --env=test
Resetting product_index
7/7 [============================] 100%
Populating product_index/product
Refreshing product_index
Refreshing product_index
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
$ 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"}]}
} ]
}
}
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"
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"
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
namespace AppBundle\Model\Product\Search;
class Result
{
/**
* @var int
*/
public $total;
/**
* @var array
*/
public $aggregations;
/**
* @var Product[]
*/
public $products;
}
namespace AppBundle\Model\Product\Search;
use AppBundle\Model\Product\Shop;
class Product
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $name;
/**
* @var Shop[]
*/
public $shops;
}
namespace AppBundle\Model\Product;
class Shop
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $name;
}
{% extends '::base.html.twig' %}
{% block body %}
{{ dump(result) }}
{% endblock %}
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
)
)
)
)
)