From 3e94dc8999ae3b691edf3d6351e5c564c968a537 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 11:31:48 +0000 Subject: [PATCH 01/15] Add support for `project_routing` for `_search` and `_async_search` --- .../action/search/SearchRequest.java | 10 ++++ .../action/search/TransportSearchAction.java | 8 +-- .../rest/action/search/RestSearchAction.java | 52 +++++++++++++++---- .../search/builder/SearchSourceBuilder.java | 20 ++++++- .../action/search/RestSearchActionTests.java | 3 +- .../search/RestSubmitAsyncSearchAction.java | 5 +- 6 files changed, 81 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index 748dc67caae3e..6df12717ab1de 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -116,6 +116,7 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest * enabling synthetic source natively in the index. */ private boolean forceSyntheticSource = false; + private String projectRouting; public SearchRequest() { this.localClusterAlias = null; @@ -168,6 +169,15 @@ public boolean allowsCrossProject() { return true; } + @Override + public String getProjectRouting() { + return projectRouting; + } + + public void setProjectRouting(@Nullable String projectRouting) { + this.projectRouting = projectRouting; + } + /** * Creates a new sub-search request starting from the original search request that is provided. * For internal use only, allows to fork a search request into multiple search requests that will be executed independently. diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index 963ba974394ad..2f65bc0ee9b68 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -620,7 +620,8 @@ public void onFailure(Exception e) { }), forceConnectTimeoutSecs, resolvesCrossProject, - rewritten.getResolvedIndexExpressions() + rewritten.getResolvedIndexExpressions(), + rewritten.getProjectRouting() ); } } @@ -1065,7 +1066,8 @@ static void collectSearchShards( ActionListener> listener, TimeValue forceConnectTimeoutSecs, boolean resolvesCrossProject, - ResolvedIndexExpressions originResolvedIdxExpressions + ResolvedIndexExpressions originResolvedIdxExpressions, + String projectRouting ) { RemoteClusterService remoteClusterService = transportService.getRemoteClusterService(); final CountDown responsesCountDown = new CountDown(remoteIndicesByCluster.size()); @@ -1104,7 +1106,7 @@ Map createFinalResponse() { // We do not use the relaxed index options here when validating indices' existence. ElasticsearchException validationEx = CrossProjectIndexResolutionValidator.validate( originalIdxOpts, - null, + projectRouting, originResolvedIdxExpressions, resolvedIndexExpressions ); 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 b369dbc450624..0a20e410aebbb 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 @@ -30,6 +30,7 @@ import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; import org.elasticsearch.search.SearchService; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.search.fetch.StoredFieldsContext; import org.elasticsearch.search.fetch.subphase.FetchSourceContext; import org.elasticsearch.search.internal.SearchContext; @@ -68,15 +69,13 @@ public class RestSearchAction extends BaseRestHandler { private final SearchUsageHolder searchUsageHolder; private final Predicate clusterSupportsFeature; private final Settings settings; - - public RestSearchAction(SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature) { - this(searchUsageHolder, clusterSupportsFeature, null); - } + 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); } @Override @@ -109,10 +108,9 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC // this might be set by old clients request.param("min_compatible_shard_node"); - final boolean crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false); - if (crossProjectEnabled) { - // accept but drop project_routing param until fully supported - request.param("project_routing"); + final boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); + if (crossProjectEnabled && searchRequest.allowsCrossProject()) { + searchRequest.setProjectRouting(request.param("project_routing")); } /* @@ -252,7 +250,7 @@ public static void parseSearchRequest( } searchRequest.indicesOptions(indicesOptions); - validateSearchRequest(request, searchRequest); + validateSearchRequest(request, searchRequest, crossProjectEnabled && searchRequest.allowsCrossProject()); if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); @@ -412,10 +410,46 @@ static void preparePointInTime(SearchRequest request, RestRequest restRequest) { * might modify the search request to align certain parameters. */ public static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest) { + validateSearchRequest(restRequest, searchRequest, false); + } + + private static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest, boolean resolvesCrossProject) { checkRestTotalHits(restRequest, searchRequest); checkSearchType(restRequest, searchRequest); // ensures that the rest param is consumed restRequest.paramAsBoolean(INCLUDE_NAMED_QUERIES_SCORE_PARAM, false); + checkProjectRouting(searchRequest, resolvesCrossProject); + } + + private static void checkProjectRouting(SearchRequest searchRequest, boolean resolvesCrossProject) { + /* + * There are 2 ways of specifying project_routing: + * - as a query parameter: /_search?project_routing=..., and, + * - within the request's body. + * + * Because we do not have access to `IndicesRequest/SearchRequest` from `SearchSourceBuilder`, and, project_routing + * can be potentially specified in 2 different places, we need to explicitly check this scenario. + */ + String projectRoutingInBody = searchRequest.source().projectRouting(); + // If it's null, either the query parameter is also null or it isn't. Either way, we're fine with it. + if (projectRoutingInBody != null) { + if (resolvesCrossProject == false) { + throw new IllegalArgumentException("project_routing is allowed only when CPS is enabled and the endpoint supports CPS"); + } + + // Query parameter was also set. This is not allowed, irrespective of the values. + if (searchRequest.getProjectRouting() != null) { + throw new IllegalArgumentException( + "project_routing is specified in both the places: as query parameter and in the request's body" + ); + } + + /* + * Bring forward the project_routing value so that TransportSearchAction can pick it up. Although TSA can pick it up + * from `SearchSourceBuilder`, let's use `IndicesRequest#getProjectRouting()` since that's the intended way. + */ + searchRequest.setProjectRouting(projectRoutingInBody); + } } /** diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index a4f56d5f4a6dc..612148e9fb97c 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -129,6 +129,7 @@ public final class SearchSourceBuilder implements Writeable, ToXContentObject, R public static final ParseField POINT_IN_TIME = new ParseField("pit"); public static final ParseField RUNTIME_MAPPINGS_FIELD = new ParseField("runtime_mappings"); public static final ParseField RETRIEVER = new ParseField("retriever"); + public static final ParseField PROJECT_ROUTING = new ParseField("project_routing"); private static final boolean RANK_SUPPORTED = Booleans.parseBoolean(System.getProperty("es.search.rank_supported"), true); @@ -212,6 +213,8 @@ public static HighlightBuilder highlight() { private boolean skipInnerHits = false; + private String projectRouting; + /** * Constructs a new search source builder. */ @@ -609,6 +612,19 @@ public TimeValue timeout() { return timeout; } + public String projectRouting() { + return projectRouting; + } + + public SearchSourceBuilder projectRouting(String projectRouting) { + if (this.projectRouting != null) { + throw new IllegalArgumentException("project_routing is already set"); + } + + this.projectRouting = projectRouting; + return this; + } + /** * An optional terminate_after to terminate the search after collecting * terminateAfter documents @@ -1382,7 +1398,9 @@ private SearchSourceBuilder parseXContent( if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token.isValue()) { - if (FROM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { + if (PROJECT_ROUTING.match(currentFieldName, parser.getDeprecationHandler())) { + projectRouting(parser.text()); + } else if (FROM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { from(parser.intValue()); } else if (SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { size(parser.intValue()); diff --git a/server/src/test/java/org/elasticsearch/rest/action/search/RestSearchActionTests.java b/server/src/test/java/org/elasticsearch/rest/action/search/RestSearchActionTests.java index ef620896e941d..379574e64009d 100644 --- a/server/src/test/java/org/elasticsearch/rest/action/search/RestSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/rest/action/search/RestSearchActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.search.SearchType; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.suggest.SuggestBuilder; @@ -33,7 +34,7 @@ public final class RestSearchActionTests extends RestActionTestCase { @Before public void setUpAction() { - action = new RestSearchAction(new UsageService().getSearchUsageHolder(), nf -> false); + action = new RestSearchAction(new UsageService().getSearchUsageHolder(), nf -> false, Settings.EMPTY); controller().registerHandler(action); verifyingClient.setExecuteVerifier((actionType, request) -> mock(SearchResponse.class)); verifyingClient.setExecuteLocallyVerifier((actionType, request) -> mock(SearchResponse.class)); 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 620a0b0fcc5a9..3c2e23353faa4 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 @@ -69,9 +69,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli SubmitAsyncSearchRequest submit = new SubmitAsyncSearchRequest(); boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); - if (crossProjectEnabled) { - // accept but drop project_routing param until fully supported - request.param("project_routing"); + if (crossProjectEnabled && submit.getSearchRequest().allowsCrossProject()) { + submit.getSearchRequest().setProjectRouting(request.param("project_routing")); } IntConsumer setSize = size -> submit.getSearchRequest().source().size(size); From 6ac9d8c11c5236791f50c1d81adda60644bd89aa Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 11:50:25 +0000 Subject: [PATCH 02/15] Fix CI --- .../action/search/TransportSearchActionTests.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java index 48e607d1b3fba..8891b2004575d 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -1104,6 +1104,7 @@ public void testCollectSearchShards() throws Exception { new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), null, false, + null, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1136,6 +1137,7 @@ public void testCollectSearchShards() throws Exception { new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch), null, false, + null, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1191,6 +1193,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch), null, false, + null, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1224,6 +1227,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), null, false, + null, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1273,6 +1277,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), null, false, + null, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); From 2d0f4c6d431d7960b41f8d29dfb172fcaf014c16 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 13:20:02 +0000 Subject: [PATCH 03/15] Fix null deref --- .../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 0a20e410aebbb..597026c5b66b3 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 @@ -430,6 +430,10 @@ private static void checkProjectRouting(SearchRequest searchRequest, boolean res * Because we do not have access to `IndicesRequest/SearchRequest` from `SearchSourceBuilder`, and, project_routing * can be potentially specified in 2 different places, we need to explicitly check this scenario. */ + if (searchRequest.source() == null) { + return; + } + String projectRoutingInBody = searchRequest.source().projectRouting(); // If it's null, either the query parameter is also null or it isn't. Either way, we're fine with it. if (projectRoutingInBody != null) { From c09e00a20ad75b94ae12d8e06e80d4bde14f55a3 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 14:29:58 +0000 Subject: [PATCH 04/15] Copy project routing during init --- .../java/org/elasticsearch/action/search/SearchRequest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index 6df12717ab1de..8cdf0fa67a1e5 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -209,6 +209,7 @@ static SearchRequest subSearchRequest( } final SearchRequest request = new SearchRequest(originalSearchRequest, indices, clusterAlias, absoluteStartMillis, finalReduce); request.setParentTask(parentTaskId); + request.setProjectRouting(originalSearchRequest.getProjectRouting()); return request; } @@ -238,6 +239,7 @@ private SearchRequest( this.waitForCheckpoints = searchRequest.waitForCheckpoints; this.waitForCheckpointsTimeout = searchRequest.waitForCheckpointsTimeout; this.forceSyntheticSource = searchRequest.forceSyntheticSource; + this.projectRouting = searchRequest.projectRouting; } /** From deb7b49eaaf98bef13d416deac95db672c700446 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 14:31:44 +0000 Subject: [PATCH 05/15] Copy project routing during init for async search too --- .../xpack/core/search/action/SubmitAsyncSearchRequest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java index 3ee87a2514c73..7c956974259f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java @@ -53,6 +53,7 @@ public SubmitAsyncSearchRequest(SearchSourceBuilder source, String... indices) { request.setPreFilterShardSize(1); request.setBatchedReduceSize(5); request.requestCache(true); + request.setProjectRouting(source.projectRouting()); } public SubmitAsyncSearchRequest(StreamInput in) throws IOException { From 692c5a25e77ce1f3a13b1f4058d6b9bb7a71bbbc Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 14:47:12 +0000 Subject: [PATCH 06/15] resolvesCps -> cpsEnabled --- .../rest/action/search/RestSearchAction.java | 12 ++++++------ .../xpack/search/RestSubmitAsyncSearchAction.java | 2 +- 2 files changed, 7 insertions(+), 7 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 597026c5b66b3..c5d4a873e23f0 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 @@ -243,14 +243,14 @@ 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()) { + if (crossProjectEnabled) { indicesOptions = IndicesOptions.builder(indicesOptions) .crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)) .build(); } searchRequest.indicesOptions(indicesOptions); - validateSearchRequest(request, searchRequest, crossProjectEnabled && searchRequest.allowsCrossProject()); + validateSearchRequest(request, searchRequest, crossProjectEnabled); if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); @@ -413,15 +413,15 @@ public static void validateSearchRequest(RestRequest restRequest, SearchRequest validateSearchRequest(restRequest, searchRequest, false); } - private static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest, boolean resolvesCrossProject) { + private static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest, boolean crossProjectEnabled) { checkRestTotalHits(restRequest, searchRequest); checkSearchType(restRequest, searchRequest); // ensures that the rest param is consumed restRequest.paramAsBoolean(INCLUDE_NAMED_QUERIES_SCORE_PARAM, false); - checkProjectRouting(searchRequest, resolvesCrossProject); + checkProjectRouting(searchRequest, crossProjectEnabled); } - private static void checkProjectRouting(SearchRequest searchRequest, boolean resolvesCrossProject) { + private static void checkProjectRouting(SearchRequest searchRequest, boolean crossProjectEnabled) { /* * There are 2 ways of specifying project_routing: * - as a query parameter: /_search?project_routing=..., and, @@ -437,7 +437,7 @@ private static void checkProjectRouting(SearchRequest searchRequest, boolean res String projectRoutingInBody = searchRequest.source().projectRouting(); // If it's null, either the query parameter is also null or it isn't. Either way, we're fine with it. if (projectRoutingInBody != null) { - if (resolvesCrossProject == false) { + if (crossProjectEnabled == false) { throw new IllegalArgumentException("project_routing is allowed only when CPS is enabled and the endpoint supports CPS"); } 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 3c2e23353faa4..c6e3a38007d88 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 @@ -69,7 +69,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli SubmitAsyncSearchRequest submit = new SubmitAsyncSearchRequest(); boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); - if (crossProjectEnabled && submit.getSearchRequest().allowsCrossProject()) { + if (crossProjectEnabled) { submit.getSearchRequest().setProjectRouting(request.param("project_routing")); } From bad85a6a75d3677eeb9f0890bf3e7381380bedc8 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 4 Nov 2025 14:50:25 +0000 Subject: [PATCH 07/15] Fix misc --- .../elasticsearch/rest/action/search/RestSearchAction.java | 4 ++-- 1 file changed, 2 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 c5d4a873e23f0..e0211fc00feac 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 @@ -109,7 +109,7 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC request.param("min_compatible_shard_node"); final boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled(); - if (crossProjectEnabled && searchRequest.allowsCrossProject()) { + if (crossProjectEnabled) { searchRequest.setProjectRouting(request.param("project_routing")); } @@ -243,7 +243,7 @@ public static void parseSearchRequest( searchRequest.routing(request.param("routing")); searchRequest.preference(request.param("preference")); IndicesOptions indicesOptions = IndicesOptions.fromRequest(request, searchRequest.indicesOptions()); - if (crossProjectEnabled) { + if (crossProjectEnabled && searchRequest.allowsCrossProject()) { indicesOptions = IndicesOptions.builder(indicesOptions) .crossProjectModeOptions(new IndicesOptions.CrossProjectModeOptions(true)) .build(); From 97c184b6c2f3b7e510cd57fff3b1f35807993cb0 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 5 Nov 2025 11:24:48 +0000 Subject: [PATCH 08/15] Update docs/changelog/137566.yaml --- docs/changelog/137566.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/137566.yaml diff --git a/docs/changelog/137566.yaml b/docs/changelog/137566.yaml new file mode 100644 index 0000000000000..0cca7a945e99e --- /dev/null +++ b/docs/changelog/137566.yaml @@ -0,0 +1,5 @@ +pr: 137566 +summary: Add support for `project_routing` for `_search` and `_async_search` +area: CCS +type: enhancement +issues: [] From 8b14d89d78c15fe1e13f035d1f1028a24b1e8e12 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 7 Nov 2025 14:10:01 +0000 Subject: [PATCH 09/15] Address review comments --- .../action/search/SearchRequest.java | 4 ++ .../rest/action/search/RestSearchAction.java | 46 +------------ .../search/builder/SearchSourceBuilder.java | 66 ++++++++++++++----- 3 files changed, 55 insertions(+), 61 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index 8cdf0fa67a1e5..27a8bc64b524c 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -175,6 +175,10 @@ public String getProjectRouting() { } public void setProjectRouting(@Nullable String projectRouting) { + if (this.projectRouting != null) { + throw new IllegalArgumentException("project_routing is already set to [" + this.projectRouting + "]"); + } + this.projectRouting = projectRouting; } 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 e0211fc00feac..f27dc5c7bee7a 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 @@ -202,9 +202,9 @@ public static void parseSearchRequest( searchRequest.indices(Strings.splitStringByCommaToArray(request.param("index"))); if (requestContentParser != null) { if (searchUsageHolder == null) { - searchRequest.source().parseXContent(requestContentParser, true, clusterSupportsFeature); + searchRequest.source().parseXContent(searchRequest, requestContentParser, true, clusterSupportsFeature); } else { - searchRequest.source().parseXContent(requestContentParser, true, searchUsageHolder, clusterSupportsFeature); + searchRequest.source().parseXContent(searchRequest, requestContentParser, true, searchUsageHolder, clusterSupportsFeature); } } @@ -250,7 +250,7 @@ public static void parseSearchRequest( } searchRequest.indicesOptions(indicesOptions); - validateSearchRequest(request, searchRequest, crossProjectEnabled); + validateSearchRequest(request, searchRequest); if (searchRequest.pointInTimeBuilder() != null) { preparePointInTime(searchRequest, request); @@ -410,50 +410,10 @@ static void preparePointInTime(SearchRequest request, RestRequest restRequest) { * might modify the search request to align certain parameters. */ public static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest) { - validateSearchRequest(restRequest, searchRequest, false); - } - - private static void validateSearchRequest(RestRequest restRequest, SearchRequest searchRequest, boolean crossProjectEnabled) { checkRestTotalHits(restRequest, searchRequest); checkSearchType(restRequest, searchRequest); // ensures that the rest param is consumed restRequest.paramAsBoolean(INCLUDE_NAMED_QUERIES_SCORE_PARAM, false); - checkProjectRouting(searchRequest, crossProjectEnabled); - } - - private static void checkProjectRouting(SearchRequest searchRequest, boolean crossProjectEnabled) { - /* - * There are 2 ways of specifying project_routing: - * - as a query parameter: /_search?project_routing=..., and, - * - within the request's body. - * - * Because we do not have access to `IndicesRequest/SearchRequest` from `SearchSourceBuilder`, and, project_routing - * can be potentially specified in 2 different places, we need to explicitly check this scenario. - */ - if (searchRequest.source() == null) { - return; - } - - String projectRoutingInBody = searchRequest.source().projectRouting(); - // If it's null, either the query parameter is also null or it isn't. Either way, we're fine with it. - if (projectRoutingInBody != null) { - if (crossProjectEnabled == false) { - throw new IllegalArgumentException("project_routing is allowed only when CPS is enabled and the endpoint supports CPS"); - } - - // Query parameter was also set. This is not allowed, irrespective of the values. - if (searchRequest.getProjectRouting() != null) { - throw new IllegalArgumentException( - "project_routing is specified in both the places: as query parameter and in the request's body" - ); - } - - /* - * Bring forward the project_routing value so that TransportSearchAction can pick it up. Although TSA can pick it up - * from `SearchSourceBuilder`, let's use `IndicesRequest#getProjectRouting()` since that's the intended way. - */ - searchRequest.setProjectRouting(projectRoutingInBody); - } } /** diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index 612148e9fb97c..e98fbbbe9db77 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -213,8 +213,6 @@ public static HighlightBuilder highlight() { private boolean skipInnerHits = false; - private String projectRouting; - /** * Constructs a new search source builder. */ @@ -612,19 +610,6 @@ public TimeValue timeout() { return timeout; } - public String projectRouting() { - return projectRouting; - } - - public SearchSourceBuilder projectRouting(String projectRouting) { - if (this.projectRouting != null) { - throw new IllegalArgumentException("project_routing is already set"); - } - - this.projectRouting = projectRouting; - return this; - } - /** * An optional terminate_after to terminate the search after collecting * terminateAfter documents @@ -1342,6 +1327,26 @@ private SearchSourceBuilder shallowCopy( return rewrittenBuilder; } + /** + * Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent. + * + * @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive + * the parsed info. + * @param parser The xContent parser. + * @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object. + * @param searchUsageHolder holder for the search usage statistics + * @param clusterSupportsFeature used to check if certain features are available on this cluster + */ + public SearchSourceBuilder parseXContent( + SearchRequest searchRequest, + XContentParser parser, + boolean checkTrailingTokens, + SearchUsageHolder searchUsageHolder, + Predicate clusterSupportsFeature + ) throws IOException { + return parseXContent(searchRequest, parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature); + } + /** * Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent. * @@ -1356,7 +1361,27 @@ public SearchSourceBuilder parseXContent( SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature ) throws IOException { - return parseXContent(parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature); + return parseXContent(null, parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature); + } + + /** + * Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent. + * This variant does not record search features usage. Most times the variant that accepts a {@link SearchUsageHolder} and records + * usage stats into it is the one to use. + * + * @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive + * the parsed info. + * @param parser The xContent parser. + * @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object. + * @param clusterSupportsFeature used to check if certain features are available on this cluster + */ + public SearchSourceBuilder parseXContent( + SearchRequest searchRequest, + XContentParser parser, + boolean checkTrailingTokens, + Predicate clusterSupportsFeature + ) throws IOException { + return parseXContent(searchRequest, parser, checkTrailingTokens, s -> {}, clusterSupportsFeature); } /** @@ -1373,10 +1398,11 @@ public SearchSourceBuilder parseXContent( boolean checkTrailingTokens, Predicate clusterSupportsFeature ) throws IOException { - return parseXContent(parser, checkTrailingTokens, s -> {}, clusterSupportsFeature); + return parseXContent(null, parser, checkTrailingTokens, s -> {}, clusterSupportsFeature); } private SearchSourceBuilder parseXContent( + SearchRequest searchRequest, XContentParser parser, boolean checkTrailingTokens, Consumer searchUsageConsumer, @@ -1399,7 +1425,11 @@ private SearchSourceBuilder parseXContent( currentFieldName = parser.currentName(); } else if (token.isValue()) { if (PROJECT_ROUTING.match(currentFieldName, parser.getDeprecationHandler())) { - projectRouting(parser.text()); + /* + * If project_routing was specified as a query parameter too, setProjectRouting() will throw + * an error to prevent setting twice or overwriting previously set value. + */ + searchRequest.setProjectRouting(parser.text()); } else if (FROM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { from(parser.intValue()); } else if (SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) { From 65a978c79cee5ee1ff7e23e20de441be0694c819 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 7 Nov 2025 14:18:14 +0000 Subject: [PATCH 10/15] Fix CI --- .../xpack/core/search/action/SubmitAsyncSearchRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java index 7c956974259f7..3ee87a2514c73 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/search/action/SubmitAsyncSearchRequest.java @@ -53,7 +53,6 @@ public SubmitAsyncSearchRequest(SearchSourceBuilder source, String... indices) { request.setPreFilterShardSize(1); request.setBatchedReduceSize(5); request.requestCache(true); - request.setProjectRouting(source.projectRouting()); } public SubmitAsyncSearchRequest(StreamInput in) throws IOException { From 301d2a0392a87bbf8b597c20094a1e8ecf4d59c7 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 7 Nov 2025 14:27:11 +0000 Subject: [PATCH 11/15] Drop unnecessary set --- .../main/java/org/elasticsearch/action/search/SearchRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index 27a8bc64b524c..1708a3ed2170b 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -213,7 +213,6 @@ static SearchRequest subSearchRequest( } final SearchRequest request = new SearchRequest(originalSearchRequest, indices, clusterAlias, absoluteStartMillis, finalReduce); request.setParentTask(parentTaskId); - request.setProjectRouting(originalSearchRequest.getProjectRouting()); return request; } From a3dc375a439e87dda4de19a8ce405fa4e63e37d7 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 7 Nov 2025 14:29:51 +0000 Subject: [PATCH 12/15] Null check --- .../org/elasticsearch/search/builder/SearchSourceBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index e98fbbbe9db77..fa7570caf8c8d 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -1424,7 +1424,7 @@ private SearchSourceBuilder parseXContent( if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); } else if (token.isValue()) { - if (PROJECT_ROUTING.match(currentFieldName, parser.getDeprecationHandler())) { + if (PROJECT_ROUTING.match(currentFieldName, parser.getDeprecationHandler()) && searchRequest != null) { /* * If project_routing was specified as a query parameter too, setProjectRouting() will throw * an error to prevent setting twice or overwriting previously set value. From 888ed14951a4820050c61c98f77cacfc4fca64cd Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 10 Nov 2025 15:00:48 +0000 Subject: [PATCH 13/15] Address review comments --- .../rest/action/search/RestSearchAction.java | 11 ++++- .../search/builder/SearchSourceBuilder.java | 8 +++- .../search/ProjectRoutingDisallowedIT.java | 44 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java 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 3a6da13aedde9..861aeb35f938b 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 @@ -200,11 +200,18 @@ public static void parseSearchRequest( searchRequest.source(new SearchSourceBuilder()); } searchRequest.indices(Strings.splitStringByCommaToArray(request.param("index"))); + /* + * We pass this object to the request body parser so that we can extract info such as project_routing. + * 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; if (requestContentParser != null) { if (searchUsageHolder == null) { - searchRequest.source().parseXContent(searchRequest, requestContentParser, true, clusterSupportsFeature); + searchRequest.source().parseXContent(searchRequestForParsing, requestContentParser, true, clusterSupportsFeature); } else { - searchRequest.source().parseXContent(searchRequest, requestContentParser, true, searchUsageHolder, clusterSupportsFeature); + searchRequest.source() + .parseXContent(searchRequestForParsing, requestContentParser, true, searchUsageHolder, clusterSupportsFeature); } } diff --git a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java index fa7570caf8c8d..37d20e338f5aa 100644 --- a/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java +++ b/server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java @@ -1331,7 +1331,9 @@ private SearchSourceBuilder shallowCopy( * Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent. * * @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive - * the parsed info. + * the parsed info. Currently, this is non-null only if we expect project_routing to appear in + * the request body, and we allow it to appear because we're in a Cross Project Search + * environment and require this info. * @param parser The xContent parser. * @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object. * @param searchUsageHolder holder for the search usage statistics @@ -1370,7 +1372,9 @@ public SearchSourceBuilder parseXContent( * usage stats into it is the one to use. * * @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive - * the parsed info. + * the parsed info. Currently, this is non-null only if we expect project_routing to appear in + * the request body, and we allow it to appear because we're in a Cross Project Search + * environment and require this info. * @param parser The xContent parser. * @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object. * @param clusterSupportsFeature used to check if certain features are available on this cluster diff --git a/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java new file mode 100644 index 0000000000000..2458e2ad19969 --- /dev/null +++ b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.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.xpack.search; + +import org.elasticsearch.client.Request; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.test.ESIntegTestCase; +import org.hamcrest.Matchers; + +import java.io.IOException; +import java.util.Collection; + +public class ProjectRoutingDisallowedIT extends ESIntegTestCase { + @Override + protected boolean addMockHttpTransport() { + return false; + } + + @Override + protected Collection> nodePlugins() { + return CollectionUtils.appendToCopyNoNullElements(super.nodePlugins(), AsyncSearch.class); + } + + public void testDisallowProjectRouting() throws IOException { + Request createAsyncRequest = new Request("POST", "/*,*:*/" + randomFrom("_async_search", "_search")); + createAsyncRequest.setJsonEntity(""" + { + "project_routing": "_alias:_origin" + } + """); + + ResponseException err = expectThrows(ResponseException.class, () -> getRestClient().performRequest(createAsyncRequest)); + assertThat(err.toString(), Matchers.containsString("Unknown key for a VALUE_STRING in [project_routing]")); + } +} From e0681d3c0fb0f38c381a4cd178139e2dc5620056 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 10 Nov 2025 15:09:52 +0000 Subject: [PATCH 14/15] Fix license header --- ...java => ProjectRoutingDisallowedInNonCpsEnvIT.java} | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) rename x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/{ProjectRoutingDisallowedIT.java => ProjectRoutingDisallowedInNonCpsEnvIT.java} (74%) diff --git a/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedInNonCpsEnvIT.java similarity index 74% rename from x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java rename to x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedInNonCpsEnvIT.java index 2458e2ad19969..5a4655e510ee3 100644 --- a/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedIT.java +++ b/x-pack/plugin/async-search/src/internalClusterTest/java/org/elasticsearch/xpack/search/ProjectRoutingDisallowedInNonCpsEnvIT.java @@ -1,10 +1,8 @@ /* * 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". + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ package org.elasticsearch.xpack.search; @@ -19,7 +17,7 @@ import java.io.IOException; import java.util.Collection; -public class ProjectRoutingDisallowedIT extends ESIntegTestCase { +public class ProjectRoutingDisallowedInNonCpsEnvIT extends ESIntegTestCase { @Override protected boolean addMockHttpTransport() { return false; From 7e8e30fb4046b1492526f7092fb1ad76d5a89330 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Thu, 13 Nov 2025 14:33:41 +0000 Subject: [PATCH 15/15] Update transport version --- .../elasticsearch/action/search/SearchRequest.java | 12 ++++++++++++ .../definitions/referable/search_project_routing.csv | 1 + .../main/resources/transport/upper_bounds/9.3.csv | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 server/src/main/resources/transport/definitions/referable/search_project_routing.csv diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java index 1708a3ed2170b..b49aedf8e12c3 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -116,8 +116,12 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest * enabling synthetic source natively in the index. */ private boolean forceSyntheticSource = false; + + @Nullable private String projectRouting; + private static final TransportVersion SEARCH_PROJECT_ROUTING = TransportVersion.fromName("search_project_routing"); + public SearchRequest() { this.localClusterAlias = null; this.absoluteStartMillis = DEFAULT_ABSOLUTE_START_MILLIS; @@ -293,6 +297,11 @@ public SearchRequest(StreamInput in) throws IOException { } else { forceSyntheticSource = false; } + if (in.getTransportVersion().supports(SEARCH_PROJECT_ROUTING)) { + this.projectRouting = in.readOptionalString(); + } else { + this.projectRouting = null; + } } @Override @@ -339,6 +348,9 @@ public void writeTo(StreamOutput out, boolean skipIndices) throws IOException { throw new IllegalArgumentException("force_synthetic_source is not supported before 8.4.0"); } } + if (out.getTransportVersion().supports(SEARCH_PROJECT_ROUTING)) { + out.writeOptionalString(this.projectRouting); + } } @Override diff --git a/server/src/main/resources/transport/definitions/referable/search_project_routing.csv b/server/src/main/resources/transport/definitions/referable/search_project_routing.csv new file mode 100644 index 0000000000000..023d490c87211 --- /dev/null +++ b/server/src/main/resources/transport/definitions/referable/search_project_routing.csv @@ -0,0 +1 @@ +9218000 diff --git a/server/src/main/resources/transport/upper_bounds/9.3.csv b/server/src/main/resources/transport/upper_bounds/9.3.csv index b29f7625613b5..22565ac3b1959 100644 --- a/server/src/main/resources/transport/upper_bounds/9.3.csv +++ b/server/src/main/resources/transport/upper_bounds/9.3.csv @@ -1 +1 @@ -resharding_shard_summary_in_esql,9217000 +search_project_routing,9218000