Symfony2 FOSElasticaBundle индекс обновления для всех объектов, связанных с обновленной сущностью
Я использую FOSElasticaBundle и Doctrine в своем проекте, а мой код работает для выборочного обновления индекса, используя события жизненного цикла Doctrine. Проблема, с которой я сталкиваюсь, заключается в том, что я обновляю связанный объект отдельно.
Например, человек может быть связан с компанией через многие отношения. Если я обновляю название компании напрямую через компанию, то индексы для лица, связанного с компанией, будут устаревшими и по-прежнему относятся к старому имени компании.
Я немного потерял, как справиться с этим, есть ли у кого-нибудь предложения? Должен ли я полагаться на обновление планового индекса и в то же время справляться с неточными данными индекса, или есть способ, которым я могу вызвать обновление для объектов, связанных с обновленным объектом.
Я полагаюсь на группы JMSSerializer, чтобы установить сопоставления. Я понимаю, что это может быть не лучший способ сделать что-то в долгосрочной перспективе.
Ответы
Ответ 1
Я думаю, что нашел решение на этой странице https://groups.google.com/forum/#!topic/elastica-php-client/WTONX-zBTI4
Спасибо Cassiano
В основном вам нужно расширить FOS\ElasticaBundle\Doctrine\ORM\Listener, чтобы вы могли искать связанные объекты, а затем обновлять их индекс.
class CompanyListener extends BaseListener
{
/** @var \Symfony\Component\DependencyInjection\ContainerInterface */
private $container;
public function setContainer(\Symfony\Component\DependencyInjection\ContainerInterface $container) {
$this->container = $container;
}
protected function initialiseJob() {
$this->objectPersisterJob = $this->container->get('fos_elastica.object_persister.application.job');
$this->em = $this->container->get('doctrine')->getEntityManager(); //maybe move this to postUpdate function so it can be used for all
}
/**
* @param \Doctrine\ORM\Event\LifecycleEventArgs $eventArgs
*/
public function postUpdate(LifecycleEventArgs $eventArgs)
{
/** @var $entity Story */
$entity = $eventArgs->getEntity();
if ($entity instanceof $this->objectClass) {
if ($this->isObjectIndexable($entity)) {
$this->objectPersister->replaceOne($entity);
$this->initialiseJob();
foreach ($entity->getJobOpenings() as $job) {
$this->objectPersisterJob->replaceOne($job);
}
} else {
$this->scheduleForRemoval($entity, $eventArgs->getEntityManager());
$this->removeIfScheduled($entity);
}
}
}
public function preRemove(\Doctrine\Common\EventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof $this->objectClass) {
$this->scheduleForDeletion($entity);
$this->initialiseJob();
foreach ($entity->getJobOpenings() as $job) {
$this->objectPersisterJob->replaceOne($job);
}
}
}
}
и ваши сервисы, определенные ниже
fos_elastica.listener.application.company:
class: 'xxx\RMSBundle\EventListener\CompanyListener'
arguments:
- '@fos_elastica.object_persister.application.company'
- 'xxx\RMSBundle\Entity\Company'
- ['postPersist', 'postUpdate', 'postRemove', 'preRemove']
- id
calls:
- [ setContainer, [ '@service_container' ] ]
tags:
- { name: 'doctrine.event_subscriber' }
это будет обновлять индексы для обоих: -)
Ответ 2
У меня была такая же проблема. Похоже, что моя установка (Symfony 2.5.4 и FOSElastica 3.0.4) немного отличается от вашей. Поэтому возникли некоторые проблемы, чтобы заставить код работать. Я отправляю свое решение, потому что он может быть полезен другим разработчикам там.
Слушатель не находится в FOS\ElasticaBundle\Doctrine\ORM \, но в FOS\ElasticaBundle\Doctrine. Поэтому вам придется использовать этот.
Также мне пришлось использовать Doctrine\Common\EventArgs вместо Doctrine\ORM\Event\LifecycleEventArgs, потому что иначе мой собственный метод postUpdate не был совместим с тем, что был в BaseListener.
В моем приложении курс (семинар) может иметь много сеансов, но в этом проекте эластика будет использовать только те сеансы. Приложение должно знать некоторые детали курса, связанные с сессией курса. Итак, вот мой код:
В config.yml конфигурация моего эластичного пакета выглядит следующим образом:
fos_elastica:
clients:
default: { host: localhost, port: 9200 }
indexes:
courses:
index_name: courses
types:
session:
mappings:
id: ~
name: ~
course:
type: "nested"
properties:
id: ~
name: ~
Немного дальше, еще в config.yml
services:
# some other services here
fos_elastica.listener.courses.course:
class: XXX\CourseBundle\EventListener\ElasticaCourseListener
arguments:
- @fos_elastica.object_persister.courses.course
- ['postPersist', 'postUpdate', 'preRemove']
- @fos_elastica.indexable
calls:
- [ setContainer, ['@service_container', @fos_elastica.object_persister.courses.session ] ]
tags:
- { name: 'doctrine.event_subscriber' }
Мой собственный слушатель (XXX\CourseBundle\EventListener\ElasticaCourseListener) выглядит следующим образом:
<?php
namespace XXX\CourseBundle\EventListener;
use Doctrine\Common\EventArgs;
use FOS\ElasticaBundle\Doctrine\Listener as BaseListener;
use FOS\ElasticaBundle\Persister\ObjectPersister;
use Symfony\Component\DependencyInjection\ContainerInterface;
use XXX\CourseBundle\Entity\Course;
class ElasticaCourseListener extends BaseListener
{
private $container;
private $objectPersisterSession;
public function setContainer(ContainerInterface $container, ObjectPersister $objectPersisterSession)
{
$this->container = $container;
$this->objectPersisterSession = $objectPersisterSession;
}
public function postUpdate(EventArgs $args)
{
$entity = $args->getEntity();
if ($entity instanceof Course) {
$this->scheduledForUpdate[] = $entity;
foreach ($entity->getSessions() as $session) {
$this->objectPersisterSession->replaceOne($session);
}
}
}
}
Теперь, когда я обновляю курс, он будет обновлен как вложенный объект в ElasticSearch; -)
Ответ 3
Я использую FosElastica 3.1.0, и я безуспешно пробовал решение, предоставленное Julien Rm: - (
После многих дней исследований я наконец нашел решение
здесь
$persister = $this->get('fos_elastica.object_persister.jaiuneidee.post');
$persister->insertOne($post);
Надеюсь на эту помощь!
Ответ 4
Извините, я не могу комментировать ваш ответ, но в решении чего-то не хватает. Вы также должны переопределить preRemove.
public function preRemove(\Doctrine\Common\EventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof $this->objectClass) {
$this->scheduleForDeletion($entity);
$this->initialiseJob();
foreach ($entity->getJobOpenings() as $job) {
$this->objectPersisterJob->replaceOne($job);
}
}
}
Ответ 5
со всеми комментариями и моими исследованиями, я сделал общий Gist для автоматического индексации дочерних объектов с помощью fosElastica:
https://gist.github.com/Nightbr/ddb586394d95877dde8ed7445c51d973
Фактически, я переопределяю прослушиватель по умолчанию из FOSElastica и добавляю function updateRelations($entity)
. Мы будем искать все отношения, связанные с $entity
, и если они индексированы в ES (существует тип ES), он обновит связанные документы.
Если кто-то хочет посмотреть на это и сделать какие-то улучшения, было бы здорово! ^^
Заранее спасибо
Ответ 6
С BC Break № 729 от FosElastica 3.1.0 все изменилось, а код выше не работал:
BC BREAK: Removed Doctrine\Listener#getSubscribedEvents. The container configuration now configures tags with the methods to call to avoid loading this class on every request where doctrine is active. #729
Для тех, кто пытается заставить его работать с FOSElastica 3.1.X, вот как мне удалось сделать вложенный объект, который был проиндексирован в его родительский элемент в Elastic Search, когда он будет продолжать/обновлять/удалять вложенный объект:
Определите слушателя службы:
fos_elastica.listener.entity.nested:
class: XX\CoreBundle\EventListener\EventSubscriber\ElasticaNestedListener
arguments:
- @fos_elastica.object_persister.app.entityname
- @fos_elastica.indexable
- {"indexName" : "app", "typeName": "entityname"}
tags:
- { name: 'doctrine.event_subscriber' }
Создайте слушателя:
<?php
class ElasticaNestedListener implements EventSubscriber
{ // some indentations missing!
public function getSubscribedEvents()
{
return array(
'postPersist',
'preRemove',
'postUpdate',
'preFlush',
'postFlush',
);
}
/**
* Object persister.
*
* @var ObjectPersisterInterface
*/
protected $objectPersister;
/**
* Configuration for the listener.
*
* @var array
*/
private $config;
/**
* Objects scheduled for insertion.
*
* @var array
*/
public $scheduledForInsertion = array();
/**
* Objects scheduled to be updated or removed.
*
* @var array
*/
public $scheduledForUpdate = array();
/**
* IDs of objects scheduled for removal.
*
* @var array
*/
public $scheduledForDeletion = array();
/**
* PropertyAccessor instance.
*
* @var PropertyAccessorInterface
*/
protected $propertyAccessor;
/**
* @var IndexableInterface
*/
private $indexable;
/**
* Constructor.
*
* @param ObjectPersisterInterface $objectPersister
* @param IndexableInterface $indexable
* @param array $config
* @param LoggerInterface $logger
*/
public function __construct(
ObjectPersisterInterface $objectPersister,
IndexableInterface $indexable,
array $config = array(),
LoggerInterface $logger = null
) {
$this->config = array_merge(array(
'identifier' => 'id',
), $config);
$this->indexable = $indexable;
$this->objectPersister = $objectPersister;
$this->propertyAccessor = PropertyAccess::createPropertyAccessor();
if ($logger && $this->objectPersister instanceof ObjectPersister) {
$this->objectPersister->setLogger($logger);
}
}
/**
* Looks for objects being updated that should be indexed or removed from the index.
*
* @param LifecycleEventArgs $eventArgs
*/
public function postUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getObject();
if ($entity instanceof EntityName) {
$question = $entity->getParent();
if ($this->objectPersister->handlesObject($question)) {
if ($this->isObjectIndexable($question)) {
$this->scheduledForUpdate[] = $question;
} else {
// Delete if no longer indexable
$this->scheduleForDeletion($question);
}
}
}
}
public function postPersist(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getObject();
if ($entity instanceof EntityName) {
$question = $entity->getParent();
if ($this->objectPersister->handlesObject($question)) {
if ($this->isObjectIndexable($question)) {
$this->scheduledForUpdate[] = $question;
} else {
// Delete if no longer indexable
$this->scheduleForDeletion($question);
}
}
}
}
/**
* Delete objects preRemove instead of postRemove so that we have access to the id. Because this is called
* preRemove, first check that the entity is managed by Doctrine.
*
* @param LifecycleEventArgs $eventArgs
*/
public function preRemove(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getObject();
if ($this->objectPersister->handlesObject($entity)) {
$this->scheduleForDeletion($entity);
}
}
/**
* Persist scheduled objects to ElasticSearch
* After persisting, clear the scheduled queue to prevent multiple data updates when using multiple flush calls.
*/
private function persistScheduled()
{
if (count($this->scheduledForInsertion)) {
$this->objectPersister->insertMany($this->scheduledForInsertion);
$this->scheduledForInsertion = array();
}
if (count($this->scheduledForUpdate)) {
$this->objectPersister->replaceMany($this->scheduledForUpdate);
$this->scheduledForUpdate = array();
}
if (count($this->scheduledForDeletion)) {
$this->objectPersister->deleteManyByIdentifiers($this->scheduledForDeletion);
$this->scheduledForDeletion = array();
}
}
/**
* Iterate through scheduled actions before flushing to emulate 2.x behavior.
* Note that the ElasticSearch index will fall out of sync with the source
* data in the event of a crash during flush.
*
* This method is only called in legacy configurations of the listener.
*
* @deprecated This method should only be called in applications that depend
* on the behaviour that entities are indexed regardless of if a
* flush is successful.
*/
public function preFlush()
{
$this->persistScheduled();
}
/**
* Iterating through scheduled actions *after* flushing ensures that the
* ElasticSearch index will be affected only if the query is successful.
*/
public function postFlush()
{
$this->persistScheduled();
}
/**
* Record the specified identifier to delete. Do not need to entire object.
*
* @param object $object
*/
private function scheduleForDeletion($object)
{
if ($identifierValue = $this->propertyAccessor->getValue($object, $this->config['identifier'])) {
$this->scheduledForDeletion[] = $identifierValue;
}
}
/**
* Checks if the object is indexable or not.
*
* @param object $object
*
* @return bool
*/
private function isObjectIndexable($object)
{
return $this->indexable->isObjectIndexable(
$this->config['indexName'],
$this->config['typeName'],
$object
);
}
}
EntityName может быть комментарием, а getParent() может быть статьей, которая владеет этим комментарием...
Надеюсь на эту помощь!
Ответ 7
Я использую Symphony 3 и FOSElasticaBundle 3.2, и я делал что-то по-другому.
Просмотрев код, приведенный в других ответах, которые очень помогли, я решил не расширять прослушиватель по умолчанию. Вместо этого я позволил этому сделать свое дело, и я просто добавил собственного слушателя.
У меня есть несколько категорий (1), которые могут иметь несколько (многие-ко-многим) темы (2), которые могут иметь несколько (один-ко-многим) Сообщения (3).
Посты - это объекты, которые сохраняются в Elasticsearch с информацией о соответствующем предмете и его собственных категориях.
Так же:
fos_elastica:
#...
indexes:
my_index:
#...
types:
post: # (3)
mappings:
field_one: ~
# ... Other fields
subject: # (2)
type: "object"
properties:
subject_field_one: ~
# ... Other fields
categories: # (1)
type: "nested"
properties:
category_field_one: ~
# ... Other fields
Определение службы (app/config/services.yml)
services:
# ...
app.update_elastica_post.listener:
class: AppBundle\EventListener\UpdateElasticaPostListener
arguments: ['@service_container']
tags:
- { name: doctrine.event_listener, event: postUpdate }
И прослушиватель AppBundle\EventListener\UpdateElasticaPostListener.php
namespace AppBundle\EventListener;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerInterface;
use AppBundle\Entity\Category;
use AppBundle\Entity\Subject;
class UpdateElasticaPostListener
{
private $container;
private $objectPersisterPost;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
$this->objectPersisterPost = null;
}
/**
* @param \Doctrine\ORM\Event\LifecycleEventArgs $eventArgs
*/
public function postUpdate(LifecycleEventArgs $eventArgs)
{
$this->checkAndUpdate($eventArgs);
}
protected function checkAndUpdate(LifecycleEventArgs $eventArgs)
{
$entity = $eventArgs->getEntity();
if ($entity instanceof Category) {
foreach ($entity->getSubjects() as $subject) {
$this->updateSubjectPosts($subject);
}
} elseif ($entity instanceof Subject) {
$this->updateSubjectPosts($entity);
}
}
protected function updateSubjectPosts(Subject $subject)
{
$this->initPostPersister();
foreach ($subject->getPosts() as $post) {
$this->objectPersisterPost->replaceOne($post);
}
}
protected function initPostPersister()
{
if (null === $this->objectPersisterPost) {
// fos_elastica.object_persister.<index_name>.<type_name>
$this->objectPersisterPost = $this->container->get('fos_elastica.object_persister.my_index.post');
}
}
}
И это! Я не пробовал это для события удаления, и теперь, когда я думаю об этом, возможно, это решение не будет лучшим для него... но, возможно, это...
Большое спасибо @Ben Stinton и @maercky выше.
Надеюсь, это поможет! (это мой первый ответ здесь, поэтому я надеюсь, что не испортил)