Technischer Leitfaden

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.

3. Februar 202618 Min. LesezeitOronts Engineering Team

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

KriteriumMeiliSearchOpenSearchElasticsearch
SpracheRustJavaJava
Tippfehler-ToleranzEingebaut, exzellentPlugin/CustomPlugin/Custom
FacettensucheEingebaut, schnellEingebaut (Aggregations)Eingebaut (Aggregations)
VektorsucheExperimentellEingebaut (k-NN)Eingebaut (dense_vector)
MehrsprachigGut (sprachspezifische Tokenizer)Exzellent (Analyzer pro Feld)Exzellent (Analyzer pro Feld)
Echtzeit-IndexierungNahezu sofort (< 50ms)Near Real-Time (1s Refresh)Near Real-Time (1s Refresh)
KomplexitätNiedrig (einzelne Binary, REST API)Hoch (Cluster, Shards, Replicas)Hoch (Cluster, Shards, Replicas)
SpeicherverbrauchNiedrig (Rust, effizient)Hoch (JVM Heap)Hoch (JVM Heap)
BetriebskostenNiedrig (läuft auf kleinen Instanzen)Mittel bis hochMittel bis hoch
SortierungEingebaute Ranking-RegelnFlexible SortierungFlexible Sortierung
LizenzMITApache 2.0SSPL (kein Open Source)
Am besten fürKleine bis mittlere Kataloge (< 500K Produkte)Große Kataloge, komplexe Queries, hybride SucheWie 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

TypBeispielImplementierung
Term-FacetteMarke: Nike (42), Adidas (38)Term-Aggregation auf brand-Feld
Bereichs-FacettePreis: 0-50 (15), 50-100 (28), 100+ (12)Range-Aggregation auf price-Feld
Hierarchische FacetteKategorie: Schuhe > Laufen > TrailMulti-Level-Term-Aggregation auf Kategorie-Hierarchie
Boolean-FacetteAuf Lager: Ja (89), Nein (11)Term-Aggregation auf inStock-Feld
Farb-FacetteFarbfelder mit AnzahlTerm-Aggregation auf colors-Array-Feld
Größen-FacetteGröß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:

  1. Löschzeitstempel tracken und in Queries nach "nicht gelöscht" filtern
  2. 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

SignalGewichtQuelle
Textübereinstimmung (Titel)HochSuchmaschine
Textübereinstimmung (Beschreibung)MittelSuchmaschine
Auf LagerKritisch (Boost oder Filter)Bestandssystem
Popularität (Verkaufszahl)MittelBestelldaten
BewertungNiedrig-MittelBewertungen
Aktualität (neue Produkte)NiedrigProdukterstellungsdatum
Marge (intern)OptionalGeschä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

  1. Normalisierte Daten indexieren. Deine Suchdokumente müssen denormalisiert sein. Alle Relationen ins Dokument einbetten. Keine IDs referenzieren, die einen zweiten Lookup erfordern.

  2. 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.

  3. 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.

  4. Sync nur über geplanten Reindex. Event-basierter Sync liefert Echtzeit-Updates. Der geplante Reindex ist ein Sicherheitsnetz, nicht der primäre Mechanismus.

  5. 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.

  6. 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.

  7. 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.

  8. 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

E-Commerce-SucheMeiliSearch E-CommerceProduktsuche ArchitekturFacettensuchehybride Suche CommerceElasticsearch MigrationOpenSearch CommerceSuchindexierung

Bereit, 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