From 9bfc687a1c5c67f4de8b1651aea38ec4651961cb Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 14 Nov 2025 14:14:13 +0000 Subject: [PATCH 01/13] MRT should default to `true` for CPS searches --- .../mustache/RestSearchTemplateAction.java | 11 ++-- .../rest/action/search/RestSearchAction.java | 57 +++++++++++++++---- .../search/RestSubmitAsyncSearchAction.java | 4 +- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java index 72e58b651e486..287f5f1079d14 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -37,11 +38,11 @@ public class RestSearchTemplateAction extends BaseRestHandler { private static final Set RESPONSE_PARAMS = Set.of(TYPED_KEYS_PARAM, RestSearchAction.TOTAL_HITS_AS_INT_PARAM); private final Predicate clusterSupportsFeature; - private final Settings settings; + private final boolean crossProjectEnabled; public RestSearchTemplateAction(Predicate clusterSupportsFeature, Settings settings) { this.clusterSupportsFeature = clusterSupportsFeature; - this.settings = settings; + this.crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false); } @Override @@ -61,7 +62,7 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + if (crossProjectEnabled) { // accept but drop project_routing param until fully supported request.param("project_routing"); } @@ -73,7 +74,9 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client request, null, clusterSupportsFeature, - size -> searchRequest.source().size(size) + size -> searchRequest.source().size(size), + null, + Optional.of(crossProjectEnabled) ); // Creates the search template request diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index 861aeb35f938b..a61019392d443 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -44,6 +44,7 @@ import java.util.Arrays; import java.util.List; import java.util.Locale; +import java.util.Optional; import java.util.Set; import java.util.function.IntConsumer; import java.util.function.Predicate; @@ -68,13 +69,11 @@ public class RestSearchAction extends BaseRestHandler { private final SearchUsageHolder searchUsageHolder; private final Predicate clusterSupportsFeature; - private final Settings settings; private final CrossProjectModeDecider crossProjectModeDecider; public RestSearchAction(SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature, Settings settings) { this.searchUsageHolder = searchUsageHolder; this.clusterSupportsFeature = clusterSupportsFeature; - this.settings = settings; this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @@ -134,7 +133,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC clusterSupportsFeature, setSize, searchUsageHolder, - crossProjectEnabled + Optional.of(crossProjectEnabled) ) ); @@ -146,7 +145,9 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC /** * Parses the rest request on top of the SearchRequest, preserving values that are not overridden by the rest request. - * + * The endpoint calling this method is treated as if it does not support Cross Project Search (CPS). In case it supports + * CPS, it should call in the other appropriate overload and pass in the CPS state explicitly either via Optional.of(true) + * or Optional.of(false). * @param searchRequest the search request that will hold what gets parsed * @param request the rest request to read from * @param requestContentParser body of the request to read. This method does not attempt to read the body from the {@code request} @@ -172,7 +173,15 @@ public static void parseSearchRequest( IntConsumer setSize, @Nullable SearchUsageHolder searchUsageHolder ) throws IOException { - parseSearchRequest(searchRequest, request, requestContentParser, clusterSupportsFeature, setSize, searchUsageHolder, false); + parseSearchRequest( + searchRequest, + request, + requestContentParser, + clusterSupportsFeature, + setSize, + searchUsageHolder, + Optional.empty() + ); } /** @@ -185,7 +194,10 @@ public static void parseSearchRequest( * @param clusterSupportsFeature used to check if certain features are available in this cluster * @param setSize how the size url parameter is handled. {@code udpate_by_query} and regular search differ here. * @param searchUsageHolder the holder of search usage stats - * @param crossProjectEnabled whether serverless.cross_project.enabled is set to true + * @param crossProjectEnabled Specifies the state of Cross Project Search (CPS) for the endpoint that's calling this method. + * Optional.of(true) - signifies that the endpoint supports CPS, + * Optional.of(false) - signifies that the endpoint supports CPS but CPS is disabled, and, + * Optional.empty() - signifies that the endpoint does not support CPS. */ public static void parseSearchRequest( SearchRequest searchRequest, @@ -194,7 +206,7 @@ public static void parseSearchRequest( Predicate clusterSupportsFeature, IntConsumer setSize, @Nullable SearchUsageHolder searchUsageHolder, - boolean crossProjectEnabled + Optional crossProjectEnabled ) throws IOException { if (searchRequest.source() == null) { searchRequest.source(new SearchSourceBuilder()); @@ -205,7 +217,7 @@ public static void parseSearchRequest( * We only do it if in a Cross Project Environment, though, because outside it, such details are not * expected and valid. */ - SearchRequest searchRequestForParsing = crossProjectEnabled ? searchRequest : null; + SearchRequest searchRequestForParsing = crossProjectEnabled.orElse(false) ? searchRequest : null; if (requestContentParser != null) { if (searchUsageHolder == null) { searchRequest.source().parseXContent(searchRequestForParsing, requestContentParser, true, clusterSupportsFeature); @@ -250,7 +262,7 @@ public static void parseSearchRequest( searchRequest.routing(request.param("routing")); searchRequest.preference(request.param("preference")); IndicesOptions indicesOptions = IndicesOptions.fromRequest(request, searchRequest.indicesOptions()); - if (crossProjectEnabled && searchRequest.allowsCrossProject() && searchRequest.pointInTimeBuilder() == null) { + if (crossProjectEnabled.orElse(false) && searchRequest.allowsCrossProject() && searchRequest.pointInTimeBuilder() == null) { indicesOptions = IndicesOptions.builder(indicesOptions) .crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)) .build(); @@ -262,9 +274,30 @@ public static void parseSearchRequest( if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); } else { - searchRequest.setCcsMinimizeRoundtrips( - request.paramAsBoolean("ccs_minimize_roundtrips", searchRequest.isCcsMinimizeRoundtrips()) - ); + if (crossProjectEnabled.orElse(false)) { + /* + * MRT should not be settable by the user when in Cross Project Search environment. + * Only _async_search uses MRT=false. However, in RestSubmitAsyncSearchAction, we + * already, explicitly, and directly call in `SearchRequest#setCcsMinimizeRoundtrips()` + * to set it to true. Although other searches that utilise SearchRequest-s do not call + * this method, SearchRequest, by default, sets MRT to true when it is instantiated. + * This way, all searches pivot to MRT=true for CPS. + * + * Since users will anyway see a banner via Kibana that setting MRT in Serverless has no + * effect, we can safely drop it. + */ + if (request.hasParam("ccs_minimize_roundtrips")) { + request.param("ccs_minimize_roundtrips"); + } + } else { + /* + * Either we're not in a Cross Project Search environment or the endpoint isn't compatible with it. Parse what's in the + * request. + */ + searchRequest.setCcsMinimizeRoundtrips( + request.paramAsBoolean("ccs_minimize_roundtrips", searchRequest.isCcsMinimizeRoundtrips()) + ); + } } if (request.paramAsBoolean("force_synthetic_source", false)) { searchRequest.setForceSyntheticSource(true); diff --git a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java index c6e3a38007d88..711e68e3908c0 100644 --- a/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java +++ b/x-pack/plugin/async-search/src/main/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchAction.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.IntConsumer; import java.util.function.Predicate; @@ -70,6 +71,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); if (crossProjectEnabled) { + submit.getSearchRequest().setCcsMinimizeRoundtrips(true); submit.getSearchRequest().setProjectRouting(request.param("project_routing")); } @@ -86,7 +88,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli clusterSupportsFeature, setSize, searchUsageHolder, - crossProjectEnabled + Optional.of(crossProjectEnabled) ) ); From ba24a24c70c0384214d0d493836ad14f8664dd88 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 14 Nov 2025 16:40:43 +0000 Subject: [PATCH 02/13] Update docs/changelog/138105.yaml --- docs/changelog/138105.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/138105.yaml diff --git a/docs/changelog/138105.yaml b/docs/changelog/138105.yaml new file mode 100644 index 0000000000000..65ff1fb5f86a8 --- /dev/null +++ b/docs/changelog/138105.yaml @@ -0,0 +1,5 @@ +pr: 138105 +summary: MRT should default to `true` for CPS searches +area: CCS +type: enhancement +issues: [] From 687e383bfdaf22bdd56727f262a59f43b1351e84 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 14 Nov 2025 17:26:15 +0000 Subject: [PATCH 03/13] Add header warning --- .../elasticsearch/rest/action/search/RestSearchAction.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index a61019392d443..983c5a71ca6f5 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -16,6 +16,7 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Booleans; import org.elasticsearch.core.Nullable; @@ -287,6 +288,9 @@ public static void parseSearchRequest( * effect, we can safely drop it. */ if (request.hasParam("ccs_minimize_roundtrips")) { + String warning = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." + + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; + HeaderWarning.addWarning(warning); request.param("ccs_minimize_roundtrips"); } } else { From e01c115ce6aa583e156e78a7ccd333dd254ee58e Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 10:59:56 +0000 Subject: [PATCH 04/13] Lock MRT to `true` for `eql` and `msearch` --- .../RestMultiSearchTemplateAction.java | 11 +-- .../mustache/RestSearchTemplateAction.java | 6 +- .../action/search/RestMultiSearchAction.java | 70 +++++++++++++++---- .../search/MultiSearchRequestTests.java | 15 ++-- .../xpack/eql/plugin/RestEqlSearchAction.java | 27 +++++-- 5 files changed, 98 insertions(+), 31 deletions(-) diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java index 5a5b1df64907d..6b98a806d0662 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.rest.action.search.RestMultiSearchAction; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.XContentType; import java.io.IOException; @@ -35,11 +36,11 @@ public class RestMultiSearchTemplateAction extends BaseRestHandler { private static final Set RESPONSE_PARAMS = Set.of(RestSearchAction.TYPED_KEYS_PARAM, RestSearchAction.TOTAL_HITS_AS_INT_PARAM); private final boolean allowExplicitIndex; - private final Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; public RestMultiSearchTemplateAction(Settings settings) { this.allowExplicitIndex = MULTI_ALLOW_EXPLICIT_INDEX.get(settings); - this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -67,7 +68,8 @@ public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client * Parses a {@link RestRequest} body and returns a {@link MultiSearchTemplateRequest} */ public MultiSearchTemplateRequest parseRequest(RestRequest restRequest, boolean allowExplicitIndex) throws IOException { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); + if (crossProjectEnabled) { // accept but drop project_routing param until fully supported restRequest.param("project_routing"); } @@ -90,7 +92,8 @@ public MultiSearchTemplateRequest parseRequest(RestRequest restRequest, boolean throw new IllegalArgumentException("Malformed search template"); } RestSearchAction.validateSearchRequest(restRequest, searchRequest); - } + }, + crossProjectEnabled ); return multiRequest; } diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java index 287f5f1079d14..7ac6d758ae04c 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestSearchTemplateAction.java @@ -19,6 +19,7 @@ import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestToXContentListener; import org.elasticsearch.rest.action.search.RestSearchAction; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.XContentParser; import java.io.IOException; @@ -38,11 +39,11 @@ public class RestSearchTemplateAction extends BaseRestHandler { private static final Set RESPONSE_PARAMS = Set.of(TYPED_KEYS_PARAM, RestSearchAction.TOTAL_HITS_AS_INT_PARAM); private final Predicate clusterSupportsFeature; - private final boolean crossProjectEnabled; + private final CrossProjectModeDecider crossProjectModeDecider; public RestSearchTemplateAction(Predicate clusterSupportsFeature, Settings settings) { this.clusterSupportsFeature = clusterSupportsFeature; - this.crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false); + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -62,6 +63,7 @@ public String getName() { @Override public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); if (crossProjectEnabled) { // accept but drop project_routing param until fully supported request.param("project_routing"); diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index 9edaff7563db5..b029cdf138f67 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -18,6 +18,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.TriFunction; import org.elasticsearch.common.bytes.ReleasableBytesReference; +import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Tuple; import org.elasticsearch.features.NodeFeature; @@ -28,6 +29,7 @@ import org.elasticsearch.rest.action.RestCancellableNodeClient; import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.usage.SearchUsageHolder; import org.elasticsearch.xcontent.XContent; import org.elasticsearch.xcontent.XContentParser; @@ -48,13 +50,15 @@ public class RestMultiSearchAction extends BaseRestHandler { private final boolean allowExplicitIndex; private final SearchUsageHolder searchUsageHolder; private final Predicate clusterSupportsFeature; - private final Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; + private static final String MRT_ENABLED_IN_CPS_WARN = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." + + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; public RestMultiSearchAction(Settings settings, SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature) { - this.settings = settings; this.allowExplicitIndex = MULTI_ALLOW_EXPLICIT_INDEX.get(settings); this.searchUsageHolder = searchUsageHolder; this.clusterSupportsFeature = clusterSupportsFeature; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -77,11 +81,18 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC if (client.threadPool() != null && client.threadPool().getThreadContext() != null) { client.threadPool().getThreadContext().setErrorTraceTransportHeader(request); } - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); + if (crossProjectEnabled) { // accept but drop project_routing param until fully supported request.param("project_routing"); } - final MultiSearchRequest multiSearchRequest = parseRequest(request, allowExplicitIndex, searchUsageHolder, clusterSupportsFeature); + final MultiSearchRequest multiSearchRequest = parseRequest( + request, + allowExplicitIndex, + searchUsageHolder, + clusterSupportsFeature, + crossProjectEnabled + ); return channel -> { final RestCancellableNodeClient cancellableClient = new RestCancellableNodeClient(client, request.getHttpChannel()); cancellableClient.execute( @@ -99,9 +110,17 @@ public static MultiSearchRequest parseRequest( RestRequest restRequest, boolean allowExplicitIndex, SearchUsageHolder searchUsageHolder, - Predicate clusterSupportsFeature + Predicate clusterSupportsFeature, + boolean crossProjectEnabled ) throws IOException { - return parseRequest(restRequest, allowExplicitIndex, searchUsageHolder, clusterSupportsFeature, (k, v, r) -> false); + return parseRequest( + restRequest, + allowExplicitIndex, + searchUsageHolder, + clusterSupportsFeature, + (k, v, r) -> false, + crossProjectEnabled + ); } /** @@ -113,7 +132,8 @@ public static MultiSearchRequest parseRequest( boolean allowExplicitIndex, SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature, - TriFunction extraParamParser + TriFunction extraParamParser, + boolean crossProjectEnabled ) throws IOException { MultiSearchRequest multiRequest = new MultiSearchRequest(); IndicesOptions indicesOptions = IndicesOptions.fromRequest(restRequest, multiRequest.indicesOptions()); @@ -142,12 +162,10 @@ public static MultiSearchRequest parseRequest( if (searchRequest.pointInTimeBuilder() != null) { RestSearchAction.preparePointInTime(searchRequest, restRequest); } else { - searchRequest.setCcsMinimizeRoundtrips( - restRequest.paramAsBoolean("ccs_minimize_roundtrips", searchRequest.isCcsMinimizeRoundtrips()) - ); + searchRequest.setCcsMinimizeRoundtrips(maybeConsumeCcsMrtParam(restRequest, crossProjectEnabled)); } multiRequest.add(searchRequest); - }, extraParamParser); + }, extraParamParser, crossProjectEnabled); List requests = multiRequest.requests(); for (SearchRequest request : requests) { // preserve if it's set on the request @@ -168,9 +186,10 @@ public static void parseMultiLineRequest( RestRequest request, IndicesOptions indicesOptions, boolean allowExplicitIndex, - CheckedBiConsumer consumer + CheckedBiConsumer consumer, + boolean crossProjectEnabled ) throws IOException { - parseMultiLineRequest(request, indicesOptions, allowExplicitIndex, consumer, (k, v, r) -> false); + parseMultiLineRequest(request, indicesOptions, allowExplicitIndex, consumer, (k, v, r) -> false, crossProjectEnabled); } /** @@ -182,12 +201,13 @@ public static void parseMultiLineRequest( IndicesOptions indicesOptions, boolean allowExplicitIndex, CheckedBiConsumer consumer, - TriFunction extraParamParser + TriFunction extraParamParser, + boolean crossProjectEnabled ) throws IOException { String[] indices = Strings.splitStringByCommaToArray(request.param("index")); String searchType = request.param("search_type"); - boolean ccsMinimizeRoundtrips = request.paramAsBoolean("ccs_minimize_roundtrips", true); + boolean ccsMinimizeRoundtrips = maybeConsumeCcsMrtParam(request, crossProjectEnabled); String routing = request.param("routing"); final Tuple sourceTuple = request.contentOrSourceParam(); @@ -208,6 +228,26 @@ public static void parseMultiLineRequest( ); } + private static boolean maybeConsumeCcsMrtParam(RestRequest request, boolean crossProjectEnabled) { + if (crossProjectEnabled) { + // Warn user, consume param, and return true. + if (request.hasParam("ccs_minimize_roundtrips")) { + HeaderWarning.addWarning(RestMultiSearchAction.MRT_ENABLED_IN_CPS_WARN); + request.param("ccs_minimize_roundtrips"); + return true; + } + + /* + * User has no preference, use the default value that's appropriate for CPS. + * CPS searches should minimise round trips. + */ + return true; + } else { + // Not in CPS environment, pick whatever user chose. + return request.paramAsBoolean("ccs_minimize_roundtrips", true); + } + } + @Override public boolean mediaTypesValid(RestRequest request) { return super.mediaTypesValid(request) && XContentType.supportsDelimitedBulkRequests(request.getXContentType()); diff --git a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java index 3ed63ff5e90d1..59de77c367ea3 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java @@ -98,7 +98,7 @@ public void testFailWithUnknownKey() { ).build(); IllegalArgumentException ex = expectThrows( IllegalArgumentException.class, - () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false) + () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false, false) ); assertEquals("key [unknown_key] is not supported in the metadata section", ex.getMessage()); } @@ -116,7 +116,8 @@ public void testSimpleAddWithCarriageReturn() throws Exception { restRequest, true, new UsageService().getSearchUsageHolder(), - nf -> false + nf -> false, + false ); assertThat(request.requests().size(), equalTo(1)); assertThat(request.requests().get(0).indices()[0], equalTo("test")); @@ -139,7 +140,8 @@ public void testDefaultIndicesOptions() throws IOException { restRequest, true, new UsageService().getSearchUsageHolder(), - nf -> false + nf -> false, + false ); assertThat(request.requests().size(), equalTo(1)); assertThat(request.requests().get(0).indices()[0], equalTo("test")); @@ -249,7 +251,7 @@ public void testMsearchTerminatedByNewline() throws Exception { ).build(); IllegalArgumentException expectThrows = expectThrows( IllegalArgumentException.class, - () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false) + () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false, false) ); assertEquals("The msearch request must be terminated by a newline [\n]", expectThrows.getMessage()); @@ -262,7 +264,8 @@ public void testMsearchTerminatedByNewline() throws Exception { restRequestWithNewLine, true, new UsageService().getSearchUsageHolder(), - nf -> false + nf -> false, + false ); assertEquals(3, msearchRequest.requests().size()); } @@ -283,7 +286,7 @@ private MultiSearchRequest parseMultiSearchRequest(RestRequest restRequest) thro new SearchSourceBuilder().parseXContent(parser, false, new UsageService().getSearchUsageHolder(), nf -> false) ); request.add(searchRequest); - }); + }, false); return request; } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java index c29d62593e884..fc3ca0674edce 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java @@ -10,6 +10,7 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.logging.LogManager; @@ -21,6 +22,7 @@ import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; import org.elasticsearch.xcontent.XContentType; @@ -40,10 +42,10 @@ public class RestEqlSearchAction extends BaseRestHandler { private static final Logger LOGGER = LogManager.getLogger(RestEqlSearchAction.class); private static final String SEARCH_PATH = "/{index}/_eql/search"; - private final Settings settings; + private final CrossProjectModeDecider crossProjectModeDecider; public RestEqlSearchAction(Settings settings) { - this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -53,7 +55,8 @@ public List routes() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); + if (crossProjectEnabled) { // accept but drop project_routing param until fully supported request.param("project_routing"); } @@ -74,7 +77,23 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli eqlRequest.keepAlive(request.paramAsTime("keep_alive", eqlRequest.keepAlive())); } eqlRequest.keepOnCompletion(request.paramAsBoolean("keep_on_completion", eqlRequest.keepOnCompletion())); - eqlRequest.ccsMinimizeRoundtrips(request.paramAsBoolean("ccs_minimize_roundtrips", eqlRequest.ccsMinimizeRoundtrips())); + if (crossProjectEnabled) { + if (request.hasParam("ccs_minimize_roundtrips")) { + /* + * MRT should not be settable by the user in Cross Project Search environment. + * Irrespective of the value, issue a warning, and set MRT to true for the request. + * + * By default, MRT is true and is picked up from RequestDefaults.CCS_MINIMIZE_ROUNDTRIPS. + */ + String warning = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." + + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; + HeaderWarning.addWarning(warning); + request.param("ccs_minimize_roundtrips"); + eqlRequest.ccsMinimizeRoundtrips(true); + } + } else { + eqlRequest.ccsMinimizeRoundtrips(request.paramAsBoolean("ccs_minimize_roundtrips", eqlRequest.ccsMinimizeRoundtrips())); + } eqlRequest.allowPartialSearchResults( request.paramAsBoolean("allow_partial_search_results", eqlRequest.allowPartialSearchResults()) ); From 06b880b9d2f1b54927a713c209df5230520127d4 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 11:24:07 +0000 Subject: [PATCH 05/13] CI --- .../xpack/fleet/rest/RestFleetMultiSearchAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java index 1550a9a942b1f..be6935b58a38e 100644 --- a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java +++ b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java @@ -95,7 +95,9 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } else { return false; } - } + }, + // Fleet searches neither support CCS nor CPS. + false ); for (SearchRequest searchRequest : multiSearchRequest.requests()) { From b3ff223e76c9c3efaac39f5b1cb4071d0b39f726 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 13:31:55 +0000 Subject: [PATCH 06/13] Move MRT parsing to a util method --- .../RestMultiSearchTemplateAction.java | 3 +- .../action/search/MultiSearchRequest.java | 13 ++++-- .../action/search/RestMultiSearchAction.java | 39 +++++----------- .../rest/action/search/RestSearchAction.java | 29 +----------- .../action/search/SearchParamsParser.java | 44 +++++++++++++++++++ .../search/MultiSearchRequestTests.java | 28 +++++++++--- .../xpack/eql/plugin/RestEqlSearchAction.java | 21 ++------- .../rest/RestFleetMultiSearchAction.java | 3 +- 8 files changed, 92 insertions(+), 88 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java diff --git a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java index 6b98a806d0662..6ce25436af54e 100644 --- a/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java +++ b/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -93,7 +94,7 @@ public MultiSearchTemplateRequest parseRequest(RestRequest restRequest, boolean } RestSearchAction.validateSearchRequest(restRequest, searchRequest); }, - crossProjectEnabled + Optional.of(crossProjectEnabled) ); return multiRequest; } diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index aa728e888b87e..5785f29f30d50 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -36,6 +36,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -167,7 +168,8 @@ public static void readMultiLineFormat( String routing, String searchType, Boolean ccsMinimizeRoundtrips, - boolean allowExplicitIndex + boolean allowExplicitIndex, + Optional crossProjectEnabled ) throws IOException { readMultiLineFormat( xContent, @@ -180,7 +182,8 @@ public static void readMultiLineFormat( searchType, ccsMinimizeRoundtrips, allowExplicitIndex, - (s, o, r) -> false + (s, o, r) -> false, + crossProjectEnabled ); } @@ -196,7 +199,8 @@ public static void readMultiLineFormat( String searchType, Boolean ccsMinimizeRoundtrips, boolean allowExplicitIndex, - TriFunction extraParamParser + TriFunction extraParamParser, + Optional crossProjectEnabled ) throws IOException { int from = 0; byte marker = xContent.bulkSeparator(); @@ -219,6 +223,7 @@ public static void readMultiLineFormat( if (searchType != null) { searchRequest.searchType(searchType); } + // When crossProjectEnabled is true, ccsMinimizeRoundtrips is guaranteed to be true. if (ccsMinimizeRoundtrips != null) { searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); } @@ -250,7 +255,7 @@ public static void readMultiLineFormat( } else if ("search_type".equals(entry.getKey()) || "searchType".equals(entry.getKey())) { searchRequest.searchType(nodeStringValue(value, null)); } else if ("ccs_minimize_roundtrips".equals(entry.getKey()) || "ccsMinimizeRoundtrips".equals(entry.getKey())) { - searchRequest.setCcsMinimizeRoundtrips(nodeBooleanValue(value)); + searchRequest.setCcsMinimizeRoundtrips(crossProjectEnabled.orElse(false) || nodeBooleanValue(value)); } else if ("request_cache".equals(entry.getKey()) || "requestCache".equals(entry.getKey())) { searchRequest.requestCache(nodeBooleanValue(value, entry.getKey())); } else if ("preference".equals(entry.getKey())) { diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index b029cdf138f67..73fb70ba0dc28 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -18,7 +18,6 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.common.TriFunction; import org.elasticsearch.common.bytes.ReleasableBytesReference; -import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Tuple; import org.elasticsearch.features.NodeFeature; @@ -37,6 +36,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -91,7 +91,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC allowExplicitIndex, searchUsageHolder, clusterSupportsFeature, - crossProjectEnabled + Optional.of(crossProjectEnabled) ); return channel -> { final RestCancellableNodeClient cancellableClient = new RestCancellableNodeClient(client, request.getHttpChannel()); @@ -111,7 +111,7 @@ public static MultiSearchRequest parseRequest( boolean allowExplicitIndex, SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature, - boolean crossProjectEnabled + Optional crossProjectEnabled ) throws IOException { return parseRequest( restRequest, @@ -133,7 +133,7 @@ public static MultiSearchRequest parseRequest( SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature, TriFunction extraParamParser, - boolean crossProjectEnabled + Optional crossProjectEnabled ) throws IOException { MultiSearchRequest multiRequest = new MultiSearchRequest(); IndicesOptions indicesOptions = IndicesOptions.fromRequest(restRequest, multiRequest.indicesOptions()); @@ -162,7 +162,7 @@ public static MultiSearchRequest parseRequest( if (searchRequest.pointInTimeBuilder() != null) { RestSearchAction.preparePointInTime(searchRequest, restRequest); } else { - searchRequest.setCcsMinimizeRoundtrips(maybeConsumeCcsMrtParam(restRequest, crossProjectEnabled)); + searchRequest.setCcsMinimizeRoundtrips(SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, restRequest)); } multiRequest.add(searchRequest); }, extraParamParser, crossProjectEnabled); @@ -187,7 +187,7 @@ public static void parseMultiLineRequest( IndicesOptions indicesOptions, boolean allowExplicitIndex, CheckedBiConsumer consumer, - boolean crossProjectEnabled + Optional crossProjectEnabled ) throws IOException { parseMultiLineRequest(request, indicesOptions, allowExplicitIndex, consumer, (k, v, r) -> false, crossProjectEnabled); } @@ -202,12 +202,12 @@ public static void parseMultiLineRequest( boolean allowExplicitIndex, CheckedBiConsumer consumer, TriFunction extraParamParser, - boolean crossProjectEnabled + Optional crossProjectEnabled ) throws IOException { String[] indices = Strings.splitStringByCommaToArray(request.param("index")); String searchType = request.param("search_type"); - boolean ccsMinimizeRoundtrips = maybeConsumeCcsMrtParam(request, crossProjectEnabled); + boolean ccsMinimizeRoundtrips = SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, request); String routing = request.param("routing"); final Tuple sourceTuple = request.contentOrSourceParam(); @@ -224,30 +224,11 @@ public static void parseMultiLineRequest( searchType, ccsMinimizeRoundtrips, allowExplicitIndex, - extraParamParser + extraParamParser, + crossProjectEnabled ); } - private static boolean maybeConsumeCcsMrtParam(RestRequest request, boolean crossProjectEnabled) { - if (crossProjectEnabled) { - // Warn user, consume param, and return true. - if (request.hasParam("ccs_minimize_roundtrips")) { - HeaderWarning.addWarning(RestMultiSearchAction.MRT_ENABLED_IN_CPS_WARN); - request.param("ccs_minimize_roundtrips"); - return true; - } - - /* - * User has no preference, use the default value that's appropriate for CPS. - * CPS searches should minimise round trips. - */ - return true; - } else { - // Not in CPS environment, pick whatever user chose. - return request.paramAsBoolean("ccs_minimize_roundtrips", true); - } - } - @Override public boolean mediaTypesValid(RestRequest request) { return super.mediaTypesValid(request) && XContentType.supportsDelimitedBulkRequests(request.getXContentType()); diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index 983c5a71ca6f5..680ae770cd56f 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -16,7 +16,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Booleans; import org.elasticsearch.core.Nullable; @@ -275,33 +274,7 @@ public static void parseSearchRequest( if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); } else { - if (crossProjectEnabled.orElse(false)) { - /* - * MRT should not be settable by the user when in Cross Project Search environment. - * Only _async_search uses MRT=false. However, in RestSubmitAsyncSearchAction, we - * already, explicitly, and directly call in `SearchRequest#setCcsMinimizeRoundtrips()` - * to set it to true. Although other searches that utilise SearchRequest-s do not call - * this method, SearchRequest, by default, sets MRT to true when it is instantiated. - * This way, all searches pivot to MRT=true for CPS. - * - * Since users will anyway see a banner via Kibana that setting MRT in Serverless has no - * effect, we can safely drop it. - */ - if (request.hasParam("ccs_minimize_roundtrips")) { - String warning = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." - + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; - HeaderWarning.addWarning(warning); - request.param("ccs_minimize_roundtrips"); - } - } else { - /* - * Either we're not in a Cross Project Search environment or the endpoint isn't compatible with it. Parse what's in the - * request. - */ - searchRequest.setCcsMinimizeRoundtrips( - request.paramAsBoolean("ccs_minimize_roundtrips", searchRequest.isCcsMinimizeRoundtrips()) - ); - } + searchRequest.setCcsMinimizeRoundtrips(SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, request)); } if (request.paramAsBoolean("force_synthetic_source", false)) { searchRequest.setForceSyntheticSource(true); diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java new file mode 100644 index 0000000000000..b4a4c0409205b --- /dev/null +++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java @@ -0,0 +1,44 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.rest.action.search; + +import org.elasticsearch.common.logging.HeaderWarning; +import org.elasticsearch.rest.RestRequest; + +import java.util.Optional; + +public class SearchParamsParser { + private static final String MRT_SET_IN_CPS_WARN = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." + + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; + + /** + * For CPS, we do not necessarily want to use the MRT value that the user has provided. + * Instead, we'd want to ignore it and default searches to `true` whilst issuing a warning + * via the headers. + * @param crossProjectEnabled If running in Cross Project Search environment. + * @param request Rest request that we're parsing. + * @return A boolean that determines if round trips should be minimised for this search request. + */ + public static boolean parseCcsMinimizeRoundtrips(Optional crossProjectEnabled, RestRequest request) { + if (crossProjectEnabled.orElse(false)) { + if (request.hasParam("ccs_minimize_roundtrips")) { + request.param("ccs_minimize_roundtrips"); + HeaderWarning.addWarning(MRT_SET_IN_CPS_WARN); + return true; + } + + // MRT was not provided; default to true. + return true; + } else { + // This is not a CPS request; use the value the user has provided. + return request.paramAsBoolean("ccs_minimize_roundtrips", true); + } + } +} diff --git a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java index 59de77c367ea3..e3998a8cddbcd 100644 --- a/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/MultiSearchRequestTests.java @@ -41,6 +41,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import static java.util.Collections.singletonList; import static org.elasticsearch.search.RandomSearchRequestGenerator.randomSearchRequest; @@ -98,7 +99,13 @@ public void testFailWithUnknownKey() { ).build(); IllegalArgumentException ex = expectThrows( IllegalArgumentException.class, - () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false, false) + () -> RestMultiSearchAction.parseRequest( + restRequest, + true, + new UsageService().getSearchUsageHolder(), + nf -> false, + Optional.empty() + ) ); assertEquals("key [unknown_key] is not supported in the metadata section", ex.getMessage()); } @@ -117,7 +124,7 @@ public void testSimpleAddWithCarriageReturn() throws Exception { true, new UsageService().getSearchUsageHolder(), nf -> false, - false + Optional.empty() ); assertThat(request.requests().size(), equalTo(1)); assertThat(request.requests().get(0).indices()[0], equalTo("test")); @@ -141,7 +148,7 @@ public void testDefaultIndicesOptions() throws IOException { true, new UsageService().getSearchUsageHolder(), nf -> false, - false + Optional.empty() ); assertThat(request.requests().size(), equalTo(1)); assertThat(request.requests().get(0).indices()[0], equalTo("test")); @@ -251,7 +258,13 @@ public void testMsearchTerminatedByNewline() throws Exception { ).build(); IllegalArgumentException expectThrows = expectThrows( IllegalArgumentException.class, - () -> RestMultiSearchAction.parseRequest(restRequest, true, new UsageService().getSearchUsageHolder(), nf -> false, false) + () -> RestMultiSearchAction.parseRequest( + restRequest, + true, + new UsageService().getSearchUsageHolder(), + nf -> false, + Optional.empty() + ) ); assertEquals("The msearch request must be terminated by a newline [\n]", expectThrows.getMessage()); @@ -265,7 +278,7 @@ public void testMsearchTerminatedByNewline() throws Exception { true, new UsageService().getSearchUsageHolder(), nf -> false, - false + Optional.empty() ); assertEquals(3, msearchRequest.requests().size()); } @@ -286,7 +299,7 @@ private MultiSearchRequest parseMultiSearchRequest(RestRequest restRequest) thro new SearchSourceBuilder().parseXContent(parser, false, new UsageService().getSearchUsageHolder(), nf -> false) ); request.add(searchRequest); - }, false); + }, Optional.empty()); return request; } @@ -343,7 +356,8 @@ public void testMultiLineSerialization() throws IOException { null, null, null, - true + true, + Optional.empty() ); assertEquals(originalRequest, parsedRequest); } diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java index fc3ca0674edce..ad27e62a38edf 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.client.internal.node.NodeClient; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.logging.LogManager; @@ -22,6 +21,7 @@ import org.elasticsearch.rest.Scope; import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestCancellableNodeClient; +import org.elasticsearch.rest.action.search.SearchParamsParser; import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentParser; @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.elasticsearch.rest.RestRequest.Method.GET; @@ -77,23 +78,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli eqlRequest.keepAlive(request.paramAsTime("keep_alive", eqlRequest.keepAlive())); } eqlRequest.keepOnCompletion(request.paramAsBoolean("keep_on_completion", eqlRequest.keepOnCompletion())); - if (crossProjectEnabled) { - if (request.hasParam("ccs_minimize_roundtrips")) { - /* - * MRT should not be settable by the user in Cross Project Search environment. - * Irrespective of the value, issue a warning, and set MRT to true for the request. - * - * By default, MRT is true and is picked up from RequestDefaults.CCS_MINIMIZE_ROUNDTRIPS. - */ - String warning = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." - + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; - HeaderWarning.addWarning(warning); - request.param("ccs_minimize_roundtrips"); - eqlRequest.ccsMinimizeRoundtrips(true); - } - } else { - eqlRequest.ccsMinimizeRoundtrips(request.paramAsBoolean("ccs_minimize_roundtrips", eqlRequest.ccsMinimizeRoundtrips())); - } + eqlRequest.ccsMinimizeRoundtrips(SearchParamsParser.parseCcsMinimizeRoundtrips(Optional.of(crossProjectEnabled), request)); eqlRequest.allowPartialSearchResults( request.paramAsBoolean("allow_partial_search_results", eqlRequest.allowPartialSearchResults()) ); diff --git a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java index be6935b58a38e..a88100cfd8932 100644 --- a/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java +++ b/x-pack/plugin/fleet/src/main/java/org/elasticsearch/xpack/fleet/rest/RestFleetMultiSearchAction.java @@ -30,6 +30,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Predicate; @@ -97,7 +98,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } }, // Fleet searches neither support CCS nor CPS. - false + Optional.empty() ); for (SearchRequest searchRequest : multiSearchRequest.requests()) { From 77807c4e1b261ebcee0baca064d247f6103f2572 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 13:34:15 +0000 Subject: [PATCH 07/13] Drop unused warn --- .../elasticsearch/rest/action/search/RestMultiSearchAction.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index 73fb70ba0dc28..8cdbe331b7e6d 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -51,8 +51,6 @@ public class RestMultiSearchAction extends BaseRestHandler { private final SearchUsageHolder searchUsageHolder; private final Predicate clusterSupportsFeature; private final CrossProjectModeDecider crossProjectModeDecider; - private static final String MRT_ENABLED_IN_CPS_WARN = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." - + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; public RestMultiSearchAction(Settings settings, SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature) { this.allowExplicitIndex = MULTI_ALLOW_EXPLICIT_INDEX.get(settings); From e540ac07d125e752da0c60a9c7d344a6b3174f5f Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 14:40:55 +0000 Subject: [PATCH 08/13] Add overload that takes in a default value --- .../elasticsearch/rest/action/search/RestSearchAction.java | 4 +++- .../rest/action/search/SearchParamsParser.java | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java index 680ae770cd56f..287e42f31dcf2 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java @@ -274,7 +274,9 @@ public static void parseSearchRequest( if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); } else { - searchRequest.setCcsMinimizeRoundtrips(SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, request)); + searchRequest.setCcsMinimizeRoundtrips( + SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, request, searchRequest.isCcsMinimizeRoundtrips()) + ); } if (request.paramAsBoolean("force_synthetic_source", false)) { searchRequest.setForceSyntheticSource(true); diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java index b4a4c0409205b..249c10994a5e4 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java @@ -27,6 +27,10 @@ public class SearchParamsParser { * @return A boolean that determines if round trips should be minimised for this search request. */ public static boolean parseCcsMinimizeRoundtrips(Optional crossProjectEnabled, RestRequest request) { + return parseCcsMinimizeRoundtrips(crossProjectEnabled, request, true); + } + + public static boolean parseCcsMinimizeRoundtrips(Optional crossProjectEnabled, RestRequest request, boolean defaultValue) { if (crossProjectEnabled.orElse(false)) { if (request.hasParam("ccs_minimize_roundtrips")) { request.param("ccs_minimize_roundtrips"); @@ -38,7 +42,7 @@ public static boolean parseCcsMinimizeRoundtrips(Optional crossProjectE return true; } else { // This is not a CPS request; use the value the user has provided. - return request.paramAsBoolean("ccs_minimize_roundtrips", true); + return request.paramAsBoolean("ccs_minimize_roundtrips", defaultValue); } } } From c7a1ad4f5d75d77e5a8c83fbbeb40d97843318e4 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 17 Nov 2025 14:43:15 +0000 Subject: [PATCH 09/13] Switch overload --- .../rest/action/search/RestMultiSearchAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java index 8cdbe331b7e6d..992a044868a3b 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/RestMultiSearchAction.java @@ -160,7 +160,9 @@ public static MultiSearchRequest parseRequest( if (searchRequest.pointInTimeBuilder() != null) { RestSearchAction.preparePointInTime(searchRequest, restRequest); } else { - searchRequest.setCcsMinimizeRoundtrips(SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, restRequest)); + searchRequest.setCcsMinimizeRoundtrips( + SearchParamsParser.parseCcsMinimizeRoundtrips(crossProjectEnabled, restRequest, searchRequest.isCcsMinimizeRoundtrips()) + ); } multiRequest.add(searchRequest); }, extraParamParser, crossProjectEnabled); From 23a1c6699f68003e22c16e088d8925b1ab2cb0bf Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 19 Nov 2025 13:02:05 +0000 Subject: [PATCH 10/13] Warn only once per multisearch request --- .../elasticsearch/action/search/MultiSearchRequest.java | 7 +++++++ .../rest/action/search/SearchParamsParser.java | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index 5785f29f30d50..4f84d7d8d082a 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -18,7 +18,9 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.rest.action.search.SearchParamsParser; import org.elasticsearch.tasks.CancellableTask; import org.elasticsearch.tasks.Task; import org.elasticsearch.tasks.TaskId; @@ -204,6 +206,7 @@ public static void readMultiLineFormat( ) throws IOException { int from = 0; byte marker = xContent.bulkSeparator(); + boolean warnedMrtForCps = false; while (true) { int nextMarker = findNextMarker(marker, from, data); if (nextMarker == -1) { @@ -256,6 +259,10 @@ public static void readMultiLineFormat( searchRequest.searchType(nodeStringValue(value, null)); } else if ("ccs_minimize_roundtrips".equals(entry.getKey()) || "ccsMinimizeRoundtrips".equals(entry.getKey())) { searchRequest.setCcsMinimizeRoundtrips(crossProjectEnabled.orElse(false) || nodeBooleanValue(value)); + if (warnedMrtForCps == false) { + HeaderWarning.addWarning(SearchParamsParser.MRT_SET_IN_CPS_WARN); + warnedMrtForCps = true; + } } else if ("request_cache".equals(entry.getKey()) || "requestCache".equals(entry.getKey())) { searchRequest.requestCache(nodeBooleanValue(value, entry.getKey())); } else if ("preference".equals(entry.getKey())) { diff --git a/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java index 249c10994a5e4..33f787dd09e61 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java +++ b/server/src/main/java/org/elasticsearch/rest/action/search/SearchParamsParser.java @@ -15,8 +15,9 @@ import java.util.Optional; public class SearchParamsParser { - private static final String MRT_SET_IN_CPS_WARN = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." - + " Setting it explicitly has no effect irrespective of the value specified and is ignored."; + public static final String MRT_SET_IN_CPS_WARN = "ccs_minimize_roundtrips always defaults to true in Cross Project Search context." + + " Setting it explicitly has no effect irrespective of the value specified and is ignored." + + " It will soon be deprecated and made unavailable for Cross project Search."; /** * For CPS, we do not necessarily want to use the MRT value that the user has provided. From 9dc4a2546da8bb9e6e1e7aa9a5d4f4069ff03bc9 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 19 Nov 2025 13:58:07 +0000 Subject: [PATCH 11/13] CI --- .../org/elasticsearch/action/search/MultiSearchRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index 4f84d7d8d082a..29e44cf8197cb 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -259,7 +259,7 @@ public static void readMultiLineFormat( searchRequest.searchType(nodeStringValue(value, null)); } else if ("ccs_minimize_roundtrips".equals(entry.getKey()) || "ccsMinimizeRoundtrips".equals(entry.getKey())) { searchRequest.setCcsMinimizeRoundtrips(crossProjectEnabled.orElse(false) || nodeBooleanValue(value)); - if (warnedMrtForCps == false) { + if (crossProjectEnabled.orElse(false) && warnedMrtForCps == false) { HeaderWarning.addWarning(SearchParamsParser.MRT_SET_IN_CPS_WARN); warnedMrtForCps = true; } From ea27c072cdedd552ea9a4cd85a1d38d2173eb5b8 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 19 Nov 2025 20:00:13 +0000 Subject: [PATCH 12/13] Clarify code comments --- .../action/search/MultiSearchRequest.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java index 29e44cf8197cb..447c3ccca42b0 100644 --- a/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/MultiSearchRequest.java @@ -171,6 +171,10 @@ public static void readMultiLineFormat( String searchType, Boolean ccsMinimizeRoundtrips, boolean allowExplicitIndex, + /* + * Refer to RestSearchAction#parseSearchRequest()'s JavaDoc to understand why this is an Optional + * and what its values mean with respect to an endpoint's Cross Project Search status/support. + */ Optional crossProjectEnabled ) throws IOException { readMultiLineFormat( @@ -202,6 +206,10 @@ public static void readMultiLineFormat( Boolean ccsMinimizeRoundtrips, boolean allowExplicitIndex, TriFunction extraParamParser, + /* + * Refer to RestSearchAction#parseSearchRequest()'s JavaDoc to understand why this is an Optional + * and what its values mean with respect to an endpoint's Cross Project Search status/support. + */ Optional crossProjectEnabled ) throws IOException { int from = 0; @@ -226,7 +234,11 @@ public static void readMultiLineFormat( if (searchType != null) { searchRequest.searchType(searchType); } - // When crossProjectEnabled is true, ccsMinimizeRoundtrips is guaranteed to be true. + /* + * This `ccsMinimizeRoundtrips` refers to the value specified as the query parameter and is extracted in + * `RestMultiSearchAction#parseMultiLineRequest()`. If in a Cross Project Search environment, it is + * guaranteed to be `true`. Otherwise, its value is whatever that the user is provided. + */ if (ccsMinimizeRoundtrips != null) { searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); } From 28dc9fd34a915d06785a202a3cc6602ac8c6646e Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 19 Nov 2025 20:02:09 +0000 Subject: [PATCH 13/13] CI --- .../elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java index bd2c360f7793a..b6e1040339705 100644 --- a/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java +++ b/x-pack/plugin/eql/src/main/java/org/elasticsearch/xpack/eql/plugin/RestEqlSearchAction.java @@ -57,11 +57,6 @@ public List routes() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); - if (crossProjectEnabled) { - // accept but drop project_routing param until fully supported - request.param("project_routing"); - } - EqlSearchRequest eqlRequest; String indices; try (XContentParser parser = request.contentOrSourceParamParser()) {