Работа с 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