Merge pull request #13188 from nik9000/limit_on_size
Limit the size of the result window to a dynamic property
This commit is contained in:
commit
766a25e3cb
|
@ -90,6 +90,7 @@ import org.elasticsearch.indices.recovery.RecoverySettings;
|
||||||
import org.elasticsearch.indices.store.IndicesStore;
|
import org.elasticsearch.indices.store.IndicesStore;
|
||||||
import org.elasticsearch.indices.ttl.IndicesTTLService;
|
import org.elasticsearch.indices.ttl.IndicesTTLService;
|
||||||
import org.elasticsearch.search.SearchService;
|
import org.elasticsearch.search.SearchService;
|
||||||
|
import org.elasticsearch.search.internal.DefaultSearchContext;
|
||||||
import org.elasticsearch.threadpool.ThreadPool;
|
import org.elasticsearch.threadpool.ThreadPool;
|
||||||
import org.elasticsearch.transport.TransportService;
|
import org.elasticsearch.transport.TransportService;
|
||||||
|
|
||||||
|
@ -275,6 +276,7 @@ public class ClusterModule extends AbstractModule {
|
||||||
registerIndexDynamicSetting(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED, Validator.BOOLEAN);
|
registerIndexDynamicSetting(IndicesRequestCache.INDEX_CACHE_REQUEST_ENABLED, Validator.BOOLEAN);
|
||||||
registerIndexDynamicSetting(IndicesRequestCache.DEPRECATED_INDEX_CACHE_REQUEST_ENABLED, Validator.BOOLEAN);
|
registerIndexDynamicSetting(IndicesRequestCache.DEPRECATED_INDEX_CACHE_REQUEST_ENABLED, Validator.BOOLEAN);
|
||||||
registerIndexDynamicSetting(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING, Validator.TIME);
|
registerIndexDynamicSetting(UnassignedInfo.INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING, Validator.TIME);
|
||||||
|
registerIndexDynamicSetting(DefaultSearchContext.MAX_RESULT_WINDOW, Validator.POSITIVE_INTEGER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void registerIndexDynamicSetting(String setting, Validator validator) {
|
public void registerIndexDynamicSetting(String setting, Validator validator) {
|
||||||
|
|
|
@ -77,6 +77,20 @@ import java.util.Map;
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public class DefaultSearchContext extends SearchContext {
|
public class DefaultSearchContext extends SearchContext {
|
||||||
|
/**
|
||||||
|
* Index setting describing the maximum value of from + size on a query.
|
||||||
|
*/
|
||||||
|
public static final String MAX_RESULT_WINDOW = "index.max_result_window";
|
||||||
|
public static class Defaults {
|
||||||
|
/**
|
||||||
|
* Default maximum value of from + size on a query. 10,000 was chosen as
|
||||||
|
* a conservative default as it is sure to not cause trouble. Users can
|
||||||
|
* certainly profile their cluster and decide to set it to 100,000
|
||||||
|
* safely. 1,000,000 is probably way to high for any cluster to set
|
||||||
|
* safely.
|
||||||
|
*/
|
||||||
|
public static final int MAX_RESULT_WINDOW = 10000;
|
||||||
|
}
|
||||||
|
|
||||||
private final long id;
|
private final long id;
|
||||||
private final ShardSearchRequest request;
|
private final ShardSearchRequest request;
|
||||||
|
@ -168,12 +182,20 @@ public class DefaultSearchContext extends SearchContext {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void preProcess() {
|
public void preProcess() {
|
||||||
if (!(from() == -1 && size() == -1)) {
|
if (scrollContext == null) {
|
||||||
// from and size have been set.
|
long from = from() == -1 ? 0 : from();
|
||||||
int numHits = from() + size();
|
long size = size() == -1 ? 10 : size();
|
||||||
if (numHits < 0) {
|
long resultWindow = from + size;
|
||||||
String msg = "Result window is too large, from + size must be less than or equal to: [" + Integer.MAX_VALUE + "] but was [" + (((long) from()) + ((long) size())) + "]";
|
// We need settingsService's view of the settings because its dynamic.
|
||||||
throw new QueryPhaseExecutionException(this, msg);
|
// indexService's isn't.
|
||||||
|
int maxResultWindow = indexService.settingsService().getSettings().getAsInt(MAX_RESULT_WINDOW, Defaults.MAX_RESULT_WINDOW);
|
||||||
|
|
||||||
|
if (resultWindow > maxResultWindow) {
|
||||||
|
throw new QueryPhaseExecutionException(this,
|
||||||
|
"Result window is too large, from + size must be less than or equal to: [" + maxResultWindow + "] but was ["
|
||||||
|
+ resultWindow + "]. See the scroll api for a more efficient way to request large data sets. "
|
||||||
|
+ "This limit can be set by changing the [" + DefaultSearchContext.MAX_RESULT_WINDOW
|
||||||
|
+ "] index level parameter.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,9 +410,7 @@ public class SearchScrollIT extends ESIntegTestCase {
|
||||||
assertThrows(internalCluster().transportClient().prepareSearchScroll(searchResponse2.getScrollId()).setScroll(TimeValue.timeValueMinutes(2)), RestStatus.NOT_FOUND);
|
assertThrows(internalCluster().transportClient().prepareSearchScroll(searchResponse2.getScrollId()).setScroll(TimeValue.timeValueMinutes(2)), RestStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testDeepScrollingDoesNotBlowUp() throws Exception {
|
||||||
// https://github.com/elasticsearch/elasticsearch/issues/4156
|
|
||||||
public void testDeepPaginationWithOneDocIndexAndDoNotBlowUp() throws Exception {
|
|
||||||
client().prepareIndex("index", "type", "1")
|
client().prepareIndex("index", "type", "1")
|
||||||
.setSource("field", "value")
|
.setSource("field", "value")
|
||||||
.setRefresh(true)
|
.setRefresh(true)
|
||||||
|
@ -422,11 +420,8 @@ public class SearchScrollIT extends ESIntegTestCase {
|
||||||
SearchRequestBuilder builder = client().prepareSearch("index")
|
SearchRequestBuilder builder = client().prepareSearch("index")
|
||||||
.setSearchType(searchType)
|
.setSearchType(searchType)
|
||||||
.setQuery(QueryBuilders.matchAllQuery())
|
.setQuery(QueryBuilders.matchAllQuery())
|
||||||
.setSize(Integer.MAX_VALUE);
|
.setSize(Integer.MAX_VALUE)
|
||||||
|
.setScroll("1m");
|
||||||
if (randomBoolean()) {
|
|
||||||
builder.setScroll("1m");
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchResponse response = builder.execute().actionGet();
|
SearchResponse response = builder.execute().actionGet();
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -22,11 +22,13 @@ package org.elasticsearch.search.simple;
|
||||||
import org.apache.lucene.util.Constants;
|
import org.apache.lucene.util.Constants;
|
||||||
import org.elasticsearch.action.index.IndexRequestBuilder;
|
import org.elasticsearch.action.index.IndexRequestBuilder;
|
||||||
import org.elasticsearch.action.search.SearchPhaseExecutionException;
|
import org.elasticsearch.action.search.SearchPhaseExecutionException;
|
||||||
|
import org.elasticsearch.action.search.SearchRequestBuilder;
|
||||||
import org.elasticsearch.action.search.SearchResponse;
|
import org.elasticsearch.action.search.SearchResponse;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.xcontent.XContentFactory;
|
import org.elasticsearch.common.xcontent.XContentFactory;
|
||||||
import org.elasticsearch.index.query.QueryBuilders;
|
import org.elasticsearch.index.query.QueryBuilders;
|
||||||
|
import org.elasticsearch.search.internal.DefaultSearchContext;
|
||||||
import org.elasticsearch.test.ESIntegTestCase;
|
import org.elasticsearch.test.ESIntegTestCase;
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -38,12 +40,12 @@ import static org.elasticsearch.cluster.metadata.IndexMetaData.SETTING_NUMBER_OF
|
||||||
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
|
||||||
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
|
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
|
||||||
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
|
import static org.elasticsearch.index.query.QueryBuilders.rangeQuery;
|
||||||
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.*;
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
|
||||||
|
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
|
||||||
public class SimpleSearchIT extends ESIntegTestCase {
|
public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSearchNullIndex() {
|
public void testSearchNullIndex() {
|
||||||
try {
|
try {
|
||||||
client().prepareSearch((String) null).setQuery(QueryBuilders.termQuery("_id", "XXX1")).execute().actionGet();
|
client().prepareSearch((String) null).setQuery(QueryBuilders.termQuery("_id", "XXX1")).execute().actionGet();
|
||||||
|
@ -60,7 +62,6 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testSearchRandomPreference() throws InterruptedException, ExecutionException {
|
public void testSearchRandomPreference() throws InterruptedException, ExecutionException {
|
||||||
createIndex("test");
|
createIndex("test");
|
||||||
indexRandom(true, client().prepareIndex("test", "type", "1").setSource("field", "value"),
|
indexRandom(true, client().prepareIndex("test", "type", "1").setSource("field", "value"),
|
||||||
|
@ -84,8 +85,7 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testSimpleIp() throws Exception {
|
||||||
public void simpleIpTests() throws Exception {
|
|
||||||
createIndex("test");
|
createIndex("test");
|
||||||
|
|
||||||
client().admin().indices().preparePutMapping("test").setType("type1")
|
client().admin().indices().preparePutMapping("test").setType("type1")
|
||||||
|
@ -104,8 +104,7 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
assertHitCount(search, 1l);
|
assertHitCount(search, 1l);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testSimpleId() {
|
||||||
public void simpleIdTests() {
|
|
||||||
createIndex("test");
|
createIndex("test");
|
||||||
|
|
||||||
client().prepareIndex("test", "type", "XXX1").setSource("field", "value").setRefresh(true).execute().actionGet();
|
client().prepareIndex("test", "type", "XXX1").setSource("field", "value").setRefresh(true).execute().actionGet();
|
||||||
|
@ -124,8 +123,7 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
assertHitCount(searchResponse, 1l);
|
assertHitCount(searchResponse, 1l);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testSimpleDateRange() throws Exception {
|
||||||
public void simpleDateRangeTests() throws Exception {
|
|
||||||
createIndex("test");
|
createIndex("test");
|
||||||
client().prepareIndex("test", "type1", "1").setSource("field", "2010-01-05T02:00").execute().actionGet();
|
client().prepareIndex("test", "type1", "1").setSource("field", "2010-01-05T02:00").execute().actionGet();
|
||||||
client().prepareIndex("test", "type1", "2").setSource("field", "2010-01-06T02:00").execute().actionGet();
|
client().prepareIndex("test", "type1", "2").setSource("field", "2010-01-06T02:00").execute().actionGet();
|
||||||
|
@ -151,8 +149,7 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
assertHitCount(searchResponse, 2l);
|
assertHitCount(searchResponse, 2l);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testLocaleDependentDate() throws Exception {
|
||||||
public void localeDependentDateTests() throws Exception {
|
|
||||||
assumeFalse("Locals are buggy on JDK9EA", Constants.JRE_IS_MINIMUM_JAVA9 && systemPropertyAsBoolean("tests.security.manager", false));
|
assumeFalse("Locals are buggy on JDK9EA", Constants.JRE_IS_MINIMUM_JAVA9 && systemPropertyAsBoolean("tests.security.manager", false));
|
||||||
assertAcked(prepareCreate("test")
|
assertAcked(prepareCreate("test")
|
||||||
.addMapping("type1",
|
.addMapping("type1",
|
||||||
|
@ -189,8 +186,7 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testSimpleTerminateAfterCount() throws Exception {
|
||||||
public void simpleTerminateAfterCountTests() throws Exception {
|
|
||||||
prepareCreate("test").setSettings(
|
prepareCreate("test").setSettings(
|
||||||
SETTING_NUMBER_OF_SHARDS, 1,
|
SETTING_NUMBER_OF_SHARDS, 1,
|
||||||
SETTING_NUMBER_OF_REPLICAS, 0).get();
|
SETTING_NUMBER_OF_REPLICAS, 0).get();
|
||||||
|
@ -225,16 +221,76 @@ public class SimpleSearchIT extends ESIntegTestCase {
|
||||||
assertFalse(searchResponse.isTerminatedEarly());
|
assertFalse(searchResponse.isTerminatedEarly());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
public void testInsaneFromAndSize() throws Exception {
|
||||||
public void testInsaneFrom() throws Exception {
|
|
||||||
createIndex("idx");
|
createIndex("idx");
|
||||||
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertWindowFails(client().prepareSearch("idx").setFrom(Integer.MAX_VALUE));
|
||||||
|
assertWindowFails(client().prepareSearch("idx").setSize(Integer.MAX_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTooLargeFromAndSize() throws Exception {
|
||||||
|
createIndex("idx");
|
||||||
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertWindowFails(client().prepareSearch("idx").setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW));
|
||||||
|
assertWindowFails(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW + 1));
|
||||||
|
assertWindowFails(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW)
|
||||||
|
.setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testLargeFromAndSizeSucceeds() throws Exception {
|
||||||
|
createIndex("idx");
|
||||||
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertHitCount(client().prepareSearch("idx").setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW - 10).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW / 2)
|
||||||
|
.setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW / 2 - 1).get(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTooLargeFromAndSizeOkBySetting() throws Exception {
|
||||||
|
prepareCreate("idx").setSettings(DefaultSearchContext.MAX_RESULT_WINDOW, DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 2).get();
|
||||||
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertHitCount(client().prepareSearch("idx").setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW + 1).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW)
|
||||||
|
.setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW).get(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTooLargeFromAndSizeOkByDynamicSetting() throws Exception {
|
||||||
|
createIndex("idx");
|
||||||
|
assertAcked(client().admin().indices().prepareUpdateSettings("idx")
|
||||||
|
.setSettings(
|
||||||
|
Settings.builder().put(DefaultSearchContext.MAX_RESULT_WINDOW, DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 2))
|
||||||
|
.get());
|
||||||
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertHitCount(client().prepareSearch("idx").setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW + 1).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW)
|
||||||
|
.setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW).get(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTooLargeFromAndSizeBackwardsCompatibilityRecommendation() throws Exception {
|
||||||
|
prepareCreate("idx").setSettings(DefaultSearchContext.MAX_RESULT_WINDOW, Integer.MAX_VALUE).get();
|
||||||
|
indexRandom(true, client().prepareIndex("idx", "type").setSource("{}"));
|
||||||
|
|
||||||
|
assertHitCount(client().prepareSearch("idx").setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 10).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 10).get(), 1);
|
||||||
|
assertHitCount(client().prepareSearch("idx").setSize(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 10)
|
||||||
|
.setFrom(DefaultSearchContext.Defaults.MAX_RESULT_WINDOW * 10).get(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertWindowFails(SearchRequestBuilder search) {
|
||||||
try {
|
try {
|
||||||
client().prepareSearch("idx").setFrom(Integer.MAX_VALUE).get();
|
search.get();
|
||||||
fail();
|
fail();
|
||||||
} catch (SearchPhaseExecutionException e) {
|
} catch (SearchPhaseExecutionException e) {
|
||||||
assertThat(e.toString(), containsString("Result window is too large, from + size must be less than or equal to:"));
|
assertThat(e.toString(), containsString("Result window is too large, from + size must be less than or equal to: ["
|
||||||
|
+ DefaultSearchContext.Defaults.MAX_RESULT_WINDOW));
|
||||||
|
assertThat(e.toString(), containsString("See the scroll api for a more efficient way to request large data sets"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,14 @@ specific index module:
|
||||||
index visible to search. Defaults to `1s`. Can be set to `-1` to disable
|
index visible to search. Defaults to `1s`. Can be set to `-1` to disable
|
||||||
refresh.
|
refresh.
|
||||||
|
|
||||||
|
`index.max_result_window`::
|
||||||
|
|
||||||
|
The maximum value of `from + size` for searches to this index. Defaults to
|
||||||
|
`10000`. Search requests take heap memory and time proportional to
|
||||||
|
`from + size` and this limits that memory. See
|
||||||
|
{ref}/search-request-scroll.html[Scroll] for a more efficient alternative
|
||||||
|
to raising this.
|
||||||
|
|
||||||
`index.blocks.read_only`::
|
`index.blocks.read_only`::
|
||||||
|
|
||||||
Set to `true` to make the index and index metadata read only, `false` to
|
Set to `true` to make the index and index metadata read only, `false` to
|
||||||
|
@ -184,5 +192,3 @@ include::index-modules/slowlog.asciidoc[]
|
||||||
include::index-modules/store.asciidoc[]
|
include::index-modules/store.asciidoc[]
|
||||||
|
|
||||||
include::index-modules/translog.asciidoc[]
|
include::index-modules/translog.asciidoc[]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,16 @@ Scroll requests sorted by `_doc` have been optimized to more efficiently resume
|
||||||
from where the previous request stopped, so this will have the same performance
|
from where the previous request stopped, so this will have the same performance
|
||||||
characteristics as the former `scan` search type.
|
characteristics as the former `scan` search type.
|
||||||
|
|
||||||
|
==== from + size limits
|
||||||
|
|
||||||
|
Elasticsearch will now return an error message if a query's `from` + `size` is
|
||||||
|
more than the `index.max_result_window` parameter. This parameter defaults to
|
||||||
|
10,000 which is safe for almost all clusters. Values higher than can consume
|
||||||
|
significant chunks of heap memory per search and per shard executing the
|
||||||
|
search. It's safest to leave this value as it is an use the scroll api for any
|
||||||
|
deep scrolling but this setting is dynamic so it can raised or lowered as
|
||||||
|
needed.
|
||||||
|
|
||||||
=== Update changes
|
=== Update changes
|
||||||
|
|
||||||
==== Updates now `detect_noop` by default
|
==== Updates now `detect_noop` by default
|
||||||
|
|
|
@ -19,3 +19,8 @@ defaults to `10`.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
--------------------------------------------------
|
--------------------------------------------------
|
||||||
|
|
||||||
|
Note that `from` + `size` can not be more than the `index.max_result_window`
|
||||||
|
index setting which defaults to 10,000. See the
|
||||||
|
{ref}/search-request-scroll.html[Scroll] api for more efficient ways to do deep
|
||||||
|
scrolling.
|
||||||
|
|
Loading…
Reference in New Issue