Акцентирую ваше внимание именно на symfony2, поскольку буду использовать FOSElasticaBundle, который позволяет описывать mapping индексов elasticsearch в удобных yaml конфигах и привязывать к ним сущности Doctrine ORM или документы Doctrine ODM. Промаппленные индексы заполняются из связанных доктриновских сущностей с помощью одной единственной консольной команды. Кроме того, он включает в себя вендорную библиотеку для конструирования поисковых и фильтрационных запросов. Результаты поиска возвращаются в виде массива объектов сущности или документа Doctrine ORM/ODM, привязанной к поисковому индексу. Подробнее о FOSElasticaBundle, традиционно, на гитхабе: github.com/FriendsOfSymfony/FOSElasticaBundle
Использование бандла позволяет полностью абстрагироваться от манипуляций с чистым JSON, что-то кодировать и декодировать функциями json_encode и json_decode, лезть куда-то с помощью сurl. Здесь только ООП подход!
Немного о схеме данных в SQL
Поскольку мои товары хранятся в реляционной СУБД, мне понадобилось реализовать EAV модель для их свойств и значений (подробнее: en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model )
В результате, у меня вышла вот такая схема данных:
дамп базы: drive.google.com/file/d/0B30Ybwiexqx6S1hCanpISHVvcjQ/edit?usp=sharing
По ней создадим доктриновские сущности и их будем маппить в ElasticSearch.
Маппим EAV модель в ElasticSearch
Итак, сначала установим FOSElasticaBundle. В composer.json нужно указать:
"friendsofsymfony/elastica-bundle": "dev-master"
Обновляем зависимости и прописываем установившийся бандл в AppKernel.php:
new FOS\ElasticaBundle\FOSElasticaBundle()
Теперь прописываем в config.yml cледующие настройки:
fos_elastica: clients: default: { host: localhost, port: 9200 } indexes: test: types: product: mappings: name: ~ price: ~ category: ~ productsOptionValues: type: "object" properties: productOption: index: not_analyzed value: type: string index: not_analyzed persistence: driver: orm model: Vendor\TestBundle\Entity\Product provider: ~ listener: immediate: ~ finder: ~
Чтобы заполнить созданный выше индекс данными следует выполнить консольную команду php app/console fos:elastica:populate. В результате чего FOSElasticaBundle заполнит индекс данными из БД.
Примечание: Внутрь товара в виде вложенного объекта мы вкладываем характеристики и их значения. Чтобы всё работало как нужно, следует указать именно type: «object» вместо type: «nested» для коллекции характеристик productsOptionValues. В противном случае, характеристики будут храниться в виде массивов как описано здесь: www.elasticsearch.org/guide/en/elasticsearch/guide/current/complex-core-fields.html#_arrays_of_inner_objects и фильтр будет работать неправильно. Также следует обратить внимание, что фильтруемые поля не должны анализироваться за что отвечает строка index: not_analyzed. В противном случае проблемы возникнут при фильтрации строк, содержащих пробелы.
Теперь вы сможете посмотреть список товаров с вложенными в них характеристиками по адресу localhost:9200/test/product/_search?pretty В моём случае ответ сервера выглядит таким образом:
gist.github.com/ArFeRR/3976778079d64d5a72cd
Рендерим форму фильтрации
Сама форма у меня выглядит следующим образом:
В контроллере выполним запросы на получение всех свойств и товаров, объявим пустой массив фильтра и передадим всё это в TWIG шаблон:
$options = $entityManager->getRepository("ParfumsTestBundle:ProductOption")->findAll(); $products = $entityManager->getRepository("ParfumsTestBundle:Product")->findAll(); $filter = array(); return $this->render('ParfumsTestBundle:Default:filter.html.twig', array('options'=>$options, 'products' => $products, 'filter' => $filter));
Здесь следует выполнить группировку по именам свойств, чтобы избежать их дублирования на форме, но для экономии места я этого не делаю. Напишите запрос на DQL в ваш репозиторий сущности/документа самостоятельно. FindAll запрос по товарам нужен, чтобы вывести весь список товаров, если на фильтре ничего не выбрано.
А вот и сам twig:
{% extends "TwigBundle::layout.html.twig" %} {% block body %} <h1>Фильтр</h1> <form> <ul> {% for option in options %} <li> {{ option.name }} <ul> {% for value in option.productsOptionValues %} <li> <input type="checkbox" value="{{ value.value }}" name="filter[{{ option.name }}][{{ value.id }}]" {% if filter[option.name][value.id] is defined %} checked="checked" {% endif %} /> {{ value.value }} </li> {% endfor %} </ul> </li> {% endfor %} </ul> <input type="submit" /> </form> <h1>Товары</h1> <table> {% for product in products %} <tr> <td>{{ product.name }}</td> <td>{{ product.price }}</td> <td> {% for option_value in product.productsOptionValues %} {{ option_value.productOption }} : {{ option_value.value }} <br /> {% endfor %} </td> </tr> {% endfor %} </table> {% endblock %}
Обрабатываем форму фильтрации
Приступим к самому интересному.
Теперь нам нужно будет сконструировать поисковый запрос (или, точнее — JSON-фильтр), который будет передан ElasticSearch’y для обработки. Делается это с помощью встроенной в FOSElasticaBundle библиотеки Elastica.io (подробнее: elastica.io/ )
Итак, в экшене вашего контроллера обрабатываем массив фильтрации, полученный от формы:
if(isset($_GET['filter'])) { $finder = $this->container->get('fos_elastica.finder.test.product'); $andOuter = new \Elastica\Filter\Bool(); foreach($_GET['filter'] as $option_key=>$arr_values) { $orOuter = new \Elastica\Filter\Bool(); foreach($arr_values as $value) { $andInner = new \Elastica\Filter\Bool(); $option_key_term = new \Elastica\Filter\Term(); $option_key_term->setTerm('productsOptionValues.productOption', $option_key); $value_term = new \Elastica\Filter\Term(); $value_term->setTerm('productsOptionValues.value', $value); $andInner->addMust($option_key_term); $andInner->addMust($value_term); $orOuter->addShould($andInner); } $andOuter->addMust($orOuter); } $filtered = new \Elastica\Query\Filtered(); $filtered->setFilter($andOuter); $products = $finder->find($filtered); $filter = $_GET['filter']; }
Здесь я достаю массив, переданный через адресную строку (для наглядности использую $_GET, но вы используйте симфонивский объект Request — он безопасный) и перебираю выбранные пользователем значения фильтра, чтобы создать древовидную структуру объектов классов по которым библиотека Elastica сгенерирует JSON строку, по которой ElasticSearch будет фильтровать наш набор данных:
gist.github.com/ArFeRR/97671e54515dfd7be012
Этот JSON примерно соответствует следующему условию в реляционной БД:
WHERE ((option=resolution AND value=1980х1020) OR (option=resolution AND value=1600×900)) AND (option=weight AND value= 2,7 kg)
В итоге, в результате мы должны получить товары, у которых обязательно должен совпадать вес и хотя бы одно разрешение экрана из двух, выбранных пользователем. В моём наборе данных — это только 1 товар.
Вроде-бы всё работает правильно.
Приведённый пример фильтрации может быть доработан. Следующим этапом должна стать реализация сортировки результатов по релевантности их постраничный вывод и настройка агрегаций (частной реализации фасетов в ES). Об этом напишу позже, если это будет интересно хабр-сообществу.
ссылка на оригинал статьи http://habrahabr.ru/post/229905/
Добавить комментарий