Painless Scripting für benutzerdefinierte Logik
AI generated
_doc
_index
Elasticsearch / Ingest & Pipelines
Painless Scripting
benutzerdefinierte Logik zwischen Flexibilität und Performance-Kosten

Elasticsearch deckt die meisten Anwendungsfälle mit deklarativen Query-Bausteinen ab, aber manche Berechnungen lassen sich nicht sinnvoll in ein festes Set an Query-Klauseln pressen: eine dynamische Preisberechnung abhängig von mehreren Feldern, eine individuelle Score-Formel oder ein bedingtes Update, das je nach vorhandenem Wert unterschiedlich reagiert. Für genau diese Fälle existiert Painless, eine eigens für Elasticsearch entwickelte, in einer strikten Sandbox laufende Skriptsprache. Dieser Artikel zeigt, in welchen Kontexten Painless zum Einsatz kommt, welche Performance-Kosten Skripte gegenüber nativen Query-Bausteinen verursachen, und wo die bewusst gezogenen Sicherheitsgrenzen der Sandbox liegen.

12 Min. Lesezeit Painless · Sandbox Function Score · Runtime Fields

1. Wo Painless in Elasticsearch zum Einsatz kommt

Painless taucht an mehreren, klar getrennten Stellen im Elasticsearch-Ökosystem auf, jede mit einem eigenen Ausführungskontext und eigenen verfügbaren Variablen. In Runtime Fields und Scripted Fields berechnet ein Skript zur Suchzeit einen Feldwert aus vorhandenen Dokumentdaten, in Update-Anfragen verändert ein Skript über die Variable ctx den gespeicherten Dokumentinhalt, und in einer function_score-Query beeinflusst ein Skript die Relevanzbewertung eines Treffers.

Zusätzlich lässt sich Painless im script-Prozessor einer Ingest Pipeline sowie in Scripted-Metric-Aggregationen einsetzen, wo es eigene Zwischenwerte über mehrere Dokumente hinweg akkumuliert. Diese Kontexte sind bewusst voneinander getrennt: Ein für den Update-Kontext geschriebenes Skript mit ctx._source funktioniert nicht unverändert in einem Suchkontext, der stattdessen die doc-Variable für Zugriff auf indexierte Feldwerte bereitstellt.

2. Scripted Fields und Runtime Fields: Werte zur Suchzeit berechnen

Ein Runtime Field definiert im Mapping keinen gespeicherten Wert, sondern eine Painless-Berechnungsvorschrift, die bei jeder Suchanfrage, die dieses Feld anfragt, live gegen die tatsächlich zurückgegebenen Dokumente ausgeführt wird. Das eignet sich für Felder, die sich selten abfragen lassen, sich aber häufig ändern würden, wenn sie klassisch indexiert wären, etwa eine abgeleitete Kennzahl aus mehreren Basisfeldern, die sich mit jeder Preisänderung neu berechnen müsste.

Der Vorteil ist, keine Neuindexierung bei einer Änderung der Berechnungslogik zu benötigen, da sich lediglich die Skriptdefinition im Mapping ändert, nicht die gespeicherten Dokumente selbst. Der Nachteil ist, dass die Berechnung bei jeder betroffenen Suchanfrage erneut für jedes zurückgegebene Dokument ausgeführt wird, statt einmalig beim Schreiben.


PUT products/_mapping
{
  "runtime": {
    "margin_percent": {
      "type": "double",
      "script": {
        "source": "if (doc['price'].size() > 0 && doc['cost'].size() > 0) { emit((doc['price'].value - doc['cost'].value) / doc['price'].value * 100) }"
      }
    }
  }
}

3. Update-Skripte: bedingte, serverseitige Dokumentänderungen

Ein Update-Request mit Skript erlaubt bedingte, serverseitige Änderungen an einem bestehenden Dokument, ohne dass die Anwendung den aktuellen Zustand vorher komplett laden und den gesamten Dokumentinhalt zurücksenden muss. Über die Variable ctx._source greift das Skript direkt auf die gespeicherten Felder zu und kann sie abhängig von übergebenen Parametern verändern, etwa eine Lagerbestandsanpassung, die nur greift, wenn der aktuelle Bestand einen Schwellenwert nicht unterschreitet.

Wichtig ist, jeden dynamischen Wert über das params-Objekt zu übergeben, statt ihn direkt in den Skripttext einzubetten. Ein Skript mit fest eingebetteten Werten erzeugt für jede unterschiedliche Wertkombination eine neue, eindeutige Skriptsignatur, was den internen Skript-Cache umgeht und bei jedem Aufruf eine teure Neukompilierung erzwingt.


POST products/_update/4711
{
  "script": {
    "source": "if (ctx._source.stock >= params.amount) { ctx._source.stock -= params.amount } else { ctx.op = 'noop' }",
    "params": { "amount": 3 }
  }
}

4. Function Score: individuelle Relevanzberechnung mit Skripten

In einer function_score-Query lässt sich über script_score eine beliebige Painless-Formel als zusätzlicher Bewertungsfaktor einbringen, etwa um Popularität, Aktualität und Textrelevanz in einer einzigen, individuell gewichteten Formel zu kombinieren, die sich mit den eingebauten Bewertungsfunktionen allein nicht abbilden ließe.

Da dieses Skript für jedes Dokument ausgeführt wird, das die vorgelagerte Query bereits als Treffer zurückgibt, sollte die vorgelagerte Query so eng wie möglich gefasst sein, bevor der Score-Skript überhaupt greift. Eine breite Match-Query mit einem teuren Score-Skript über Millionen Treffer ist fast immer langsamer als eine eng gefasste Query mit einem einfacheren Score-Skript über wenige tausend Treffer.


GET products/_search
{
  "query": {
    "function_score": {
      "query": { "match": { "category": "schuhe" } },
      "script_score": {
        "script": {
          "source": "doc['popularity'].value / (1 + params.now_days - doc['created_days'].value)",
          "params": { "now_days": 20320 }
        }
      }
    }
  }
}

5. Performance-Implikationen im Vergleich zu nativen Query-Bausteinen

Native Query-Bausteine wie range, term oder eingebaute Bewertungsfunktionen wie field_value_factor arbeiten direkt gegen vorindexierte, optimierte Datenstrukturen und sind für genau diese Zugriffsmuster kompiliert. Ein Painless-Skript dagegen wird zwar ebenfalls zu Bytecode kompiliert, muss aber pro Dokument interpretiert ausgewertet werden und hat damit strukturell höhere Kosten pro Auswertung als ein nativer Vergleichsoperator.

Für Felder, die häufig abgefragt werden, lohnt sich deshalb fast immer, den Wert einmalig beim Schreiben zu berechnen und als normales, indexiertes Feld zu speichern, statt ihn bei jeder Suchanfrage über ein Runtime Field oder ein Score-Skript neu zu berechnen. Skripte bleiben dort sinnvoll, wo die Berechnung selten genug abgefragt wird oder sich zu häufig ändert, um eine feste Indexierung zu rechtfertigen.

6. Sicherheits-Sandbox: was Painless bewusst nicht darf

Painless läuft in einer strikten Sandbox ohne Zugriff auf das Dateisystem, das Netzwerk oder beliebige Java-Klassen über Reflection, und kann auch keine eigenen Threads starten. Erlaubt ist ausschließlich eine explizit freigegebene, kontextabhängige API-Oberfläche, etwa der Zugriff auf Dokumentwerte über doc oder auf Parameter über params, nicht aber der freie Zugriff auf beliebige Systemressourcen.

Diese Einschränkung ist eine bewusste Lehre aus der Vorgängerlösung Groovy, die vor Version 5 als Standard-Skriptsprache diente und aufgrund fehlender Sandbox-Grenzen mehrfach für Sicherheitslücken sorgte, über die beliebiger Code auf dem Server ausgeführt werden konnte. Zusätzlich sind reguläre Ausdrücke in Painless standardmäßig deaktiviert und müssen über die Einstellung script.painless.regex.enabled explizit aktiviert werden, da unkontrollierte Regex-Muster zu erheblicher CPU-Last führen können.

7. Praxisbeispiel: dynamische Preisberechnung per Update-Skript

Ein realistisches Beispiel ist ein Rabatt-Update, das über ein Skript den gespeicherten Preis nur dann anpasst, wenn eine Kampagnenbedingung erfüllt ist, statt den vollständigen Dokumentinhalt aus der Anwendung neu zu senden. Das Skript liest den aktuellen Basispreis aus ctx._source, wendet den über params übergebenen Rabattfaktor an und schreibt den neuen Preis zurück, alles serverseitig in einem einzigen Request.

Dieser Ansatz reduziert Netzwerkverkehr gegenüber einem klassischen Lesen-Ändern-Schreiben-Zyklus aus der Anwendung, bringt aber dieselbe strukturelle Voraussetzung mit sich wie jedes Painless-Skript: Der Rabattfaktor muss über params übergeben werden, damit dieselbe Skriptsignatur für unterschiedliche Rabattwerte wiederverwendet und aus dem Skript-Cache bedient werden kann, statt bei jedem abweichenden Faktor neu kompiliert zu werden.


POST products/_update_by_query
{
  "script": {
    "source": "ctx._source.price = Math.round(ctx._source.base_price * params.discount_factor * 100) / 100.0",
    "params": { "discount_factor": 0.85 }
  },
  "query": { "term": { "campaign": "summer-sale" } }
}

8. Skript-Caching und die Kosten der Kompilierung

Jedes neu kompilierte Painless-Skript landet im internen Skript-Cache, dessen Größe über script.cache.max_size und dessen Ablaufzeit über script.cache.expire konfigurierbar sind. Ein bereits kompiliertes Skript mit identischer Signatur wird aus diesem Cache bedient und muss nicht erneut geparst und kompiliert werden, was den eigentlichen Ausführungsaufwand für wiederkehrende Aufrufe deutlich reduziert.

Wird dieselbe Skriptlogik jedoch mit unterschiedlichen, direkt im Skripttext eingebetteten Werten aufgerufen, statt sie als Parameter zu übergeben, erzeugt jeder Aufruf eine eigene Cache-Signatur, was zu ständiger Neukompilierung führt und ab einer gewissen Rate die konfigurierte Kompilierungsrate über script.max_compilations_rate überschreiten kann, woraufhin Elasticsearch weitere Kompilierungen mit einem Fehler ablehnt.

9. Debugging und Fehlerbehandlung bei Painless-Skripten

Schlägt ein Painless-Skript fehl, liefert Elasticsearch eine detaillierte Fehlerantwort mit Skript-Stacktrace, betroffener Zeile und Spalte, was die Fehlersuche gegenüber einer generischen Fehlermeldung deutlich erleichtert. Ein häufiger Laufzeitfehler ist der Zugriff auf ein Feld, das im aktuellen Dokument nicht vorhanden ist, was sich mit einer expliziten Prüfung über doc['feld'].size() == 0 vor dem eigentlichen Zugriff abfangen lässt.

Für die Entwicklung neuer Skripte empfiehlt sich, sie zunächst gegen einzelne Testdokumente über die _scripts/painless/_execute-API zu prüfen, bevor sie in einer produktiven Query oder einem Update-Vorgang zum Einsatz kommen. Diese API führt ein Skript isoliert aus und liefert entweder das Ergebnis oder eine präzise Fehlermeldung zurück, ganz ohne dass dafür ein echtes Dokument verändert werden müsste.


POST _scripts/painless/_execute
{
  "script": {
    "source": "params.price * params.discount_factor",
    "params": { "price": 49.90, "discount_factor": 0.85 }
  }
}
Einsatzort Ausführungszeitpunkt Typische Nutzung Performance-Hinweis
Runtime Field Bei jeder passenden Suchanfrage Selten abgefragte, dynamische Felder Bei häufiger Abfrage besser indexieren
Update-Skript Beim Schreiben, serverseitig Bedingte Dokumentänderungen Werte immer über params übergeben
Function Score Pro Treffer der vorgelagerten Query Individuelle Relevanzformel Vorgelagerte Query eng fassen
Ingest script-Prozessor Beim Indexieren, einmalig Individuelle Transformationslogik Sparsam einsetzen, CPU-Kosten pro Dokument
Scripted-Metric-Aggregation Über mehrere Dokumente akkumulierend Komplexe, mehrstufige Berechnungen Nur wenn native Aggregationen nicht ausreichen

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 Scripting: Das Wichtigste auf einen Blick

Kernprinzip

Painless bringt individuelle Logik in Runtime Fields, Update-Anfragen, Function Score und Ingest-Pipelines, jeweils mit eigenem, klar abgegrenztem Ausführungskontext.

Performance-Regel

Native Query-Bausteine sind für häufig abgefragte Werte fast immer schneller. Skripte lohnen sich vor allem bei seltenen Abfragen oder sich häufig ändernder Logik.

Cache-Falle

Dynamische Werte gehören immer in params, nicht in den Skripttext, sonst erzeugt jede Wertkombination eine neue Signatur und erzwingt teure Neukompilierung.

Sicherheitsgrenzen

Kein Datei-, Netzwerk- oder Reflection-Zugriff, keine eigenen Threads, reguläre Ausdrücke standardmäßig deaktiviert, als bewusste Lehre aus der unsicheren Groovy-Ära.

11. FAQ: Painless Scripting: Das Wichtigste auf einen Blick

1In welchen Kontexten kommt Painless in Elasticsearch zum Einsatz?
In Runtime und Scripted Fields, in Update-Anfragen über ctx, in function_score-Queries über script_score, im script-Prozessor einer Ingest Pipeline und in Scripted-Metric-Aggregationen, jeweils mit eigenem Ausführungskontext.
2Warum funktioniert ein für Updates geschriebenes Skript nicht automatisch im Suchkontext?
Weil jeder Kontext eigene Variablen bereitstellt. Der Update-Kontext nutzt ctx._source, der Suchkontext stattdessen doc für den Zugriff auf indexierte Feldwerte.
3Warum sollten dynamische Werte immer über params statt direkt im Skripttext übergeben werden?
Weil eingebettete Werte für jede Kombination eine neue Skriptsignatur erzeugen, den Skript-Cache umgehen und bei jedem Aufruf eine teure Neukompilierung erzwingen.
4Wann lohnt sich ein Runtime Field gegenüber einem klassisch indexierten Feld?
Wenn das Feld selten abgefragt wird oder sich die Berechnungslogik häufig ändert. Bei häufigen Abfragen ist ein einmalig zur Schreibzeit berechnetes, indexiertes Feld performanter.
5Warum sollte die vorgelagerte Query bei einem script_score möglichst eng gefasst sein?
Weil das Skript für jeden Treffer der vorgelagerten Query ausgeführt wird. Eine breite Query mit vielen Treffern multipliziert die Skriptkosten entsprechend.
6Was verbietet die Painless-Sandbox konkret?
Zugriff auf das Dateisystem, das Netzwerk, beliebige Java-Klassen über Reflection sowie das Starten eigener Threads. Erlaubt ist nur eine explizit freigegebene, kontextabhängige API.
7Warum sind reguläre Ausdrücke in Painless standardmäßig deaktiviert?
Weil unkontrollierte Regex-Muster zu erheblicher CPU-Last führen können. Sie müssen über script.painless.regex.enabled explizit aktiviert werden.
8Wie lässt sich ein Skript vor dem produktiven Einsatz testen?
Über die _scripts/painless/_execute-API, die ein Skript isoliert gegen Testdaten ausführt und Ergebnis oder Fehlermeldung liefert, ohne ein echtes Dokument zu verändern.
9Was passiert, wenn die konfigurierte Kompilierungsrate überschritten wird?
Elasticsearch lehnt weitere Kompilierungen mit einem Fehler ab, bis sich die Rate wieder innerhalb des über script.max_compilations_rate konfigurierten Limits bewegt.
10Wie behandelt man den Zugriff auf ein möglicherweise fehlendes Feld in einem Skript?
Mit einer expliziten Prüfung über doc['feld'].size() == 0 vor dem eigentlichen Zugriff, um einen Laufzeitfehler bei fehlendem Feld zu vermeiden.