Chapitre 6.7 — Recherche avec Meilisearch
Partie 6 : Doctrine ORM — Chapitre 7/7 Version de référence : Symfony 7.4 / Meilisearch.
⏱️ TL;DR
LIKE '%mot%'en SQL est lent (pas d’index utilisable) et pauvre (pas de tolérance aux fautes, pas de pertinence). Pour une vraie recherche, on utilise un moteur dédié.- Meilisearch est un moteur de recherche instantané, tolérant aux fautes, simple à opérer (un binaire / un conteneur Docker). Idéal pour BookLab (chercher un prestataire, un service).
- On l’intègre via le bundle Meilisearch-Symfony : on déclare les entités indexables, et le bundle synchronise l’index sur
persist/update/remove(événements Doctrine) + une commande d’import initial.- La recherche renvoie des résultats classés par pertinence ; on peut ensuite recharger les entités Doctrine correspondantes (ou renvoyer directement les documents).
- Côté Next.js, on peut interroger l’API Symfony ou Meilisearch directement avec une clé « search-only » (recherche as-you-type ultra-rapide).
- Alternatives : recherche plein-texte PostgreSQL (suffisante pour des besoins simples), Elasticsearch (plus lourd, plus puissant).
🎯 Objectifs
- Comprendre pourquoi
LIKEne suffit pas et ce qu’apporte un moteur de recherche. - Intégrer Meilisearch à des entités Doctrine.
- Indexer, synchroniser et interroger.
- Savoir comment le front Next.js consomme la recherche.
1. Pourquoi pas LIKE ?
Chercher « coach » dans des noms de services avec WHERE name LIKE '%coach%' semble simple, mais :
- c’est lent : un
LIKE '%...%'(joker au début) ne peut pas utiliser d’index → scan complet ; - c’est bête : « coch » (faute de frappe) ne trouve rien ; pas de classement par pertinence ; pas de recherche sur plusieurs champs pondérés ; pas de mise en évidence.
Un moteur de recherche résout tout ça : index inversé (rapide), tolérance aux fautes, pertinence, facettes, mise en surbrillance. Meilisearch offre ça avec une simplicité d’exploitation remarquable.
2. Démarrer Meilisearch
En Docker, on ajoute un service :
# compose.yaml (extrait)
services:
meilisearch:
image: getmeili/meilisearch:latest
environment:
MEILI_MASTER_KEY: '${MEILI_MASTER_KEY:-masterKey}'
ports:
- "7700"
volumes:
- meili_data:/meili_dataPuis on installe le bundle et le client :
composer require meilisearch/search-bundleConfiguration : on déclare les entités indexables et le mapping index → entité.
# config/packages/meilisearch.yaml
meilisearch:
url: '%env(MEILISEARCH_URL)%'
api_key: '%env(MEILISEARCH_API_KEY)%'
indices:
- name: providers
class: App\Entity\Provider
- name: services
class: App\Entity\Service3. Rendre une entité indexable
On expose les champs à indexer (via des attributs de sérialisation / groupes). Le bundle prend un snapshot des champs choisis pour chaque entité et l’envoie à Meilisearch.
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity]
class Service
{
#[Groups('searchable')]
private string $name;
#[Groups('searchable')]
private ?string $description = null;
// les champs marqués 'searchable' partent dans l'index Meilisearch
}4. Synchroniser l’index
Deux moments :
- Import initial (données existantes) via une commande fournie par le bundle :
php bin/console meilisearch:import- Mises à jour en continu : le bundle écoute les événements Doctrine (
postPersist,postUpdate,postRemove) et synchronise automatiquement l’index à chaque changement d’entité. Créer/modifier/supprimer unServicemet l’index à jour sans code de votre part.
⚠️ Piège : la synchronisation via événements Doctrine se fait dans la requête. Si Meilisearch est momentanément indisponible, l’indexation peut échouer et ralentir la requête. En production robuste, on découple l’indexation via Messenger (Partie 12) : à chaque changement, on dispatche un message « réindexer l’entité X », traité par un worker. L’écriture en base ne dépend alors plus de la disponibilité du moteur de recherche.
5. Interroger
La recherche passe par le service du bundle (ou le client Meilisearch), et renvoie les documents classés par pertinence. On peut ensuite recharger les entités Doctrine correspondantes par leurs id, ou renvoyer directement les documents (plus rapide pour une API de recherche).
use Meilisearch\Bundle\SearchService;
final class ServiceSearcher
{
public function __construct(private readonly SearchService $search) {}
/** @return Service[] */
public function search(string $query): array
{
// renvoie des entités Service hydratées, classées par pertinence
return $this->search->search($this->em, Service::class, $query);
}
}Un endpoint d’API expose ça :
#[Route('/api/search/services', methods: ['GET'])]
public function searchServices(#[MapQueryParameter] string $q, ServiceSearcher $searcher): JsonResponse
{
return $this->json($searcher->search($q));
}6. Côté Next.js : deux stratégies
- A) Via l’API Symfony : le front appelle
/api/search/services?q=.... Symfony contrôle l’accès, filtre, et façonne la réponse. Simple et sécurisé. - B) En direct : le front interroge Meilisearch avec une clé « search-only » (une clé publique restreinte à la lecture d’index précis). C’est ce qui permet la recherche as-you-type ultra-rapide (chaque frappe interroge Meilisearch sans passer par votre backend). On expose alors uniquement une clé de recherche, jamais la master key.
💡 Pour un dev Next.js : la stratégie B est celle qu’utilisent des composants comme InstantSearch (Algolia/Meilisearch) — la recherche instantanée côté client. Symfony reste maître de l’indexation (ce qui entre dans l’index, avec quels droits) et délègue la lecture au moteur via une clé restreinte. Vous connaissez ce pattern côté Algolia ; Meilisearch en est l’équivalent open source auto-hébergeable.
📚 Aller plus loin : meilisearch.com/docs (réglages de pertinence, facettes, clés d’API) et le dépôt du bundle Symfony. Pour des besoins simples, la recherche plein-texte de PostgreSQL (
tsvector) évite même d’ajouter un service.
✏️ Exercices
Exercice 1. Pourquoi WHERE name LIKE '%coach%' est-il un mauvais choix pour une recherche utilisateur à grande échelle ?
✅ Solution
Parce qu’un LIKE '%...%' avec joker en tête ne peut pas utiliser d’index (scan complet → lent sur de gros volumes), et parce qu’il est pauvre : aucune tolérance aux fautes (« coch » ne trouve rien), pas de classement par pertinence, pas de recherche multi-champs pondérée ni de surbrillance. Un moteur dédié (Meilisearch) apporte index rapide, tolérance aux fautes et pertinence.
Exercice 2. L’indexation Meilisearch synchrone ralentit vos écritures et échoue quand le moteur redémarre. Comment rendre ça robuste ?
✅ Solution
Découpler l’indexation via Messenger (Partie 12) : à chaque changement d’entité, dispatcher un message « réindexer X » traité par un worker (avec retries). L’écriture en base ne dépend plus de la disponibilité de Meilisearch ; l’index se met à jour en arrière-plan, et un échec temporaire est retenté sans casser la requête utilisateur.
Exercice 3. Le front Next.js veut une recherche as-you-type très rapide. Quelle stratégie d’accès, et quelle précaution de sécurité ?
✅ Solution
Interroger Meilisearch directement depuis le front (stratégie B) pour la latence minimale. Précaution : n’exposer qu’une clé « search-only » (lecture restreinte à certains index), jamais la master key ni une clé d’écriture. Symfony reste maître de l’indexation et de la génération de clés restreintes ; le front ne fait que lire.
🧠 Quiz de révision
1. Pourquoi utiliser un moteur de recherche plutôt que LIKE ?
LIKE ?Pour la vitesse (index inversé vs scan complet) et la qualité : tolérance aux fautes, pertinence, multi-champs, surbrillance — que LIKE n’offre pas.
2. Comment l’index Meilisearch reste-t-il synchronisé avec la base ?
Le bundle écoute les événements Doctrine (postPersist/postUpdate/postRemove) pour mettre à jour l’index ; un import initial (meilisearch:import) charge l’existant. En prod robuste, on découple via Messenger.
3. Que renvoie une recherche Meilisearch ?
Des documents/entités classés par pertinence (tolérants aux fautes), qu’on peut réhydrater en entités Doctrine ou renvoyer directement.
4. Quelles deux stratégies pour la recherche côté Next.js ?
(A) via l’API Symfony (contrôle/sécurité) ; (B) en direct vers Meilisearch avec une clé search-only (recherche as-you-type ultra-rapide).
5. Quelle alternative plus légère pour des besoins de recherche simples ?
La recherche plein-texte de PostgreSQL (tsvector), qui évite d’ajouter un service dédié.
Fin de la Partie 6. BookLab a désormais un modèle de données complet : entités, relations, requêtes performantes, migrations, fixtures, cycle de vie et recherche. C’est la colonne vertébrale que les prochaines parties vont exposer et sécuriser.
Prochaine étape : Partie 7 — Twig, AssetMapper & back-office — le rendu full-stack et le back-office admin de BookLab.