From 72ec623adc92cb49483f9ea798c18bdbdb09a896 Mon Sep 17 00:00:00 2001 From: Nik Everett Date: Tue, 28 Jul 2020 11:31:37 -0400 Subject: [PATCH] Prevent deep recursion in doc values This prevents loops and deep recursion in doc values. This is important because the doc values for runtime fields can depend on one another. We expect that mostly we can catch deep recursion and loops when we prepare the doc values, but we're not going to rely on that. This adds the test when looking up the doc values. It adds it to `SearchLookup` which means that it is automatically provided to all plugins that implement runtime fields. This is important because we mean for it to be a "last resort" to prevent stack overflows. --- .../ExpressionFieldScriptTests.java | 2 +- .../ExpressionNumberSortScriptTests.java | 2 +- .../ExpressionTermsSetQueryTests.java | 2 +- .../painless/NeedsScoreTests.java | 10 +- .../index/query/QueryShardContext.java | 7 +- .../search/lookup/SearchLookup.java | 82 +++++- .../fetch/subphase/FetchSourcePhaseTests.java | 4 +- .../search/lookup/SearchLookupTests.java | 257 ++++++++++++++++++ .../aggregations/AggregatorTestCase.java | 2 +- .../mapper/FlatObjectFieldLookupTests.java | 7 +- ...AbstractScriptMappedFieldTypeTestCase.java | 2 +- .../test/runtime_fields/90_loop.yml | 91 +++++++ 12 files changed, 443 insertions(+), 25 deletions(-) create mode 100644 server/src/test/java/org/elasticsearch/search/lookup/SearchLookupTests.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/90_loop.yml diff --git a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionFieldScriptTests.java b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionFieldScriptTests.java index aa4daf3179adb..8f53d96cd6ab7 100644 --- a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionFieldScriptTests.java +++ b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionFieldScriptTests.java @@ -64,7 +64,7 @@ public void setUp() throws Exception { when(fieldData.load(anyObject())).thenReturn(atomicFieldData); service = new ExpressionScriptEngine(); - lookup = new SearchLookup(mapperService, ignored -> fieldData); + lookup = new SearchLookup(mapperService, (ignored, l) -> fieldData); } private FieldScript.LeafFactory compile(String expression) { diff --git a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java index 007f4d608c82d..0177854c78f78 100644 --- a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java +++ b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionNumberSortScriptTests.java @@ -64,7 +64,7 @@ public void setUp() throws Exception { when(fieldData.load(anyObject())).thenReturn(atomicFieldData); service = new ExpressionScriptEngine(); - lookup = new SearchLookup(mapperService, ignored -> fieldData); + lookup = new SearchLookup(mapperService, (ignored, l) -> fieldData); } private NumberSortScript.LeafFactory compile(String expression) { diff --git a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionTermsSetQueryTests.java b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionTermsSetQueryTests.java index fd279a9fa14e9..e4bc2db619411 100644 --- a/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionTermsSetQueryTests.java +++ b/modules/lang-expression/src/test/java/org/elasticsearch/script/expression/ExpressionTermsSetQueryTests.java @@ -64,7 +64,7 @@ public void setUp() throws Exception { when(fieldData.load(anyObject())).thenReturn(atomicFieldData); service = new ExpressionScriptEngine(); - lookup = new SearchLookup(mapperService, ignored -> fieldData); + lookup = new SearchLookup(mapperService, (ignored, l) -> fieldData); } private TermsSetQueryScript.LeafFactory compile(String expression) { diff --git a/modules/lang-painless/src/test/java/org/elasticsearch/painless/NeedsScoreTests.java b/modules/lang-painless/src/test/java/org/elasticsearch/painless/NeedsScoreTests.java index 92816426eaf47..06c90a3c66e6c 100644 --- a/modules/lang-painless/src/test/java/org/elasticsearch/painless/NeedsScoreTests.java +++ b/modules/lang-painless/src/test/java/org/elasticsearch/painless/NeedsScoreTests.java @@ -25,7 +25,6 @@ import org.elasticsearch.painless.spi.Whitelist; import org.elasticsearch.script.NumberSortScript; import org.elasticsearch.script.ScriptContext; -import org.elasticsearch.search.lookup.SearchLookup; import org.elasticsearch.test.ESSingleNodeTestCase; import java.util.Collections; @@ -47,22 +46,21 @@ public void testNeedsScores() { PainlessScriptEngine service = new PainlessScriptEngine(Settings.EMPTY, contexts); QueryShardContext shardContext = index.newQueryShardContext(0, null, () -> 0, null); - SearchLookup lookup = new SearchLookup(index.mapperService(), shardContext::getForField); NumberSortScript.Factory factory = service.compile(null, "1.2", NumberSortScript.CONTEXT, Collections.emptyMap()); - NumberSortScript.LeafFactory ss = factory.newFactory(Collections.emptyMap(), lookup); + NumberSortScript.LeafFactory ss = factory.newFactory(Collections.emptyMap(), shardContext.lookup()); assertFalse(ss.needs_score()); factory = service.compile(null, "doc['d'].value", NumberSortScript.CONTEXT, Collections.emptyMap()); - ss = factory.newFactory(Collections.emptyMap(), lookup); + ss = factory.newFactory(Collections.emptyMap(), shardContext.lookup()); assertFalse(ss.needs_score()); factory = service.compile(null, "1/_score", NumberSortScript.CONTEXT, Collections.emptyMap()); - ss = factory.newFactory(Collections.emptyMap(), lookup); + ss = factory.newFactory(Collections.emptyMap(), shardContext.lookup()); assertTrue(ss.needs_score()); factory = service.compile(null, "doc['d'].value * _score", NumberSortScript.CONTEXT, Collections.emptyMap()); - ss = factory.newFactory(Collections.emptyMap(), lookup); + ss = factory.newFactory(Collections.emptyMap(), shardContext.lookup()); assertTrue(ss.needs_score()); } diff --git a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java index 68ac85ee7f576..c907e85ed9bcf 100644 --- a/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java +++ b/server/src/main/java/org/elasticsearch/index/query/QueryShardContext.java @@ -209,7 +209,7 @@ public boolean allowExpensiveQueries() { @SuppressWarnings("unchecked") public > IFD getForField(MappedFieldType fieldType) { - return (IFD) indexFieldDataService.apply(fieldType, fullyQualifiedIndex.getName(), this::lookup); + return (IFD) indexFieldDataService.apply(fieldType, fullyQualifiedIndex.getName(), () -> lookup().forField(fieldType.name())); } public void addNamedQuery(String name, Query query) { @@ -291,7 +291,10 @@ MappedFieldType failIfFieldMappingNotFound(String name, MappedFieldType fieldMap public SearchLookup lookup() { if (lookup == null) { - lookup = new SearchLookup(getMapperService(), this::getForField); + lookup = new SearchLookup( + getMapperService(), + (ft, lookup) -> indexFieldDataService.apply(ft, fullyQualifiedIndex.getName(), lookup) + ); } return lookup; } diff --git a/server/src/main/java/org/elasticsearch/search/lookup/SearchLookup.java b/server/src/main/java/org/elasticsearch/search/lookup/SearchLookup.java index 04aef7d2e8f63..5705b1c51de13 100644 --- a/server/src/main/java/org/elasticsearch/search/lookup/SearchLookup.java +++ b/server/src/main/java/org/elasticsearch/search/lookup/SearchLookup.java @@ -24,22 +24,88 @@ import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.index.mapper.MapperService; -import java.util.function.Function; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Supplier; + +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; public class SearchLookup { + /** + * The maximum "depth" of field dependencies. When a field's doc values + * depends on another field's doc values and depends on another field's + * doc values that depends on another field's doc values, etc, etc, + * it can make a very deep stack. At some point we're concerned about + * {@link StackOverflowError}. This limits that "depth". + */ + static final int MAX_FIELD_CHAIN = 30; - final DocLookup docMap; + /** + * The chain of fields for which this lookup was created used for detecting + * loops in doc values calculations. It is empty for the "top level" lookup + * created for the entire search. When a lookup is created for a field we + * make sure its name isn't in the list then add it to the end. So the + * lookup for the a field named {@code foo} will be {@code ["foo"]}. If + * that field looks up the values of a field named {@code bar} then + * {@code bar}'s lookup will contain {@code ["foo", "bar"]}. + */ + private final List fieldChain; + private final MapperService mapperService; + private final BiFunction, IndexFieldData> indexFieldDataService; + private final DocLookup docMap; + private final SourceLookup sourceLookup; + private final FieldsLookup fieldsLookup; - final SourceLookup sourceLookup; + /** + * Create the "top level" field lookup for a search request. + */ + public SearchLookup( + MapperService mapperService, + BiFunction, IndexFieldData> indexFieldDataService + ) { + this(emptyList(), mapperService, indexFieldDataService, new SourceLookup(), new FieldsLookup(mapperService)); + } - final FieldsLookup fieldsLookup; + private SearchLookup( + List fieldChain, + MapperService mapperService, + BiFunction, IndexFieldData> indexFieldDataService, + SourceLookup sourceLookup, + FieldsLookup fieldsLookup + ) { + this.fieldChain = fieldChain; + this.mapperService = mapperService;; + this.indexFieldDataService = indexFieldDataService; + this.docMap = new DocLookup(mapperService, ft -> this.indexFieldDataService.apply(ft, () -> forField(ft.name()))); + this.sourceLookup = sourceLookup; + this.fieldsLookup = fieldsLookup; + } - public SearchLookup(MapperService mapperService, Function> fieldDataLookup) { - docMap = new DocLookup(mapperService, fieldDataLookup); - sourceLookup = new SourceLookup(); - fieldsLookup = new FieldsLookup(mapperService); + /** + * Build a new lookup for a field that needs the lookup to create doc values. + * + * @throws IllegalArgumentException if it detects a loop in the fields required to build doc values + */ + public SearchLookup forField(String name) { + List newFieldChain = new ArrayList<>(fieldChain.size() + 1); + newFieldChain.addAll(fieldChain); + newFieldChain.add(name); + if (fieldChain.contains(name)) { + throw new IllegalArgumentException("loop in field definitions: " + newFieldChain); + } + if (newFieldChain.size() > MAX_FIELD_CHAIN) { + throw new IllegalArgumentException("field definition too deep: " + newFieldChain); + } + return new SearchLookup(unmodifiableList(newFieldChain), mapperService, indexFieldDataService, sourceLookup, fieldsLookup); } + /** + * Builds a new {@linkplain LeafSearchLookup} for the caller to handle + * a {@linkplain LeafReaderContext}. This lookup shares the {@code _source} + * lookup but doesn't share doc values or stored fields. + */ public LeafSearchLookup getLeafSearchLookup(LeafReaderContext context) { return new LeafSearchLookup(context, docMap.getLeafDocLookup(context), diff --git a/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhaseTests.java b/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhaseTests.java index 3cdb177386c48..7ebe8dbdce9fb 100644 --- a/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhaseTests.java +++ b/server/src/test/java/org/elasticsearch/search/fetch/subphase/FetchSourcePhaseTests.java @@ -184,7 +184,9 @@ public FetchSourceContext fetchSourceContext() { @Override public SearchLookup lookup() { - SearchLookup lookup = new SearchLookup(this.mapperService(), this::getForField); + SearchLookup lookup = new SearchLookup(this.mapperService(), (ft, l) -> { + throw new UnsupportedOperationException(); + }); lookup.source().setSource(source); return lookup; } diff --git a/server/src/test/java/org/elasticsearch/search/lookup/SearchLookupTests.java b/server/src/test/java/org/elasticsearch/search/lookup/SearchLookupTests.java new file mode 100644 index 0000000000000..84885161f7d86 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/search/lookup/SearchLookupTests.java @@ -0,0 +1,257 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.search.lookup; + +import org.apache.lucene.document.SortedNumericDocValuesField; +import org.apache.lucene.index.DirectoryReader; +import org.apache.lucene.index.LeafReaderContext; +import org.apache.lucene.index.RandomIndexWriter; +import org.apache.lucene.search.Collector; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.LeafCollector; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Scorable; +import org.apache.lucene.search.ScoreMode; +import org.apache.lucene.store.Directory; +import org.elasticsearch.common.CheckedFunction; +import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.IndexNumericFieldData.NumericType; +import org.elasticsearch.index.fielddata.LeafFieldData; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.index.fielddata.plain.SortedNumericIndexFieldData; +import org.elasticsearch.index.mapper.MappedFieldType; +import org.elasticsearch.index.mapper.MapperService; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.startsWith; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class SearchLookupTests extends ESTestCase { + public void testDocValuesLoop() { + SearchLookup lookup = new SearchLookup( + mockMapperService(), + (ft, l) -> mockIFDNeverReturns(lrc -> l.get().getLeafSearchLookup(lrc).doc().get(ft.name())) + ); + Exception e = expectThrows(IllegalArgumentException.class, () -> collect(lookup, l -> l.doc().get("a"))); + assertThat(e.getMessage(), equalTo("loop in field definitions: [a, a]")); + } + + public void testDocValuesLooseLoop() { + SearchLookup lookup = new SearchLookup(mockMapperService(), (ft, l) -> { + if (ft.name().length() > 3) { + return mockIFDNeverReturns(lrc -> l.get().forField("a")); + } + return mockIFDNeverReturns(lrc -> l.get().getLeafSearchLookup(lrc).doc().get(ft.name() + "a")); + }); + Exception e = expectThrows(IllegalArgumentException.class, () -> collect(lookup, l -> l.doc().get("a"))); + assertThat(e.getMessage(), equalTo("loop in field definitions: [a, aa, aaa, aaaa, a]")); + } + + public void testDocValuesTerminatesInLoop() { + SearchLookup lookup = new SearchLookup(mockMapperService(), (ft, l) -> { + if (ft.name().length() > 3) { + return mockIFDNeverReturns(lrc -> l.get().forField(ft.name())); + } + return mockIFDNeverReturns(lrc -> l.get().getLeafSearchLookup(lrc).doc().get(ft.name() + "a")); + }); + Exception e = expectThrows(IllegalArgumentException.class, () -> collect(lookup, l -> l.doc().get("a"))); + assertThat(e.getMessage(), equalTo("loop in field definitions: [a, aa, aaa, aaaa, aaaa]")); + } + + /** + * Test a loop that only happens on some documents. + */ + public void testDocValuesSometimesLoop() { + SearchLookup lookup = new SearchLookup(mockMapperService(), (ft, l) -> { + if (ft.name().equals("test")) { + return new SortedNumericIndexFieldData("test", NumericType.LONG); + } + if (ft.name().length() > 3) { + return mockIFDNeverReturns(lrc -> l.get().forField("a")); + } + return mockIFD(lrc -> { + LeafFieldData lfd = mock(LeafFieldData.class); + when(lfd.getScriptValues()).thenAnswer(inv -> { + return new ScriptDocValues() { + @Override + public void setNextDocId(int docId) throws IOException { + LeafSearchLookup lookup = l.get().getLeafSearchLookup(lrc); + lookup.setDocument(docId); + ScriptDocValues testDv = lookup.doc().get("test"); + if (testDv.get(0).equals(2L)) { + lookup.doc().get(ft.name() + "a"); + } + } + + @Override + public String get(int index) { + return null; + } + + @Override + public int size() { + return 0; + } + }; + }); + return lfd; + }); + }); + Exception e = expectThrows(IllegalArgumentException.class, () -> collect(lookup, l -> l.doc().get("a"))); + assertThat(e.getMessage(), equalTo("loop in field definitions: [a, aa, aaa, aaaa, a]")); + } + + public void testDeepDocValues() throws IOException { + SearchLookup lookup = new SearchLookup(mockMapperService(), (ft, l) -> { + if (ft.name().length() > 3) { + return mockIFD(lrc -> { + LeafFieldData lfd = mock(LeafFieldData.class); + when(lfd.getScriptValues()).thenAnswer(inv -> { + return new ScriptDocValues() { + @Override + public void setNextDocId(int docId) throws IOException {} + + @Override + public String get(int index) { + return "test"; + } + + @Override + public int size() { + return 1; + } + }; + }); + return lfd; + }); + } + return mockIFD(lrc -> { + LeafFieldData lfd = mock(LeafFieldData.class); + when(lfd.getScriptValues()).thenAnswer(inv -> { + return new ScriptDocValues() { + String value; + + @Override + public void setNextDocId(int docId) throws IOException { + LeafSearchLookup lookup = l.get().getLeafSearchLookup(lrc); + lookup.setDocument(docId); + ScriptDocValues next = lookup.doc().get(ft.name() + "a"); + value = next.get(0) + "a"; + } + + @Override + public String get(int index) { + return value; + } + + @Override + public int size() { + return 1; + } + }; + }); + return lfd; + }); + }); + assertThat(collect(lookup, l -> l.doc().get("a")), equalTo(List.of("testaaa", "testaaa"))); + } + + public void testDocValuesTooDeep() { + SearchLookup lookup = new SearchLookup( + mockMapperService(), + (ft, l) -> mockIFDNeverReturns(lrc -> l.get().getLeafSearchLookup(lrc).doc().get(ft.name() + "a")) + ); + Exception e = expectThrows(IllegalArgumentException.class, () -> collect(lookup, l -> l.doc().get("a"))); + assertThat(e.getMessage(), startsWith("field definition too deep: [a, aa, aaa, aaaa,")); + assertThat(e.getMessage(), endsWith(", aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]")); + } + + private IndexFieldData mockIFDNeverReturns(Consumer load) { + return mockIFD(lrc -> { + load.accept(lrc); + return null; + }); + } + + private IndexFieldData mockIFD(Function load) { + IndexFieldData ifd = mock(IndexFieldData.class); + when(ifd.load(any())).thenAnswer(inv -> load.apply((LeafReaderContext) inv.getArguments()[0])); + return ifd; + } + + private MapperService mockMapperService() { + MapperService mapperService = mock(MapperService.class); + when(mapperService.fieldType(any())).thenAnswer(inv -> { + String name = (String) inv.getArguments()[0]; + MappedFieldType ft = mock(MappedFieldType.class); + when(ft.name()).thenReturn(name); + return ft; + }); + return mapperService; + } + + private List collect(SearchLookup lookup, CheckedFunction, IOException> getDocValues) + throws IOException { + List result = new ArrayList<>(); + try (Directory directory = newDirectory(); RandomIndexWriter indexWriter = new RandomIndexWriter(random(), directory)) { + indexWriter.addDocument(List.of(new SortedNumericDocValuesField("test", 1))); + indexWriter.addDocument(List.of(new SortedNumericDocValuesField("test", 2))); + try (DirectoryReader reader = indexWriter.getReader()) { + IndexSearcher searcher = newSearcher(reader); + searcher.search(new MatchAllDocsQuery(), new Collector() { + @Override + public ScoreMode scoreMode() { + return ScoreMode.COMPLETE_NO_SCORES; + } + + @Override + public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException { + LeafSearchLookup leafLookup = lookup.getLeafSearchLookup(context); + return new LeafCollector() { + @Override + public void setScorer(Scorable scorer) throws IOException {} + + @Override + public void collect(int doc) throws IOException { + leafLookup.setDocument(doc); + ScriptDocValues sdv = getDocValues.apply(leafLookup); + sdv.setNextDocId(doc); + for (Object v : sdv) { + result.add(v.toString()); + } + } + }; + } + }); + } + return result; + } + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java index 58f69c2658906..f06420f3d4a02 100644 --- a/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java +++ b/test/framework/src/main/java/org/elasticsearch/search/aggregations/AggregatorTestCase.java @@ -316,7 +316,7 @@ public boolean shouldCache(Query query) { when(searchContext.getForField(Mockito.any(MappedFieldType.class))) .thenAnswer(invocationOnMock -> ifds.getForField((MappedFieldType) invocationOnMock.getArguments()[0])); - SearchLookup searchLookup = new SearchLookup(mapperService, ifds::getForField); + SearchLookup searchLookup = new SearchLookup(mapperService, (ft, l) -> ifds.getForField(ft, "test", l)); when(searchContext.lookup()).thenReturn(searchLookup); QueryShardContext queryShardContext = diff --git a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/index/mapper/FlatObjectFieldLookupTests.java b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/index/mapper/FlatObjectFieldLookupTests.java index 64b55b246f2de..9e42bd3409d6c 100644 --- a/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/index/mapper/FlatObjectFieldLookupTests.java +++ b/x-pack/plugin/mapper-flattened/src/test/java/org/elasticsearch/index/mapper/FlatObjectFieldLookupTests.java @@ -8,8 +8,8 @@ import org.elasticsearch.Version; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.index.fielddata.LeafFieldData; import org.elasticsearch.index.fielddata.IndexFieldData; +import org.elasticsearch.index.fielddata.LeafFieldData; import org.elasticsearch.index.fielddata.ScriptDocValues; import org.elasticsearch.search.lookup.LeafDocLookup; import org.elasticsearch.search.lookup.SearchLookup; @@ -23,7 +23,8 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.function.Function; +import java.util.function.BiFunction; +import java.util.function.Supplier; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -161,7 +162,7 @@ public void testScriptDocValuesLookup() { = new KeyedFlatObjectFieldType( "field", true, true, "key2", false, Collections.emptyMap()); when(mapperService.fieldType("json.key2")).thenReturn(fieldType2); - Function> fieldDataSupplier = fieldType -> { + BiFunction, IndexFieldData> fieldDataSupplier = (fieldType, l) -> { KeyedFlatObjectFieldType keyedFieldType = (KeyedFlatObjectFieldType) fieldType; return keyedFieldType.key().equals("key1") ? fieldData1 : fieldData2; }; diff --git a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java index 266395bedd0a6..5d020c204e47c 100644 --- a/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java +++ b/x-pack/plugin/runtime-fields/src/test/java/org/elasticsearch/xpack/runtimefields/mapper/AbstractScriptMappedFieldTypeTestCase.java @@ -77,7 +77,7 @@ protected static QueryShardContext mockContext(boolean allowExpensiveQueries, Ab when(context.allowExpensiveQueries()).thenReturn(allowExpensiveQueries); SearchLookup lookup = new SearchLookup( mapperService, - mft -> mft.fielddataBuilder("test", context::lookup).build(null, null, mapperService) + (mft, l) -> mft.fielddataBuilder("test", context::lookup).build(null, null, mapperService) ); when(context.lookup()).thenReturn(lookup); return context; diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/90_loop.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/90_loop.yml new file mode 100644 index 0000000000000..35d908318d782 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/runtime_fields/90_loop.yml @@ -0,0 +1,91 @@ +--- +setup: + - do: + indices.create: + index: sensor + body: + settings: + number_of_shards: 1 + number_of_replicas: 0 + mappings: + properties: + timestamp: + type: date + temperature: + type: long + voltage: + type: double + node: + type: keyword + tight_loop: + type: runtime_script + runtime_type: keyword + script: value(doc['tight_loop'].value) + loose_loop: + type: runtime_script + runtime_type: keyword + script: value(doc['lla'].value) + lla: + type: runtime_script + runtime_type: keyword + script: value(doc['llb'].value) + llb: + type: runtime_script + runtime_type: keyword + script: value(doc['llc'].value) + llc: + type: runtime_script + runtime_type: keyword + script: value(doc['lld'].value) + lld: + type: runtime_script + runtime_type: keyword + script: value(doc['lle'].value) + lle: + type: runtime_script + runtime_type: keyword + script: value(doc['llf'].value) + llf: + type: runtime_script + runtime_type: keyword + script: value(doc['llg'].value) + llg: + type: runtime_script + runtime_type: keyword + script: value(doc['loose_loop'].value) + + - do: + bulk: + index: sensor + refresh: true + body: | + {"index":{}} + {"timestamp": 1516729294000, "temperature": 200, "voltage": 5.2, "node": "a"} + +--- +"tight loop": + - do: + catch: '/loop in field definitions: \[tight_loop, tight_loop\]/' + search: + index: sensor + body: + sort: timestamp + docvalue_fields: [tight_loop] + +--- +"loose loop": + - do: + catch: '/loop in field definitions: \[loose_loop, lla, llb, llc, lld, lle, llf, llg, loose_loop\]/' + search: + index: sensor + body: + sort: timestamp + docvalue_fields: [loose_loop] + + - do: + catch: '/loop in field definitions: \[lld, lle, llf, llg, loose_loop, lla, llb, llc, lld\]/' + search: + index: sensor + body: + sort: timestamp + docvalue_fields: [lld]