Custom Scoring in Magento mit Painless: Lagerbestand und Marge als Ranking-Faktor
AI generated
_doc
_index
Elasticsearch / Magento
Custom Scoring in Magento mit Painless
Lagerbestand und Marge als Ranking-Faktor in der Storefront-Suche

Das textbasierte BM25-Scoring von Elasticsearch bewertet Relevanz allein anhand von Textübereinstimmung, ignoriert dabei aber geschäftlich relevante Signale wie Lagerbestand oder Marge. Über Painless-Skripte in einer Function-Score-Query lässt sich diese Lücke schließen, allerdings nur, wenn Integration in Magento und Performance-Auswirkungen sauber verstanden werden.

12 Min. Lesezeit Painless Function Score Magento CatalogSearch

1. Wo das Standard-Relevance-Scoring von Magento an Grenzen stößt

Magento nutzt für die Katalogsuche standardmäßig eine BM25-basierte Bewertung, die Textübereinstimmung, Feldgewichtung und Termhäufigkeit berücksichtigt, aber keinerlei geschäftliche Kennzahlen kennt. Zwei Produkte mit identischer Textrelevanz erscheinen in der Trefferliste damit in praktisch zufälliger Reihenfolge, selbst wenn eines davon ausverkauft ist und das andere hohe Verfügbarkeit sowie eine deutlich bessere Marge bietet.

Für Merchandising-Teams ist das ein wiederkehrendes Problem: Ausverkaufte oder margenschwache Produkte landen oft prominent in den Suchergebnissen, während lagerhaltige, margenstarke Alternativen weiter unten erscheinen. Eine Function-Score-Query mit Painless-Skript erlaubt es, diese Signale gezielt in die Rangordnung einzubeziehen, ohne die reine Textrelevanz komplett zu verdrängen.

2. Painless-Grundlagen: Sandbox, Typisierung und Performance-Charakteristik

Painless ist die von Elastic und OpenSearch gemeinsam genutzte, eigens für Suchcluster entwickelte Skriptsprache mit Java-ähnlicher Syntax, die in einer strikten Sandbox läuft und keinen Zugriff auf das Dateisystem, das Netzwerk oder beliebige Java-Klassen erlaubt. Diese Einschränkung ist bewusst gewählt, damit ein fehlerhaftes oder bösartiges Skript den Cluster nicht destabilisieren kann.

Im Gegensatz zu interpretierten Skriptsprachen wird Painless bei der ersten Ausführung zu Bytecode kompiliert und danach gecacht, wodurch wiederholte Aufrufe desselben Skripts nahezu die Performance von nativem Java erreichen. Der Zugriff auf Dokumentfelder erfolgt über doc['feldname'].value auf Basis von Doc Values, was deutlich schneller ist als ein Zugriff über _source, aber voraussetzt, dass das jeweilige Feld überhaupt Doc Values aktiviert hat.

3. Aufbau einer Function-Score-Query mit script_score

Eine Function-Score-Query kapselt die ursprüngliche Textsuche als query und ergänzt sie um eine oder mehrere functions, von denen script_score die flexibelste ist, weil sie beliebige Painless-Logik gegen jedes Dokument ausführt. Über boost_mode wird gesteuert, wie der ursprüngliche Textscore mit dem Skript-Ergebnis kombiniert wird, üblich sind multiply für eine multiplikative Verstärkung oder sum für eine additive Kombination.

Für Magento-Suchergebnisse hat sich multiply in der Praxis bewährt, weil dadurch ein Produkt mit hoher Textrelevanz und gutem Lagerbestand deutlich stärker profitiert als ein Produkt mit schwacher Textrelevanz, selbst wenn beide denselben Lagerbestand-Faktor haben. Eine additive Kombination würde dagegen auch bei schwacher Textrelevanz einen konstanten Bonus vergeben, was die eigentliche Suchrelevanz zu stark verwässerte.


{
  "query": {
    "function_score": {
      "query": { "bool": { "must": [{ "match": { "name": "laufschuh" } }] } },
      "functions": [{
        "script_score": {
          "script": {
            "source": "params.min_stock_boost + (doc['salable_quantity'].value > 0 ? 0.3 : 0.0)",
            "params": { "min_stock_boost": 1.0 }
          }
        }
      }],
      "boost_mode": "multiply",
      "score_mode": "sum"
    }
  }
}

4. Wo die Function-Score-Logik in die Magento-CatalogSearch-Query eingebunden wird

Magento baut die an Elasticsearch beziehungsweise OpenSearch gesendete Query über \Magento\Elasticsearch\SearchAdapter\Query\Builder zusammen, gesteuert durch die Konfiguration in search_request.xml. Eine eigene function_score-Query ist dort standardmäßig nicht als Query-Typ vorgesehen, weshalb der übliche Weg ein Plugin auf die build()-Methode des Builders ist, das die fertig gebaute Query nachträglich in eine Function-Score-Query einwickelt.

Der Plugin-Ansatz hat den Vorteil, dass die bestehende Magento-Query-Logik, also Facetten, Filter und Sortierungen aus dem Layered Navigation, vollständig unangetastet bleibt und lediglich der finale Score-Anteil um das Painless-Skript ergänzt wird. Alternativ lässt sich ein eigener queryReference-Typ in search_request.xml registrieren, was mehr Kontrolle bietet, aber deutlich mehr Implementierungsaufwand bedeutet.


<?php
declare(strict_types=1);

namespace Mironsoft\SearchScoring\Plugin;

use Magento\Elasticsearch\SearchAdapter\Query\Builder;

/**
 * Wraps the built Elasticsearch/OpenSearch query in a function_score query
 * that boosts salable, high margin products via a Painless script.
 */
class AddStockAndMarginScoringPlugin
{
    /**
     * Injects a script_score function around the query built by Magento.
     *
     * @param Builder $subject Original query builder.
     * @param array $result Query array as built by Magento core.
     * @return array Modified query array with function_score wrapper.
     */
    public function afterBuild(Builder $subject, array $result): array
    {
        $originalQuery = $result['query'] ?? ['match_all' => new \stdClass()];

        $result['query'] = [
            'function_score' => [
                'query' => $originalQuery,
                'functions' => [[
                    'script_score' => [
                        'script' => [
                            'source' => 'params.base + (doc[\'salable_quantity\'].value > 0 ? 0.3 : 0.0)'
                                . ' + (doc[\'margin_percent\'].size() > 0 ? doc[\'margin_percent\'].value / 100 : 0.0)',
                            'params' => ['base' => 1.0],
                        ],
                    ],
                ]],
                'boost_mode' => 'multiply',
                'score_mode' => 'sum',
            ],
        ];

        return $result;
    }
}

5. Praxisbeispiel: Lagerbestand als Scoring-Faktor

Um Lagerbestand als Ranking-Signal zu nutzen, muss das entsprechende Feld, etwa salable_quantity, überhaupt im Katalogindex vorhanden und mit Doc Values indexiert sein, was bei Magento-Standardinstallationen für die verkaufbare Menge in der Regel bereits der Fall ist. Im Skript wird dann ein binärer oder gestaffelter Bonus vergeben, je nachdem ob eine feinere Abstufung nach Bestandshöhe gewünscht ist.

Ein gestaffelter Ansatz, der etwa Produkte mit mehr als zehn Einheiten stärker gewichtet als Produkte mit nur einer verbleibenden Einheit, bildet reale Verfügbarkeitsrisiken realistischer ab als ein reiner Ja-Nein-Bonus. Wichtig ist dabei, den Bonus auf einen sinnvollen Maximalwert zu begrenzen, damit ein extrem hoher Lagerbestand die Textrelevanz nicht vollständig überlagert.


// Painless-Skript: gestaffelter Lagerbestand-Bonus statt binärer Bewertung
double stock = doc['salable_quantity'].size() > 0 ? doc['salable_quantity'].value : 0;
double stockBoost;
if (stock <= 0) {
    stockBoost = 0.0;
} else if (stock < 5) {
    stockBoost = 0.1;
} else if (stock < 20) {
    stockBoost = 0.25;
} else {
    stockBoost = 0.4;
}
return params.base + stockBoost;

6. Praxisbeispiel: Marge als Scoring-Faktor kombiniert mit Textrelevanz

Marge als indexiertes Attribut ist sensibler als Lagerbestand, weil dieser Wert typischerweise nicht für die Storefront-Ausgabe gedacht ist und über Field-Level-Security oder eine separate, nicht öffentlich lesbare Rolle abgesichert werden sollte, sofern der Cluster überhaupt Zugriffskontrolle einsetzt. Für das Scoring selbst reicht ein numerisches Attribut, das prozentual oder als normalisierter Wert zwischen null und eins vorliegt.

In der Praxis bewährt sich eine Kombination aus Lagerbestand- und Margen-Boost in einem einzigen Skript, wobei beide Faktoren jeweils nur einen kleinen Bruchteil des Gesamtscores ausmachen sollten, typischerweise im Bereich von zehn bis dreißig Prozent des ursprünglichen Textscores. Wird der Anteil zu hoch gewählt, dominieren geschäftliche Signale die Suche vollständig und Nutzer finden bei spezifischen Suchbegriffen nicht mehr die tatsächlich relevantesten Produkte.

7. Skript-Caching und Compile-Limits beachten

Jedes inline übergebene Skript, dessen Quelltext sich zwischen Anfragen unterscheidet, etwa weil Parameter direkt im Skripttext statt in params stehen, erzwingt eine Neukompilierung und kann das Cluster-Limit script.max_compilations_rate überschreiten, was zu Fehlern bei allen Suchanfragen führt. Die Lösung ist, ausschließlich statische Skripte mit variablen params zu verwenden, wie in den vorherigen Beispielen gezeigt, damit derselbe kompilierte Bytecode für alle Anfragen wiederverwendet wird.

Für häufig genutzte Scoring-Skripte empfiehlt sich zusätzlich ein Stored Script, das einmalig über die _scripts-API im Cluster hinterlegt und danach per ID referenziert wird, statt bei jeder Magento-Anfrage erneut im Query-Body mitgesendet zu werden. Das reduziert nicht nur die Netzwerklast, sondern macht Änderungen am Scoring auch zentral wartbar, ohne den PHP-Code des Plugins anzufassen.


# Stored Script einmalig im Cluster registrieren
curl -X PUT "https://search.example.com/_scripts/magento_stock_margin_boost" \
  -H "Content-Type: application/json" -d '{
    "script": {
      "lang": "painless",
      "source": "params.base + (doc[\"salable_quantity\"].value > 0 ? 0.3 : 0.0)"
    }
  }'

8. Performance-Vorsicht bei skriptbasiertem Scoring auf großen Katalogen

Ein Script-Score wird für jedes einzelne Dokument ausgeführt, das die ursprüngliche Query bereits als Treffer liefert, nicht nur für die letztlich angezeigten Ergebnisse. Bei einer breiten Suchanfrage, die auf einem sehr großen Magento-Katalog mehrere Hunderttausend Treffer liefert, kann das Skript zum dominanten Kostenfaktor der gesamten Anfrage werden, selbst wenn jede einzelne Ausführung für sich genommen nur Mikrosekunden dauert.

Entscheidend ist deshalb, möglichst wenige, ausschließlich über Doc Values zugreifbare Felder im Skript zu verwenden und komplexe Berechnungen wie Stringverarbeitung oder verschachtelte Bedingungen zu vermeiden. In der Praxis lohnt sich ein Benchmark mit der Profiler-API vor dem produktiven Rollout, um den zusätzlichen Zeitanteil des Scorings gegenüber der reinen Textsuche konkret zu messen, statt sich auf Vermutungen zu verlassen.

9. Testing und Debugging von Custom Scoring mit Profiler und Explain API

Die explain-API liefert für ein einzelnes Dokument eine vollständige Aufschlüsselung, wie sich der finale Score aus Textrelevanz, den einzelnen Function-Score-Beiträgen und dem gewählten boost_mode zusammensetzt, was beim Debugging unerwarteter Rankings unverzichtbar ist. Bei einer Beschwerde, dass ein bestimmtes Produkt zu weit unten erscheint, lässt sich damit direkt nachvollziehen, ob Textrelevanz oder das Scoring-Skript die Ursache ist.

Die Profiler-API ergänzt das um eine detaillierte Zeitmessung pro Query-Komponente, einschließlich des Skriptanteils, und sollte fester Bestandteil jedes Lasttests vor der Einführung eines neuen Scoring-Skripts sein. Ein einfacher Regressionstest, der die Top-Treffer für eine Reihe repräsentativer Suchbegriffe vor und nach einer Skriptänderung vergleicht, verhindert zudem, dass eine gut gemeinte Anpassung am Merchandising die tatsächliche Relevanz für Nutzer unbeabsichtigt verschlechtert.

Ansatz Textrelevanz berücksichtigt Geschäftliche Signale Performance-Risiko
Standard BM25 ohne Function Score Ja, ausschließlich Keine Kein zusätzliches Risiko
Function Score mit field_value_factor Ja, multiplikativ kombiniert Ein einzelnes numerisches Feld Gering, keine Skriptausführung
Function Score mit script_score (Painless) Ja, frei konfigurierbar kombiniert Beliebig komplexe Logik, mehrere Felder Mittel bis hoch, abhängig von Skriptkomplexität
Vorberechneter Boost-Wert als Indexfeld Ja, über field_value_factor kombiniert Beliebig komplex, aber zur Indexzeit berechnet Gering zur Suchzeit, Aufwand verschiebt sich auf Indexierung

Mironsoft

Suchindex-Setup, Relevanz-Tuning und Magento-Suche

Magento-Suche, die die falschen Produkte zuerst zeigt?

Wir richten Elasticsearch oder OpenSearch für Magento sauber ein, tunen Relevanz und Facetten auf das tatsächliche Sortiment und optimieren Indexierungsprozesse für große Kataloge.

Relevanz-Tuning

Suchergebnisse und Facetten auf die tatsächlichen Kundenbedürfnisse abstimmen.

Such-Migration

Umstieg von Solr oder MySQL-Suche auf Elasticsearch/OpenSearch sauber begleiten.

Index-Performance

Indexierungsprozesse für große Kataloge zuverlässig und performant gestalten.

10. Zusammenfassung

Painless Custom Scoring

Einstiegspunkt in Magento

Plugin auf SearchAdapter\Query\Builder::build()

Empfohlener boost_mode

multiply, damit Textrelevanz nicht verwässert wird

Wichtigste Performance-Regel

Nur Doc-Values-Felder nutzen, Skripte per params parametrisieren

Vor Rollout prüfen

Explain- und Profiler-API sowie Regressionstest auf Top-Treffern

11. FAQ: Painless Custom Scoring

1Muss ich für Custom Scoring zwingend Painless verwenden?
Für komplexe, mehrere Felder kombinierende Logik ist Painless meist die einzige praktikable Option. Für einfache Fälle mit nur einem numerischen Feld reicht oft schon die einfachere field_value_factor-Funktion ohne eigenes Skript.
2Wo genau setze ich die Function-Score-Logik in Magento an?
Ueblich ist ein Plugin auf die build()-Methode von Magento\Elasticsearch\SearchAdapter\Query\Builder, das die von Magento erzeugte Query nachträglich in eine function_score-Query einwickelt, ohne die bestehende Filter- und Facettenlogik zu verändern.
3Wie stark sollte der Einfluss von Lagerbestand oder Marge auf das Scoring sein?
In der Praxis bewährt sich ein Anteil von etwa zehn bis dreißig Prozent des ursprünglichen Textscores. Ein höherer Anteil lässt geschäftliche Signale die eigentliche Suchrelevanz zu stark dominieren.
4Warum sollte ich params statt fest kodierter Werte im Skript verwenden?
Nur wenn der Skripttext selbst unverändert bleibt, kann Elasticsearch beziehungsweise OpenSearch den kompilierten Bytecode zwischen Anfragen wiederverwenden. Fest kodierte, sich ändernde Werte erzwingen eine Neukompilierung und können das Compile-Rate-Limit des Clusters überschreiten.
5Was ist der Unterschied zwischen doc['feld'].value und Zugriff über _source im Skript?
doc['feld'].value greift auf Doc Values zu, eine spaltenorientierte, für Skripte optimierte Datenstruktur, und ist deutlich schneller als das Parsen von _source, das JSON-Deserialisierung für jedes Dokument erfordert.
6Kann ein fehlerhaftes Painless-Skript den gesamten Cluster gefährden?
Nein, die Sandbox verhindert Zugriff auf Dateisystem, Netzwerk und beliebige Java-Klassen. Ein fehlerhaftes Skript führt bestenfalls zu einem Fehler bei der betroffenen Suchanfrage, nicht zu einer Instabilität des Clusters.
7Lohnt sich ein Stored Script gegenüber einem Inline-Skript?
Ja, besonders bei häufig genutzten Scoring-Skripten reduziert ein Stored Script die übertragene Datenmenge pro Anfrage und macht Änderungen zentral wartbar, ohne den PHP-Code des Magento-Plugins anzupassen.
8Wie messe ich den Performance-Einfluss eines neuen Scoring-Skripts konkret?
Über die Profiler-API lässt sich der Zeitanteil des Skripts gegenüber der reinen Textsuche für eine einzelne Anfrage exakt messen. Ein Lasttest mit repräsentativen Suchbegriffen vor und nach der Einführung zeigt den Effekt auf die gesamte Antwortzeitverteilung.
9Kann ich Margendaten so absichern, dass sie nur im Scoring, aber nicht im Suchergebnis erscheinen?
Ja, das Feld wird für das Scoring im Skript ausgewertet, muss aber nicht in den für die Storefront zurückgegebenen Ergebnisfeldern enthalten sein. Zusätzlich lässt sich Field-Level-Security einsetzen, um das Feld auch bei direkten Cluster-Zugriffen abzusichern.
10Was ist die Alternative zu Function Score, wenn Performance zum Problem wird?
Ein vorberechneter Boost-Wert, der bereits während der Indexierung als eigenes numerisches Feld gespeichert wird, lässt sich zur Suchzeit über die deutlich günstigere field_value_factor-Funktion einbinden und verschiebt den Rechenaufwand von der Suchzeit auf die Indexierung.