diff --git a/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java b/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java index 1b57a2919f3b1..5a6c17bf2f0c1 100644 --- a/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java +++ b/core/src/main/java/org/elasticsearch/common/settings/IndexScopedSettings.java @@ -18,7 +18,6 @@ */ package org.elasticsearch.common.settings; -import org.elasticsearch.index.IndexSortConfig; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.routing.UnassignedInfo; import org.elasticsearch.cluster.routing.allocation.decider.EnableAllocationDecider; @@ -27,6 +26,7 @@ import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.index.IndexModule; import org.elasticsearch.index.IndexSettings; +import org.elasticsearch.index.IndexSortConfig; import org.elasticsearch.index.IndexingSlowLog; import org.elasticsearch.index.MergePolicyConfig; import org.elasticsearch.index.MergeSchedulerConfig; @@ -110,6 +110,7 @@ public final class IndexScopedSettings extends AbstractScopedSettings { IndexSettings.INDEX_WARMER_ENABLED_SETTING, IndexSettings.INDEX_REFRESH_INTERVAL_SETTING, IndexSettings.MAX_RESULT_WINDOW_SETTING, + IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING, IndexSettings.MAX_RESCORE_WINDOW_SETTING, IndexSettings.MAX_ADJACENCY_MATRIX_FILTERS_SETTING, IndexSettings.INDEX_TRANSLOG_SYNC_INTERVAL_SETTING, diff --git a/core/src/main/java/org/elasticsearch/index/IndexSettings.java b/core/src/main/java/org/elasticsearch/index/IndexSettings.java index 7f84d201d0f4e..ad3a13ded4d56 100644 --- a/core/src/main/java/org/elasticsearch/index/IndexSettings.java +++ b/core/src/main/java/org/elasticsearch/index/IndexSettings.java @@ -104,6 +104,13 @@ public final class IndexSettings { */ public static final Setting MAX_RESULT_WINDOW_SETTING = Setting.intSetting("index.max_result_window", 10000, 1, Property.Dynamic, Property.IndexScope); + /** + * Index setting describing the maximum value of from + size on an individual inner hit definition or + * top hits aggregation. The default maximum of 100 is defensive for the reason that the number of inner hit responses + * and number of top hits buckets returned is unbounded. Profile your cluster when increasing this setting. + */ + public static final Setting MAX_INNER_RESULT_WINDOW_SETTING = + Setting.intSetting("index.max_inner_result_window", 100, 1, Property.Dynamic, Property.IndexScope); /** * Index setting describing the maximum size of the rescore window. Defaults to {@link #MAX_RESULT_WINDOW_SETTING} * because they both do the same thing: control the size of the heap of hits. @@ -224,6 +231,7 @@ public final class IndexSettings { private long gcDeletesInMillis = DEFAULT_GC_DELETES.millis(); private volatile boolean warmerEnabled; private volatile int maxResultWindow; + private volatile int maxInnerResultWindow; private volatile int maxAdjacencyMatrixFilters; private volatile int maxRescoreWindow; private volatile boolean TTLPurgeDisabled; @@ -324,6 +332,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti gcDeletesInMillis = scopedSettings.get(INDEX_GC_DELETES_SETTING).getMillis(); warmerEnabled = scopedSettings.get(INDEX_WARMER_ENABLED_SETTING); maxResultWindow = scopedSettings.get(MAX_RESULT_WINDOW_SETTING); + maxInnerResultWindow = scopedSettings.get(MAX_INNER_RESULT_WINDOW_SETTING); maxAdjacencyMatrixFilters = scopedSettings.get(MAX_ADJACENCY_MATRIX_FILTERS_SETTING); maxRescoreWindow = scopedSettings.get(MAX_RESCORE_WINDOW_SETTING); TTLPurgeDisabled = scopedSettings.get(INDEX_TTL_DISABLE_PURGE_SETTING); @@ -352,6 +361,7 @@ public IndexSettings(final IndexMetaData indexMetaData, final Settings nodeSetti scopedSettings.addSettingsUpdateConsumer(INDEX_TRANSLOG_DURABILITY_SETTING, this::setTranslogDurability); scopedSettings.addSettingsUpdateConsumer(INDEX_TTL_DISABLE_PURGE_SETTING, this::setTTLPurgeDisabled); scopedSettings.addSettingsUpdateConsumer(MAX_RESULT_WINDOW_SETTING, this::setMaxResultWindow); + scopedSettings.addSettingsUpdateConsumer(MAX_INNER_RESULT_WINDOW_SETTING, this::setMaxInnerResultWindow); scopedSettings.addSettingsUpdateConsumer(MAX_ADJACENCY_MATRIX_FILTERS_SETTING, this::setMaxAdjacencyMatrixFilters); scopedSettings.addSettingsUpdateConsumer(MAX_RESCORE_WINDOW_SETTING, this::setMaxRescoreWindow); scopedSettings.addSettingsUpdateConsumer(INDEX_WARMER_ENABLED_SETTING, this::setEnableWarmer); @@ -577,6 +587,17 @@ private void setMaxResultWindow(int maxResultWindow) { this.maxResultWindow = maxResultWindow; } + /** + * Returns the max result window for an individual inner hit definition or top hits aggregation. + */ + public int getMaxInnerResultWindow() { + return maxInnerResultWindow; + } + + private void setMaxInnerResultWindow(int maxInnerResultWindow) { + this.maxInnerResultWindow = maxInnerResultWindow; + } + /** * Returns the max number of filters in adjacency_matrix aggregation search requests */ diff --git a/core/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java b/core/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java index f13aa22f7d914..58d271bb8206c 100644 --- a/core/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/InnerHitContextBuilder.java @@ -19,7 +19,7 @@ package org.elasticsearch.index.query; -import org.elasticsearch.script.ScriptContext; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.script.SearchScript; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.fetch.subphase.DocValueFieldsContext; @@ -47,8 +47,21 @@ protected InnerHitContextBuilder(QueryBuilder query, InnerHitBuilder innerHitBui this.query = query; } - public abstract void build(SearchContext parentSearchContext, - InnerHitsContext innerHitsContext) throws IOException; + public final void build(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException { + long innerResultWindow = innerHitBuilder.getFrom() + innerHitBuilder.getSize(); + int maxInnerResultWindow = parentSearchContext.mapperService().getIndexSettings().getMaxInnerResultWindow(); + if (innerResultWindow > maxInnerResultWindow) { + throw new IllegalArgumentException( + "Inner result window is too large, the inner hit definition's [" + innerHitBuilder.getName() + + "]'s from + size must be less than or equal to: [" + maxInnerResultWindow + "] but was [" + innerResultWindow + + "]. This limit can be set by changing the [" + IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey() + + "] index level setting." + ); + } + doBuild(parentSearchContext, innerHitsContext); + } + + protected abstract void doBuild(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException; public static void extractInnerHits(QueryBuilder query, Map innerHitBuilders) { if (query instanceof AbstractQueryBuilder) { diff --git a/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java index b9037110b1c59..4e3429e1a2088 100644 --- a/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/NestedQueryBuilder.java @@ -336,7 +336,7 @@ static class NestedInnerHitContextBuilder extends InnerHitContextBuilder { } @Override - public void build(SearchContext parentSearchContext, + protected void doBuild(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException { QueryShardContext queryShardContext = parentSearchContext.getQueryShardContext(); ObjectMapper nestedObjectMapper = queryShardContext.getObjectMapper(path); diff --git a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java index c5be9f3551b15..cede3ae9661db 100644 --- a/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java +++ b/core/src/main/java/org/elasticsearch/search/aggregations/metrics/tophits/TopHitsAggregationBuilder.java @@ -26,6 +26,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.script.Script; import org.elasticsearch.script.SearchScript; @@ -529,6 +530,17 @@ public TopHitsAggregationBuilder subAggregations(Builder subFactories) { @Override protected TopHitsAggregatorFactory doBuild(SearchContext context, AggregatorFactory parent, Builder subfactoriesBuilder) throws IOException { + long innerResultWindow = from() + size(); + int maxInnerResultWindow = context.mapperService().getIndexSettings().getMaxInnerResultWindow(); + if (innerResultWindow > maxInnerResultWindow) { + throw new IllegalArgumentException( + "Top hits result window is too large, the top hits aggregator [" + name + "]'s from + size must be less " + + "than or equal to: [" + maxInnerResultWindow + "] but was [" + innerResultWindow + + "]. This limit can be set by changing the [" + IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey() + + "] index level setting." + ); + } + List fields = new ArrayList<>(); if (scriptFields != null) { for (ScriptField field : scriptFields) { diff --git a/core/src/test/java/org/elasticsearch/index/IndexSettingsTests.java b/core/src/test/java/org/elasticsearch/index/IndexSettingsTests.java index c6fd5d4fbdfd5..dad2b4e7d9153 100644 --- a/core/src/test/java/org/elasticsearch/index/IndexSettingsTests.java +++ b/core/src/test/java/org/elasticsearch/index/IndexSettingsTests.java @@ -290,6 +290,26 @@ public void testMaxResultWindow() { assertEquals(IndexSettings.MAX_RESULT_WINDOW_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxResultWindow()); } + public void testMaxInnerResultWindow() { + IndexMetaData metaData = newIndexMeta("index", Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .put(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), 200) + .build()); + IndexSettings settings = new IndexSettings(metaData, Settings.EMPTY); + assertEquals(200, settings.getMaxInnerResultWindow()); + settings.updateIndexMetaData(newIndexMeta("index", Settings.builder().put(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), + 50).build())); + assertEquals(50, settings.getMaxInnerResultWindow()); + settings.updateIndexMetaData(newIndexMeta("index", Settings.EMPTY)); + assertEquals(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxInnerResultWindow()); + + metaData = newIndexMeta("index", Settings.builder() + .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) + .build()); + settings = new IndexSettings(metaData, Settings.EMPTY); + assertEquals(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.get(Settings.EMPTY).intValue(), settings.getMaxInnerResultWindow()); + } + public void testMaxAdjacencyMatrixFiltersSetting() { IndexMetaData metaData = newIndexMeta("index", Settings.builder() .put(IndexMetaData.SETTING_VERSION_CREATED, Version.CURRENT) diff --git a/core/src/test/java/org/elasticsearch/index/query/InnerHitBuilderTests.java b/core/src/test/java/org/elasticsearch/index/query/InnerHitBuilderTests.java index 78975bf7b1784..a4e6856166272 100644 --- a/core/src/test/java/org/elasticsearch/index/query/InnerHitBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/index/query/InnerHitBuilderTests.java @@ -139,8 +139,8 @@ public void testEqualsAndHashcode() { public static InnerHitBuilder randomInnerHits() { InnerHitBuilder innerHits = new InnerHitBuilder(); innerHits.setName(randomAlphaOfLengthBetween(1, 16)); - innerHits.setFrom(randomIntBetween(0, 128)); - innerHits.setSize(randomIntBetween(0, 128)); + innerHits.setFrom(randomIntBetween(0, 32)); + innerHits.setSize(randomIntBetween(0, 32)); innerHits.setExplain(randomBoolean()); innerHits.setVersion(randomBoolean()); innerHits.setTrackScores(randomBoolean()); diff --git a/core/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java b/core/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java index 0e3928c89883c..9d674a1a0d05a 100644 --- a/core/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java +++ b/core/src/test/java/org/elasticsearch/index/query/NestedQueryBuilderTests.java @@ -26,6 +26,8 @@ import org.elasticsearch.Version; import org.elasticsearch.action.admin.indices.mapping.put.PutMappingRequest; import org.elasticsearch.common.compress.CompressedXContent; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder; import org.elasticsearch.index.search.ESToParentBlockJoinQuery; @@ -41,6 +43,7 @@ import java.util.HashMap; import java.util.Map; +import static org.elasticsearch.index.IndexSettingsTests.newIndexMeta; import static org.elasticsearch.index.query.InnerHitBuilderTests.randomInnerHits; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; @@ -325,6 +328,11 @@ public void testBuildIgnoreUnmappedNestQuery() throws Exception { SearchContext searchContext = mock(SearchContext.class); when(searchContext.getQueryShardContext()).thenReturn(queryShardContext); + MapperService mapperService = mock(MapperService.class); + IndexSettings settings = new IndexSettings(newIndexMeta("index", Settings.EMPTY), Settings.EMPTY); + when(mapperService.getIndexSettings()).thenReturn(settings); + when(searchContext.mapperService()).thenReturn(mapperService); + InnerHitBuilder leafInnerHits = randomInnerHits(); NestedQueryBuilder query1 = new NestedQueryBuilder("path", new MatchAllQueryBuilder(), ScoreMode.None); query1.innerHit(leafInnerHits); diff --git a/core/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java b/core/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java index c43924728a2a4..3822455b83c3a 100644 --- a/core/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java +++ b/core/src/test/java/org/elasticsearch/search/aggregations/metrics/TopHitsIT.java @@ -28,6 +28,7 @@ import org.elasticsearch.common.document.DocumentField; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.MatchAllQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.plugins.Plugin; @@ -942,7 +943,10 @@ public void testTopHitsInNested() throws Exception { } } - public void testDontExplode() throws Exception { + public void testUseMaxDocInsteadOfSize() throws Exception { + client().admin().indices().prepareUpdateSettings("idx") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), ArrayUtil.MAX_ARRAY_LENGTH)) + .get(); SearchResponse response = client() .prepareSearch("idx") .addAggregation(terms("terms") @@ -954,6 +958,67 @@ public void testDontExplode() throws Exception { ) .get(); assertNoFailures(response); + client().admin().indices().prepareUpdateSettings("idx") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), null)) + .get(); + } + + public void testTooHighResultWindow() throws Exception { + SearchResponse response = client() + .prepareSearch("idx") + .addAggregation(terms("terms") + .executionHint(randomExecutionHint()) + .field(TERMS_AGGS_FIELD) + .subAggregation( + topHits("hits").from(50).size(10).sort(SortBuilders.fieldSort(SORT_FIELD).order(SortOrder.DESC)) + ) + ) + .get(); + assertNoFailures(response); + + Exception e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("idx") + .addAggregation(terms("terms") + .executionHint(randomExecutionHint()) + .field(TERMS_AGGS_FIELD) + .subAggregation( + topHits("hits").from(100).size(10).sort(SortBuilders.fieldSort(SORT_FIELD).order(SortOrder.DESC)) + ) + ).get()); + assertThat(e.getCause().getMessage(), + containsString("the top hits aggregator [hits]'s from + size must be less than or equal to: [100] but was [110]")); + e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("idx") + .addAggregation(terms("terms") + .executionHint(randomExecutionHint()) + .field(TERMS_AGGS_FIELD) + .subAggregation( + topHits("hits").from(10).size(100).sort(SortBuilders.fieldSort(SORT_FIELD).order(SortOrder.DESC)) + ) + ).get()); + assertThat(e.getCause().getMessage(), + containsString("the top hits aggregator [hits]'s from + size must be less than or equal to: [100] but was [110]")); + + client().admin().indices().prepareUpdateSettings("idx") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), 110)) + .get(); + response = client().prepareSearch("idx") + .addAggregation(terms("terms") + .executionHint(randomExecutionHint()) + .field(TERMS_AGGS_FIELD) + .subAggregation( + topHits("hits").from(100).size(10).sort(SortBuilders.fieldSort(SORT_FIELD).order(SortOrder.DESC)) + )).get(); + assertNoFailures(response); + response = client().prepareSearch("idx") + .addAggregation(terms("terms") + .executionHint(randomExecutionHint()) + .field(TERMS_AGGS_FIELD) + .subAggregation( + topHits("hits").from(10).size(100).sort(SortBuilders.fieldSort(SORT_FIELD).order(SortOrder.DESC)) + )).get(); + assertNoFailures(response); + client().admin().indices().prepareUpdateSettings("idx") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), null)) + .get(); } public void testNoStoredFields() throws Exception { diff --git a/core/src/test/java/org/elasticsearch/search/fetch/subphase/InnerHitsIT.java b/core/src/test/java/org/elasticsearch/search/fetch/subphase/InnerHitsIT.java index 55a424754d51f..079db8097f2b0 100644 --- a/core/src/test/java/org/elasticsearch/search/fetch/subphase/InnerHitsIT.java +++ b/core/src/test/java/org/elasticsearch/search/fetch/subphase/InnerHitsIT.java @@ -23,10 +23,12 @@ import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.Version; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.InnerHitBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -629,8 +631,11 @@ public void testInnerHitsWithIgnoreUnmapped() throws Exception { assertSearchHits(response, "1", "3"); } - public void testDontExplode() throws Exception { + public void testUseMaxDocInsteadOfSize() throws Exception { assertAcked(prepareCreate("index2").addMapping("type", "nested", "type=nested")); + client().admin().indices().prepareUpdateSettings("index2") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), ArrayUtil.MAX_ARRAY_LENGTH)) + .get(); client().prepareIndex("index2", "type", "1").setSource(jsonBuilder().startObject() .startArray("nested") .startObject() @@ -650,4 +655,50 @@ public void testDontExplode() throws Exception { assertHitCount(response, 1); } + public void testTooHighResultWindow() throws Exception { + assertAcked(prepareCreate("index2").addMapping("type", "nested", "type=nested")); + client().prepareIndex("index2", "type", "1").setSource(jsonBuilder().startObject() + .startArray("nested") + .startObject() + .field("field", "value1") + .endObject() + .endArray() + .endObject()) + .setRefreshPolicy(IMMEDIATE) + .get(); + SearchResponse response = client().prepareSearch("index2") + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg) + .innerHit(new InnerHitBuilder().setFrom(50).setSize(10).setName("_name"))) + .get(); + assertNoFailures(response); + assertHitCount(response, 1); + + Exception e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("index2") + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg) + .innerHit(new InnerHitBuilder().setFrom(100).setSize(10).setName("_name"))) + .get()); + assertThat(e.getCause().getMessage(), + containsString("the inner hit definition's [_name]'s from + size must be less than or equal to: [100] but was [110]")); + e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("index2") + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg) + .innerHit(new InnerHitBuilder().setFrom(10).setSize(100).setName("_name"))) + .get()); + assertThat(e.getCause().getMessage(), + containsString("the inner hit definition's [_name]'s from + size must be less than or equal to: [100] but was [110]")); + + client().admin().indices().prepareUpdateSettings("index2") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), 110)) + .get(); + response = client().prepareSearch("index2") + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg) + .innerHit(new InnerHitBuilder().setFrom(100).setSize(10).setName("_name"))) + .get(); + assertNoFailures(response); + response = client().prepareSearch("index2") + .setQuery(nestedQuery("nested", matchQuery("nested.field", "value1"), ScoreMode.Avg) + .innerHit(new InnerHitBuilder().setFrom(10).setSize(100).setName("_name"))) + .get(); + assertNoFailures(response); + } + } diff --git a/docs/reference/index-modules.asciidoc b/docs/reference/index-modules.asciidoc index 51d2291c4d87d..5347fd875d798 100644 --- a/docs/reference/index-modules.asciidoc +++ b/docs/reference/index-modules.asciidoc @@ -121,6 +121,11 @@ specific index module: <> or <> for a more efficient alternative to raising this. +`index.max_inner_result_window`:: + + The maximum value of `from + size` for inner hits definition and top hits aggregations to this index. Defaults to + `100`. Inner hits and top hits aggregation take heap memory and time proportional to `from + size` and this limits that memory. + `index.max_rescore_window`:: The maximum value of `window_size` for `rescore` requests in searches of this index. diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java index c60babf5b2dc2..dd1009d775223 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/ParentChildInnerHitContextBuilder.java @@ -70,7 +70,7 @@ class ParentChildInnerHitContextBuilder extends InnerHitContextBuilder { } @Override - public void build(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException { + protected void doBuild(SearchContext parentSearchContext, InnerHitsContext innerHitsContext) throws IOException { if (parentSearchContext.mapperService().getIndexSettings().isSingleType()) { handleJoinFieldInnerHits(parentSearchContext, innerHitsContext); } else { diff --git a/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java b/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java index d9c8aaddc78e1..6efd5256e5422 100644 --- a/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java +++ b/modules/parent-join/src/test/java/org/elasticsearch/join/query/InnerHitsIT.java @@ -22,7 +22,9 @@ import org.apache.lucene.search.join.ScoreMode; import org.apache.lucene.util.ArrayUtil; import org.elasticsearch.action.index.IndexRequestBuilder; +import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.IndexSettings; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.InnerHitBuilder; import org.elasticsearch.index.query.QueryBuilder; @@ -506,13 +508,16 @@ public void testMatchesQueriesParentChildInnerHits() throws Exception { assertThat(response.getHits().getAt(0).getInnerHits().get("child").getAt(0).getMatchedQueries()[0], equalTo("_name2")); } - public void testDontExplode() throws Exception { + public void testUseMaxDocInsteadOfSize() throws Exception { if (legacy()) { assertAcked(prepareCreate("index1").addMapping("child", "_parent", "type=parent")); } else { assertAcked(prepareCreate("index1") .addMapping("doc", buildParentJoinFieldMappingFromSimplifiedDef("join_field", true, "parent", "child"))); } + client().admin().indices().prepareUpdateSettings("index1") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), ArrayUtil.MAX_ARRAY_LENGTH)) + .get(); List requests = new ArrayList<>(); requests.add(createIndexRequest("index1", "parent", "1", null)); requests.add(createIndexRequest("index1", "child", "2", "1", "field", "value1")); @@ -585,4 +590,56 @@ public void testInnerHitsWithIgnoreUnmapped() throws Exception { assertHitCount(response, 2); assertSearchHits(response, "1", "3"); } + + public void testTooHighResultWindow() throws Exception { + if (legacy()) { + assertAcked(prepareCreate("index1") + .addMapping("parent_type", "nested_type", "type=nested") + .addMapping("child_type", "_parent", "type=parent_type") + ); + } else { + assertAcked(prepareCreate("index1") + .addMapping("doc", addFieldMappings( + buildParentJoinFieldMappingFromSimplifiedDef("join_field", true, "parent_type", "child_type"), + "nested_type", "nested")) + ); + } + createIndexRequest("index1", "parent_type", "1", null, "nested_type", Collections.singletonMap("key", "value")).get(); + createIndexRequest("index1", "child_type", "2", "1").get(); + refresh(); + + SearchResponse response = client().prepareSearch("index1") + .setQuery(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true) + .innerHit(new InnerHitBuilder().setFrom(50).setSize(10).setName("_name"))) + .get(); + assertNoFailures(response); + assertHitCount(response, 1); + + Exception e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("index1") + .setQuery(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true) + .innerHit(new InnerHitBuilder().setFrom(100).setSize(10).setName("_name"))) + .get()); + assertThat(e.getCause().getMessage(), + containsString("the inner hit definition's [_name]'s from + size must be less than or equal to: [100] but was [110]")); + e = expectThrows(SearchPhaseExecutionException.class, () -> client().prepareSearch("index1") + .setQuery(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true) + .innerHit(new InnerHitBuilder().setFrom(10).setSize(100).setName("_name"))) + .get()); + assertThat(e.getCause().getMessage(), + containsString("the inner hit definition's [_name]'s from + size must be less than or equal to: [100] but was [110]")); + + client().admin().indices().prepareUpdateSettings("index1") + .setSettings(Collections.singletonMap(IndexSettings.MAX_INNER_RESULT_WINDOW_SETTING.getKey(), 110)) + .get(); + response = client().prepareSearch("index1") + .setQuery(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true) + .innerHit(new InnerHitBuilder().setFrom(100).setSize(10).setName("_name"))) + .get(); + assertNoFailures(response); + response = client().prepareSearch("index1") + .setQuery(hasChildQuery("child_type", matchAllQuery(), ScoreMode.None).ignoreUnmapped(true) + .innerHit(new InnerHitBuilder().setFrom(10).setSize(100).setName("_name"))) + .get(); + assertNoFailures(response); + } }