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.
Inhaltsverzeichnis
- 1. Wo Painless in Elasticsearch zum Einsatz kommt
- 2. Scripted Fields und Runtime Fields: Werte zur Suchzeit berechnen
- 3. Update-Skripte: bedingte, serverseitige Dokumentänderungen
- 4. Function Score: individuelle Relevanzberechnung mit Skripten
- 5. Performance-Implikationen im Vergleich zu nativen Query-Bausteinen
- 6. Sicherheits-Sandbox: was Painless bewusst nicht darf
- 7. Praxisbeispiel: dynamische Preisberechnung per Update-Skript
- 8. Skript-Caching und die Kosten der Kompilierung
- 9. Debugging und Fehlerbehandlung bei Painless-Skripten
- 10. Zusammenfassung
- 11. FAQ
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.