Aller au contenu principal

Meilisearch : l'art de la recherche instantanée

Marre du SQL LIKE ? Découvrez comment intégrer Meilisearch à Symfony pour une recherche résiliente, asynchrone et une latence inférieure à 50ms.
Catégorie

PHP

Le PHP de 2025 n'a plus rien à voir avec celui d'il y a 10 ans. Êtes-vous sûr d'utiliser tout le potentiel des dernières versions pour écrire un code moderne et ultra-performant ?

Lecture
5 min
Niveau
Intermédiaire
janv 19 2026
Partager

La recherche interne est le talon d'Achille du web moderne. En 2026, l'utilisateur a été conditionné par des années d'instantanéité : il ne cherche pas, il trouve.

Pourtant, la majorité des plateformes techniques continuent de s'appuyer sur des architectures archaïques, où une simple faute de frappe mène à une impasse et où la latence se compte en secondes. C'est une dette technique invisible qui dégrade silencieusement l'expérience utilisateur.

Sur mon site, j'ai pris le parti de traiter la recherche non pas comme une fonctionnalité annexe, mais comme le coeur de la navigation. L'objectif était clair : atteindre une faible latence et offrir une tolérance totale aux erreurs syntaxiques.

Pour y parvenir sans déployer une infrastructure lourde de type ElasticSearch, mon choix s'est porté sur Meilisearch.

Au-delà du SQL

L'approche traditionnelle — une requête SQL LIKE via Doctrine — atteint rapidement ses limites structurelles. Elle souffre d'un triple handicap : performance, rigidité et absence de pertinence sémantique.

J'ai brièvement évalué les solutions tierces comme Google Programmable Search. Le verdict est sans appel : déléguer la recherche, c'est diluer son identité visuelle et compromettre la confidentialité des données.

Meilisearch s'impose alors comme la solution de référence pour les architectures modernes. Écrit en Rust, il offre une empreinte mémoire minimale et une API typée qui s'intègre naturellement aux standards de développement actuels.

L'abstraction comme standard

L'erreur classique consiste à coupler le moteur de recherche à une entité spécifique. Dans une vision à long terme, l'infrastructure doit rester agnostique.

J'ai donc introduit une interface stricte, SearchableContentInterface, qui agit comme un contrat universel pour tout contenu indexable. Qu'il s'agisse d'un article, d'une page de documentation ou d'un snippet de code, le moteur de recherche ne voit qu'une projection standardisée des données.

PHP
interface SearchableContentInterface
{
    public function getId(): ?Uuid;
    public function getSearchableTitle(): string;
    public function getSearchableContent(): string; // Cleaned, raw text
    public function getSearchableType(): string;    // 'post', 'page', 'snippet'
    public function getSearchableAttributes(): array;
}

C'est une application directe du principe de ségrégation des interfaces (ISP) : le service d'indexation consomme un comportement, pas une implémentation.

Résilience et dégradation gracieuse

Dans une architecture distribuée, la panne n'est pas une anomalie, c'est un état possible. Si le conteneur Meilisearch ne répond plus, l'interface utilisateur ne doit pas s'effondrer.

J'ai conçu le ResilientSearchService autour d'un pattern de proxy défensif. Il encapsule l'appel au moteur de recherche dans un bloc de contrôle qui, en cas d'échec critique (timeout, erreur réseau), bascule silencieusement vers une implémentation SQL LIKE.

PHP
#[AsAlias(SearchServiceInterface::class)]
final readonly class ResilientSearchService implements SearchServiceInterface
{
    public function __construct(
        private Client $meilisearch,
        private SqlSearchService $sqlFallback,
        private LoggerInterface $logger
    ) {}

    public function search(string $query, int $limit = 10): SearchResultCollection
    {
        try {
            return $this->searchWithMeilisearch($query, $limit);
        } catch (ApiException | CommunicationException $e) {
            $this->logger->critical('Search infrastructure failure. Fallback activated.', [
                'error' => $e->getMessage()
            ]);

            return $this->sqlFallback->search($query, $limit);
        }
    }
}

Ce mécanisme garantit la continuité de service. L'expérience est dégradée (perte de la tolérance aux fautes), mais la fonctionnalité reste opérationnelle.

Configuration

Un moteur de recherche est rarement pertinent pour un domaine spécialisé. Toute la valeur réside dans la configuration de l'index.

Via le MeilisearchIndexConfigurator, j'injecte une couche de synonymes techniques qui agit comme un dictionnaire de traduction pour développeurs. Taper "sf" renvoie les résultats "Symfony" ; "regex" mappe vers "Expression Régulière".

En parallèle, l'application d'une liste de mots vides drastique permet de nettoyer le bruit sémantique. Les prépositions et articles sont éliminés avant même l'indexation, optimisant ainsi la densité pertinence/octet de l'index.

Synchronisation asynchrone

La mise à jour de l'index pose un défi de performance : l'indexation synchrone ralentit l'écriture en base de données.

La réponse réside dans Symfony Messenger. J'utilise les événements du cycle de vie Doctrine (postPersist, postUpdate) non pas pour indexer, mais pour dispatcher un message léger.

PHP
#[AsDoctrineListener(event: Events::postPersist)]
final class ContentChangeDoctrineSubscriber
{
    public function __construct(private MessageBusInterface $bus) {}

    public function postPersist(PostLegacyEvent $event): void
    {
        $this->bus->dispatch(new IndexContentMessage(
            $event->getEntity()->getId(),
            get_class($event->getEntity())
        ));
    }
}

Ce découplage est total. Le worker traite l'indexation en arrière-plan, avec une politique de réessai automatique propre à Messenger.

Interface

Le frontend, propulsé par Symfony UX LiveComponent, élimine la complexité d'un framework JS complet tout en conservant la réactivité.

L'expérience est construite autour de deux états clés :

  1. L'attente active : L'utilisation de squelettes de chargement (Skeleton UI) préserve la structure visuelle de la page, éliminant tout Layout Shift.
  2. La découverte contextuelle : Le surlignage (<mark>) des termes trouvés offre un feedback immédiat sur la pertinence du résultat.

Mais le véritable atout UX réside dans la gestion du zéro Résultat. Plutôt que de laisser l'utilisateur face au vide, le système bascule en mode "Sérendipité", suggérant des contenus techniques connexes ou analysant l'URL d'une page 404 pour en extraire des mots-clés probables.

Le mot de la fin

L'intégration de Meilisearch dépasse la simple commodité technique. C'est une brique fondamentale qui transforme une base de connaissances statique en un système d'information vivant.

En combinant la performance brute du Rust, la rigueur architecturale de Symfony et une couche de résilience pragmatique, on obtient une solution qui ne se contente pas de répondre aux requêtes : elle anticipe l'intention de l'utilisateur.

1 réaction

Poursuivre la lecture

Sélectionné avec soin pour vous.

Comment j'ai industrialisé mon side-project avec Google Jules

Ne laissez plus l'IA "deviner" si le code fonctionne. Apprenez à configurer une sandbox Docker pour Google Jules afin d'automatiser la QA, la sécurité et la performance, loin du "Vibe Coding".

Les tests : comment bien jouer au Cluedo

Comprendre enfin la différence entre Mock et Functional Test. Un guide pratique sur PHPUnit basés sur des cas réels.

Docker et les containers : parce-qu'il faut expliquer avec des pancakes

Comprendre le multi-stage build, les healthchecks et l'isolation kernel via la métaphore des pancakes.