13

我在我的项目中使用 FOSElasticaBundle 和 Doctrine,我的代码适用于使用 Doctrine 生命周期事件的选择性索引更新。我遇到的问题是我是否单独更新相关实体。

例如,一个人可能通过多对多关系与一家公司相关联。如果我直接通过公司实体更新公司名称,那么与公司相关的人员的索引将过期,仍然与公司的旧名称相关。

我对如何处理这个问题有点迷茫,有人有什么建议吗?我是否必须依赖计划的索引更新并同时处理不准确的索引数据,或者有没有一种方法可以调用与已更新实体相关的实体的更新。

我依靠 JMSSerializer 组来建立映射。我很欣赏这可能不是长期做事的最佳方式。

4

7 回答 7

11

我有同样的问题。不过,我的安装(Symfony 2.5.4 和 FOSElastica 3.0.4)似乎与您的安装有很大不同。因此,使代码正常工作存在一些问题。我正在发布我的解决方案,因为它可能对其他开发人员有用。

Listener 不在 FOS\ElasticaBundle\Doctrine\ORM\ 中,而是在 FOS\ElasticaBundle\Doctrine 中。所以你必须使用那个。我还必须使用 Doctrine\Common\EventArgs 而不是 Doctrine\ORM\Event\LifecycleEventArgs,因为否则我自己的 postUpdate 方法与 BaseListener 中的方法不兼容。

在我的应用程序中,一个课程(研讨会)可以有很多会话,但在这个项目中,elastica 只会使用这些会话。该应用程序需要知道与课程会话相关的课程的一些详细信息。所以,这是我的代码:

在 config.yml 我的 elastica bundle 配置如下所示:

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 中的嵌套对象进行更新;-)

于 2014-09-16T13:48:11.280 回答
4

我想我已经在这个页面上找到了解决方案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' }

然后这将更新两者的索引:-)

于 2014-02-21T14:05:47.240 回答
4

我正在使用 FosElastica 3.1.0,我尝试了 Julien Rm 提供的解决方案但没有成功:-(

经过多天的研究,我终于在 这里找到了解决方案

$persister = $this->get('fos_elastica.object_persister.jaiuneidee.post');
$persister->insertOne($post);

希望这有帮助!

于 2015-10-23T13:26:51.230 回答
3

with all comments and my research, I made a generic Gist for auto index child objects with fosElastica:

https://gist.github.com/Nightbr/ddb586394d95877dde8ed7445c51d973

In fact, I override the default Listener from FOSElastica and I add the function updateRelations($entity). We will search all relations linked to the $entity and if there are indexed in ES (the ES type exists) it will update the related documents.

If anyone want to look at it and make any improvement it would be great! ^^

Thanks in advance

于 2016-07-12T15:53:49.220 回答
2

抱歉,我无法在您的回答下发表评论,但解决方案中缺少某些内容。您也必须覆盖 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);
            }
    }
}
于 2014-06-17T10:58:56.043 回答
1

随着 FosElastica 3.1.0 的 BC Break #729,事情发生了变化,上面的代码不起作用:

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() 可以是拥有该评论的文章...

希望这有帮助!

于 2015-09-26T16:35:36.713 回答
1

我正在使用 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');
        }
    }
}

And that's it! I didn't try it for the remove event and now that I think about it, maybe this solution wouldn't be the best one for it... but maybe it is...

Thanks a lot to @Ben Stinton and @maercky above.

I hope it helps! (this is my first answer around here so I hope I didn't screw up)

于 2016-04-29T10:31:04.173 回答