E-Commerce-Sucharchitektur: MeiliSearch, OpenSearch und echte Migrationsgeschichten
Wie du Produktsuche für Commerce designst. MeiliSearch vs OpenSearch vs Elasticsearch, Index-Design, Facettensuche, mehrsprachige Strategien, hybride Suche und Echtzeit-Sync aus PIM- und Commerce-Systemen.
Warum Produktsuche keine Textsuche ist
Produktsuche sieht einfach aus. Ein Benutzer tippt "blaue Laufschuhe Größe 42" und erwartet relevante Ergebnisse. Aber die Implementierung ist grundlegend anders als Dokumentensuche oder Websuche. Produkte haben strukturierte Attribute (Größe, Farbe, Preis, Marke), hierarchische Kategorien, Verfügbarkeit die sich in Echtzeit ändert, lokalisierte Namen und Beschreibungen, und Facetten nach denen Benutzer filtern wollen.
Eine Dokumenten-Suchmaschine findet Dokumente, die zu einer Anfrage passen. Eine Produkt-Suchmaschine muss Produkte finden, sie nach kommerzieller Relevanz ranken (nicht nur nach Textrelevanz), filterbare Facetten anzeigen, Tippfehler und Synonyme behandeln, mehrere Sprachen unterstützen und in Echtzeit aktualisieren, wenn sich der Bestand ändert.
Wir haben Produktsuche-Systeme sowohl auf MeiliSearch als auch auf OpenSearch gebaut. In einem Fall migrierten wir von Elasticsearch 7.4, in einem anderen bauten wir von Grund auf. Dieser Artikel behandelt die Architekturentscheidungen, nicht die Konfigurationsdetails. Für Vektor-Suchmuster im Speziellen, schau dir unseren Leitfaden zur Vektorsuche-Architektur an. Für den breiteren Commerce-Kontext, siehe unseren E-Commerce-Plattformen-Leitfaden.
MeiliSearch vs OpenSearch vs Elasticsearch
| Kriterium | MeiliSearch | OpenSearch | Elasticsearch |
|---|---|---|---|
| Sprache | Rust | Java | Java |
| Tippfehler-Toleranz | Eingebaut, exzellent | Plugin/Custom | Plugin/Custom |
| Facettensuche | Eingebaut, schnell | Eingebaut (Aggregations) | Eingebaut (Aggregations) |
| Vektorsuche | Experimentell | Eingebaut (k-NN) | Eingebaut (dense_vector) |
| Mehrsprachig | Gut (sprachspezifische Tokenizer) | Exzellent (Analyzer pro Feld) | Exzellent (Analyzer pro Feld) |
| Echtzeit-Indexierung | Nahezu sofort (< 50ms) | Near Real-Time (1s Refresh) | Near Real-Time (1s Refresh) |
| Komplexität | Niedrig (einzelne Binary, REST API) | Hoch (Cluster, Shards, Replicas) | Hoch (Cluster, Shards, Replicas) |
| Speicherverbrauch | Niedrig (Rust, effizient) | Hoch (JVM Heap) | Hoch (JVM Heap) |
| Betriebskosten | Niedrig (läuft auf kleinen Instanzen) | Mittel bis hoch | Mittel bis hoch |
| Sortierung | Eingebaute Ranking-Regeln | Flexible Sortierung | Flexible Sortierung |
| Lizenz | MIT | Apache 2.0 | SSPL (kein Open Source) |
| Am besten für | Kleine bis mittlere Kataloge (< 500K Produkte) | Große Kataloge, komplexe Queries, hybride Suche | Wie OpenSearch (wenn bereits investiert) |
Wann du MeiliSearch wählen solltest
- Katalog unter 500K Produkten
- Tippfehler-Toleranz ist entscheidend (Endkunden-Suche)
- Dein Team hat wenig Erfahrung mit Such-Infrastruktur
- Schneller Setup ist wichtiger als erweiterte Query-Features
- Budget ist knapp (läuft auf einer einzelnen kleinen Instanz)
Wann du OpenSearch wählen solltest
- Katalog über 100K Produkte mit komplexen Facetten
- Du brauchst hybride Suche (Text + Vektor / k-NN)
- Mehrere Consumer-Gruppen verarbeiten denselben Index
- Du bist bereits auf AWS (OpenSearch Serverless ist managed)
- Du brauchst erweiterte Aggregationen und Analytics über Suchdaten
Wann du Elasticsearch wählen solltest
- Du betreibst bereits Elasticsearch und es gibt keinen Grund zu migrieren
- Du brauchst spezifische Elastic-Only-Features (ML-Inference, Security)
- Ein Enterprise-Support-Vertrag ist erforderlich
Für die meisten neuen Commerce-Projekte empfehlen wir MeiliSearch für Einfachheit oder OpenSearch für Power. Die SSPL-Lizenz von Elasticsearch macht es für neue Deployments weniger attraktiv.
Index-Design
Der häufigste Fehler: das Datenbankschema direkt indexieren. Produkttabellen sind normalisiert. Suchindizes müssen denormalisiert sein.
// Datenbank: normalisiert (relational)
// products-Tabelle: id, name, category_id, brand_id
// categories-Tabelle: id, name, parent_id
// variants-Tabelle: id, product_id, sku, price, size, color
// translations-Tabelle: id, product_id, locale, name, description
// Suchindex: denormalisiert (flaches Dokument)
interface ProductSearchDocument {
id: string;
name: string; // aktuelle Locale
description: string; // aktuelle Locale
slug: string;
sku: string[]; // alle Varianten-SKUs
brand: string; // denormalisiert aus Brand-Tabelle
categories: string[]; // volle Hierarchie: ["Schuhe", "Laufen", "Trail"]
categoryIds: string[]; // für Facetten-Filterung
price: number; // niedrigster Varianten-Preis (zum Sortieren)
priceRange: { min: number; max: number };
sizes: string[]; // alle verfügbaren Größen
colors: string[]; // alle verfügbaren Farben
inStock: boolean; // irgendeine Variante auf Lager
imageUrl: string; // Hauptbild
rating: number; // durchschnittliche Bewertung
reviewCount: number;
tags: string[]; // durchsuchbare Tags
createdAt: number; // für "Neueste zuerst"-Sortierung
popularity: number; // Verkaufszahl oder Aufrufzahl
}
Regeln für die Denormalisierung:
- Alle Relationen in das Dokument flach einbetten (Markenname, nicht Marken-ID)
- Die vollständige Kategorie-Hierarchie als Array einschließen (ermöglicht Facetten-Drill-down)
- Alle Varianten-Attribute (Größen, Farben) als Arrays auf dem Produkt einschließen
- Den niedrigsten Preis zum Sortieren verwenden, Preisspanne zur Anzeige
- Berechnete Felder (Rating, Bewertungsanzahl, Popularität) für das Ranking einschließen
- Ein Dokument pro Produkt pro Locale (nicht ein Dokument mit allen Locales)
Ein Index pro Locale
Für mehrsprachigen Commerce einen Index pro Locale erstellen:
products_en
products_de
products_fr
products_ar
Jeder Index verwendet sprachspezifische Analyzer, Tokenizer und Stoppwörter. Eine deutsche Suche nach "Laufschuhe" nutzt deutsches Stemming. Eine arabische Suche nutzt arabische morphologische Analyse. Locales in einem Index zu mischen erzwingt Kompromisse bei der Analyse, die die Qualität für jede Sprache verschlechtern.
// MeiliSearch: ein Index pro 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'],
});
Facettensuche-Architektur
Facetten sind die Filter auf der linken Seite jeder Commerce-Suchseite. Sie sehen einfach aus, erfordern aber sorgfältiges Design.
Facetten-Typen
| Typ | Beispiel | Implementierung |
|---|---|---|
| Term-Facette | Marke: Nike (42), Adidas (38) | Term-Aggregation auf brand-Feld |
| Bereichs-Facette | Preis: 0-50 (15), 50-100 (28), 100+ (12) | Range-Aggregation auf price-Feld |
| Hierarchische Facette | Kategorie: Schuhe > Laufen > Trail | Multi-Level-Term-Aggregation auf Kategorie-Hierarchie |
| Boolean-Facette | Auf Lager: Ja (89), Nein (11) | Term-Aggregation auf inStock-Feld |
| Farb-Facette | Farbfelder mit Anzahl | Term-Aggregation auf colors-Array-Feld |
| Größen-Facette | Größe: 40 (5), 41 (8), 42 (12) | Term-Aggregation auf sizes-Array-Feld |
Facetten-Interaktion
Wenn ein Benutzer eine Facette auswählt, müssen sich die anderen Facetten aktualisieren, um die gefilterten Ergebnisse widerzuspiegeln. Das nennt man "Facetten-Verfeinerung" und ist der komplexeste Teil der Such-UI.
// MeiliSearch: Facetten-Counts mit aktiven Filtern
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 }, // nur Nike (weil gefiltert)
// sizes: { "40": 5, "41": 8, "42": 12, "43": 10, "44": 7 },
// colors: { "Black": 20, "White": 15, "Blue": 7 },
// }
Die entscheidende UX-Entscheidung: Wenn ein Markenfilter aktiv ist, soll die Marken-Facette nur die ausgewählte Marke anzeigen (mit ihrer Anzahl) oder alle Marken (mit Anzahlen, die die aktuelle Query minus den Markenfilter widerspiegeln)? Der zweite Ansatz ("disjunktive Facettierung") lässt Benutzer Anzahlen über Marken hinweg vergleichen. MeiliSearch unterstützt das nativ. OpenSearch erfordert separate Aggregation-Queries pro disjunktiver Facette.
Echtzeit-Sync aus Quellsystemen
Suchindizes müssen mit der Source of Truth synchron bleiben (PIM, Commerce-Datenbank, ERP). Die Sync-Architektur hängt vom Quellsystem ab.
Event-basierter Sync (Empfohlen)
Das Quellsystem emittiert Events bei Datenänderungen. Ein Worker konsumiert Events und aktualisiert den Suchindex.
// Vendure: Sync bei Produkt-Events
@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 => {
// Varianten-Änderung betrifft das Suchdokument des Elternprodukts
await this.searchService.indexProduct(event.ctx, event.productVariant.productId);
});
}
}
Geplanter vollständiger Reindex
Selbst mit event-basiertem Sync solltest du einen geplanten vollständigen Reindex als Sicherheitsnetz ausführen. Events können verloren gehen (Broker-Downtime, Worker-Crash). Ein nächtlicher vollständiger Reindex fängt alles auf, was der event-basierte Sync verpasst hat.
// Nächtlicher vollständiger Reindex-Job
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);
}
Umgang mit Löschungen
Produkt-Löschungen sind knifflig. Wenn du ein Produkt aus der Datenbank löschst, entfernt der event-basierte Sync es aus dem Index. Aber wenn das Event verloren geht, bleibt das gelöschte Produkt in den Suchergebnissen.
Zwei Lösungen:
- Löschzeitstempel tracken und in Queries nach "nicht gelöscht" filtern
- Vollständiger Reindex ersetzt den gesamten Index atomar (Alias tauschen)
// Atomarer Reindex mit Alias-Swap (OpenSearch/Elasticsearch)
async function atomicReindex(locale: string) {
const newIndex = `products_${locale}_${Date.now()}`;
await opensearch.indices.create({ index: newIndex, body: indexSettings });
// Alle Produkte in den neuen Index indexieren
await bulkIndex(newIndex, locale);
// Alias atomar tauschen
await opensearch.indices.updateAliases({
body: {
actions: [
{ remove: { index: `products_${locale}_*`, alias: `products_${locale}` } },
{ add: { index: newIndex, alias: `products_${locale}` } },
],
},
});
// Alte Indizes löschen
await cleanupOldIndices(`products_${locale}_*`, keepLast: 2);
}
Für den Umgang mit Daten-Sync-Pipelines im großen Maßstab implementiert unser Vendure Data Hub Plugin all diese Patterns mit 7 verschiedenen Such-Sinks.
Relevanz-Tuning
Standard-Suchrelevanz ist für Commerce falsch. Textrelevanz (wie gut die Query zum Dokument passt) ist ein Signal. Kommerzielle Relevanz (wie wahrscheinlich es ist, dass der Benutzer kauft) ist genauso wichtig.
Ranking-Signale
| Signal | Gewicht | Quelle |
|---|---|---|
| Textübereinstimmung (Titel) | Hoch | Suchmaschine |
| Textübereinstimmung (Beschreibung) | Mittel | Suchmaschine |
| Auf Lager | Kritisch (Boost oder Filter) | Bestandssystem |
| Popularität (Verkaufszahl) | Mittel | Bestelldaten |
| Bewertung | Niedrig-Mittel | Bewertungen |
| Aktualität (neue Produkte) | Niedrig | Produkterstellungsdatum |
| Marge (intern) | Optional | Geschäftsregeln |
// MeiliSearch: benutzerdefinierte Ranking-Regeln
await meili.index('products_de').updateSettings({
rankingRules: [
'words', // 1. Textübereinstimmungsqualität
'typo', // 2. Tippfehler-Toleranz
'proximity', // 3. Wortnähe
'attribute', // 4. Welches Feld gematcht hat (Titel > Beschreibung)
'sort', // 5. Vom Benutzer gewünschte Sortierung
'exactness', // 6. Exakter vs. partieller Treffer
'popularity:desc', // 7. Beliebte Produkte ranken höher
'rating:desc', // 8. Besser bewertete Produkte ranken höher
],
});
In-Stock-Produkte boosten
Nicht vorrätige Produkte sollten in den Ergebnissen weiter unten erscheinen, nicht komplett verschwinden. Benutzer möchten vielleicht kommende Produkte sehen oder sich für Benachrichtigungen bei Wiederverfügbarkeit anmelden.
// OpenSearch: In-Stock-Produkte boosten
const query = {
bool: {
must: [{ match: { searchText: userQuery } }],
should: [
{ term: { inStock: { value: true, boost: 5.0 } } }, // Starker Boost für auf Lager
],
filter: [
{ term: { tenant_id: tenantId } },
],
},
};
Hybride Suche für Commerce
Die Kombination von Textsuche mit Vektorsuche verbessert Ergebnisse für natürlichsprachige Anfragen und bewahrt gleichzeitig die Fähigkeit zur exakten Übereinstimmung für SKUs und Produktcodes.
// OpenSearch: hybride Suche (Text + Vektor)
const results = await opensearch.search({
index: 'products_en',
body: {
query: {
bool: {
should: [
// Textsuche (verarbeitet SKUs, exakte Produktnamen)
{ multi_match: { query: userQuery, fields: ['name^3', 'description', 'sku^5', 'tags'], type: 'best_fields' } },
// Vektorsuche (verarbeitet natürliche Sprache, semantische Ähnlichkeit)
{ knn: { embedding: { vector: queryEmbedding, k: 20 } } },
],
},
},
// Facetten
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 }] } },
},
},
});
SKU-Anfragen ("ABC-12345") treffen den Textsuche-Pfad mit hoher Präzision. Natürlichsprachige Anfragen ("bequeme Schuhe für lange Spaziergänge") treffen den Vektorsuche-Pfad mit semantischem Verständnis. Beide tragen zum endgültigen Ranking bei.
Für mehr Details zu Vektorsuche-Internals, siehe unseren Leitfaden zur Vektorsuche-Architektur.
Häufige Fallstricke
-
Normalisierte Daten indexieren. Deine Suchdokumente müssen denormalisiert sein. Alle Relationen ins Dokument einbetten. Keine IDs referenzieren, die einen zweiten Lookup erfordern.
-
Ein Index für alle Locales. Erstelle einen Index pro Locale. Mixed-Locale-Indizes können keine sprachspezifischen Analyzer verwenden, und die Suchqualität verschlechtert sich für jede Sprache.
-
Kein Facetten-Design. Facetten sind kein Nachgedanke. Plane welche Attribute filterbar sind, wie hierarchische Kategorien funktionieren und wie Facetten-Counts sich aktualisieren, wenn Filter angewendet werden.
-
Sync nur über geplanten Reindex. Event-basierter Sync liefert Echtzeit-Updates. Der geplante Reindex ist ein Sicherheitsnetz, nicht der primäre Mechanismus.
-
Kein Relevanz-Tuning. Standard-Textrelevanz ist für Commerce falsch. Booste Produkte auf Lager, integriere Popularität und Bewertungen und gewichte Titel-Treffer höher als Beschreibungs-Treffer.
-
Nicht vorrätige Produkte ignorieren. Entferne sie nicht aus dem Index. Stufe sie im Ranking herab. Benutzer möchten vielleicht Benachrichtigungen bei Wiederverfügbarkeit oder kommende Produkte durchstöbern.
-
Kein atomarer Reindex. Wenn dein Reindex-Prozess auf halbem Weg fehlschlägt, hast du einen teilweise aktualisierten Index. Verwende Alias-Swapping für atomare Umschaltung.
-
Suche als Feature statt als Infrastruktur behandeln. Suche ist ein Kerndienst. Sie braucht ihren eigenen Cluster, ihr eigenes Monitoring, ihre eigene Skalierungsstrategie. Betreibe sie nicht auf demselben Server wie deine Datenbank.
Zentrale Erkenntnisse
-
Produktsuche ist keine Textsuche. Strukturierte Attribute, Facetten, kommerzielle Relevanz, Echtzeit-Bestand und mehrsprachige Unterstützung machen sie fundamental anders.
-
Denormalisieren für die Suche, normalisieren für die Speicherung. Das Suchdokument ist eine flache, eigenständige Repräsentation von allem, was nötig ist, um ein Suchergebnis zu rendern. Keine Joins, keine Lookups.
-
Ein Index pro Locale. Sprachspezifische Analyzer, Tokenizer und Stoppwörter liefern dramatisch bessere Ergebnisse als ein einzelner Index mit gemischten Sprachen.
-
Event-basierter Sync mit geplantem Reindex als Sicherheitsnetz. Echtzeit-Updates für den Normalbetrieb. Vollständiger Reindex jede Nacht, um alles aufzufangen, was Events verpasst haben.
-
Relevanz-Tuning ist eine Geschäftsentscheidung. Textübereinstimmungsqualität, Lagerstatus, Popularität, Bewertungen und Marge sind alles Ranking-Signale. Standard-Relevanz ist für Commerce falsch.
-
MeiliSearch für Einfachheit, OpenSearch für Power. MeiliSearch ist perfekt für Kataloge unter 500K mit großartiger Tippfehler-Toleranz. OpenSearch bewältigt komplexe Aggregationen, hybride Suche und große Deployments.
Wir bauen Such-Infrastruktur als Teil unserer Data-Engineering- und E-Commerce-Praxis. Wenn du Produktsuche aufbaust oder migrierst, sprich mit unserem Team oder fordere ein Angebot an. Unser Vendure Data Hub Plugin enthält Such-Sinks für MeiliSearch, OpenSearch, Elasticsearch, Algolia und Typesense.
Behandelte Themen
Verwandte Guides
Unternehmenshandbuch zu Agentischen KI-Systemen
Technischer Leitfaden zu agentischen KI-Systemen in Unternehmen. Erfahre mehr ueber Architektur, Faehigkeiten und Anwendungen autonomer KI-Agenten.
Guide lesenAgentic Commerce: Wie du KI-Agenten sicher einkaufen lässt
Wie du gesteuerten, KI-initiierten Handel designst. Policy Engines, HITL-Freigabe-Gates, HMAC-Quittungen, Idempotenz, Tenant-Scoping und das vollständige Agentic Checkout Protocol.
Guide lesenDie 9 Stellen, an denen dein KI-System Daten verliert (und wie du jede einzelne abdichtest)
Eine systematische Übersicht aller Stellen, an denen KI-Systeme Daten preisgeben. Prompts, Embeddings, Logs, Tool Calls, Agent Memory, Fehlermeldungen, Cache, Fine-Tuning-Daten und Agent Handoffs.
Guide lesenBereit, produktionsreife KI-Systeme zu bauen?
Unser Team ist spezialisiert auf produktionsreife KI-Systeme. Lass uns besprechen, wie wir deinem Unternehmen helfen können.
Gespräch starten