En l’ecosistema modern, Es tracta de , i Com a desenvolupador sènior de simfonia, vostè sap que les consultes LIKE %...% són una trampa tècnica de deute. “search” is not just about finding text performance relevance user experience En aquest article es detalla com fer una La integració d'Elasticsearch a Symfony 7.4.No estem simplement "instal·lant un paquet"; estem construint una arquitectura de cerca resilient i sense temps d'aturada utilitzant , i per . production-grade PHP 8.4 features Attributes Symfony Messenger asynchronous indexing L’arquitectura: Performance & Resilience En una implementació júnior, una actualització d'entitat desencadena una trucada HTTP sincronitzada a Elasticsearch. Si Elastic està avall, l'usuari no pot guardar les seves dades. La nostra estratègia de producció: Zero-Downtime Indexing: Mai escrivim directament a l'índex en viu. Escrivim a un índex amb timestamp i utilitzem un alias per apuntar l'aplicació a la versió en viu actual. Índex Async: Els escrits de base de dades es desconnecten dels escrits de cerca utilitzant Symfony Messenger. Tipificació estricta: Utilitzem DTOs i serveis de tipus fort, evitant "arranjaments màgics" on sigui possible. Requisits i instal·lació Utilitzarem f (v7.0+). proporciona la millor abstracció sobre el cru client mentre s'adhereix als estàndards de configuració de Symfony. riendsofsymfony/elastica-bundle elasticsearch-php Requisits ambientals: PHP 8.2 i més (rec. Simfonia 7.4 Elasticsearch 8.x Instal·lació de dependències Executa el següent en el teu terminal: composer require friendsofsymfony/elastica-bundle "^7.0" composer require symfony/messenger composer require symfony/serializer Configuració ambiental Afegeix el teu El teu En la producció, assegureu-vos que s'emmagatzema en un gestor secret (com o ) i Elasticsearch DSN .env Symfony Secrets HashiCorp Vault # .env ELASTICSEARCH_URL=http://localhost:9200/ Instal·lació “Zero-Downtime” Això és on la majoria dels tutorials fracassen. configuren un nom d'índex estàtic. Configurarem una estratègia aliada per permetre reindexar el fons sense treure el lloc. Creació o actualització : config/packages/fos_elastica.yaml # config/packages/fos_elastica.yaml fos_elastica: clients: default: url: '%env(ELASTICSEARCH_URL)%' # Production Tip: Increase timeout for bulk operations config: connect_timeout: 5 timeout: 10 indexes: app_products: # "use_alias: true" is critical for zero-downtime reindexing use_alias: true # Define your distinct settings (analyzers, filters) settings: index: analysis: analyzer: app_analyzer: type: custom tokenizer: standard filter: [lowercase, asciifolding] # Your Persistence strategy (Doctrine integration) persistence: driver: orm model: App\Entity\Product provider: ~ # CRITICAL: We disable the default listener to use Messenger instead listener: insert: false update: false delete: false finder: ~ # Explicit Mapping (Always prefer explicit over dynamic for production) properties: id: { type: integer } name: type: text analyzer: app_analyzer fields: keyword: { type: keyword, ignore_above: 256 } description: { type: text, analyzer: app_analyzer } price: { type: float } stock: { type: integer } created_at: { type: date } El nivell de domini Suposem una entitat de producte estàndard. Utilitzem els atributs estàndard de PHP 8. namespace App\Entity; use App\Repository\ProductRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ProductRepository::class)] #[ORM\Table(name: 'products')] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; #[ORM\Column(type: Types::TEXT)] private ?string $description = null; #[ORM\Column] private ?float $price = null; #[ORM\Column] private ?int $stock = 0; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // ... Getters and Setters public function getId(): ?int { return $this->id; } // ... } Índex Async amb Messenger (el patró sènior) En lloc de deixar retardar les sol·licituds dels nostres usuaris indexant immediatament, enviarem un missatge a una cua. fos_elastica El missatge Una senzilla Identificació de l’entitat que ha canviat. DTO (Data Transfer Object) namespace App\Message; final readonly class IndexProductMessage { public function __construct( public int $productId, // 'index' or 'delete' public string $action = 'index' ) {} } Subscriptor de l'esdeveniment del cicle vital Escoltem els esdeveniments de la Doctrina per enviar automàticament el nostre missatge. namespace App\EventListener; use App\Entity\Product; use App\Message\IndexProductMessage; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\ORM\Event\PostPersistEventArgs; use Doctrine\ORM\Event\PostRemoveEventArgs; use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Events; use Symfony\Component\Messenger\MessageBusInterface; #[AsDoctrineListener(event: Events::postPersist, priority: 500, connection: 'default')] #[AsDoctrineListener(event: Events::postUpdate, priority: 500, connection: 'default')] #[AsDoctrineListener(event: Events::postRemove, priority: 500, connection: 'default')] class ProductIndexerSubscriber { public function __construct( private MessageBusInterface $bus ) {} public function postPersist(PostPersistEventArgs $args): void { $this->dispatch($args->getObject(), 'index'); } public function postUpdate(PostUpdateEventArgs $args): void { $this->dispatch($args->getObject(), 'index'); } public function postRemove(PostRemoveEventArgs $args): void { // When removing, we still need the ID, but the object is technically gone from DB. // Ensure you capture the ID before it's fully detached if needed, // but postRemove usually still has access to the object instance. $this->dispatch($args->getObject(), 'delete'); } private function dispatch(object $entity, string $action): void { if (!$entity instanceof Product) { return; } $this->bus->dispatch(new IndexProductMessage($entity->getId(), $action)); } } El comerciant Aquí és on es realitza el treball real en el treballador de fons. namespace App\MessageHandler; use App\Entity\Product; use App\Message\IndexProductMessage; use App\Repository\ProductRepository; use FOS\ElasticaBundle\Persister\ObjectPersisterInterface; use Symfony\Component\Messenger\Attribute\AsMessageHandler; #[AsMessageHandler] final class IndexProductHandler { public function __construct( // Inject the specific persister for 'app_products' index // The service ID usually follows the pattern fos_elastica.object_persister.<index_name>.<type_name> // Or you can bind it via services.yaml if autowiring fails private ObjectPersisterInterface $productPersister, private ProductRepository $productRepository ) {} public function __invoke(IndexProductMessage $message): void { if ($message->action === 'delete') { // For deletion, we can't fetch the entity as it's gone. // We pass the ID directly to the persister. // Note: In some setups, you might need a stub object or just the ID. // The ObjectPersisterInterface typically expects an object, // but strictly speaking, Elastica needs an ID. // A cleaner way for delete is often using the Elastica Client directly // if the Persister insists on an Entity object. // For simplicity here, we assume the persister handles ID lookups or we use a custom service. // Production-grade approach: Use the Raw Index Service for deletes to avoid hydration issues // But for this example, let's focus on Indexing. return; } $product = $this->productRepository->find($message->productId); if (!$product) { // Product might have been deleted before this worker ran return; } // This pushes the single object to Elasticsearch $this->productPersister->replaceOne($product); } } Hauràs de registrar la persistència explícitament en El cotxe correcte, o utilitzar atributs si teniu múltiples índexs. services.yaml ObjectPersisterInterface #[Target] # config/services.yaml services: _defaults: bind: # Bind the specific persister to the argument name or type $productPersister: '@fos_elastica.object_persister.app_products' Títol: El patró del repositori No poseu lògica Elastica en els vostres controladors. namespace App\Service\Search; use FOS\ElasticaBundle\Finder\TransformedFinder; class ProductSearchService { public function __construct( // The TransformedFinder returns Doctrine Entities. // If you want raw speed and arrays, use the 'index' service directly. private TransformedFinder $productFinder ) {} /** * @return array<int, \App\Entity\Product> */ public function search(string $query, int $limit = 20): array { // Elastica Query Builder $boolQuery = new \Elastica\Query\BoolQuery(); // Match name or description $matchQuery = new \Elastica\Query\MultiMatch(); $matchQuery->setQuery($query); $matchQuery->setFields(['name^3', 'description']); // Boost name by 3x $matchQuery->setFuzziness('AUTO'); // Handle typos $boolQuery->addMust($matchQuery); // Filter by stock (only in stock items) $stockFilter = new \Elastica\Query\Range('stock', ['gt' => 0]); $boolQuery->addFilter($stockFilter); $elasticaQuery = new \Elastica\Query($boolQuery); $elasticaQuery->setSize($limit); // Returns Hydrated Doctrine Objects return $this->productFinder->find($elasticaQuery); } } Verificació i desplegament Creació de l'Índex Abans que la vostra aplicació pugui funcionar, heu d'iniciar l'índex. php bin/console fos:elastica:create Dades de població (carregament inicial) Si teniu dades existents en MySQL, empreu-les a Elastic. php bin/console fos:elastica:populate Aquest comandament utilitza la lògica Zero-Downtime: crea un nou índex, el omple i després canvia atòmicament l'alias. Verificació a través de cURL Comproveu si el vostre mapa és correcte directament a Elastic. curl -X GET "http://localhost:9200/app_products/_mapping?pretty" Conclusions Ara tens una arquitectura de cerca que: Sobreviu a la càrrega de DB: La cerca arriba a Elastic, no a MySQL. Sobreviu el temps d'aturada elàstica: els missatges s'acosten a la cua en Messenger (RabbitMQ/Redis) i es retreuen més tard. Survives Reindexing: Pot canviar els seus analitzadors i mapes, executar fos:elastica:populate i els usuaris no notaran una cosa. Aquest és l'estàndard per a aplicacions Symfony d'alt rendiment. Connecta’t amb mi a Linkedin ] per a més informació sobre l'arquitectura de PHP i Symfony. https://www.linkedin.com/in/matthew-mochalkin/ https://www.linkedin.com/in/matthew-mochalkin/