PHP Symfony Search Sphinx

Работа с real-time индексом Sphinxsearch

В данном примере используются в php-7.4, symfony 5.4, sphinxsearch 3.3.1, postgresql 12

Настраиваем соединение к sphinxsearch в config/packages/doctrine.yaml

doctrine:
  dbal:
    default_connection: default
    connections:
      default:
        dbname: '%env(resolve:DATABASE_NAME)%'
        user: '%env(resolve:DATABASE_USERNAME)%'
        password: '%env(resolve:DATABASE_PASSWORD)%'
        host: '%env(resolve:DATABASE_HOST)%'
        port: '%env(resolve:DATABASE_PORT)%'
        server_version: '%env(resolve:DATABASE_SERVER_VERSION)%'
        driver: '%env(resolve:DATABASE_DRIVER)%'
        default_table_options:
          charset: utf8mb4
          collate: utf8mb4_unicode_ci
      search:
        host: '%env(resolve:SPHINX_HOST)%'
        port: '%env(resolve:SPHINX_PORT)%'
        driver: 'pdo_mysql'

Подготовка сущности для отправки в индекс

Для работы с индексом нужно реализовать App\Entity\RealTimeIndexInterface. В методе getTypeFields() возвращать массив, в котором ключи - наименование полей, значения - типы полей.

namespace App\Entity;

use DateTimeImmutable;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\TagRepository")
 * @ORM\Table(name="tags", schema="public")
 */
class Tag implements RealTimeIndexInterface
{
    /**
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     * @ORM\Id
     */
    private ?int $id;
    /**
     * @var string
     * @ORM\Column(type="string", nullable=false, unique=true)
     */
    private string $slug;
    /**
     * @ORM\Column(type="string", nullable=false)
     */
    private string $title;
    /**
     * @ORM\Column(type="datetime_immutable")
     */
    private DateTimeImmutable $created;
    /**
     * @ORM\Column(type="datetime_immutable")
     */
    private DateTimeImmutable $updated;

// ...

    public function getDataForSave(): array
    {
        return [
            'id' => $this->getId(),
            'slug' => $this->getSlug(),
            'title' => $this->getTitle()
        ];
    }

    public function getTypeFields(): array
    {
        return [
            'id' => ParameterType::INTEGER,
            'slug' => ParameterType::STRING,
            'title' => ParameterType::STRING
        ];
    }
}

Реализация CRUD для индекса

В App\Service\Tag\TagService при добавлении, изменении и удалении, вызывается соответствующие события. Событие ожидает получить сущность реализующую App\Entity\RealTimeIndexInterface.
На эти события подписан App\Event\Listener\UpdateSearchIndexSubscriber. Слушатель получает соединение с sphinxsearch.

services:
  App\Service\Search\SphinxSearchService:
    class: App\Service\Search\SphinxSearchService
    arguments:
      - '@doctrine.dbal.search_connection'
namespace App\Event\Listener;

use App\Event\Search\CreatedEvent;
use App\Event\Search\DeletedEvent;
use App\Event\Search\UpdatedEvent;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class UpdateSearchIndexSubscriber implements EventSubscriberInterface
{
    private Connection $connection;
    private LoggerInterface $logger;

    public function __construct(Connection $connection, LoggerInterface $logger)
    {
        $this->connection = $connection;
        $this->logger = $logger;
    }

    public static function getSubscribedEvents(): array
    {
        return [
            CreatedEvent::class => 'onEntityCreated',
            UpdatedEvent::class => 'onEntityUpdated',
            DeletedEvent::class => 'onEntityDeleted',
        ];
    }

    public function onEntityCreated(CreatedEvent $event): void
    {
        try {
            $entity = $event->getEntity();

            $this->connection->insert($event->getIndexName(), $entity->getDataForSave(), $entity->getTypeFields());
        } catch (Exception $exception) {
            $this->logger->error('Create failed: ' . $exception->getMessage());
        }
    }

    public function onEntityUpdated(UpdatedEvent $event): void
    {
        try {
            $entity = $event->getEntity();

            $this->connection->delete($event->getIndexName(), $event->getCriteria(), $entity->getTypeFields());

            $this->connection->insert($event->getIndexName(), $entity->getDataForSave(), $entity->getTypeFields());
        } catch (Exception $exception) {
            $this->logger->error('Index do not updated: ' . $exception->getMessage());
        }
    }

    public function onEntityDeleted(DeletedEvent $event): void
    {
        try {
            $entity = $event->getEntity();

            $this->connection->delete($event->getIndexName(), $event->getCriteria(), $entity->getTypeFields());
        } catch (Exception $exception) {
            $this->logger->error('Index do not deleted: ' . $exception->getMessage());
        }
    }
}

Методы onEntityCreated и onEntityDeleted не особо интересны, т.к. операторы INSERT INTO и DELETE FROM одинаковы для всех субд, включая sphinxsearch.

Метод обновления данных onEntityUpdated сначала удаляем запись по критерию, а затем добавляет снова. Сделано так потому что sphinxsearch не поддерживает оператор UPDATE, только REPLACE. Ничего лучше я придумать не смог.

Исходный код: github.com

Авторизуйтесь, что бы оставить комментарий!