From f8c0f8fdb477883d694d0a29a1dc160dc1b15078 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 21 Oct 2025 14:13:31 +0100 Subject: [PATCH 01/13] Modify `SearchShards` API and `TransportSearchAction` for CPS when MRT=false --- .../action/search/SearchShardsRequest.java | 13 ++++ .../action/search/SearchShardsResponse.java | 28 ++++++- .../action/search/TransportSearchAction.java | 74 ++++++++++++++++--- .../search/TransportSearchShardsAction.java | 14 +++- .../search/TransportSearchActionTests.java | 10 +++ .../search/RestSubmitAsyncSearchAction.java | 13 +++- 6 files changed, 138 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchShardsRequest.java b/server/src/main/java/org/elasticsearch/action/search/SearchShardsRequest.java index 9e2837aa1f8b5..cb9ffac547554 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchShardsRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchShardsRequest.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.LegacyActionRequest; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -43,6 +44,8 @@ public final class SearchShardsRequest extends LegacyActionRequest implements In private final String clusterAlias; + private ResolvedIndexExpressions resolvedIndexExpressions; + public SearchShardsRequest( String[] indices, IndicesOptions indicesOptions, @@ -179,4 +182,14 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(indices); return result; } + + @Override + public void setResolvedIndexExpressions(ResolvedIndexExpressions expressions) { + this.resolvedIndexExpressions = expressions; + } + + @Override + public ResolvedIndexExpressions getResolvedIndexExpressions() { + return resolvedIndexExpressions; + } } diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java index 69fb0b0c2aaa0..358cec6860dda 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java @@ -10,12 +10,14 @@ package org.elasticsearch.action.search; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsGroup; import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsResponse; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.core.Nullable; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.search.internal.AliasFilter; @@ -35,21 +37,37 @@ public final class SearchShardsResponse extends ActionResponse { private final Collection groups; private final Collection nodes; private final Map aliasFilters; + private final ResolvedIndexExpressions resolvedIndexExpressions; public SearchShardsResponse( Collection groups, Collection nodes, - Map aliasFilters + Map aliasFilters, + @Nullable ResolvedIndexExpressions resolvedIndexExpressions ) { this.groups = groups; this.nodes = nodes; this.aliasFilters = aliasFilters; + this.resolvedIndexExpressions = resolvedIndexExpressions; + } + + public SearchShardsResponse( + Collection groups, + Collection nodes, + Map aliasFilters + ) { + this(groups, nodes, aliasFilters, null); } public SearchShardsResponse(StreamInput in) throws IOException { this.groups = in.readCollectionAsList(SearchShardsGroup::new); this.nodes = in.readCollectionAsList(DiscoveryNode::new); this.aliasFilters = in.readMap(AliasFilter::readFrom); + if (in.getTransportVersion().onOrAfter(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + this.resolvedIndexExpressions = in.readOptionalWriteable(ResolvedIndexExpressions::new); + } else { + this.resolvedIndexExpressions = null; + } } @Override @@ -57,6 +75,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeCollection(groups); out.writeCollection(nodes); out.writeMap(aliasFilters, StreamOutput::writeWriteable); + if (out.getTransportVersion().onOrAfter(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + out.writeOptionalWriteable(resolvedIndexExpressions); + } } /** @@ -114,4 +135,9 @@ static SearchShardsResponse fromLegacyResponse(ClusterSearchShardsResponse oldRe public String toString() { return "SearchShardsResponse{" + "groups=" + groups + ", nodes=" + nodes + ", aliasFilters=" + aliasFilters + '}'; } + + @Nullable + public ResolvedIndexExpressions getResolvedIndexExpressions() { + return resolvedIndexExpressions; + } } 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 685389e10a937..d1b602cedcd85 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.TransportVersions; import org.elasticsearch.action.ActionListener; @@ -20,6 +21,7 @@ import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.RemoteClusterActionType; +import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.ResolvedIndices; import org.elasticsearch.action.ShardOperationFailedException; import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsRequest; @@ -79,6 +81,8 @@ import org.elasticsearch.search.aggregations.AggregationReduceContext; import org.elasticsearch.search.builder.PointInTimeBuilder; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.search.internal.AliasFilter; import org.elasticsearch.search.internal.SearchContext; import org.elasticsearch.search.internal.ShardSearchContextId; @@ -120,6 +124,7 @@ import static org.elasticsearch.action.search.SearchType.DFS_QUERY_THEN_FETCH; import static org.elasticsearch.action.search.SearchType.QUERY_THEN_FETCH; import static org.elasticsearch.action.search.TransportSearchHelper.checkCCSVersionCompatibility; +import static org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator.indicesOptionsForCrossProjectFanout; import static org.elasticsearch.search.sort.FieldSortBuilder.hasPrimaryFieldSort; import static org.elasticsearch.threadpool.ThreadPool.Names.SYSTEM_CRITICAL_READ; import static org.elasticsearch.threadpool.ThreadPool.Names.SYSTEM_READ; @@ -170,6 +175,7 @@ public class TransportSearchAction extends HandledTransportAction buildPerIndexOriginalIndices( @@ -353,6 +360,7 @@ private void executeRequest( Function, SearchPhaseProvider> searchPhaseProvider, boolean collectSearchTelemetry ) { + boolean resolvesCrossProject = crossProjectModeDecider.resolvesCrossProject(original); final long relativeStartNanos = System.nanoTime(); final SearchTimeProvider timeProvider = new SearchTimeProvider( original.getOrCreateAbsoluteStartMillis(), @@ -365,16 +373,28 @@ private void executeRequest( ProjectState projectState = projectResolver.getProjectState(clusterState); final ResolvedIndices resolvedIndices; + + /* + * Irrespective of whether this is the origin project or a linked project, it's wiser to relax + * the index options to prevent the index resolution APIs from throwing index not found errors. + * Also, we do not replace the indices options on the SearchRequest because we'd be needing it + * downstream when validating the indices from both the origin and the linked projects. + */ + IndicesOptions resolutionIdxOpts = crossProjectModeDecider.resolvesCrossProject(original) + ? indicesOptionsForCrossProjectFanout(original.indicesOptions()) + : original.indicesOptions(); + if (original.pointInTimeBuilder() != null) { resolvedIndices = ResolvedIndices.resolveWithPIT( original.pointInTimeBuilder(), - original.indicesOptions(), + resolutionIdxOpts, projectState.metadata(), namedWriteableRegistry ); } else { - resolvedIndices = ResolvedIndices.resolveWithIndicesRequest( - original, + resolvedIndices = ResolvedIndices.resolveWithIndexNamesAndOptions( + original.indices(), + resolutionIdxOpts, projectState.metadata(), indexNameExpressionResolver, remoteClusterService, @@ -589,7 +609,9 @@ public void onFailure(Exception e) { searchPhaseProvider.apply(finalDelegate) ); }), - forceConnectTimeoutSecs + forceConnectTimeoutSecs, + resolvesCrossProject, + rewritten.getResolvedIndexExpressions() ); } } @@ -939,7 +961,7 @@ static SearchResponseMerger createSearchResponseMerger( * Used for ccs_minimize_roundtrips=false */ static void collectSearchShards( - IndicesOptions indicesOptions, + IndicesOptions originalIdxOpts, String preference, String routing, QueryBuilder query, @@ -950,7 +972,9 @@ static void collectSearchShards( SearchTimeProvider timeProvider, TransportService transportService, ActionListener> listener, - TimeValue forceConnectTimeoutSecs + TimeValue forceConnectTimeoutSecs, + boolean resolvesCrossProject, + ResolvedIndexExpressions originResolvedIdxExpressions ) { RemoteClusterService remoteClusterService = transportService.getRemoteClusterService(); final CountDown responsesCountDown = new CountDown(remoteIndicesByCluster.size()); @@ -976,6 +1000,27 @@ void innerOnResponse(SearchShardsResponse searchShardsResponse) { @Override Map createFinalResponse() { + // TODO: Perhaps, it's wiser to check for resolvesCrossProject too. + if (originResolvedIdxExpressions != null) { + Map resolvedIndexExpressions = new HashMap<>(); + for (Map.Entry entry : searchShardsResponses.entrySet()) { + if (entry.getValue().getResolvedIndexExpressions() == null) { + throw new IllegalArgumentException( + "Failed to get resolved index expressions for cluster [" + entry.getKey() + "]" + ); + } + resolvedIndexExpressions.put(entry.getKey(), entry.getValue().getResolvedIndexExpressions()); + } + // We do not use the related index options here when validating indices' existence. + ElasticsearchException validationEx = CrossProjectIndexResolutionValidator.validate( + originalIdxOpts, + originResolvedIdxExpressions, + resolvedIndexExpressions + ); + if (validationEx != null) { + throw validationEx; + } + } return searchShardsResponses; } }; @@ -988,13 +1033,24 @@ Map createFinalResponse() { ); connectionListener.addListener(singleListener.delegateFailure((responseListener, connection) -> { + /* + * It may be possible that indices do not exist on the project that SearchShards API is targeting. + * In such cases, it throws an error because it calls the index resolution APIs underneath. We relax + * the index options to prevent this from happening. Also, it's fine to pass in these relaxed options + * to it because SearchShardsRequest#allowsCrossProject() returns false anyway and the index rewriting + * does not happen downstream. + */ + IndicesOptions searchShardsIdxOpts = resolvesCrossProject + ? indicesOptionsForCrossProjectFanout(originalIdxOpts) + : originalIdxOpts; + final String[] indices = entry.getValue().indices(); final Executor responseExecutor = transportService.getThreadPool().executor(ThreadPool.Names.SEARCH_COORDINATION); // TODO: support point-in-time if (searchContext == null && connection.getTransportVersion().onOrAfter(TransportVersions.V_8_9_X)) { SearchShardsRequest searchShardsRequest = new SearchShardsRequest( indices, - indicesOptions, + searchShardsIdxOpts, query, routing, preference, @@ -1013,7 +1069,7 @@ Map createFinalResponse() { ClusterSearchShardsRequest searchShardsRequest = new ClusterSearchShardsRequest( MasterNodeRequest.INFINITE_MASTER_NODE_TIMEOUT, indices - ).indicesOptions(indicesOptions).local(true).preference(preference).routing(routing); + ).indicesOptions(searchShardsIdxOpts).local(true).preference(preference).routing(routing); transportService.sendRequest( connection, TransportClusterSearchShardsAction.TYPE.name(), diff --git a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java index 507893df0c0c1..035a5005b4a73 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchShardsAction.java @@ -160,7 +160,12 @@ public void searchShards(Task task, SearchShardsRequest searchShardsRequest, Act CollectionUtil.timSort(shardIts); if (SearchService.canRewriteToMatchNone(searchRequest.source()) == false) { delegate.onResponse( - new SearchShardsResponse(toGroups(shardIts), project.cluster().nodes().getAllNodes(), aliasFilters) + new SearchShardsResponse( + toGroups(shardIts), + project.cluster().nodes().getAllNodes(), + aliasFilters, + searchShardsRequest.getResolvedIndexExpressions() + ) ); } else { CanMatchPreFilterSearchPhase.execute(logger, searchTransportService, (clusterAlias, node) -> { @@ -179,7 +184,12 @@ public void searchShards(Task task, SearchShardsRequest searchShardsRequest, Act ) .addListener( delegate.map( - its -> new SearchShardsResponse(toGroups(its), project.cluster().nodes().getAllNodes(), aliasFilters) + its -> new SearchShardsResponse( + toGroups(its), + project.cluster().nodes().getAllNodes(), + aliasFilters, + searchShardsRequest.getResolvedIndexExpressions() + ) ) ); } 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 61aa05f703018..58c07ef189d81 100644 --- a/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java @@ -1101,6 +1101,8 @@ public void testCollectSearchShards() throws Exception { timeProvider, service, new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), + null, + false, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1131,6 +1133,8 @@ public void testCollectSearchShards() throws Exception { timeProvider, service, new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch), + null, + false, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1184,6 +1188,8 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce timeProvider, service, new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch), + null, + false, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1215,6 +1221,8 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce timeProvider, service, new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), + null, + false, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); @@ -1262,6 +1270,8 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce timeProvider, service, new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch), + null, + false, null ); awaitLatch(latch, 5, TimeUnit.SECONDS); 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 39f2539dbd791..eb1e5788d7994 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 @@ -71,7 +71,8 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } SubmitAsyncSearchRequest submit = new SubmitAsyncSearchRequest(); - if (settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false)) { + 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"); } @@ -82,7 +83,15 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli // them as supported. We rely on SubmitAsyncSearchRequest#validate to fail in case they are set. // Note that ccs_minimize_roundtrips is also set this way, which is a supported option. request.withContentOrSourceParamParserOrNull( - parser -> parseSearchRequest(submit.getSearchRequest(), request, parser, clusterSupportsFeature, setSize, searchUsageHolder) + parser -> parseSearchRequest( + submit.getSearchRequest(), + request, + parser, + clusterSupportsFeature, + setSize, + searchUsageHolder, + crossProjectEnabled + ) ); if (request.hasParam("wait_for_completion_timeout")) { From 30a65beed59724585e2bff1a8a433ecc28d868ea Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Tue, 21 Oct 2025 14:52:29 +0100 Subject: [PATCH 02/13] Use `support()` --- .../org/elasticsearch/action/search/SearchShardsResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java index 358cec6860dda..1b5b5a1042089 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java @@ -63,7 +63,7 @@ public SearchShardsResponse(StreamInput in) throws IOException { this.groups = in.readCollectionAsList(SearchShardsGroup::new); this.nodes = in.readCollectionAsList(DiscoveryNode::new); this.aliasFilters = in.readMap(AliasFilter::readFrom); - if (in.getTransportVersion().onOrAfter(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + if (in.getTransportVersion().supports(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { this.resolvedIndexExpressions = in.readOptionalWriteable(ResolvedIndexExpressions::new); } else { this.resolvedIndexExpressions = null; @@ -75,7 +75,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeCollection(groups); out.writeCollection(nodes); out.writeMap(aliasFilters, StreamOutput::writeWriteable); - if (out.getTransportVersion().onOrAfter(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + if (out.getTransportVersion().supports(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { out.writeOptionalWriteable(resolvedIndexExpressions); } } From d8bba0c65659b1508c2609a601152426c309322c Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 22 Oct 2025 00:19:31 +0100 Subject: [PATCH 03/13] Do not track project in metadata if it doesn't participate in search --- .../action/search/TransportSearchAction.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 d1b602cedcd85..cd9d0d50bdf54 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -994,7 +994,21 @@ static void collectSearchShards( @Override void innerOnResponse(SearchShardsResponse searchShardsResponse) { assert ThreadPool.assertCurrentThreadPool(ThreadPool.Names.SEARCH_COORDINATION); - ccsClusterInfoUpdate(searchShardsResponse, clusters, clusterAlias, timeProvider); + /* + * This particular linked project returned empty shards and that's because none of the requested + * indices are on it. So we need to prevent it from appearing in the metadata. In case the very + * same indices don't exist on the origin too, we use `CrossProjectIndexResolutionValidator#validate()` + * to throw an error downstream. + * + * TODO: Handle the `total` count that tracks total projects since it populates that info + * within Cluster#ctor(). + */ + boolean canPurge = resolvesCrossProject && searchShardsResponse.getGroups().isEmpty(); + if (canPurge) { + clusters.swapCluster(clusterAlias, (ignored1, ignored2) -> null); + } else { + ccsClusterInfoUpdate(searchShardsResponse, clusters, clusterAlias, timeProvider); + } searchShardsResponses.put(clusterAlias, searchShardsResponse); } From d72b8f929c24950b7a2d7a9be32b0635f1aa7b53 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 24 Oct 2025 05:08:36 +0100 Subject: [PATCH 04/13] Fix reconciliation --- .../action/search/SearchResponse.java | 6 +- .../action/search/TransportSearchAction.java | 113 ++++++++++++++---- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index 3b67d0b5ac160..b56f76f69f5c9 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -609,13 +609,17 @@ public Clusters(StreamInput in) throws IOException { } public Clusters(Map clusterInfoMap) { + this(clusterInfoMap, true); + } + + public Clusters(Map clusterInfoMap, boolean ccsMinimizeRoundtrips) { assert clusterInfoMap.size() > 0 : "this constructor should not be called with an empty Cluster info map"; this.total = clusterInfoMap.size(); this.clusterInfo = clusterInfoMap; this.successful = getClusterStateCount(Cluster.Status.SUCCESSFUL); this.skipped = getClusterStateCount(Cluster.Status.SKIPPED); // should only be called if "details" section of fromXContent is present (for ccsMinimizeRoundtrips) - this.ccsMinimizeRoundtrips = true; + this.ccsMinimizeRoundtrips = ccsMinimizeRoundtrips; } @Override 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 cd9d0d50bdf54..f22c2a881d136 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -21,6 +21,7 @@ import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.OriginalIndices; import org.elasticsearch.action.RemoteClusterActionType; +import org.elasticsearch.action.ResolvedIndexExpression; import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.ResolvedIndices; import org.elasticsearch.action.ShardOperationFailedException; @@ -62,6 +63,7 @@ import org.elasticsearch.common.util.ArrayUtils; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; +import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.TimeValue; @@ -442,12 +444,6 @@ public void onFailure(Exception e) { if (ccsCheckCompatibility) { checkCCSVersionCompatibility(rewritten); } - if (rewritten.indicesOptions().resolveCrossProjectIndexExpression()) { - IndicesOptions indicesOptions = IndicesOptions.builder(rewritten.indicesOptions()) - .crossProjectModeOptions(IndicesOptions.CrossProjectModeOptions.DEFAULT) - .build(); - rewritten.indicesOptions(indicesOptions); - } final ActionListener searchResponseActionListener; if (collectSearchTelemetry) { @@ -572,6 +568,15 @@ public void onFailure(Exception e) { timeProvider, transportService, searchResponseActionListener.delegateFailureAndWrap((finalDelegate, searchShardsResponses) -> { + SearchResponse.Clusters participatingProjects = clusters; + if (resolvesCrossProject && rewritten.getResolvedIndexExpressions() != null) { + participatingProjects = reconcileProjects( + rewritten.getResolvedIndexExpressions(), + searchShardsResponses, + participatingProjects + ); + } + final BiFunction clusterNodeLookup = getRemoteClusterNodeLookup( searchShardsResponses ); @@ -605,7 +610,7 @@ public void onFailure(Exception e) { clusterNodeLookup, projectState, remoteAliasFilters, - clusters, + participatingProjects, searchPhaseProvider.apply(finalDelegate) ); }), @@ -956,6 +961,81 @@ static SearchResponseMerger createSearchResponseMerger( return new SearchResponseMerger(from, size, trackTotalHitsUpTo, timeProvider, aggReduceContextBuilder); } + /** + * Outside Cross Project Search, we're sure of projects involved and their corresponding indices. However, + * in CPS, it may be possible that indices can exist anywhere: + *
    + *
  • Only on the origin
  • + *
  • Only on the linked project(s)
  • + *
  • Both on the origin and the linked project(s), and,
  • + *
  • Nowhere
  • + *
+ * + * Therefore, we only need to include the details of those projects hosting our indices and participating + * in the search. Otherwise, we risk unnecessarily including them in the execution metadata and marking + * their statuses as "successful", potentially misleading users into believing that they returned results + * and participated in the search. + * + * Note that this code runs after the SearchShards API's responses have been pieced back and the CPS index + * validation is complete. + * @param originResolvedIdxExpressions The resolution result from origin's Security Action Filter. + * @param shardResponses Responses pieced back from SearchShards API. + * @param projects Clusters object to build upon. + * @return A new Clusters object containing only the Search-participating projects. + */ + static SearchResponse.Clusters reconcileProjects( + ResolvedIndexExpressions originResolvedIdxExpressions, + Map shardResponses, + SearchResponse.Clusters projects + ) { + /* + * We only fire a SearchShards API for a project if it needs to be searched. This can either mean that it was + * part of the search due to the flatworld behaviour, or that it was targeted specifically. If it returns an + * empty response, it's because the project does not host any of our specified indices. + */ + Set linkedProjectsWithResponses = shardResponses.entrySet() + .stream() + .filter(ssr -> ssr.getValue().getGroups().isEmpty() == false) + .map(Map.Entry::getKey) + .collect(Collectors.toSet()); + + // Same as we do in stateful right now. + if (linkedProjectsWithResponses.isEmpty()) { + return SearchResponse.Clusters.EMPTY; + } + + boolean shouldIncludeOrigin = originResolvedIdxExpressions.expressions() + .stream() + .anyMatch( + expr -> expr.localExpressions().localIndexResolutionResult() == ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS + ); + + Map reconciledMap = ConcurrentCollections.newConcurrentMap(); + for (String project : projects.getClusterAliases()) { + SearchResponse.Cluster computedProjectInfo = projects.getCluster(project); + /* + * Selection criteria for a `project` to be included in the metadata: + * - This is the origin project, and there was a "success"ful resolution by the Security Action Filter, + * - This is a linked project with a non-empty response from SearchShards API, or, + * - There was an issue with this project, so let's carry over the failures and reporting them. + */ + boolean shouldAdd = false; + if (shouldIncludeOrigin && project.equals(RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY)) { + shouldAdd = true; + } else if (linkedProjectsWithResponses.contains(project)) { + shouldAdd = true; + } else if (computedProjectInfo.getFailures().isEmpty() == false) { + shouldAdd = true; + } + + if (shouldAdd) { + reconciledMap.put(project, computedProjectInfo); + } + } + + return new SearchResponse.Clusters(reconciledMap, false); + } + /** * Collect remote search shards that we need to search for potential matches. * Used for ccs_minimize_roundtrips=false @@ -994,28 +1074,13 @@ static void collectSearchShards( @Override void innerOnResponse(SearchShardsResponse searchShardsResponse) { assert ThreadPool.assertCurrentThreadPool(ThreadPool.Names.SEARCH_COORDINATION); - /* - * This particular linked project returned empty shards and that's because none of the requested - * indices are on it. So we need to prevent it from appearing in the metadata. In case the very - * same indices don't exist on the origin too, we use `CrossProjectIndexResolutionValidator#validate()` - * to throw an error downstream. - * - * TODO: Handle the `total` count that tracks total projects since it populates that info - * within Cluster#ctor(). - */ - boolean canPurge = resolvesCrossProject && searchShardsResponse.getGroups().isEmpty(); - if (canPurge) { - clusters.swapCluster(clusterAlias, (ignored1, ignored2) -> null); - } else { - ccsClusterInfoUpdate(searchShardsResponse, clusters, clusterAlias, timeProvider); - } + ccsClusterInfoUpdate(searchShardsResponse, clusters, clusterAlias, timeProvider); searchShardsResponses.put(clusterAlias, searchShardsResponse); } @Override Map createFinalResponse() { - // TODO: Perhaps, it's wiser to check for resolvesCrossProject too. - if (originResolvedIdxExpressions != null) { + if (resolvesCrossProject && originResolvedIdxExpressions != null) { Map resolvedIndexExpressions = new HashMap<>(); for (Map.Entry entry : searchShardsResponses.entrySet()) { if (entry.getValue().getResolvedIndexExpressions() == null) { From fed05cbe21b32a41c0ce32919499602c38b867ec Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Fri, 24 Oct 2025 11:05:44 +0100 Subject: [PATCH 05/13] Set `allowsCrossProject()` to `true` for Search --- .../java/org/elasticsearch/action/search/SearchRequest.java | 5 +++++ 1 file changed, 5 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 d9cfabcd45b97..748dc67caae3e 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -163,6 +163,11 @@ public boolean allowsRemoteIndices() { return true; } + @Override + public boolean allowsCrossProject() { + return true; + } + /** * 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. From 49a341b1a579d8c26ea6e2c46826914282b62bf9 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 27 Oct 2025 10:54:04 +0000 Subject: [PATCH 06/13] Address review comments --- .../action/search/TransportSearchAction.java | 24 +++++++++++-------- .../search/RestSubmitAsyncSearchAction.java | 11 ++++----- .../RestSubmitAsyncSearchActionTests.java | 7 +++++- 3 files changed, 24 insertions(+), 18 deletions(-) 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 867508cfa0857..4fe9e0a6629ad 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -379,12 +379,15 @@ private void executeRequest( final ResolvedIndices resolvedIndices; /* - * Irrespective of whether this is the origin project or a linked project, it's wiser to relax - * the index options to prevent the index resolution APIs from throwing index not found errors. - * Also, we do not replace the indices options on the SearchRequest because we'd be needing it - * downstream when validating the indices from both the origin and the linked projects. + * If this search request is originating from a search endpoint that is CPS compatible, and, + * CPS is enabled, i.e. resolvesCrossProject() returns true, we need to modify and relax the + * indices options. This is to: + * a) Prevent the downstream code from throwing an error if an index is not found since in + * CPS, an index can exist either on the origin or on different linked project(s). + * b) Prevent the linked projects from re-interpreting the index expressions as CPS expressions + * and rather treat them as canonical/standard ones. */ - IndicesOptions resolutionIdxOpts = crossProjectModeDecider.resolvesCrossProject(original) + IndicesOptions resolutionIdxOpts = resolvesCrossProject ? indicesOptionsForCrossProjectFanout(original.indicesOptions()) : original.indicesOptions(); @@ -964,8 +967,9 @@ static SearchResponseMerger createSearchResponseMerger( } /** - * Outside Cross Project Search, we're sure of projects involved and their corresponding indices. However, - * in CPS, it may be possible that indices can exist anywhere: + * Reconciliation is only done when Cross Project Search is enabled and requests originate from a CPS + * compatible endpoints. Outside CPS, we're sure of projects involved and their corresponding indices. + * However, in CPS, it may be possible that indices can exist anywhere: *
    *
  • Only on the origin
  • *
  • Only on the linked project(s)
  • @@ -982,7 +986,7 @@ static SearchResponseMerger createSearchResponseMerger( * validation is complete. * @param originResolvedIdxExpressions The resolution result from origin's Security Action Filter. * @param shardResponses Responses pieced back from SearchShards API. - * @param projects Clusters object to build upon. + * @param projects The clusters originally in scope for the query. * @return A new Clusters object containing only the Search-participating projects. */ static SearchResponse.Clusters reconcileProjects( @@ -991,7 +995,7 @@ static SearchResponse.Clusters reconcileProjects( SearchResponse.Clusters projects ) { /* - * We only fire a SearchShards API for a project if it needs to be searched. This can either mean that it was + * We only fire a SearchShards API call for a project if it needs to be searched. This can either mean that it was * part of the search due to the flatworld behaviour, or that it was targeted specifically. If it returns an * empty response, it's because the project does not host any of our specified indices. */ @@ -1092,7 +1096,7 @@ Map createFinalResponse() { } resolvedIndexExpressions.put(entry.getKey(), entry.getValue().getResolvedIndexExpressions()); } - // We do not use the related index options here when validating indices' existence. + // We do not use the relaxed index options here when validating indices' existence. ElasticsearchException validationEx = CrossProjectIndexResolutionValidator.validate( originalIdxOpts, originResolvedIdxExpressions, 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 eb1e5788d7994..620a0b0fcc5a9 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 @@ -16,6 +16,7 @@ import org.elasticsearch.rest.ServerlessScope; import org.elasticsearch.rest.action.RestCancellableNodeClient; import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener; +import org.elasticsearch.search.crossproject.CrossProjectModeDecider; import org.elasticsearch.usage.SearchUsageHolder; import org.elasticsearch.xpack.core.search.action.AsyncSearchResponse; import org.elasticsearch.xpack.core.search.action.SubmitAsyncSearchAction; @@ -38,11 +39,7 @@ public final class RestSubmitAsyncSearchAction extends BaseRestHandler { private final SearchUsageHolder searchUsageHolder; private final Predicate clusterSupportsFeature; - private final Settings settings; - - public RestSubmitAsyncSearchAction(SearchUsageHolder searchUsageHolder, Predicate clusterSupportsFeature) { - this(searchUsageHolder, clusterSupportsFeature, null); - } + private final CrossProjectModeDecider crossProjectModeDecider; public RestSubmitAsyncSearchAction( SearchUsageHolder searchUsageHolder, @@ -51,7 +48,7 @@ public RestSubmitAsyncSearchAction( ) { this.searchUsageHolder = searchUsageHolder; this.clusterSupportsFeature = clusterSupportsFeature; - this.settings = settings; + this.crossProjectModeDecider = new CrossProjectModeDecider(settings); } @Override @@ -71,7 +68,7 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli } SubmitAsyncSearchRequest submit = new SubmitAsyncSearchRequest(); - boolean crossProjectEnabled = 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"); diff --git a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java index efb289bf04e66..f28e6cf2cd03c 100644 --- a/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java +++ b/x-pack/plugin/async-search/src/test/java/org/elasticsearch/xpack/search/RestSubmitAsyncSearchActionTests.java @@ -8,6 +8,7 @@ import org.apache.lucene.util.SetOnce; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.core.TimeValue; import org.elasticsearch.rest.RestRequest; @@ -31,7 +32,11 @@ public class RestSubmitAsyncSearchActionTests extends RestActionTestCase { @Before public void setUpAction() { - RestSubmitAsyncSearchAction action = new RestSubmitAsyncSearchAction(new UsageService().getSearchUsageHolder(), nf -> false); + RestSubmitAsyncSearchAction action = new RestSubmitAsyncSearchAction( + new UsageService().getSearchUsageHolder(), + nf -> false, + Settings.EMPTY + ); controller().registerHandler(action); } From cf69a1c3f097ae54220e0bf0764ba738d3a853ab Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Mon, 27 Oct 2025 11:19:17 +0000 Subject: [PATCH 07/13] Slight code comment changes --- .../action/search/TransportSearchAction.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 4fe9e0a6629ad..e2132a8699734 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -383,7 +383,7 @@ private void executeRequest( * CPS is enabled, i.e. resolvesCrossProject() returns true, we need to modify and relax the * indices options. This is to: * a) Prevent the downstream code from throwing an error if an index is not found since in - * CPS, an index can exist either on the origin or on different linked project(s). + * CPS, an index can exist anywhere. * b) Prevent the linked projects from re-interpreting the index expressions as CPS expressions * and rather treat them as canonical/standard ones. */ @@ -1121,9 +1121,9 @@ Map createFinalResponse() { /* * It may be possible that indices do not exist on the project that SearchShards API is targeting. * In such cases, it throws an error because it calls the index resolution APIs underneath. We relax - * the index options to prevent this from happening. Also, it's fine to pass in these relaxed options - * to it because SearchShardsRequest#allowsCrossProject() returns false anyway and the index rewriting - * does not happen downstream. + * the index options to prevent this from happening, iff this is a CPS request and CPS is enabled. + * Also, it's fine to pass in these relaxed options to it because SearchShardsRequest#allowsCrossProject() + * returns false anyway and the index rewriting does not happen downstream. */ IndicesOptions searchShardsIdxOpts = resolvesCrossProject ? indicesOptionsForCrossProjectFanout(originalIdxOpts) From 60a197e2f5724a7afe8e805bab4b773c80728612 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Wed, 29 Oct 2025 17:55:24 +0000 Subject: [PATCH 08/13] Clarify code comment as per review suggestion --- .../elasticsearch/action/search/TransportSearchAction.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 e2132a8699734..7b08df13d819e 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -1005,7 +1005,11 @@ static SearchResponse.Clusters reconcileProjects( .map(Map.Entry::getKey) .collect(Collectors.toSet()); - // Same as we do in stateful right now. + /* + * We don't show any metadata in the response if there are no linked projects to search. + * In that case, it's alright to return `Clusters.EMPTY` and is in fact what we do in + * stateful, i.e. outside CPS. + */ if (linkedProjectsWithResponses.isEmpty()) { return SearchResponse.Clusters.EMPTY; } From ecfa2b650b76710110f80e693b2460a84fea961b Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Thu, 30 Oct 2025 13:22:26 +0000 Subject: [PATCH 09/13] Address review around map --- .../java/org/elasticsearch/action/search/SearchResponse.java | 3 ++- .../org/elasticsearch/action/search/TransportSearchAction.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java index b56f76f69f5c9..e03fb57a6f27f 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchResponse.java @@ -615,7 +615,8 @@ public Clusters(Map clusterInfoMap) { public Clusters(Map clusterInfoMap, boolean ccsMinimizeRoundtrips) { assert clusterInfoMap.size() > 0 : "this constructor should not be called with an empty Cluster info map"; this.total = clusterInfoMap.size(); - this.clusterInfo = clusterInfoMap; + this.clusterInfo = ConcurrentCollections.newConcurrentMap(); + this.clusterInfo.putAll(clusterInfoMap); this.successful = getClusterStateCount(Cluster.Status.SUCCESSFUL); this.skipped = getClusterStateCount(Cluster.Status.SKIPPED); // should only be called if "details" section of fromXContent is present (for ccsMinimizeRoundtrips) 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 7b08df13d819e..df5806872f4d5 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -1020,7 +1020,7 @@ static SearchResponse.Clusters reconcileProjects( expr -> expr.localExpressions().localIndexResolutionResult() == ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS ); - Map reconciledMap = ConcurrentCollections.newConcurrentMap(); + Map reconciledMap = new HashMap<>(); for (String project : projects.getClusterAliases()) { SearchResponse.Cluster computedProjectInfo = projects.getCluster(project); /* From d7a16a726b962ff8d64463510c550a03ee262b5b Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 30 Oct 2025 13:31:41 +0000 Subject: [PATCH 10/13] [CI] Auto commit changes from spotless --- .../org/elasticsearch/action/search/TransportSearchAction.java | 1 - 1 file changed, 1 deletion(-) 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 4bd4d3a1e86cf..f9b5b1616b46b 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -65,7 +65,6 @@ import org.elasticsearch.common.util.ArrayUtils; import org.elasticsearch.common.util.CollectionUtils; import org.elasticsearch.common.util.Maps; -import org.elasticsearch.common.util.concurrent.ConcurrentCollections; import org.elasticsearch.common.util.concurrent.CountDown; import org.elasticsearch.common.util.concurrent.EsExecutors; import org.elasticsearch.core.TimeValue; From 0d241a890395d544526573db5510e784378a668b Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Thu, 30 Oct 2025 13:48:03 +0000 Subject: [PATCH 11/13] Set project routing to null for now --- .../org/elasticsearch/action/search/TransportSearchAction.java | 1 + 1 file changed, 1 insertion(+) 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 f9b5b1616b46b..51dd4cc09a415 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -1104,6 +1104,7 @@ Map createFinalResponse() { // We do not use the relaxed index options here when validating indices' existence. ElasticsearchException validationEx = CrossProjectIndexResolutionValidator.validate( originalIdxOpts, + null, originResolvedIdxExpressions, resolvedIndexExpressions ); From 9943965f939440f4f273461eafa845e37e0e3799 Mon Sep 17 00:00:00 2001 From: Pawan Kartik Date: Thu, 30 Oct 2025 14:54:47 +0000 Subject: [PATCH 12/13] Use a dedicated transport version --- .../elasticsearch/action/search/SearchShardsResponse.java | 8 ++++++-- .../search_shards_resolved_index_expressions.csv | 1 + server/src/main/resources/transport/upper_bounds/9.3.csv | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv diff --git a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java index 1b5b5a1042089..5425be4e6229c 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchShardsResponse.java @@ -9,6 +9,7 @@ package org.elasticsearch.action.search; +import org.elasticsearch.TransportVersion; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ResolvedIndexExpressions; import org.elasticsearch.action.admin.cluster.shards.ClusterSearchShardsGroup; @@ -38,6 +39,9 @@ public final class SearchShardsResponse extends ActionResponse { private final Collection nodes; private final Map aliasFilters; private final ResolvedIndexExpressions resolvedIndexExpressions; + public static final TransportVersion SEARCH_SHARDS_RESOLVED_INDEX_EXPRESSIONS = TransportVersion.fromName( + "search_shards_resolved_index_expressions" + ); public SearchShardsResponse( Collection groups, @@ -63,7 +67,7 @@ public SearchShardsResponse(StreamInput in) throws IOException { this.groups = in.readCollectionAsList(SearchShardsGroup::new); this.nodes = in.readCollectionAsList(DiscoveryNode::new); this.aliasFilters = in.readMap(AliasFilter::readFrom); - if (in.getTransportVersion().supports(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + if (in.getTransportVersion().supports(SEARCH_SHARDS_RESOLVED_INDEX_EXPRESSIONS)) { this.resolvedIndexExpressions = in.readOptionalWriteable(ResolvedIndexExpressions::new); } else { this.resolvedIndexExpressions = null; @@ -75,7 +79,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeCollection(groups); out.writeCollection(nodes); out.writeMap(aliasFilters, StreamOutput::writeWriteable); - if (out.getTransportVersion().supports(ResolvedIndexExpressions.RESOLVED_INDEX_EXPRESSIONS)) { + if (out.getTransportVersion().supports(SEARCH_SHARDS_RESOLVED_INDEX_EXPRESSIONS)) { out.writeOptionalWriteable(resolvedIndexExpressions); } } diff --git a/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv b/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv new file mode 100644 index 0000000000000..773be2c2c150c --- /dev/null +++ b/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv @@ -0,0 +1 @@ +9198000 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 1acd7ceede226..c59768c91a2bd 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 @@ -text_similarity_rank_docs_explain_chunks,9205000 +search_shards_resolved_index_expressions,9198000 From 94718266301a403630f36ab28ce2825453b256a9 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 30 Oct 2025 15:01:37 +0000 Subject: [PATCH 13/13] [CI] Update transport version definitions --- .../referable/search_shards_resolved_index_expressions.csv | 2 +- server/src/main/resources/transport/upper_bounds/9.3.csv | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv b/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv index 773be2c2c150c..44b76d9df26ec 100644 --- a/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv +++ b/server/src/main/resources/transport/definitions/referable/search_shards_resolved_index_expressions.csv @@ -1 +1 @@ -9198000 +9206000 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 c59768c91a2bd..b18cb86c5eea4 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 @@ -search_shards_resolved_index_expressions,9198000 +search_shards_resolved_index_expressions,9206000