Architecture de Recherche E-Commerce : MeiliSearch, OpenSearch et Histoires de Migration Reelles
Comment concevoir la recherche produit pour le commerce. MeiliSearch vs OpenSearch vs Elasticsearch, conception d'index, recherche a facettes, strategies multilingues, recherche hybride et synchronisation temps reel depuis PIM et systemes commerce.
Pourquoi la recherche produit n'est pas de la recherche textuelle
La recherche produit a l'air simple. Un utilisateur tape "chaussures de running bleues taille 42" et attend des resultats pertinents. Mais l'implementation est fondamentalement differente de la recherche documentaire ou de la recherche web. Les produits ont des attributs structures (taille, couleur, prix, marque), des categories hierarchiques, une disponibilite qui change en temps reel, des noms et descriptions localises, et des facettes que les utilisateurs s'attendent a pouvoir filtrer.
Un moteur de recherche documentaire trouve des documents qui correspondent a une requete. Un moteur de recherche produit doit trouver des produits, les classer par pertinence commerciale (pas juste par pertinence textuelle), presenter des facettes filtrables, gerer les fautes de frappe et les synonymes, supporter plusieurs langues, et se mettre a jour en temps reel quand le stock change.
On a construit des systemes de recherche produit sur MeiliSearch et OpenSearch, en migrant depuis Elasticsearch 7.4 dans un cas et en construisant de zero dans un autre. Cet article couvre les decisions d'architecture, pas les details de configuration. Pour les patterns de recherche vectorielle specifiquement, consulte notre guide d'architecture de recherche vectorielle. Pour le contexte commerce plus large, consulte notre guide des plateformes ecommerce.
MeiliSearch vs OpenSearch vs Elasticsearch
| Critere | MeiliSearch | OpenSearch | Elasticsearch |
|---|---|---|---|
| Langage | Rust | Java | Java |
| Tolerance aux fautes | Integree, excellente | Plugin/custom | Plugin/custom |
| Recherche a facettes | Integree, rapide | Integree (aggregations) | Integree (aggregations) |
| Recherche vectorielle | Experimentale | Integree (k-NN) | Integree (dense_vector) |
| Multilingue | Bon (tokenizers specifiques par langue) | Excellent (analyseurs par champ) | Excellent (analyseurs par champ) |
| Indexation temps reel | Quasi instantanee (< 50ms) | Quasi temps reel (refresh 1s) | Quasi temps reel (refresh 1s) |
| Complexite | Faible (binaire unique, API REST) | Elevee (cluster, shards, replicas) | Elevee (cluster, shards, replicas) |
| Utilisation memoire | Faible (Rust, efficace) | Elevee (heap JVM) | Elevee (heap JVM) |
| Cout operationnel | Faible (tourne sur de petites instances) | Moyen a eleve | Moyen a eleve |
| Tri | Regles de classement integrees | Tri flexible | Tri flexible |
| Licence | MIT | Apache 2.0 | SSPL (pas open source) |
| Ideal pour | Catalogues petits a moyens (< 500K produits) | Gros catalogues, requetes complexes, recherche hybride | Comme OpenSearch (si deja investi) |
Quand choisir MeiliSearch
- Catalogue de moins de 500K produits
- La tolerance aux fautes de frappe est critique (recherche grand public)
- L'equipe a peu d'experience en infrastructure de recherche
- La mise en place rapide compte plus que les fonctionnalites de requete avancees
- Le budget est serre (tourne sur une seule petite instance)
Quand choisir OpenSearch
- Catalogue de plus de 100K produits avec des facettes complexes
- Besoin de recherche hybride (texte + vecteur / k-NN)
- Plusieurs groupes de consommateurs traitent le meme index
- Deja sur AWS (OpenSearch Serverless est manage)
- Besoin d'aggregations avancees et d'analytics sur les donnees de recherche
Quand choisir Elasticsearch
- Tu utilises deja Elasticsearch et il n'y a aucune raison de migrer
- Tu as besoin de fonctionnalites specifiques a Elastic (inference ML, securite)
- Un contrat de support entreprise est necessaire
Pour la plupart des nouveaux projets commerce, on recommande MeiliSearch pour la simplicite ou OpenSearch pour la puissance. La licence SSPL d'Elasticsearch le rend moins attractif pour les nouveaux deploiements.
Conception d'Index
L'erreur la plus courante : indexer directement le schema de ta base de donnees. Les tables produit sont normalisees. Les index de recherche doivent etre denormalises.
// Base de donnees : normalisee (relationnelle)
// table products : id, name, category_id, brand_id
// table categories : id, name, parent_id
// table variants : id, product_id, sku, price, size, color
// table translations : id, product_id, locale, name, description
// Index de recherche : denormalise (document plat)
interface ProductSearchDocument {
id: string;
name: string; // locale courante
description: string; // locale courante
slug: string;
sku: string[]; // tous les SKU des variantes
brand: string; // denormalise depuis la table brand
categories: string[]; // hierarchie complete : ["Chaussures", "Running", "Trail"]
categoryIds: string[]; // pour le filtrage par facettes
price: number; // prix le plus bas des variantes (pour le tri)
priceRange: { min: number; max: number };
sizes: string[]; // toutes les tailles disponibles
colors: string[]; // toutes les couleurs disponibles
inStock: boolean; // au moins une variante en stock
imageUrl: string; // image principale
rating: number; // note moyenne des avis
reviewCount: number;
tags: string[]; // tags cherchables
createdAt: number; // pour le tri "plus recent d'abord"
popularity: number; // nombre de ventes ou de vues
}
Regles de denormalisation :
- Aplatir toutes les relations dans le document (le nom de marque, pas l'ID de marque)
- Inclure la hierarchie de categories complete sous forme de tableau (permet le drill-down par facettes)
- Inclure tous les attributs de variantes (tailles, couleurs) sous forme de tableaux sur le produit
- Utiliser le prix le plus bas pour le tri, la fourchette de prix pour l'affichage
- Inclure les champs calcules (rating, reviewCount, popularity) pour le classement
- Un document par produit par locale (pas un document avec toutes les locales)
Un Index Par Locale
Pour le commerce multilingue, cree un index par locale :
products_en
products_de
products_fr
products_ar
Chaque index utilise des analyseurs, tokenizers et stop words specifiques a la langue. Une recherche en allemand pour "Laufschuhe" utilise le stemming allemand. Une recherche en arabe utilise l'analyse morphologique arabe. Melanger les locales dans un seul index force des compromis d'analyse qui degradent la qualite pour chaque langue.
// MeiliSearch : un index par locale
await meili.createIndex('products_de', { primaryKey: 'id' });
await meili.index('products_de').updateSettings({
searchableAttributes: ['name', 'description', 'brand', 'tags', 'categories'],
filterableAttributes: ['categories', 'brand', 'sizes', 'colors', 'price', 'inStock'],
sortableAttributes: ['price', 'createdAt', 'popularity', 'rating'],
});
Architecture de Recherche a Facettes
Les facettes sont les filtres sur le cote gauche de chaque page de recherche commerce. Ca a l'air simple mais ca demande une conception soignee.
Types de Facettes
| Type | Exemple | Implementation |
|---|---|---|
| Facette terme | Marque : Nike (42), Adidas (38) | Aggregation de termes sur le champ brand |
| Facette plage | Prix : 0-50 (15), 50-100 (28), 100+ (12) | Aggregation de plage sur le champ price |
| Facette hierarchique | Categorie : Chaussures > Running > Trail | Aggregation de termes multi-niveaux sur la hierarchie de categories |
| Facette booleenne | En stock : Oui (89), Non (11) | Aggregation de termes sur le champ inStock |
| Facette couleur | Nuanciers de couleurs avec compteurs | Aggregation de termes sur le champ tableau colors |
| Facette taille | Taille : 40 (5), 41 (8), 42 (12) | Aggregation de termes sur le champ tableau sizes |
Interaction des Facettes
Quand un utilisateur selectionne une facette, les autres facettes doivent se mettre a jour pour refleter les resultats filtres. Ca s'appelle le "raffinement de facettes" et c'est la partie la plus complexe de l'UI de recherche.
// MeiliSearch : compteurs de facettes avec filtres actifs
const results = await meili.index('products_de').search('laufschuhe', {
filter: ['brand = "Nike"', 'inStock = true'],
facets: ['categories', 'brand', 'sizes', 'colors', 'price'],
});
// results.facetDistribution :
// {
// categories: { "Running": 42, "Trail": 18, "Road": 24 },
// brand: { "Nike": 42 }, // seulement Nike (car filtre)
// sizes: { "40": 5, "41": 8, "42": 12, "43": 10, "44": 7 },
// colors: { "Black": 20, "White": 15, "Blue": 7 },
// }
La decision UX cle : quand un filtre de marque est actif, est-ce que la facette marque doit montrer uniquement la marque selectionnee (avec son compteur) ou toutes les marques (avec des compteurs refletant la requete courante moins le filtre de marque) ? La deuxieme approche ("facettage disjonctif") permet aux utilisateurs de comparer les compteurs entre les marques. MeiliSearch supporte ca nativement. OpenSearch necessite des requetes d'aggregation separees par facette disjonctive.
Synchronisation Temps Reel depuis les Systemes Source
Les index de recherche doivent rester synchronises avec la source de verite (PIM, base de donnees commerce, ERP). L'architecture de synchronisation depend du systeme source.
Synchronisation Evenementielle (Recommandee)
Le systeme source emet des evenements lors des changements de donnees. Un worker consomme les evenements et met a jour l'index de recherche.
// Vendure : synchronisation sur les evenements produit
@Injectable()
export class SearchIndexSubscriber {
constructor(
private eventBus: EventBus,
private searchService: SearchIndexService,
) {
this.eventBus.ofType(ProductEvent).subscribe(async event => {
if (event.type === 'updated' || event.type === 'created') {
await this.searchService.indexProduct(event.ctx, event.product.id);
}
if (event.type === 'deleted') {
await this.searchService.removeProduct(event.product.id);
}
});
this.eventBus.ofType(ProductVariantEvent).subscribe(async event => {
// Un changement de variante affecte le document de recherche du produit parent
await this.searchService.indexProduct(event.ctx, event.productVariant.productId);
});
}
}
Reindexation Complete Planifiee
Meme avec une synchronisation evenementielle, lance une reindexation complete planifiee comme filet de securite. Les evenements peuvent etre perdus (panne du broker, crash du worker). Une reindexation complete nocturne rattrape tout ce que la synchronisation evenementielle a loupe.
// Job de reindexation complete nocturne
async function fullReindex(locale: string) {
const batchSize = 500;
let offset = 0;
let products = [];
do {
products = await productService.findAll({ take: batchSize, skip: offset });
const documents = products.map(p => buildSearchDocument(p, locale));
await meili.index(`products_${locale}`).addDocuments(documents);
offset += batchSize;
} while (products.length === batchSize);
}
Gestion des Suppressions
Les suppressions de produits sont piegeuses. Si tu supprimes un produit de la base de donnees, la synchronisation evenementielle le retire de l'index. Mais si l'evenement est perdu, le produit supprime reste dans les resultats de recherche.
Deux solutions :
- Suivre les timestamps de suppression et filtrer par "non supprime" dans les requetes
- La reindexation complete remplace l'index entier de maniere atomique (swap d'alias)
// Reindexation atomique avec swap d'alias (OpenSearch/Elasticsearch)
async function atomicReindex(locale: string) {
const newIndex = `products_${locale}_${Date.now()}`;
await opensearch.indices.create({ index: newIndex, body: indexSettings });
// Indexer tous les produits dans le nouvel index
await bulkIndex(newIndex, locale);
// Swap d'alias atomique
await opensearch.indices.updateAliases({
body: {
actions: [
{ remove: { index: `products_${locale}_*`, alias: `products_${locale}` } },
{ add: { index: newIndex, alias: `products_${locale}` } },
],
},
});
// Supprimer les anciens index
await cleanupOldIndices(`products_${locale}_*`, keepLast: 2);
}
Pour voir comment on gere les pipelines de synchronisation de donnees a l'echelle, notre Vendure Data Hub Plugin implemente tous ces patterns avec 7 sinks de recherche differents.
Ajustement de la Pertinence
La pertinence de recherche par defaut est mauvaise pour le commerce. La pertinence textuelle (a quel point la requete correspond au document) est un signal. La pertinence commerciale (a quel point l'utilisateur est susceptible d'acheter) est tout aussi importante.
Signaux de Classement
| Signal | Poids | Source |
|---|---|---|
| Correspondance texte (titre) | Eleve | Moteur de recherche |
| Correspondance texte (description) | Moyen | Moteur de recherche |
| En stock | Critique (boost ou filtre) | Systeme d'inventaire |
| Popularite (nombre de ventes) | Moyen | Donnees de commandes |
| Note des avis | Faible a moyen | Avis |
| Nouveaute (produits recents) | Faible | Date de creation produit |
| Marge (interne) | Optionnel | Regles metier |
// MeiliSearch : regles de classement personnalisees
await meili.index('products_de').updateSettings({
rankingRules: [
'words', // 1. Qualite de correspondance texte
'typo', // 2. Tolerance aux fautes
'proximity', // 3. Proximite des mots
'attribute', // 4. Quel champ a matche (titre > description)
'sort', // 5. Tri demande par l'utilisateur
'exactness', // 6. Correspondance exacte vs partielle
'popularity:desc', // 7. Les produits populaires montent
'rating:desc', // 8. Les produits mieux notes montent
],
});
Booster les Produits En Stock
Les produits en rupture de stock devraient apparaitre plus bas dans les resultats, pas disparaitre completement. Les utilisateurs veulent peut-etre voir les produits a venir ou s'abonner aux notifications de retour en stock.
// OpenSearch : booster les produits en stock
const query = {
bool: {
must: [{ match: { searchText: userQuery } }],
should: [
{ term: { inStock: { value: true, boost: 5.0 } } }, // Boost fort pour les produits en stock
],
filter: [
{ term: { tenant_id: tenantId } },
],
},
};
Recherche Hybride pour le Commerce
Combiner la recherche textuelle avec la recherche vectorielle ameliore les resultats pour les requetes en langage naturel tout en preservant la capacite de correspondance exacte pour les SKU et les codes produit.
// OpenSearch : recherche hybride (texte + vecteur)
const results = await opensearch.search({
index: 'products_en',
body: {
query: {
bool: {
should: [
// Recherche textuelle (gere les SKU, noms de produit exacts)
{ multi_match: { query: userQuery, fields: ['name^3', 'description', 'sku^5', 'tags'], type: 'best_fields' } },
// Recherche vectorielle (gere le langage naturel, la similarite semantique)
{ knn: { embedding: { vector: queryEmbedding, k: 20 } } },
],
},
},
// Facettes
aggs: {
brands: { terms: { field: 'brand.keyword', size: 20 } },
categories: { terms: { field: 'categories.keyword', size: 30 } },
price_ranges: { range: { field: 'price', ranges: [{ to: 50 }, { from: 50, to: 100 }, { from: 100 }] } },
},
},
});
Les requetes SKU ("ABC-12345") passent par la recherche textuelle avec une haute precision. Les requetes en langage naturel ("chaussures confortables pour longues marches") passent par la recherche vectorielle avec une comprehension semantique. Les deux contribuent au classement final.
Pour plus de details sur le fonctionnement interne de la recherche vectorielle, consulte notre guide d'architecture de recherche vectorielle.
Pieges Courants
-
Indexer des donnees normalisees. Tes documents de recherche doivent etre denormalises. Aplatir toutes les relations dans le document. Ne pas referencer des ID qui necessitent un second lookup.
-
Un seul index pour toutes les locales. Cree un index par locale. Les index a locales melangees ne peuvent pas utiliser d'analyseurs specifiques a la langue, et la qualite de recherche se degrade pour chaque langue.
-
Pas de conception de facettes. Les facettes ne sont pas une reflexion apres coup. Planifie quels attributs sont filtrables, comment les categories hierarchiques fonctionnent, et comment les compteurs de facettes se mettent a jour quand des filtres sont appliques.
-
Synchronisation uniquement par reindexation planifiee. La synchronisation evenementielle donne des mises a jour quasi temps reel. La reindexation planifiee est un filet de securite, pas le mecanisme principal.
-
Pas d'ajustement de pertinence. La pertinence textuelle par defaut est mauvaise pour le commerce. Booste les produits en stock, integre la popularite et les notes, et donne plus de poids aux correspondances de titre qu'aux correspondances de description.
-
Ignorer les produits en rupture de stock. Ne les retire pas de l'index. Retrograde-les dans le classement. Les utilisateurs veulent peut-etre des alertes de retour en stock ou parcourir les produits a venir.
-
Pas de reindexation atomique. Si ton processus de reindexation echoue a mi-chemin, tu as un index partiellement mis a jour. Utilise le swap d'alias pour un basculement atomique.
-
Traiter la recherche comme une fonctionnalite, pas comme de l'infrastructure. La recherche est un service central. Elle a besoin de son propre cluster, de son propre monitoring, de sa propre strategie de scaling. Ne la fais pas tourner sur le meme serveur que ta base de donnees.
Points Cles a Retenir
-
La recherche produit n'est pas de la recherche textuelle. Les attributs structures, les facettes, la pertinence commerciale, l'inventaire temps reel et le support multilingue la rendent fondamentalement differente.
-
Denormalise pour la recherche, normalise pour le stockage. Le document de recherche est une representation plate et autonome de tout ce qui est necessaire pour afficher un resultat de recherche. Pas de jointures, pas de lookups.
-
Un index par locale. Les analyseurs, tokenizers et stop words specifiques a la langue produisent des resultats radicalement meilleurs qu'un seul index multi-langues.
-
Synchronisation evenementielle avec reindexation planifiee comme filet de securite. Mises a jour temps reel pour les operations normales. Reindexation complete nocturne pour rattraper ce que les evenements ont loupe.
-
L'ajustement de pertinence est une decision business. Qualite de correspondance texte, statut en stock, popularite, notes et marge sont tous des signaux de classement. La pertinence par defaut est mauvaise pour le commerce.
-
MeiliSearch pour la simplicite, OpenSearch pour la puissance. MeiliSearch est parfait pour les catalogues de moins de 500K avec une excellente tolerance aux fautes. OpenSearch gere les aggregations complexes, la recherche hybride et les deploiements a grande echelle.
On construit l'infrastructure de recherche dans le cadre de nos pratiques data engineering et ecommerce. Si tu construis ou migres ta recherche produit, parle a notre equipe ou demande un devis. Notre Vendure Data Hub Plugin inclut des sinks de recherche pour MeiliSearch, OpenSearch, Elasticsearch, Algolia et Typesense.
Sujets couverts
Guides connexes
Guide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guideLes 9 endroits où ton système IA laisse fuir des données (et comment colmater chacun)
Cartographie systématique de chaque point de fuite de données dans les systèmes IA. Prompts, embeddings, logs, appels d'outils, mémoire d'agent, messages d'erreur, cache, données de fine-tuning et transferts entre agents.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation