12/03/2016 - ELASTICSEARCH, SYMFONY
Elasticsearch ile sorgu yapmak için aşağıdaki symfony uygulama temelini kullanabilirsiniz. Bu örnek FOSElasticaBundle ve Pagerfanta sayfalayıcı paketlerini kullanıyor.
Note : Bu versiyon, Pagerfanta paketinden kaynaklanmak şartıyla diğer versiyona nazaran daha yavaş. Versiyon 2'yi kullanmanızı tavsiye ederim.
Öncelikle composer ile friendsofsymfony/elastica-bundle
ve pagerfanta/pagerfanta
paketlerini kurun. Bu yazıyı yazarken friendsofsymfony/elastica-bundle
paketi sadece elasticsearch 1.7.4 ve alt versiyonlarını destekliyordu.
Bu sadece bir örnek index. Aşağıdaki ayarlara bakarsanız, aynı sorgu içinde sadece "title" ve "description" alanları üzerinde hem full-text hem de sıralama yapabilirsiniz. Daha fazla bilgi için String Sorting and Multifields sayfasını okuyun.
fos_elastica:
clients:
default: { host: 127.0.0.1, port: 9203 }
indexes:
post_index:
client: default
index_name: post_%kernel.environment%
types:
post:
mappings:
id:
type: integer
index: not_analyzed
title:
type: string
analyzer: english
fields:
raw:
type: string
index: not_analyzed
description:
type: string
analyzer: english
fields:
raw:
type: string
index: not_analyzed
year:
type: integer
index: not_analyzed
price:
type: double
index: not_analyzed
is_published:
type: boolean
index: not_analyzed
created_at:
type: date
index: not_analyzed
persistence:
driver: orm
model: Application\SearchBundle\Entity\Post
finder: ~
provider: ~
listener: ~
Yukarıda gördüğümüz gibi "author" alanını index içinde kullanmadık, bu nedenle elasticsearch içinde o alanı kullanamayız.
namespace Application\SearchBundle\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="post")
* @ORM\HasLifecycleCallbacks
*/
class Post
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=100)
*/
private $title;
/**
* @ORM\Column(name="description", type="text")
*/
private $description;
/**
* @ORM\Column(name="author", type="string", length=100)
*/
private $author;
/**
* @ORM\Column(name="year", type="string", length=4)
*/
private $year;
/**
* @ORM\Column(name="price", type="decimal", precision=4, scale=2)
*/
private $price;
/**
* @ORM\Column(name="is_published", type="boolean")
*/
private $isPublished;
/**
* @ORM\Column(name="created_at", type="datetime")
*/
private $createdAt;
}
Aşağıda gördüğümüz gibi, arama yapabileceğimiz örnek bir URL GET /search/elasticsearch?page=1&limit=10&sort=title,-description&fields=title,description&keywords=hello%20world
gibi olacak.
namespace Application\SearchBundle\Controller;
use Application\SearchBundle\Service\PostElasticsearchServiceInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
/**
* @Route("/search", service="application_search.controller.search")
*/
class SearchController extends Controller
{
private $postElasticsearchService;
public function __construct(
PostElasticsearchServiceInterface $postElasticsearchService
) {
$this->postElasticsearchService = $postElasticsearchService;
}
/**
* @param Request $request
*
* @Method({"GET"})
* @Route("/elasticsearch")
* @Template()
*
* @return array
*/
public function searchElasticsearchAction(Request $request)
{
$page = $request->query->get('page', 1);
$limit = $request->query->get('limit', 10);
$sort = $request->query->get('sort');
$fields = $request->query->get('fields');
$keywords = $request->query->get('keywords');
$posts = $this->postElasticsearchService->getPosts($page, $limit, $sort, $fields, $keywords);
return [
'page' => $page,
'limit' => $limit,
'sort' => $sort,
'fields' => $fields,
'keywords' => $keywords,
'posts' => $posts
];
}
}
services:
application_search.controller.search:
class: Application\SearchBundle\Controller\SearchController
arguments:
- @application_search.service.post_elasticsearch
namespace Application\SearchBundle\Service;
interface PostElasticsearchServiceInterface
{
public function getPosts($page, $limit, $sort = null, $fields = null, $keywords = null);
}
namespace Application\SearchBundle\Service;
use Application\SearchBundle\Factory\PostFactoryInterface;
use Application\SearchBundle\Util\Elasticsearch;
class PostElasticsearchService implements PostElasticsearchServiceInterface
{
private $elasticsearch;
private $postFactory;
public function __construct(
Elasticsearch $elasticsearch,
PostFactoryInterface $postFactory
) {
$this->elasticsearch = $elasticsearch;
$this->postFactory = $postFactory;
}
public function getPosts($page, $limit, $sort = null, $fields = null, $keywords = null)
{
return $this->postFactory->createPostResult(
$this->elasticsearch->find($page, $limit, $sort, $fields, $keywords)
);
}
}
services:
application_search.service.post_elasticsearch:
class: Application\SearchBundle\Service\PostElasticsearchService
arguments:
- @application_search.util.elasticsearch
- @application_search.factory.post
namespace Application\SearchBundle\Factory;
interface PostFactoryInterface
{
public function createPostResult(array $result = []);
}
namespace Application\SearchBundle\Factory;
use Application\SearchBundle\Entity\Post as PostEntity;
use Application\SearchBundle\Model\Post;
use Application\SearchBundle\Model\Result;
class PostFactory implements PostFactoryInterface
{
public function createPostResult(array $result = [])
{
$postResult = new Result();
$postResult->page = $result['page'];
$postResult->limit = $result['limit'];
$postResult->total = $result['total'];
$postResult->posts = $this->getPosts($result['data']);
return $postResult;
}
private function getPosts(array $posts = [])
{
$data = [];
/** @var PostEntity $postEntity */
foreach ($posts as $postEntity) {
$post = new Post();
$post->id = $postEntity->getId();
$post->title = $postEntity->getTitle();
$post->description = $postEntity->getDescription();
$post->author = $postEntity->getAuthor();
$post->year = $postEntity->getYear();
$post->price = $postEntity->getPrice();
$post->isPublished = $postEntity->getIsPublished();
$post->createdAt = $postEntity->getCreatedAt()->format(DATE_ISO8601);
$data[] = $post;
}
return $data;
}
}
services:
application_search.factory.post:
class: Application\SearchBundle\Factory\PostFactory
Eğer daha farklı sorgu yapma gibi bir planınız olursa, getSearchByQuery()
methodunu değiştirmeniz yeterli olur. Şu an için sadece basit bir full-text arama yapmaya ayarlıdır.
namespace Application\SearchBundle\Util;
use FOS\ElasticaBundle\Finder\TransformedFinder;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class Elasticsearch
{
const ANALYSED_SUFFIX = '.raw';
// These fields must be set as "multivalue" fields in config.yml
private $stringSortingMultivalueFields = ['title', 'description'];
private $postFinder;
public function __construct(TransformedFinder $postFinder)
{
$this->postFinder = $postFinder;
}
public function find($page, $limit, $sort = null, $fields = null, $keywords = null)
{
$page = $page > 0 ? $page : 1;
$query = null;
if (!$fields && !$keywords) {
$query = $this->getListByQuery($sort);
} elseif ($fields && $keywords) {
$query = $this->getSearchByQuery($sort, $fields, $keywords);
}
if (!$query) {
throw new BadRequestHttpException('Invalid request.');
}
/*
$query['from'] = $page-1;
$query['size'] = $limit;
print_r($query); echo PHP_EOL; echo json_encode($query); exit;
*/
$paginator = $this->postFinder
->findPaginated($query)
->setNormalizeOutOfRangePages(true)
->setMaxPerPage($limit)
->setCurrentPage($page);
return [
'page' => $paginator->getCurrentPage(),
'limit' => $paginator->getMaxPerPage(),
'total' => $paginator->getNbResults(),
'data' => $paginator->getCurrentPageResults()
];
}
private function getListByQuery($sort = null)
{
$query = [];
$query['query']['bool']['must'][]['match_all'] = [];
$query['sort'] = $this->getSort($sort);
return $query;
}
private function getSearchByQuery($sort, $fields, $keywords)
{
$query = [];
$searchFields = explode(',', $fields);
if (count($searchFields) == 1) {
$query['query']['match'][$searchFields[0]]['query'] = $keywords;
} else {
$query['query']['multi_match']['type'] = 'most_fields';
$query['query']['multi_match']['query'] = $keywords;
$query['query']['multi_match']['fields'] = $searchFields;
}
$query['sort'] = $this->getSort($sort, true);
return $query;
}
private function getSort($sort = null, $search = false)
{
$id = true;
$query = [];
// Applies to only "search" requests
if ($search) {
$query[]['_score']['order'] = 'desc';
}
if (!is_null($sort)) {
$sortFields = explode(',', $sort);
foreach ($sortFields as $sortField) {
// If the field is one of string sorting ones then use not_analyzed "*.raw" version
if (in_array($sortField, $this->stringSortingMultivalueFields)) {
$sortField = $sortField.self::ANALYSED_SUFFIX;
}
// Get correct direction
if (substr($sortField, 0, 1) === '-') {
$sortField = substr($sortField, 1);
$query[][$sortField]['order'] = 'desc';
} else {
$query[][$sortField]['order'] = 'asc';
}
if ($sortField == 'id') {
$id = false;
}
}
// If "id" field is not specified by user in request then include it by default
if ($id) {
$query[]['id']['order'] = 'asc';
}
} else {
$query[]['id']['order'] = 'asc';
}
return $query;
}
}
Elasticsearch servis ismi config.yml içindeki bilgilere göre otomatik olarak yaratılır yani, @fos_elastica.finder.indexes_name.type_name
bizim için @fos_elastica.finder.post_index.post
olacaktır.
services:
application_search.util.elasticsearch:
class: Application\SearchBundle\Util\Elasticsearch
arguments:
- @fos_elastica.finder.post_index.post
namespace Application\SearchBundle\Model;
class Result
{
/**
* @var int
*/
public $page;
/**
* @var int
*/
public $limit;
/**
* @var int
*/
public $total;
/**
* @var Post[]
*/
public $posts = [];
}
namespace Application\SearchBundle\Model;
class Post
{
/**
* @var int
*/
public $id;
/**
* @var string
*/
public $title;
/**
* @var string
*/
public $description;
/**
* @var string
*/
public $author;
/**
* @var int
*/
public $year;
/**
* @var double
*/
public $price;
/**
* @var boolean
*/
public $isPublished;
/**
* @var string
*/
public $createdAt;
}
Öncelikle veritabanını doldurmak için $ app/console populate:post --total=5000
komutunu kullanın.
namespace Application\SearchBundle\Command;
use Application\SearchBundle\Entity\Post;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class PopulatePostCommand extends Command
{
const TOTAL = 'total';
private $entityManager;
private $authors = ['Robert DeNiro', 'Robert', 'DeNiro', 'Al Pacino', 'Al', 'Pacino', 'Andy Garcia', 'Andy'];
private $titles = ['Title', 'Title 1', 'Title 2', 'Title 3', 'Eltit', 'Eltit A', 'Eltit B', 'Desc', 'Desc 3'];
private $descriptions = ['Desc', 'Desc 1', 'Desc 2', 'Cript', 'Cript A', 'Cript B', 'Title', 'Title 3'];
private $years = [2000, 2005, 2010, 2015];
private $prices = [0.5, 1, 2.5, 3.99, 4.00, 5.55];
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct();
$this->entityManager = $entityManager;
}
protected function configure()
{
$this
->setName('populate:post')
->setDescription('Populates post entity.')
->addOption(
self::TOTAL,
null,
InputOption::VALUE_REQUIRED,
'Defines total amount of records to be created.'
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
for ($i = 0; $i < $input->getOption(self::TOTAL); $i++) {
$post = new Post();
$post->setAuthor($this->authors[array_rand($this->authors)]);
$post->setTitle($this->titles[array_rand($this->titles)]);
$post->setDescription($this->descriptions[array_rand($this->descriptions)]);
$post->setYear($this->years[array_rand($this->years)]);
$post->setPrice($this->prices[array_rand($this->prices)]);
$post->setIsPublished(array_rand([true, false]));
$this->entityManager->persist($post);
if ($i%10 == 0) {
$this->entityManager->flush();
}
}
$this->entityManager->flush();
}
}
services:
application_search.command.populate_post:
class: Application\SearchBundle\Command\PopulatePostCommand
tags:
- { name: console.command }
arguments:
- @doctrine.orm.entity_manager
$ app/console fos:elastica:populate
5000/5000 [============================] 100%
Populating post_index/post
Refreshing post_index
Refreshing post_index
# Request
GET /search/elasticsearch?page=1&limit=10
# Response array
Array
(
[query] => Array
(
[bool] => Array
(
[must] => Array
(
[0] => Array
(
[match_all] => Array
(
)
)
)
)
)
[sort] => Array
(
[0] => Array
(
[id] => Array
(
[order] => asc
)
)
)
[from] => 0
[size] => 10
)
# Response json
{
"query": {
"bool": {
"must": [
{
"match_all": [
]
}
]
}
},
"sort": [
{
"id": {
"order": "asc"
}
}
],
"from": 0,
"size": "10"
}
# Request
GET /search/elasticsearch?page=1&limit=10&sort=title,-year
# Response array
Array
(
[query] => Array
(
[bool] => Array
(
[must] => Array
(
[0] => Array
(
[match_all] => Array
(
)
)
)
)
)
[sort] => Array
(
[0] => Array
(
[title.raw] => Array
(
[order] => asc
)
)
[1] => Array
(
[year] => Array
(
[order] => desc
)
)
[2] => Array
(
[id] => Array
(
[order] => asc
)
)
)
[from] => 0
[size] => 10
)
# Response json
{
"query": {
"bool": {
"must": [
{
"match_all": [
]
}
]
}
},
"sort": [
{
"title.raw": {
"order": "asc"
}
},
{
"year": {
"order": "desc"
}
},
{
"id": {
"order": "asc"
}
}
],
"from": 0,
"size": "10"
}
# Request
GET /search/elasticsearch?page=1&limit=10&sort=description&fields=title&keywords=inanzzz
# Response array
Array
(
[query] => Array
(
[match] => Array
(
[title] => Array
(
[query] => inanzzz
)
)
)
[sort] => Array
(
[0] => Array
(
[_score] => Array
(
[order] => desc
)
)
[1] => Array
(
[description.raw] => Array
(
[order] => asc
)
)
[2] => Array
(
[id] => Array
(
[order] => asc
)
)
)
[from] => 0
[size] => 10
)
# Response json
{
"query": {
"match": {
"title": {
"query": "inanzzz"
}
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"description.raw": {
"order": "asc"
}
},
{
"id": {
"order": "asc"
}
}
],
"from": 0,
"size": "10"
}
# Request
GET /search/elasticsearch?page=1&limit=10&sort=description&fields=title,description&keywords=inanzzz
# Response array
Array
(
[query] => Array
(
[multi_match] => Array
(
[type] => most_fields
[query] => inanzzz
[fields] => Array
(
[0] => title
[1] => description
)
)
)
[sort] => Array
(
[0] => Array
(
[_score] => Array
(
[order] => desc
)
)
[1] => Array
(
[description.raw] => Array
(
[order] => asc
)
)
[2] => Array
(
[id] => Array
(
[order] => asc
)
)
)
[from] => 0
[size] => 10
)
# Response json
{
"query": {
"multi_match": {
"type": "most_fields",
"query": "inanzzz",
"fields": [
"title",
"description"
]
}
},
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"description.raw": {
"order": "asc"
}
},
{
"id": {
"order": "asc"
}
}
],
"from": 0,
"size": "10"
}