Forráskód Böngészése

Script: Fields API for Sort and Score scripts (#75863)

Adds minimal fields API support to sort and score scripts.

Example: `field('myfield').getValue(123)` where `123` is the default if the field has no values.

Refs: #61388
Stuart Tettemer 4 éve
szülő
commit
6c02a6c657
42 módosított fájl, 736 hozzáadás és 133 törlés
  1. 6 5
      benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java
  2. 12 2
      modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionNumberSortScript.java
  3. 11 1
      modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScoreScript.java
  4. 2 1
      modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java
  5. 2 1
      modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java
  6. 1 0
      modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java
  7. 14 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java
  8. 5 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java
  9. 20 0
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.numbersort.txt
  10. 20 0
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.score.txt
  11. 19 0
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.stringsort.txt
  12. 21 0
      modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.txt
  13. 65 0
      modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/40_sort_script_fields_api.yml
  14. 51 0
      modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/88_script_score_fields_api.yml
  15. 8 6
      plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java
  16. 5 3
      server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java
  17. 6 2
      server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreFunction.java
  18. 7 3
      server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java
  19. 4 2
      server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java
  20. 4 2
      server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java
  21. 5 28
      server/src/main/java/org/elasticsearch/script/AbstractSortScript.java
  22. 75 0
      server/src/main/java/org/elasticsearch/script/DocBasedScript.java
  23. 42 0
      server/src/main/java/org/elasticsearch/script/DocReader.java
  24. 75 0
      server/src/main/java/org/elasticsearch/script/DocValuesDocReader.java
  25. 46 0
      server/src/main/java/org/elasticsearch/script/DocValuesField.java
  26. 40 0
      server/src/main/java/org/elasticsearch/script/EmptyField.java
  27. 35 0
      server/src/main/java/org/elasticsearch/script/Field.java
  28. 19 0
      server/src/main/java/org/elasticsearch/script/LeafReaderContextSupplier.java
  29. 20 5
      server/src/main/java/org/elasticsearch/script/NumberSortScript.java
  30. 25 23
      server/src/main/java/org/elasticsearch/script/ScoreScript.java
  31. 16 6
      server/src/main/java/org/elasticsearch/script/StringSortScript.java
  32. 8 4
      server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java
  33. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java
  34. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/DateScriptFieldTypeTests.java
  35. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/DoubleScriptFieldTypeTests.java
  36. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/GeoPointScriptFieldTypeTests.java
  37. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/IpScriptFieldTypeTests.java
  38. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/KeywordScriptFieldTypeTests.java
  39. 4 3
      server/src/test/java/org/elasticsearch/index/mapper/LongScriptFieldTypeTests.java
  40. 9 6
      server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryTests.java
  41. 6 6
      test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java
  42. 4 4
      x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldTypeTests.java

+ 6 - 5
benchmarks/src/main/java/org/elasticsearch/benchmark/script/ScriptScoreBenchmark.java

@@ -14,7 +14,6 @@ import org.apache.lucene.index.IndexReader;
 import org.apache.lucene.index.IndexWriter;
 import org.apache.lucene.index.IndexWriterConfig;
 import org.apache.lucene.index.IndexWriterConfig.OpenMode;
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.SortedNumericDocValues;
 import org.apache.lucene.search.IndexSearcher;
 import org.apache.lucene.search.MatchAllDocsQuery;
@@ -35,6 +34,8 @@ import org.elasticsearch.indices.breaker.CircuitBreakerService;
 import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
 import org.elasticsearch.plugins.PluginsService;
 import org.elasticsearch.plugins.ScriptPlugin;
+import org.elasticsearch.script.DocReader;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.ScriptModule;
 import org.elasticsearch.search.lookup.SearchLookup;
@@ -154,7 +155,7 @@ public class ScriptScoreBenchmark {
 
     private Query scriptScoreQuery(ScoreScript.Factory factory) {
         ScoreScript.LeafFactory leafFactory = factory.newFactory(Map.of(), lookup);
-        return new ScriptScoreQuery(new MatchAllDocsQuery(), null, leafFactory, null, "test", 0, Version.CURRENT);
+        return new ScriptScoreQuery(new MatchAllDocsQuery(), null, leafFactory, lookup, null, "test", 0, Version.CURRENT);
     }
 
     private ScoreScript.Factory bareMetalScript() {
@@ -163,9 +164,9 @@ public class ScriptScoreBenchmark {
             IndexNumericFieldData ifd = (IndexNumericFieldData) lookup.getForField(type);
             return new ScoreScript.LeafFactory() {
                 @Override
-                public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
-                    SortedNumericDocValues values = ifd.load(ctx).getLongValues();
-                    return new ScoreScript(params, lookup, ctx) {
+                public ScoreScript newInstance(DocReader docReader) throws IOException {
+                    SortedNumericDocValues values = ifd.load(((DocValuesDocReader) docReader).getLeafReaderContext()).getLongValues();
+                    return new ScoreScript(params, null, docReader) {
                         private int docId;
 
                         @Override

+ 12 - 2
modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionNumberSortScript.java

@@ -15,7 +15,9 @@ import org.apache.lucene.expressions.SimpleBindings;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.GeneralScriptException;
+import org.elasticsearch.script.LeafReaderContextSupplier;
 import org.elasticsearch.script.NumberSortScript;
 
 /**
@@ -37,10 +39,18 @@ class ExpressionNumberSortScript implements NumberSortScript.LeafFactory {
     }
 
     @Override
-    public NumberSortScript newInstance(final LeafReaderContext leaf) throws IOException {
+    public NumberSortScript newInstance(final DocReader reader) throws IOException {
+        // Use DocReader to get the leaf context while transitioning to DocReader for Painless.  DocReader for expressions should follow.
+        if (reader instanceof LeafReaderContextSupplier == false) {
+            throw new IllegalStateException(
+                "Expected LeafReaderContextSupplier when creating expression NumberSortScript instead of [" + reader + "]"
+            );
+        }
+        final LeafReaderContext ctx = ((LeafReaderContextSupplier) reader).getLeafReaderContext();
+
         return new NumberSortScript() {
             // Fake the scorer until setScorer is called.
-            DoubleValues values = source.getValues(leaf, new DoubleValues() {
+            DoubleValues values = source.getValues(ctx, new DoubleValues() {
                 @Override
                 public double doubleValue() {
                     return 0.0D;

+ 11 - 1
modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScoreScript.java

@@ -14,7 +14,9 @@ import org.apache.lucene.expressions.SimpleBindings;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.DoubleValues;
 import org.apache.lucene.search.DoubleValuesSource;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.GeneralScriptException;
+import org.elasticsearch.script.LeafReaderContextSupplier;
 import org.elasticsearch.script.ScoreScript;
 
 import java.io.IOException;
@@ -41,7 +43,15 @@ class ExpressionScoreScript implements ScoreScript.LeafFactory {
     }
 
     @Override
-    public ScoreScript newInstance(final LeafReaderContext leaf) throws IOException {
+    public ScoreScript newInstance(final DocReader reader) throws IOException {
+        // Use DocReader to get the leaf context while transitioning to DocReader for Painless.  DocReader for expressions should follow.
+        if (reader instanceof LeafReaderContextSupplier == false) {
+            throw new IllegalStateException(
+                "Expected LeafReaderContextSupplier when creating expression ExpressionScoreScript instead of [" + reader + "]"
+            );
+        }
+        final LeafReaderContext leaf = ((LeafReaderContextSupplier) reader).getLeafReaderContext();
+
         return new ScoreScript(null, null, null) {
             // Fake the scorer until setScorer is called.
             DoubleValues values = source.getValues(leaf, new DoubleValues() {

+ 2 - 1
modules/lang-expression/src/main/java/org/elasticsearch/script/expression/ExpressionScriptEngine.java

@@ -25,6 +25,7 @@ import org.elasticsearch.script.AggregationScript;
 import org.elasticsearch.script.BucketAggregationScript;
 import org.elasticsearch.script.BucketAggregationSelectorScript;
 import org.elasticsearch.script.ClassPermission;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.FieldScript;
 import org.elasticsearch.script.FilterScript;
 import org.elasticsearch.script.NumberSortScript;
@@ -332,7 +333,7 @@ public class ExpressionScriptEngine implements ScriptEngine {
     private static FilterScript.LeafFactory newFilterScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
         ScoreScript.LeafFactory searchLeafFactory = newScoreScript(expr, lookup, vars);
         return ctx -> {
-            ScoreScript script = searchLeafFactory.newInstance(ctx);
+            ScoreScript script = searchLeafFactory.newInstance(new DocValuesDocReader(lookup, ctx));
             return new FilterScript(vars, lookup, ctx) {
                 @Override
                 public boolean execute() {

+ 2 - 1
modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java

@@ -13,6 +13,7 @@ import org.elasticsearch.index.fielddata.LeafNumericFieldData;
 import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
 import org.elasticsearch.index.mapper.NumberFieldMapper.NumberFieldType;
 import org.elasticsearch.index.mapper.NumberFieldMapper.NumberType;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.NumberSortScript;
 import org.elasticsearch.script.ScriptException;
 import org.elasticsearch.search.lookup.SearchLookup;
@@ -73,7 +74,7 @@ public class ExpressionNumberSortScriptTests extends ESTestCase {
     }
 
     public void testFieldAccess() throws IOException {
-        NumberSortScript script = compile("doc['field'].value").newInstance(null);
+        NumberSortScript script = compile("doc['field'].value").newInstance(mock(DocValuesDocReader.class));
         script.setDocument(1);
 
         double result = script.execute();

+ 1 - 0
modules/lang-painless/spi/src/main/java/org/elasticsearch/painless/spi/Whitelist.java

@@ -29,6 +29,7 @@ public final class Whitelist {
     private static final String[] BASE_WHITELIST_FILES = new String[] {
         "org.elasticsearch.txt",
         "org.elasticsearch.net.txt",
+        "org.elasticsearch.script.fields.txt",
         "java.lang.txt",
         "java.math.txt",
         "java.text.txt",

+ 14 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java

@@ -38,11 +38,13 @@ import org.elasticsearch.repositories.RepositoriesService;
 import org.elasticsearch.rest.RestController;
 import org.elasticsearch.rest.RestHandler;
 import org.elasticsearch.script.IngestScript;
+import org.elasticsearch.script.NumberSortScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.ScriptContext;
 import org.elasticsearch.script.ScriptEngine;
 import org.elasticsearch.script.ScriptModule;
 import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.StringSortScript;
 import org.elasticsearch.search.aggregations.pipeline.MovingFunctionScript;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.watcher.ResourceWatcherService;
@@ -80,7 +82,9 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
         // Functions used for scoring docs
         List<Whitelist> scoreFn = new ArrayList<>(Whitelist.BASE_WHITELISTS);
         Whitelist scoreFnWhitelist = WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.score.txt");
+        Whitelist scoreFieldWhitelist = WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.script.fields.score.txt");
         scoreFn.add(scoreFnWhitelist);
+        scoreFn.add(scoreFieldWhitelist);
         map.put(ScoreScript.CONTEXT, scoreFn);
 
         // Functions available to ingest pipelines
@@ -95,6 +99,16 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
             map.put(scriptContext, getRuntimeFieldWhitelist(scriptContext.name));
         }
 
+        List<Whitelist> numSort = new ArrayList<>(Whitelist.BASE_WHITELISTS);
+        Whitelist numSortField = WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.script.fields.numbersort.txt");
+        numSort.add(numSortField);
+        map.put(NumberSortScript.CONTEXT, numSort);
+
+        List<Whitelist> strSort = new ArrayList<>(Whitelist.BASE_WHITELISTS);
+        Whitelist strSortField = WhitelistLoader.loadFromResourceFiles(Whitelist.class, "org.elasticsearch.script.fields.stringsort.txt");
+        strSort.add(strSortField);
+        map.put(StringSortScript.CONTEXT, strSort);
+
         // Execute context gets everything
         List<Whitelist> test = new ArrayList<>(Whitelist.BASE_WHITELISTS);
         test.add(movFnWhitelist);

+ 5 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessExecuteAction.java

@@ -69,6 +69,7 @@ import org.elasticsearch.rest.RestRequest;
 import org.elasticsearch.rest.action.RestToXContentListener;
 import org.elasticsearch.script.BooleanFieldScript;
 import org.elasticsearch.script.DateFieldScript;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.DoubleFieldScript;
 import org.elasticsearch.script.FilterScript;
 import org.elasticsearch.script.GeoPointFieldScript;
@@ -81,6 +82,7 @@ import org.elasticsearch.script.ScriptModule;
 import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.script.ScriptType;
 import org.elasticsearch.script.StringFieldScript;
+import org.elasticsearch.search.lookup.SearchLookup;
 import org.elasticsearch.threadpool.ThreadPool;
 import org.elasticsearch.transport.TransportService;
 
@@ -528,9 +530,10 @@ public class PainlessExecuteAction extends ActionType<PainlessExecuteAction.Resp
             } else if (scriptContext == ScoreScript.CONTEXT) {
                 return prepareRamIndex(request, (context, leafReaderContext) -> {
                     ScoreScript.Factory factory = scriptService.compile(request.script, ScoreScript.CONTEXT);
+                    SearchLookup lookup = context.lookup();
                     ScoreScript.LeafFactory leafFactory =
-                            factory.newFactory(request.getScript().getParams(), context.lookup());
-                    ScoreScript scoreScript = leafFactory.newInstance(leafReaderContext);
+                            factory.newFactory(request.getScript().getParams(), lookup);
+                    ScoreScript scoreScript = leafFactory.newInstance(new DocValuesDocReader(lookup, leafReaderContext));
                     scoreScript.setDocument(0);
 
                     if (request.contextSetup.query != null) {

+ 20 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.numbersort.txt

@@ -0,0 +1,20 @@
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License
+# 2.0 and the Server Side Public License, v 1; you may not use this file except
+# in compliance with, at your election, the Elastic License 2.0 or the Server
+# Side Public License, v 1.
+#
+
+# The whitelist for the fields api
+
+# The scripts must be whitelisted for painless to find the classes
+class org.elasticsearch.script.NumberSortScript @no_import {
+}
+class org.elasticsearch.script.NumberSortScript$Factory @no_import {
+}
+
+# Class bindings
+static_import {
+    org.elasticsearch.script.Field field(org.elasticsearch.script.NumberSortScript, String) bound_to org.elasticsearch.script.NumberSortScript$FieldAccess
+}

+ 20 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.score.txt

@@ -0,0 +1,20 @@
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License
+# 2.0 and the Server Side Public License, v 1; you may not use this file except
+# in compliance with, at your election, the Elastic License 2.0 or the Server
+# Side Public License, v 1.
+#
+
+# The whitelist for the fields api
+
+# The scripts must be whitelisted for painless to find the classes
+class org.elasticsearch.script.ScoreScript @no_import {
+}
+class org.elasticsearch.script.ScoreScript$Factory @no_import {
+}
+
+# Class bindings
+static_import {
+    org.elasticsearch.script.Field field(org.elasticsearch.script.ScoreScript, String) bound_to org.elasticsearch.script.ScoreScript$FieldAccess
+}

+ 19 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.stringsort.txt

@@ -0,0 +1,19 @@
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License
+# 2.0 and the Server Side Public License, v 1; you may not use this file except
+# in compliance with, at your election, the Elastic License 2.0 or the Server
+# Side Public License, v 1.
+#
+
+# The whitelist for the fields api
+# The scripts must be whitelisted for painless to find the classes
+class org.elasticsearch.script.StringSortScript @no_import {
+}
+class org.elasticsearch.script.StringSortScript$Factory @no_import {
+}
+
+# Class bindings
+static_import {
+    org.elasticsearch.script.Field field(org.elasticsearch.script.StringSortScript, String) bound_to org.elasticsearch.script.StringSortScript$FieldAccess
+}

+ 21 - 0
modules/lang-painless/src/main/resources/org/elasticsearch/painless/spi/org.elasticsearch.script.fields.txt

@@ -0,0 +1,21 @@
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License
+# 2.0 and the Server Side Public License, v 1; you may not use this file except
+# in compliance with, at your election, the Elastic License 2.0 or the Server
+# Side Public License, v 1.
+#
+
+# The whitelist for the fields api
+
+# API
+class org.elasticsearch.script.Field {
+  String getName()
+  boolean isEmpty()
+  List getValues()
+  def getValue(def)
+}
+
+class org.elasticsearch.script.DocBasedScript {
+    org.elasticsearch.script.Field field(String)
+}

+ 65 - 0
modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/40_sort_script_fields_api.yml

@@ -0,0 +1,65 @@
+# Integration tests for sort script queries using Painless
+
+setup:
+- skip:
+    version: " - 7.14.99"
+    reason: "sort script fields api was added in 7.15.0"
+
+---
+"sort script fields api":
+     - do:
+         indices.create:
+           index: test
+           body:
+             settings:
+               number_of_shards: 2
+             mappings:
+               properties:
+                 dval:
+                   type: double
+     - do:
+         index:
+           index: test
+           id: d1
+           body: {"dval": 10, "sval": "f"}
+     - do:
+         index:
+           index: test
+           id: d2
+           body: {}
+     - do:
+         index:
+           index: test
+           id: d3
+           body: {"dval": 5, "sval": "a"}
+     - do:
+         indices.refresh: {}
+     - do:
+         search:
+           rest_total_hits_as_int: true
+           index: test
+           body:
+             sort:
+               _script:
+                 type: number
+                 script:
+                   source: "field('dval').getValue(3)"
+     - match: { hits.total: 3 }
+     - match: { hits.hits.0._id: d2 }
+     - match: { hits.hits.1._id: d3 }
+     - match: { hits.hits.2._id: d1 }
+     - do:
+         search:
+           rest_total_hits_as_int: true
+           index: test
+           body:
+             sort:
+               _script:
+                 type: string
+                 script:
+                   source: "field('sval.keyword').getValue('g')"
+     - match: { hits.total: 3 }
+     - match: { hits.hits.0._id: d3 }
+     - match: { hits.hits.1._id: d1 }
+     - match: { hits.hits.2._id: d2 }
+

+ 51 - 0
modules/lang-painless/src/yamlRestTest/resources/rest-api-spec/test/painless/88_script_score_fields_api.yml

@@ -0,0 +1,51 @@
+# Integration tests for ScriptScoreQuery using Painless
+
+setup:
+- skip:
+    version: " - 7.14.99"
+    reason: "script score fields api was added in 7.15.0"
+
+---
+"script score fields api":
+     - do:
+         indices.create:
+           index: test
+           body:
+             settings:
+               number_of_shards: 2
+             mappings:
+               properties:
+                 dval:
+                   type: double
+     - do:
+         index:
+           index: test
+           id: d1
+           body: {"dval": 10}
+     - do:
+         index:
+           index: test
+           id: d2
+           body: {}
+     - do:
+         index:
+           index: test
+           id: d3
+           body: {"dval": 5}
+     - do:
+         indices.refresh: {}
+     - do:
+         search:
+           rest_total_hits_as_int: true
+           index: test
+           body:
+             query:
+               script_score:
+                 query: {match_all: {} }
+                 script:
+                   source: "field('dval').getValue(3)"
+     - match: { hits.total: 3 }
+     - match: { hits.hits.0._id: d1 }
+     - match: { hits.hits.1._id: d3 }
+     - match: { hits.hits.2._id: d2 }
+

+ 8 - 6
plugins/examples/script-expert-scoring/src/main/java/org/elasticsearch/example/expertscript/ExpertScriptPlugin.java

@@ -8,12 +8,13 @@
 
 package org.elasticsearch.example.expertscript;
 
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.PostingsEnum;
 import org.apache.lucene.index.Term;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.plugins.ScriptPlugin;
+import org.elasticsearch.script.DocReader;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.ScoreScript.LeafFactory;
 import org.elasticsearch.script.ScriptContext;
@@ -127,16 +128,17 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
             }
 
             @Override
-            public ScoreScript newInstance(LeafReaderContext context)
+            public ScoreScript newInstance(DocReader docReader)
                     throws IOException {
-                PostingsEnum postings = context.reader().postings(
-                        new Term(field, term));
+                DocValuesDocReader dvReader = ((DocValuesDocReader) docReader);
+                PostingsEnum postings = dvReader.getLeafReaderContext()
+                        .reader().postings(new Term(field, term));
                 if (postings == null) {
                     /*
                      * the field and/or term don't exist in this segment,
                      * so always return 0
                      */
-                    return new ScoreScript(params, lookup, context) {
+                    return new ScoreScript(params, lookup, docReader) {
                         @Override
                         public double execute(
                             ExplanationHolder explanation
@@ -145,7 +147,7 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
                         }
                     };
                 }
-                return new ScoreScript(params, lookup, context) {
+                return new ScoreScript(params, lookup, docReader) {
                     int currentDocid = -1;
                     @Override
                     public void setDocument(int docid) {

+ 5 - 3
server/src/internalClusterTest/java/org/elasticsearch/search/functionscore/ExplainableScriptIT.java

@@ -18,6 +18,8 @@ import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.plugins.Plugin;
 import org.elasticsearch.plugins.ScriptPlugin;
+import org.elasticsearch.script.DocReader;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.ExplainableScoreScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
@@ -79,8 +81,8 @@ public class ExplainableScriptIT extends ESIntegTestCase {
                         }
 
                         @Override
-                        public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
-                            return new MyScript(params1, lookup, ctx);
+                        public ScoreScript newInstance(DocReader docReader) throws IOException {
+                            return new MyScript(params1, lookup, ((DocValuesDocReader) docReader).getLeafReaderContext());
                         }
                     };
                     return context.factoryClazz.cast(factory);
@@ -97,7 +99,7 @@ public class ExplainableScriptIT extends ESIntegTestCase {
     static class MyScript extends ScoreScript implements ExplainableScoreScript {
 
         MyScript(Map<String, Object> params, SearchLookup lookup, LeafReaderContext leafContext) {
-            super(params, lookup, leafContext);
+            super(params, null, new DocValuesDocReader(lookup, leafContext));
         }
 
         @Override

+ 6 - 2
server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreFunction.java

@@ -11,9 +11,11 @@ package org.elasticsearch.common.lucene.search.function;
 import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.Scorable;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.ExplainableScoreScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -38,21 +40,23 @@ public class ScriptScoreFunction extends ScoreFunction {
     private final Script sScript;
 
     private final ScoreScript.LeafFactory script;
+    private final SearchLookup lookup;
 
     private final int shardId;
     private final String indexName;
 
-    public ScriptScoreFunction(Script sScript, ScoreScript.LeafFactory script, String indexName, int shardId) {
+    public ScriptScoreFunction(Script sScript, ScoreScript.LeafFactory script, SearchLookup lookup, String indexName, int shardId) {
         super(CombineFunction.REPLACE);
         this.sScript = sScript;
         this.script = script;
+        this.lookup = lookup;
         this.indexName = indexName;
         this.shardId = shardId;
     }
 
     @Override
     public LeafScoreFunction getLeafScoreFunction(LeafReaderContext ctx) throws IOException {
-        final ScoreScript leafScript = script.newInstance(ctx);
+        final ScoreScript leafScript = script.newInstance(new DocValuesDocReader(lookup, ctx));
         final CannedScorer scorer = new CannedScorer();
         leafScript.setScorer(scorer);
         leafScript._setIndexName(indexName);

+ 7 - 3
server/src/main/java/org/elasticsearch/common/lucene/search/function/ScriptScoreQuery.java

@@ -26,9 +26,11 @@ import org.apache.lucene.search.Scorer;
 import org.apache.lucene.search.BulkScorer;
 import org.apache.lucene.util.Bits;
 import org.elasticsearch.Version;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.ScoreScript.ExplanationHolder;
 import org.elasticsearch.script.Script;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -41,16 +43,18 @@ public class ScriptScoreQuery extends Query {
     private final Query subQuery;
     private final Script script;
     private final ScoreScript.LeafFactory scriptBuilder;
+    private final SearchLookup lookup;
     private final Float minScore;
     private final String indexName;
     private final int shardId;
     private final Version indexVersion;
 
-    public ScriptScoreQuery(Query subQuery, Script script, ScoreScript.LeafFactory scriptBuilder,
+    public ScriptScoreQuery(Query subQuery, Script script, ScoreScript.LeafFactory scriptBuilder, SearchLookup lookup,
                             Float minScore, String indexName, int shardId, Version indexVersion) {
         this.subQuery = subQuery;
         this.script = script;
         this.scriptBuilder = scriptBuilder;
+        this.lookup = lookup;
         this.minScore = minScore;
         this.indexName = indexName;
         this.shardId = shardId;
@@ -61,7 +65,7 @@ public class ScriptScoreQuery extends Query {
     public Query rewrite(IndexReader reader) throws IOException {
         Query newQ = subQuery.rewrite(reader);
         if (newQ != subQuery) {
-            return new ScriptScoreQuery(newQ, script, scriptBuilder, minScore, indexName, shardId, indexVersion);
+            return new ScriptScoreQuery(newQ, script, scriptBuilder, lookup, minScore, indexName, shardId, indexVersion);
         }
         return super.rewrite(reader);
     }
@@ -143,7 +147,7 @@ public class ScriptScoreQuery extends Query {
             }
 
             private ScoreScript makeScoreScript(LeafReaderContext context) throws IOException {
-                final ScoreScript scoreScript = scriptBuilder.newInstance(context);
+                final ScoreScript scoreScript = scriptBuilder.newInstance(new DocValuesDocReader(lookup, context));
                 scoreScript._setIndexName(indexName);
                 scoreScript._setShard(shardId);
                 return scoreScript;

+ 4 - 2
server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreFunctionBuilder.java

@@ -19,6 +19,7 @@ import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.query.QueryShardException;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
 import java.util.Objects;
@@ -82,8 +83,9 @@ public class ScriptScoreFunctionBuilder extends ScoreFunctionBuilder<ScriptScore
     protected ScoreFunction doToFunction(SearchExecutionContext context) {
         try {
             ScoreScript.Factory factory = context.compile(script, ScoreScript.CONTEXT);
-            ScoreScript.LeafFactory searchScript = factory.newFactory(script.getParams(), context.lookup());
-            return new ScriptScoreFunction(script, searchScript, context.index().getName(), context.getShardId());
+            SearchLookup lookup = context.lookup();
+            ScoreScript.LeafFactory searchScript = factory.newFactory(script.getParams(), lookup);
+            return new ScriptScoreFunction(script, searchScript, lookup, context.index().getName(), context.getShardId());
         } catch (Exception e) {
             throw new QueryShardException(context, "script_score: the script could not be loaded", e);
         }

+ 4 - 2
server/src/main/java/org/elasticsearch/index/query/functionscore/ScriptScoreQueryBuilder.java

@@ -25,6 +25,7 @@ import org.elasticsearch.index.query.QueryRewriteContext;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
 import java.util.Map;
@@ -158,9 +159,10 @@ public class ScriptScoreQueryBuilder extends AbstractQueryBuilder<ScriptScoreQue
                     + ALLOW_EXPENSIVE_QUERIES.getKey() + "' is set to false.");
         }
         ScoreScript.Factory factory = context.compile(script, ScoreScript.CONTEXT);
-        ScoreScript.LeafFactory scoreScriptFactory = factory.newFactory(script.getParams(), context.lookup());
+        SearchLookup lookup = context.lookup();
+        ScoreScript.LeafFactory scoreScriptFactory = factory.newFactory(script.getParams(), lookup);
         Query query = this.query.toQuery(context);
-        return new ScriptScoreQuery(query, script, scoreScriptFactory, minScore,
+        return new ScriptScoreQuery(query, script, scoreScriptFactory, context.lookup(), minScore,
             context.index().getName(), context.getShardId(), context.indexVersionCreated());
     }
 

+ 5 - 28
server/src/main/java/org/elasticsearch/script/AbstractSortScript.java

@@ -7,15 +7,11 @@
  */
 package org.elasticsearch.script;
 
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.Scorable;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.common.logging.DeprecationCategory;
 import org.elasticsearch.common.logging.DeprecationLogger;
 import org.elasticsearch.common.lucene.ScorerAware;
-import org.elasticsearch.index.fielddata.ScriptDocValues;
-import org.elasticsearch.search.lookup.LeafSearchLookup;
-import org.elasticsearch.search.lookup.SearchLookup;
 import org.elasticsearch.search.lookup.SourceLookup;
 
 import java.io.IOException;
@@ -23,7 +19,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.function.Function;
 
-abstract class AbstractSortScript implements ScorerAware {
+abstract class AbstractSortScript extends DocBasedScript implements ScorerAware {
 
     private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(DynamicMap.class);
     private static final Map<String, Function<Object, Object>> PARAMS_FUNCTIONS = Map.of(
@@ -50,21 +46,16 @@ abstract class AbstractSortScript implements ScorerAware {
     /** A scorer that will return the score for the current document when the script is run. */
     private Scorable scorer;
 
-    /**
-     * A leaf lookup for the bound segment this script will operate on.
-     */
-    private final LeafSearchLookup leafLookup;
-
-    AbstractSortScript(Map<String, Object> params, SearchLookup lookup, LeafReaderContext leafContext) {
-        this.leafLookup = lookup.getLeafSearchLookup(leafContext);
+    AbstractSortScript(Map<String, Object> params, DocReader docReader) {
+        super(docReader);
         Map<String, Object> parameters = new HashMap<>(params);
-        parameters.putAll(leafLookup.asMap());
+        parameters.putAll(docReader.docAsMap());
         this.params = new DynamicMap(parameters, PARAMS_FUNCTIONS);
     }
 
     protected AbstractSortScript() {
+        super(null);
         this.params = null;
-        this.leafLookup = null;
     }
 
     /**
@@ -87,18 +78,4 @@ abstract class AbstractSortScript implements ScorerAware {
             throw new ElasticsearchException("couldn't lookup score", e);
         }
     }
-
-    /**
-     * The doc lookup for the Lucene segment this script was created for.
-     */
-    public Map<String, ScriptDocValues<?>> getDoc() {
-        return leafLookup.doc();
-    }
-
-    /**
-     * Set the current document to run the script on next.
-     */
-    public void setDocument(int docid) {
-        leafLookup.setDocument(docid);
-    }
 }

+ 75 - 0
server/src/main/java/org/elasticsearch/script/DocBasedScript.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public abstract class DocBasedScript {
+    protected final DocReader docReader;
+
+    public DocBasedScript(DocReader docReader) {
+        this.docReader = docReader;
+    }
+
+    public Field<?> field(String fieldName) {
+        if (docReader == null) {
+            return new EmptyField<>(fieldName);
+        }
+        return docReader.field(fieldName);
+    }
+
+    public Stream<Field<?>> fields(String fieldGlob) {
+        if (docReader == null) {
+            return Stream.empty();
+        }
+        return docReader.fields(fieldGlob);
+    }
+
+    /**
+     * Set the current document to run the script on next.
+     */
+    public void setDocument(int docID) {
+        if (docReader != null) {
+            docReader.setDocument(docID);
+        }
+    }
+
+    public Map<String, Object> docAsMap() {
+        if (docReader == null) {
+            return Collections.emptyMap();
+        }
+        return docReader.docAsMap();
+    }
+
+    /**
+     * The doc lookup for the Lucene segment this script was created for.
+     */
+    public Map<String, ScriptDocValues<?>> getDoc() {
+        if (docReader == null) {
+            return Collections.emptyMap();
+        }
+        return docReader.doc();
+    }
+
+    public static class FieldAccess {
+        private final DocBasedScript script;
+
+        public FieldAccess(DocBasedScript script) {
+            this.script = script;
+        }
+
+        public Field<?> field(String fieldName) {
+            return script.field(fieldName);
+        }
+    }
+}

+ 42 - 0
server/src/main/java/org/elasticsearch/script/DocReader.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * Access the document in a script, provides both old-style, doc['fieldname'], and new style field('fieldname') access to the fields.
+ *
+ * {@code field(String)} and {@code fields(String)} may pull field contents from source as well as doc-values.  Old style access
+ * only reads doc-values.
+ */
+public interface DocReader {
+    /** New-style field access */
+    Field<?> field(String fieldName);
+
+    /** New-style field iterator */
+    Stream<Field<?>> fields(String fieldGlob);
+
+    /** Set the underlying docId */
+    void setDocument(int docID);
+
+    // Compatibility APIS
+    /** Old-style doc access for contexts that map some doc contents in params */
+    Map<String, Object> docAsMap();
+
+    /** Old-style doc['field'] access */
+    Map<String, ScriptDocValues<?>> doc();
+
+    /** Base document-id of the current reader, used as seed for RandomScore */
+    // should be replaced
+    int getDocBase();
+}

+ 75 - 0
server/src/main/java/org/elasticsearch/script/DocValuesDocReader.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.apache.lucene.index.LeafReaderContext;
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+import org.elasticsearch.search.lookup.LeafSearchLookup;
+import org.elasticsearch.search.lookup.SearchLookup;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+public class DocValuesDocReader implements DocReader, LeafReaderContextSupplier {
+    /** A leaf lookup for the bound segment this proxy will operate on. */
+    protected LeafSearchLookup leafLookup;
+
+    // provide access to the leaf context reader for expressions
+    protected final LeafReaderContext leafReaderContext;
+
+    // backwards compatibility access for random score script
+    protected final int docBase;
+
+    public DocValuesDocReader(SearchLookup lookup, LeafReaderContext leafContext) {
+        this.leafReaderContext = leafContext;
+        this.leafLookup = lookup.getLeafSearchLookup(leafReaderContext);
+        this.docBase = leafContext.docBase;
+    }
+
+    @Override
+    public Field<?> field(String fieldName) {
+        Map<String, ScriptDocValues<?>> doc = leafLookup.doc();
+
+        if (doc.containsKey(fieldName) == false) {
+            return new EmptyField<Number>(fieldName);
+        }
+        return new DocValuesField<>(fieldName, doc.get(fieldName));
+    }
+
+
+    @Override
+    public Stream<Field<?>> fields(String fieldGlob) {
+        return Stream.empty();
+    }
+
+    @Override
+    public void setDocument(int docID) {
+        leafLookup.setDocument(docID);
+    }
+
+    @Override
+    public Map<String, Object> docAsMap() {
+        return leafLookup.asMap();
+    }
+
+    @Override
+    public Map<String, ScriptDocValues<?>> doc() {
+        return leafLookup.doc();
+    }
+
+    @Override
+    public int getDocBase() {
+        return docBase;
+    }
+
+    @Override
+    public LeafReaderContext getLeafReaderContext() {
+        return leafReaderContext;
+    }
+}

+ 46 - 0
server/src/main/java/org/elasticsearch/script/DocValuesField.java

@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.elasticsearch.index.fielddata.ScriptDocValues;
+
+import java.util.List;
+
+public class DocValuesField<T> implements Field<T> {
+    protected final String name;
+    protected final ScriptDocValues<T> scriptDocValues;
+
+    public DocValuesField(String name, ScriptDocValues<T> scriptDocValues) {
+        this.name = name;
+        this.scriptDocValues = scriptDocValues;
+    }
+
+    @Override
+    public T getValue(T defaultValue) {
+        if (scriptDocValues.isEmpty()) {
+            return defaultValue;
+        }
+        return scriptDocValues.get(0);
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return scriptDocValues.isEmpty();
+    }
+
+    @Override
+    public List<T> getValues() {
+        return scriptDocValues;
+    }
+}

+ 40 - 0
server/src/main/java/org/elasticsearch/script/EmptyField.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import java.util.Collections;
+import java.util.List;
+
+public class EmptyField<T> implements Field<Object> {
+    protected final String name;
+
+    public EmptyField(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+
+    @Override
+    public Object getValue(Object defaultValue) {
+        return defaultValue;
+    }
+
+    @Override
+    public List<Object> getValues() {
+        return Collections.emptyList();
+    }
+}

+ 35 - 0
server/src/main/java/org/elasticsearch/script/Field.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+
+package org.elasticsearch.script;
+
+import java.util.List;
+
+/**
+ * A field in a document accessible via scripting.  In search contexts, the Field may be backed by doc values, source
+ * or both.  In ingestion, the field may be in the source document or being added to the document.
+ *
+ * Field's methods must not throw exceptions nor return null.  A Field object representing a empty or unmapped field will have
+ * * {@code isEmpty() == true}
+ * * {@code getValues().equals(Collections.emptyList())}
+ * * {@code getValue(defaultValue) == defaultValue}
+ * @param <T>
+ */
+public interface Field<T> {
+    String getName();
+
+    /** Does the field have any values? An unmapped field may have values from source */
+    boolean isEmpty();
+
+    /** Get all values of a multivalued field.  If {@code isEmpty()} this returns an empty list */
+    List<T> getValues();
+
+    /** Get the first value of a field, if {@code isEmpty()} return defaultValue instead */
+    T getValue(T defaultValue);
+}

+ 19 - 0
server/src/main/java/org/elasticsearch/script/LeafReaderContextSupplier.java

@@ -0,0 +1,19 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.script;
+
+import org.apache.lucene.index.LeafReaderContext;
+
+/**
+ * Provides direct access to a LeafReaderContext
+ */
+// Use while transitioning to using DocReader in expression contexts
+public interface LeafReaderContextSupplier {
+    LeafReaderContext getLeafReaderContext();
+}

+ 20 - 5
server/src/main/java/org/elasticsearch/script/NumberSortScript.java

@@ -9,7 +9,6 @@ package org.elasticsearch.script;
 
 import java.io.IOException;
 import java.util.Map;
-import org.apache.lucene.index.LeafReaderContext;
 import org.elasticsearch.search.lookup.SearchLookup;
 
 public abstract class NumberSortScript extends AbstractSortScript {
@@ -18,8 +17,10 @@ public abstract class NumberSortScript extends AbstractSortScript {
 
     public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("number_sort", Factory.class);
 
-    public NumberSortScript(Map<String, Object> params, SearchLookup lookup, LeafReaderContext leafContext) {
-        super(params, lookup, leafContext);
+    public NumberSortScript(Map<String, Object> params, SearchLookup searchLookup, DocReader docReader) {
+        // searchLookup is used taken in for compatibility with expressions.  See ExpressionScriptEngine.newScoreScript and
+        // ExpressionScriptEngine.getDocValueSource for where it's used.
+        super(params, docReader);
     }
 
     protected NumberSortScript() {
@@ -32,7 +33,7 @@ public abstract class NumberSortScript extends AbstractSortScript {
      * A factory to construct {@link NumberSortScript} instances.
      */
     public interface LeafFactory {
-        NumberSortScript newInstance(LeafReaderContext ctx) throws IOException;
+        NumberSortScript newInstance(DocReader reader) throws IOException;
 
         /**
          * Return {@code true} if the script needs {@code _score} calculated, or {@code false} otherwise.
@@ -44,6 +45,20 @@ public abstract class NumberSortScript extends AbstractSortScript {
      * A factory to construct stateful {@link NumberSortScript} factories for a specific index.
      */
     public interface Factory extends ScriptFactory {
-        LeafFactory newFactory(Map<String, Object> params, SearchLookup lookup);
+        // searchLookup is needed for **expressions-only** to look up bindings.  Painless callers should use the DocReader
+        // in LeafFactory.newInstance to set fallbacks.
+        LeafFactory newFactory(Map<String, Object> params, SearchLookup searchLookup);
+    }
+
+    public static class FieldAccess {
+        private final NumberSortScript script;
+
+        public FieldAccess(NumberSortScript script) {
+            this.script = script;
+        }
+
+        public Field<?> field(String fieldName) {
+            return script.field(fieldName);
+        }
     }
 }

+ 25 - 23
server/src/main/java/org/elasticsearch/script/ScoreScript.java

@@ -7,13 +7,10 @@
  */
 package org.elasticsearch.script;
 
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.search.Explanation;
 import org.apache.lucene.search.Scorable;
 import org.elasticsearch.common.logging.DeprecationCategory;
 import org.elasticsearch.common.logging.DeprecationLogger;
-import org.elasticsearch.index.fielddata.ScriptDocValues;
-import org.elasticsearch.search.lookup.LeafSearchLookup;
 import org.elasticsearch.search.lookup.SearchLookup;
 import org.elasticsearch.search.lookup.SourceLookup;
 
@@ -27,7 +24,7 @@ import java.util.function.Function;
 /**
  * A script used for adjusting the score on a per document basis.
  */
-public abstract class ScoreScript {
+public abstract class ScoreScript extends DocBasedScript {
 
     /** A helper to take in an explanation from a script and turn it into an {@link org.apache.lucene.search.Explanation}  */
     public static class ExplanationHolder {
@@ -75,9 +72,6 @@ public abstract class ScoreScript {
     /** The generic runtime parameters for the script. */
     private final Map<String, Object> params;
 
-    /** A leaf lookup for the bound segment this script will operate on. */
-    private final LeafSearchLookup leafLookup;
-
     private DoubleSupplier scoreSupplier = () -> 0.0;
 
     private final int docBase;
@@ -85,20 +79,20 @@ public abstract class ScoreScript {
     private int shardId = -1;
     private String indexName = null;
 
-    public ScoreScript(Map<String, Object> params, SearchLookup lookup, LeafReaderContext leafContext) {
+    public ScoreScript(Map<String, Object> params, SearchLookup searchLookup, DocReader docReader) {
+        // searchLookup parameter is ignored but part of the ScriptFactory contract.  It is part of that contract because it's required
+        // for expressions.  Expressions should eventually be transitioned to using DocReader.
+        super(docReader);
         // null check needed b/c of expression engine subclass
-        if (lookup == null) {
+        if (docReader == null) {
             assert params == null;
-            assert leafContext == null;
-            this.params = null;
-            this.leafLookup = null;
+            this.params = null;;
             this.docBase = 0;
         } else {
-            this.leafLookup = lookup.getLeafSearchLookup(leafContext);
             params = new HashMap<>(params);
-            params.putAll(leafLookup.asMap());
+            params.putAll(docReader.docAsMap());
             this.params = new DynamicMap(params, PARAMS_FUNCTIONS);
-            this.docBase = leafContext.docBase;
+            this.docBase = docReader.getDocBase();
         }
     }
 
@@ -109,15 +103,10 @@ public abstract class ScoreScript {
         return params;
     }
 
-    /** The doc lookup for the Lucene segment this script was created for. */
-    public Map<String, ScriptDocValues<?>> getDoc() {
-        return leafLookup.doc();
-    }
-
     /** Set the current document to run the script on next. */
     public void setDocument(int docid) {
+        super.setDocument(docid);
         this.docId = docid;
-        leafLookup.setDocument(docid);
     }
 
     public void setScorer(Scorable scorer) {
@@ -206,15 +195,28 @@ public abstract class ScoreScript {
          */
         boolean needs_score();
 
-        ScoreScript newInstance(LeafReaderContext ctx) throws IOException;
+        ScoreScript newInstance(DocReader reader) throws IOException;
     }
 
     /** A factory to construct stateful {@link ScoreScript} factories for a specific index. */
     public interface Factory extends ScriptFactory {
-
+        // searchLookup is used taken in for compatibility with expressions.  See ExpressionScriptEngine.newScoreScript and
+        // ExpressionScriptEngine.getDocValueSource for where it's used.
         ScoreScript.LeafFactory newFactory(Map<String, Object> params, SearchLookup lookup);
 
     }
 
     public static final ScriptContext<ScoreScript.Factory> CONTEXT = new ScriptContext<>("score", ScoreScript.Factory.class);
+
+    public static class FieldAccess {
+        private final ScoreScript script;
+
+        public FieldAccess(ScoreScript script) {
+            this.script = script;
+        }
+
+        public Field<?> field(String fieldName) {
+            return script.field(fieldName);
+        }
+    }
 }

+ 16 - 6
server/src/main/java/org/elasticsearch/script/StringSortScript.java

@@ -9,8 +9,6 @@ package org.elasticsearch.script;
 
 import java.io.IOException;
 import java.util.Map;
-import org.apache.lucene.index.LeafReaderContext;
-import org.elasticsearch.search.lookup.SearchLookup;
 
 public abstract class StringSortScript extends AbstractSortScript {
 
@@ -18,8 +16,8 @@ public abstract class StringSortScript extends AbstractSortScript {
 
     public static final ScriptContext<Factory> CONTEXT = new ScriptContext<>("string_sort", Factory.class);
 
-    public StringSortScript(Map<String, Object> params, SearchLookup lookup, LeafReaderContext leafContext) {
-        super(params, lookup, leafContext);
+    public StringSortScript(Map<String, Object> params, DocReader docReader) {
+        super(params, docReader);
     }
 
     public abstract String execute();
@@ -28,13 +26,25 @@ public abstract class StringSortScript extends AbstractSortScript {
      * A factory to construct {@link StringSortScript} instances.
      */
     public interface LeafFactory {
-        StringSortScript newInstance(LeafReaderContext ctx) throws IOException;
+        StringSortScript newInstance(DocReader reader) throws IOException;
     }
 
     /**
      * A factory to construct stateful {@link StringSortScript} factories for a specific index.
      */
     public interface Factory extends ScriptFactory {
-        LeafFactory newFactory(Map<String, Object> params, SearchLookup lookup);
+        LeafFactory newFactory(Map<String, Object> params);
+    }
+
+    public static class FieldAccess {
+        private final StringSortScript script;
+
+        public FieldAccess(StringSortScript script) {
+            this.script = script;
+        }
+
+        public Field<?> field(String fieldName) {
+            return script.field(fieldName);
+        }
     }
 }

+ 8 - 4
server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java

@@ -37,11 +37,13 @@ import org.elasticsearch.index.query.QueryBuilder;
 import org.elasticsearch.index.query.QueryRewriteContext;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.query.QueryShardException;
+import org.elasticsearch.script.DocValuesDocReader;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.NumberSortScript;
 import org.elasticsearch.script.StringSortScript;
 import org.elasticsearch.search.DocValueFormat;
 import org.elasticsearch.search.MultiValueMode;
+import org.elasticsearch.search.lookup.SearchLookup;
 
 import java.io.IOException;
 import java.util.Locale;
@@ -248,15 +250,16 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
             nested = resolveNested(context, nestedSort);
         }
 
+        SearchLookup searchLookup = context.lookup();
         switch (type) {
             case STRING:
                 final StringSortScript.Factory factory = context.compile(script, StringSortScript.CONTEXT);
-                final StringSortScript.LeafFactory searchScript = factory.newFactory(script.getParams(), context.lookup());
+                final StringSortScript.LeafFactory searchScript = factory.newFactory(script.getParams());
                 return new BytesRefFieldComparatorSource(null, null, valueMode, nested) {
                     StringSortScript leafScript;
                     @Override
                     protected SortedBinaryDocValues getValues(LeafReaderContext context) throws IOException {
-                        leafScript = searchScript.newInstance(context);
+                        leafScript = searchScript.newInstance(new DocValuesDocReader(searchLookup, context));
                         final BinaryDocValues values = new AbstractBinaryDocValues() {
                             final BytesRefBuilder spare = new BytesRefBuilder();
                             @Override
@@ -286,12 +289,13 @@ public class ScriptSortBuilder extends SortBuilder<ScriptSortBuilder> {
                 };
             case NUMBER:
                 final NumberSortScript.Factory numberSortFactory = context.compile(script, NumberSortScript.CONTEXT);
-                final NumberSortScript.LeafFactory numberSortScript = numberSortFactory.newFactory(script.getParams(), context.lookup());
+                // searchLookup is unnecessary here, as it's just used for expressions
+                final NumberSortScript.LeafFactory numberSortScript = numberSortFactory.newFactory(script.getParams(), searchLookup);
                 return new DoubleValuesComparatorSource(null, Double.MAX_VALUE, valueMode, nested) {
                     NumberSortScript leafScript;
                     @Override
                     protected SortedNumericDoubleValues getValues(LeafReaderContext context) throws IOException {
-                        leafScript = numberSortScript.newInstance(context);
+                        leafScript = numberSortScript.newInstance(new DocValuesDocReader(searchLookup, context));
                         final NumericDoubleValues values = new NumericDoubleValues() {
                             @Override
                             public boolean advanceExact(int doc) throws IOException {

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/BooleanScriptFieldTypeTests.java

@@ -37,6 +37,7 @@ import org.elasticsearch.index.fielddata.BooleanScriptFieldData;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.script.BooleanFieldScript;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptCompiler;
@@ -126,8 +127,8 @@ public class BooleanScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeT
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.Booleans booleans = (ScriptDocValues.Booleans) getDoc().get("test");
@@ -135,7 +136,7 @@ public class BooleanScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeT
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/DateScriptFieldTypeTests.java

@@ -40,6 +40,7 @@ import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.DateScriptFieldData;
 import org.elasticsearch.script.DateFieldScript;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptType;
@@ -216,8 +217,8 @@ public class DateScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTest
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) throws IOException {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.Dates dates = (ScriptDocValues.Dates) getDoc().get("test");
@@ -225,7 +226,7 @@ public class DateScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTest
                             }
                         };
                     }
-                }, 354.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 354.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/DoubleScriptFieldTypeTests.java

@@ -30,6 +30,7 @@ import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.fielddata.SortedNumericDoubleValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.DoubleScriptFieldData;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.DoubleFieldScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
@@ -128,8 +129,8 @@ public class DoubleScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTe
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.Doubles doubles = (ScriptDocValues.Doubles) getDoc().get("test");
@@ -137,7 +138,7 @@ public class DoubleScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTe
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/GeoPointScriptFieldTypeTests.java

@@ -29,6 +29,7 @@ import org.elasticsearch.index.fielddata.MultiGeoPointValues;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.GeoPointScriptFieldData;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.GeoPointFieldScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
@@ -117,8 +118,8 @@ public class GeoPointScriptFieldTypeTests extends AbstractNonTextScriptFieldType
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.GeoPoints points = (ScriptDocValues.GeoPoints) getDoc().get("test");
@@ -126,7 +127,7 @@ public class GeoPointScriptFieldTypeTests extends AbstractNonTextScriptFieldType
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/IpScriptFieldTypeTests.java

@@ -30,6 +30,7 @@ import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.BinaryScriptFieldData;
 import org.elasticsearch.index.fielddata.IpScriptFieldData;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.IpFieldScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
@@ -140,8 +141,8 @@ public class IpScriptFieldTypeTests extends AbstractScriptFieldTypeTestCase {
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 IpScriptFieldData.IpScriptDocValues bytes = (IpScriptFieldData.IpScriptDocValues) getDoc().get("test");
@@ -149,7 +150,7 @@ public class IpScriptFieldTypeTests extends AbstractScriptFieldTypeTestCase {
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/KeywordScriptFieldTypeTests.java

@@ -34,6 +34,7 @@ import org.elasticsearch.index.query.MatchQueryBuilder;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.BinaryScriptFieldData;
 import org.elasticsearch.index.fielddata.StringScriptFieldData;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.script.ScriptType;
@@ -123,8 +124,8 @@ public class KeywordScriptFieldTypeTests extends AbstractScriptFieldTypeTestCase
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.Strings bytes = (ScriptDocValues.Strings) getDoc().get("test");
@@ -132,7 +133,7 @@ public class KeywordScriptFieldTypeTests extends AbstractScriptFieldTypeTestCase
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 4 - 3
server/src/test/java/org/elasticsearch/index/mapper/LongScriptFieldTypeTests.java

@@ -32,6 +32,7 @@ import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
 import org.elasticsearch.index.fielddata.ScriptDocValues;
 import org.elasticsearch.index.query.SearchExecutionContext;
 import org.elasticsearch.index.fielddata.LongScriptFieldData;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.LongFieldScript;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
@@ -163,8 +164,8 @@ public class LongScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTest
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 ScriptDocValues.Longs longs = (ScriptDocValues.Longs) getDoc().get("test");
@@ -172,7 +173,7 @@ public class LongScriptFieldTypeTests extends AbstractNonTextScriptFieldTypeTest
                             }
                         };
                     }
-                }, 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
+                }, searchContext.lookup(), 2.5f, "test", 0, Version.CURRENT)), equalTo(1));
             }
         }
     }

+ 9 - 6
server/src/test/java/org/elasticsearch/search/query/ScriptScoreQueryTests.java

@@ -23,6 +23,7 @@ import org.apache.lucene.store.Directory;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.lucene.search.Queries;
 import org.elasticsearch.common.lucene.search.function.ScriptScoreQuery;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.search.lookup.LeafSearchLookup;
@@ -48,6 +49,7 @@ public class ScriptScoreQueryTests extends ESTestCase {
     private DirectoryReader reader;
     private IndexSearcher searcher;
     private LeafReaderContext leafReaderContext;
+    private final SearchLookup lookup = new SearchLookup(null, null);
 
     @Before
     public void initSearcher() throws IOException {
@@ -79,7 +81,7 @@ public class ScriptScoreQueryTests extends ESTestCase {
         });
 
         ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
-            null, "index", 0, Version.CURRENT);
+            lookup, null, "index", 0, Version.CURRENT);
         Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
         Explanation explanation = weight.explain(leafReaderContext, 0);
         assertNotNull(explanation);
@@ -92,7 +94,7 @@ public class ScriptScoreQueryTests extends ESTestCase {
         ScoreScript.LeafFactory factory = newFactory(script, true, explanation -> 1.5);
 
         ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
-            null, "index", 0, Version.CURRENT);
+            lookup, null, "index", 0, Version.CURRENT);
         Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
         Explanation explanation = weight.explain(leafReaderContext, 0);
         assertNotNull(explanation);
@@ -109,7 +111,7 @@ public class ScriptScoreQueryTests extends ESTestCase {
         ScoreScript.LeafFactory factory = newFactory(script, false, explanation -> 2.0);
 
         ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory,
-            null, "index", 0, Version.CURRENT);
+            lookup, null, "index", 0, Version.CURRENT);
         Weight weight = query.createWeight(searcher, ScoreMode.COMPLETE, 1.0f);
         Explanation explanation = weight.explain(leafReaderContext, 0);
         assertNotNull(explanation);
@@ -123,7 +125,8 @@ public class ScriptScoreQueryTests extends ESTestCase {
     public void testScriptScoreErrorOnNegativeScore() {
         Script script = new Script("script that returns a negative score");
         ScoreScript.LeafFactory factory = newFactory(script, false, explanation -> -1000.0);
-        ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory, null, "index", 0, Version.CURRENT);
+        ScriptScoreQuery query = new ScriptScoreQuery(Queries.newMatchAllQuery(), script, factory, lookup, null, "index", 0,
+                Version.CURRENT);
         IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> searcher.search(query, 1));
         assertTrue(e.getMessage().contains("Must be a non-negative score!"));
     }
@@ -140,8 +143,8 @@ public class ScriptScoreQueryTests extends ESTestCase {
             }
 
             @Override
-            public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
-                return new ScoreScript(script.getParams(), lookup, leafReaderContext) {
+            public ScoreScript newInstance(DocReader docReader) throws IOException {
+                return new ScoreScript(script.getParams(), lookup, docReader) {
                     @Override
                     public double execute(ExplanationHolder explanation) {
                         return function.apply(explanation);

+ 6 - 6
test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java

@@ -108,8 +108,8 @@ public class MockScriptEngine implements ScriptEngine {
         } else if (context.instanceClazz.equals(NumberSortScript.class)) {
             NumberSortScript.Factory factory = (parameters, lookup) -> new NumberSortScript.LeafFactory() {
                 @Override
-                public NumberSortScript newInstance(final LeafReaderContext ctx) {
-                    return new NumberSortScript(parameters, lookup, ctx) {
+                public NumberSortScript newInstance(DocReader reader) {
+                    return new NumberSortScript(parameters, lookup, reader) {
                         @Override
                         public double execute() {
                             Map<String, Object> vars = new HashMap<>(parameters);
@@ -605,9 +605,9 @@ public class MockScriptEngine implements ScriptEngine {
                 }
 
                 @Override
-                public ScoreScript newInstance(LeafReaderContext ctx) throws IOException {
+                public ScoreScript newInstance(DocReader docReader) throws IOException {
                     Scorable[] scorerHolder = new Scorable[1];
-                    return new ScoreScript(params, lookup, ctx) {
+                    return new ScoreScript(params, null, docReader) {
                         @Override
                         public double execute(ExplanationHolder explanation) {
                             Map<String, Object> vars = new HashMap<>(getParams());
@@ -705,8 +705,8 @@ public class MockScriptEngine implements ScriptEngine {
         @Override public boolean isResultDeterministic() { return script.isResultDeterministic(); }
 
         @Override
-        public StringSortScript.LeafFactory newFactory(Map<String, Object> parameters, SearchLookup lookup) {
-            return ctx -> new StringSortScript(parameters, lookup, ctx) {
+        public StringSortScript.LeafFactory newFactory(Map<String, Object> parameters) {
+            return docReader -> new StringSortScript(parameters, docReader) {
                 @Override
                 public String execute() {
                     Map<String, Object> vars = new HashMap<>(parameters);

+ 4 - 4
x-pack/plugin/mapper-aggregate-metric/src/test/java/org/elasticsearch/xpack/aggregatemetric/mapper/AggregateDoubleMetricFieldTypeTests.java

@@ -9,7 +9,6 @@ package org.elasticsearch.xpack.aggregatemetric.mapper;
 import org.apache.lucene.document.DoublePoint;
 import org.apache.lucene.document.NumericDocValuesField;
 import org.apache.lucene.index.DirectoryReader;
-import org.apache.lucene.index.LeafReaderContext;
 import org.apache.lucene.index.RandomIndexWriter;
 import org.apache.lucene.search.IndexOrDocValuesQuery;
 import org.apache.lucene.search.IndexSearcher;
@@ -23,6 +22,7 @@ import org.elasticsearch.index.mapper.FieldTypeTestCase;
 import org.elasticsearch.index.mapper.MappedFieldType;
 import org.elasticsearch.index.mapper.NumberFieldMapper;
 import org.elasticsearch.index.query.SearchExecutionContext;
+import org.elasticsearch.script.DocReader;
 import org.elasticsearch.script.ScoreScript;
 import org.elasticsearch.script.Script;
 import org.elasticsearch.search.lookup.SearchLookup;
@@ -132,8 +132,8 @@ public class AggregateDoubleMetricFieldTypeTests extends FieldTypeTestCase {
                     }
 
                     @Override
-                    public ScoreScript newInstance(LeafReaderContext ctx) {
-                        return new ScoreScript(Map.of(), searchExecutionContext.lookup(), ctx) {
+                    public ScoreScript newInstance(DocReader docReader) {
+                        return new ScoreScript(Map.of(), searchExecutionContext.lookup(), docReader) {
                             @Override
                             public double execute(ExplanationHolder explanation) {
                                 Map<String, ScriptDocValues<?>> doc = getDoc();
@@ -142,7 +142,7 @@ public class AggregateDoubleMetricFieldTypeTests extends FieldTypeTestCase {
                             }
                         };
                     }
-                }, 7f, "test", 0, Version.CURRENT)), equalTo(2));
+                }, searchExecutionContext.lookup(), 7f, "test", 0, Version.CURRENT)), equalTo(2));
             }
         }
     }