diff --git a/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java b/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java index 89f46bee4b709..2127a19a9af99 100644 --- a/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java +++ b/server/src/internalClusterTest/java/org/elasticsearch/action/search/TransportSearchIT.java @@ -139,6 +139,7 @@ public void testLocalClusterAlias() throws ExecutionException, InterruptedExcept parentTaskId, new SearchRequest(), Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, "local", nowInMillis, randomBoolean() @@ -158,6 +159,7 @@ public void testLocalClusterAlias() throws ExecutionException, InterruptedExcept parentTaskId, new SearchRequest(), Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, "", nowInMillis, randomBoolean() @@ -205,6 +207,7 @@ public void testAbsoluteStartMillis() throws ExecutionException, InterruptedExce parentTaskId, new SearchRequest(), Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, "", 0, randomBoolean() @@ -216,6 +219,7 @@ public void testAbsoluteStartMillis() throws ExecutionException, InterruptedExce parentTaskId, new SearchRequest(), Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, "", 0, randomBoolean() @@ -231,6 +235,7 @@ public void testAbsoluteStartMillis() throws ExecutionException, InterruptedExce parentTaskId, new SearchRequest(), Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, "", 0, randomBoolean() @@ -279,7 +284,15 @@ public void testFinalReduce() throws ExecutionException, InterruptedException { { SearchRequest searchRequest = randomBoolean() ? originalRequest - : SearchRequest.subSearchRequest(taskId, originalRequest, Strings.EMPTY_ARRAY, "remote", nowInMillis, true); + : SearchRequest.subSearchRequest( + taskId, + originalRequest, + Strings.EMPTY_ARRAY, + originalRequest.indicesOptions(), + "remote", + nowInMillis, + true + ); assertResponse(client().search(searchRequest), searchResponse -> { assertEquals(2, searchResponse.getHits().getTotalHits().value()); InternalAggregations aggregations = searchResponse.getAggregations(); @@ -292,6 +305,7 @@ public void testFinalReduce() throws ExecutionException, InterruptedException { taskId, originalRequest, Strings.EMPTY_ARRAY, + originalRequest.indicesOptions(), "remote", nowInMillis, false diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java index 5bab04188a7a7..27ac5420adb48 100644 --- a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java +++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java @@ -253,6 +253,44 @@ public static ResolvedIndices resolveWithPIT( ); } + /** + * Create a new {@link ResolvedIndices} instance from a Map of Projects to {@link ResolvedIndexExpressions}. This is intended to be + * used for Cross-Project Search (CPS). + * + * @param localIndices this value is set as-is in the resulting ResolvedIndices. + * @param localIndexMetadata this value is set as-is in the resulting ResolvedIndices. + * @param remoteExpressions the map of project names to {@link ResolvedIndexExpressions}. This map is used to create the + * {@link ResolvedIndices#getRemoteClusterIndices()} for the resulting ResolvedIndices. Each project keyed + * in the map is guaranteed to have at least one index for the index expression provided by the user. + * The resulting {@link ResolvedIndices#getRemoteClusterIndices()} will map to the original index expression + * provided by the user. For example, if the user requested "logs" and "project-1" resolved that to "logs-1", + * then the result will map "project-1" to "logs". We rely on the remote search request to expand "logs" back + * to "logs-1". + * @param indicesOptions this value is set as-is in the resulting ResolvedIndices. + */ + public static ResolvedIndices resolveWithIndexExpressions( + OriginalIndices localIndices, + Map localIndexMetadata, + Map remoteExpressions, + IndicesOptions indicesOptions + ) { + Map remoteIndices = remoteExpressions.entrySet().stream().collect(HashMap::new, (map, entry) -> { + var indices = entry.getValue().expressions().stream().filter(expression -> { + var resolvedExpressions = expression.localExpressions(); + var successfulResolution = resolvedExpressions + .localIndexResolutionResult() == ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS; + // if the expression is a wildcard, it will be successful even if there are no indices, so filter for no indices + var hasResolvedIndices = resolvedExpressions.indices().isEmpty() == false; + return successfulResolution && hasResolvedIndices; + }).map(ResolvedIndexExpression::original).toArray(String[]::new); + if (indices.length > 0) { + map.put(entry.getKey(), new OriginalIndices(indices, indicesOptions)); + } + }, Map::putAll); + + return new ResolvedIndices(remoteIndices, localIndices, localIndexMetadata); + } + private static Map resolveLocalIndexMetadata( Index[] concreteLocalIndices, ProjectMetadata projectMetadata, 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 b49aedf8e12c3..c611462079f63 100644 --- a/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java +++ b/server/src/main/java/org/elasticsearch/action/search/SearchRequest.java @@ -196,6 +196,7 @@ public void setProjectRouting(@Nullable String projectRouting) { * @param parentTaskId the parent taskId of the original search request * @param originalSearchRequest the original search request * @param indices the indices to search against + * @param indicesOptions the indicesOptions to search with * @param clusterAlias the alias to prefix index names with in the returned search results * @param absoluteStartMillis the absolute start time to be used on the remote clusters to ensure that the same value is used * @param finalReduce whether the reduction should be final or not @@ -204,6 +205,7 @@ static SearchRequest subSearchRequest( TaskId parentTaskId, SearchRequest originalSearchRequest, String[] indices, + IndicesOptions indicesOptions, String clusterAlias, long absoluteStartMillis, boolean finalReduce @@ -217,6 +219,7 @@ static SearchRequest subSearchRequest( } final SearchRequest request = new SearchRequest(originalSearchRequest, indices, clusterAlias, absoluteStartMillis, finalReduce); request.setParentTask(parentTaskId); + request.indicesOptions(indicesOptions); return request; } @@ -414,8 +417,8 @@ boolean isFinalReduce() { /** * Returns the current time in milliseconds from the time epoch, to be used for the execution of this search request. Used to * ensure that the same value, determined by the coordinating node, is used on all nodes involved in the execution of the search - * request. When created through {@link #subSearchRequest(TaskId, SearchRequest, String[], String, long, boolean)}, this method returns - * the provided current time, otherwise it will return {@link System#currentTimeMillis()}. + * request. When created through {@link #subSearchRequest(TaskId, SearchRequest, String[], IndicesOptions, String, long, boolean)}, + * this method returns the provided current time, otherwise it will return {@link System#currentTimeMillis()}. */ long getOrCreateAbsoluteStartMillis() { return absoluteStartMillis == DEFAULT_ABSOLUTE_START_MILLIS ? System.currentTimeMillis() : absoluteStartMillis; 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 2b8cc3d4454ac..a773340dac65f 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -30,7 +30,9 @@ import org.elasticsearch.action.admin.cluster.shards.TransportClusterSearchShardsAction; import org.elasticsearch.action.admin.cluster.stats.CCSUsage; import org.elasticsearch.action.admin.cluster.stats.CCSUsageTelemetry; +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.GroupedActionListener; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.SubscribableListener; @@ -107,6 +109,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -512,43 +515,70 @@ public void onFailure(Exception e) { } else { final TaskId parentTaskId = task.taskInfo(clusterService.localNode().getId(), false).taskId(); if (shouldMinimizeRoundtrips(rewritten)) { - final AggregationReduceContext.Builder aggregationReduceContextBuilder = rewritten.source() != null - && rewritten.source().aggregations() != null - ? searchService.aggReduceContextBuilder(task::isCancelled, rewritten.source().aggregations()) - : null; - SearchResponse.Clusters clusters = new SearchResponse.Clusters( - resolvedIndices.getLocalIndices(), - resolvedIndices.getRemoteClusterIndices(), - true, - (clusterAlias) -> remoteClusterService.shouldSkipOnFailure(clusterAlias, rewritten.allowPartialSearchResults()) - ); - if (resolvedIndices.getLocalIndices() == null) { - // Notify the progress listener that a CCS with minimize_roundtrips is happening remote-only (no local shards) - task.getProgressListener() - .notifyListShards(Collections.emptyList(), Collections.emptyList(), clusters, false, timeProvider); - } - ccsRemoteReduce( - task, - parentTaskId, + collectRemoteResolvedIndices( + resolvesCrossProject, rewritten, + resolutionIdxOpts, resolvedIndices, - clusters, - timeProvider, - aggregationReduceContextBuilder, - remoteClusterService, - threadPool, - searchResponseActionListener, - (r, l) -> executeLocalSearch( - task, - timeProvider, - r, - resolvedIndices, - projectState, - clusters, - searchPhaseProvider.apply(l) - ), - transportService, - forceConnectTimeoutSecs + searchResponseActionListener.delegateFailureAndWrap((searchListener, replacedIndices) -> { + if (replacedIndices.getRemoteClusterIndices().isEmpty()) { + // if the original resolvedIndices had remote clusters, but the replacedIndices no longer does, then we + // treat this like a local search. If there is no local index, then executeLocalSearch will return an + // empty result. + executeLocalSearch( + task, + timeProvider, + rewritten, + replacedIndices, + projectState, + SearchResponse.Clusters.EMPTY, + searchPhaseProvider.apply(searchResponseActionListener) + ); + } else { + final var aggregationReduceContextBuilder = rewritten.source() != null + && rewritten.source().aggregations() != null + ? searchService.aggReduceContextBuilder(task::isCancelled, rewritten.source().aggregations()) + : null; + var clusters = new SearchResponse.Clusters( + replacedIndices.getLocalIndices(), + replacedIndices.getRemoteClusterIndices(), + true, + (clusterAlias) -> remoteClusterService.shouldSkipOnFailure( + clusterAlias, + rewritten.allowPartialSearchResults() + ) + ); + if (replacedIndices.getLocalIndices() == null) { + // Notify the progress listener that a CCS with minimize_roundtrips is happening remote-only (no local + // shards) + task.getProgressListener() + .notifyListShards(Collections.emptyList(), Collections.emptyList(), clusters, false, timeProvider); + } + ccsRemoteReduce( + task, + parentTaskId, + rewritten, + replacedIndices, + clusters, + timeProvider, + aggregationReduceContextBuilder, + remoteClusterService, + threadPool, + searchListener, + (r, l) -> executeLocalSearch( + task, + timeProvider, + r, + replacedIndices, + projectState, + clusters, + searchPhaseProvider.apply(l) + ), + transportService, + forceConnectTimeoutSecs + ); + } + }) ); } else { final SearchContextId searchContext = resolvedIndices.getSearchContextId(); @@ -789,6 +819,7 @@ static void ccsRemoteReduce( parentTaskId, searchRequest, indices.indices(), + indices.indicesOptions(), clusterAlias, timeProvider.absoluteStartMillis(), true @@ -880,6 +911,7 @@ public void onFailure(Exception e) { parentTaskId, searchRequest, indices.indices(), + indices.indicesOptions(), clusterAlias, timeProvider.absoluteStartMillis(), false @@ -934,6 +966,7 @@ public void onFailure(Exception e) { parentTaskId, searchRequest, resolvedIndices.getLocalIndices().indices(), + resolvedIndices.getLocalIndices().indicesOptions(), RemoteClusterAware.LOCAL_CLUSTER_GROUP_KEY, timeProvider.absoluteStartMillis(), false @@ -1048,6 +1081,134 @@ static SearchResponse.Clusters reconcileProjects( return new SearchResponse.Clusters(reconciledMap, false); } + /** + * Only used for ccs_minimize_roundtrips=true pathway + * If Cross-Project Search (CPS) is enabled, then this method will fan out to the linked projects to resolve and validate their indices. + */ + void collectRemoteResolvedIndices( + boolean resolvesCrossProject, + SearchRequest rewritten, + IndicesOptions resolutionIdxOpts, + ResolvedIndices originalResolvedIndices, + ActionListener listener + ) { + if (resolvesCrossProject) { + var numProjectsToResolve = originalResolvedIndices.getRemoteClusterIndices().size(); + assert numProjectsToResolve > 0 : "At least one index is required to resolve cross project indices"; + assert rewritten.getResolvedIndexExpressions() != null : "ResolvedIndexExpressions must be set when cross project is enabled"; + + ActionListener>> responsesByProjectListener = listener + .delegateFailureAndWrap( + (l, responsesByProject) -> mergeResolvedIndices( + originalResolvedIndices, + responsesByProject, + rewritten, + resolutionIdxOpts, + l + ) + ); + + ActionListener> resolveIndexFanOutListener; + if (numProjectsToResolve > 1) { + resolveIndexFanOutListener = new GroupedActionListener<>(numProjectsToResolve, responsesByProjectListener); + } else { + resolveIndexFanOutListener = responsesByProjectListener.map(Collections::singleton); + } + + originalResolvedIndices.getRemoteClusterIndices() + .forEach( + (projectName, projectIndices) -> resolveRemoteCrossProjectIndex( + rewritten, + resolutionIdxOpts, + projectName, + projectIndices, + resolveIndexFanOutListener + ) + ); + } else { + listener.onResponse(originalResolvedIndices); + } + } + + /** + * Only used for ccs_minimize_roundtrips=true pathway + */ + private static void mergeResolvedIndices( + ResolvedIndices originalResolvedIndices, + Collection> responsesByProject, + SearchRequest rewritten, + IndicesOptions resolutionIdxOpts, + ActionListener listener + ) { + Map resolvedExpressions = responsesByProject.stream() + .collect(Collectors.toMap(Map.Entry::getKey, response -> { + var resolvedIndexExpressions = response.getValue().getResolvedIndexExpressions(); + assert resolvedIndexExpressions != null + : "remote response from cluster [" + response.getKey() + "] is missing resolved index expressions"; + return resolvedIndexExpressions; + })); + + var ex = CrossProjectIndexResolutionValidator.validate( + rewritten.indicesOptions(), + rewritten.getProjectRouting(), + rewritten.getResolvedIndexExpressions(), + resolvedExpressions + ); + if (ex != null) { + listener.onFailure(ex); + } else { + listener.onResponse( + ResolvedIndices.resolveWithIndexExpressions( + originalResolvedIndices.getLocalIndices(), + originalResolvedIndices.getConcreteLocalIndicesMetadata(), + resolvedExpressions, + resolutionIdxOpts + ) + ); + } + } + + /** + * Only used for ccs_minimize_roundtrips=true pathway + */ + private void resolveRemoteCrossProjectIndex( + SearchRequest rewritten, + IndicesOptions resolutionIdxOpts, + String projectName, + OriginalIndices projectIndices, + ActionListener> listener + ) { + SubscribableListener connectionListener = getListenerWithOptionalTimeout( + forceConnectTimeoutSecs, + threadPool, + threadPool.executor(ThreadPool.Names.SEARCH_COORDINATION) + ); + + connectionListener.addListener( + listener.delegateFailure( + (responseListener, connection) -> transportService.sendRequest( + connection, + ResolveIndexAction.REMOTE_TYPE.name(), + new ResolveIndexAction.Request(projectIndices.indices(), resolutionIdxOpts), + TransportRequestOptions.EMPTY, + new ActionListenerResponseHandler<>( + listener.map(response -> Map.entry(projectName, response)), + ResolveIndexAction.Response::new, + threadPool.executor(ThreadPool.Names.SEARCH_COORDINATION) + ) + ) + ) + ); + remoteClusterService.maybeEnsureConnectedAndGetConnection( + projectName, + shouldEstablishConnection( + forceConnectTimeoutSecs, + remoteClusterService.shouldSkipOnFailure(projectName, rewritten.allowPartialSearchResults()) + ), + connectionListener + ); + } + /** * Collect remote search shards that we need to search for potential matches. * Used for ccs_minimize_roundtrips=false diff --git a/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java b/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java new file mode 100644 index 0000000000000..7b60d14440b4d --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java @@ -0,0 +1,210 @@ +/* + * 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.action; + +import org.elasticsearch.action.support.IndicesOptions; +import org.elasticsearch.test.ESTestCase; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_NOT_VISIBLE; +import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.CONCRETE_RESOURCE_UNAUTHORIZED; +import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.NONE; +import static org.elasticsearch.action.ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS; +import static org.elasticsearch.search.crossproject.CrossProjectIndexResolutionValidator.indicesOptionsForCrossProjectFanout; +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; + +public class ResolvedIndicesTests extends ESTestCase { + private static final Set NO_REMOTE_EXPRESSIONS = Set.of(); + + /** + * Given project-1 resolved to foo indices foo-1, foo-2, foo-3 + * When we create ResolvedIndices + * Then project-1 maps to foo + */ + public void testResolveWithIndexExpressions() { + var remoteExpressions = Map.of( + "project-1", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "foo", + new ResolvedIndexExpression.LocalExpressions(Set.of("foo-1", "foo-2", "foo-3"), SUCCESS, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ); + + var resolvedIndices = resolveWithIndexExpressions(remoteExpressions); + + assertThat(resolvedIndices.getRemoteClusterIndices(), is(not(anEmptyMap()))); + + assertThat(resolvedIndices.getRemoteClusterIndices(), hasKey("project-1")); + assertThat(Arrays.stream(resolvedIndices.getRemoteClusterIndices().get("project-1").indices()).toList(), containsInAnyOrder("foo")); + } + + /** + * Given project-1 resolved logs* to no indices + * When we create ResolvedIndices + * Then we do not include project-1 in the results + */ + public void testResolveWithIndexExpressionsWithEmptyWildcard() { + var remoteExpressions = Map.of( + "project-1", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "logs*", + new ResolvedIndexExpression.LocalExpressions(Set.of(), SUCCESS, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ); + + var resolvedIndices = resolveWithIndexExpressions(remoteExpressions); + + assertThat(resolvedIndices.getRemoteClusterIndices(), is(anEmptyMap())); + } + + /** + * Given project-1 resolved logs to no indices + * When we create ResolvedIndices + * Then we do not include project-1 in the results + */ + public void testResolveWithIndexExpressionsWithNoMatch() { + var localExpressionNotFoundReason = randomFrom(CONCRETE_RESOURCE_NOT_VISIBLE, CONCRETE_RESOURCE_UNAUTHORIZED, NONE); + var remoteExpressions = Map.of( + "project-1", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "logs", + new ResolvedIndexExpression.LocalExpressions(Set.of(), localExpressionNotFoundReason, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ); + + var resolvedIndices = resolveWithIndexExpressions(remoteExpressions); + + assertThat(resolvedIndices.getRemoteClusterIndices(), is(anEmptyMap())); + } + + /** + * Given project-1 resolved foo to foo-1, bar to no indices, and blah to no indices + * And project-2 resolved foo to no indices, bar to bar-1, and blah to no indices + * And project-3 resolved foo to no indices, bar to no indices, and blah to no indices + * When we create ResolvedIndices + * Then project-1 maps to foo + * And project-2 maps to bar + * And we do not include project-3 in the results + */ + public void testResolveWithIndexExpressionsWithMultipleProjects() { + var remoteExpressions = Map.ofEntries( + Map.entry( + "project-1", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "foo", + new ResolvedIndexExpression.LocalExpressions(Set.of("foo-1"), SUCCESS, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "bar", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "blah", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ), + Map.entry( + "project-2", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "foo", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "bar", + new ResolvedIndexExpression.LocalExpressions(Set.of("bar-1"), SUCCESS, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "blah", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ), + Map.entry( + "project-3", + new ResolvedIndexExpressions( + List.of( + new ResolvedIndexExpression( + "foo", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "bar", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ), + new ResolvedIndexExpression( + "blah", + new ResolvedIndexExpression.LocalExpressions(Set.of(), NONE, null), + NO_REMOTE_EXPRESSIONS + ) + ) + ) + ) + ); + + var resolvedIndices = resolveWithIndexExpressions(remoteExpressions); + + assertThat(resolvedIndices.getRemoteClusterIndices(), is(not(anEmptyMap()))); + + assertThat(resolvedIndices.getRemoteClusterIndices(), hasKey("project-1")); + assertThat(resolvedIndices.getRemoteClusterIndices(), hasKey("project-2")); + assertThat(resolvedIndices.getRemoteClusterIndices(), not(hasKey("project-3"))); + assertThat(Arrays.stream(resolvedIndices.getRemoteClusterIndices().get("project-1").indices()).toList(), containsInAnyOrder("foo")); + assertThat(Arrays.stream(resolvedIndices.getRemoteClusterIndices().get("project-2").indices()).toList(), containsInAnyOrder("bar")); + } + + private static ResolvedIndices resolveWithIndexExpressions(Map remoteExpressions) { + var cpsIndicesOptions = indicesOptionsForCrossProjectFanout(IndicesOptions.DEFAULT); + return ResolvedIndices.resolveWithIndexExpressions( + new OriginalIndices(new String[] { "some-local-index" }, cpsIndicesOptions), + Map.of(mock(), mock()), + remoteExpressions, + cpsIndicesOptions + ); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchPhaseControllerTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchPhaseControllerTests.java index 2db3b4f1e7ec8..8f57da5e157cb 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchPhaseControllerTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchPhaseControllerTests.java @@ -586,7 +586,15 @@ private static AtomicArray generateFetchResults( private static SearchRequest randomSearchRequest() { return randomBoolean() ? new SearchRequest() - : SearchRequest.subSearchRequest(new TaskId("n", 1), new SearchRequest(), Strings.EMPTY_ARRAY, "remote", 0, randomBoolean()); + : SearchRequest.subSearchRequest( + new TaskId("n", 1), + new SearchRequest(), + Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, + "remote", + 0, + randomBoolean() + ); } public void testConsumer() throws Exception { diff --git a/server/src/test/java/org/elasticsearch/action/search/SearchRequestTests.java b/server/src/test/java/org/elasticsearch/action/search/SearchRequestTests.java index 25c4c1672e852..bfdf3d71f3463 100644 --- a/server/src/test/java/org/elasticsearch/action/search/SearchRequestTests.java +++ b/server/src/test/java/org/elasticsearch/action/search/SearchRequestTests.java @@ -64,6 +64,7 @@ protected SearchRequest createSearchRequest() throws IOException { new TaskId("node", 1), request, request.indices(), + request.indicesOptions(), randomAlphaOfLengthBetween(5, 10), randomNonNegativeLong(), randomBoolean() @@ -74,23 +75,42 @@ public void testWithLocalReduction() { final TaskId taskId = new TaskId("n", 1); expectThrows( NullPointerException.class, - () -> SearchRequest.subSearchRequest(taskId, null, Strings.EMPTY_ARRAY, "", 0, randomBoolean()) + () -> SearchRequest.subSearchRequest( + taskId, + null, + Strings.EMPTY_ARRAY, + SearchRequest.DEFAULT_INDICES_OPTIONS, + "", + 0, + randomBoolean() + ) ); SearchRequest request = new SearchRequest(); - expectThrows(NullPointerException.class, () -> SearchRequest.subSearchRequest(taskId, request, null, "", 0, randomBoolean())); expectThrows( NullPointerException.class, - () -> SearchRequest.subSearchRequest(taskId, request, new String[] { null }, "", 0, randomBoolean()) + () -> SearchRequest.subSearchRequest(taskId, request, null, request.indicesOptions(), "", 0, randomBoolean()) + ); + expectThrows( + NullPointerException.class, + () -> SearchRequest.subSearchRequest(taskId, request, new String[] { null }, request.indicesOptions(), "", 0, randomBoolean()) ); expectThrows( NullPointerException.class, - () -> SearchRequest.subSearchRequest(taskId, request, Strings.EMPTY_ARRAY, null, 0, randomBoolean()) + () -> SearchRequest.subSearchRequest(taskId, request, Strings.EMPTY_ARRAY, request.indicesOptions(), null, 0, randomBoolean()) ); expectThrows( IllegalArgumentException.class, - () -> SearchRequest.subSearchRequest(taskId, request, Strings.EMPTY_ARRAY, "", -1, randomBoolean()) + () -> SearchRequest.subSearchRequest(taskId, request, Strings.EMPTY_ARRAY, request.indicesOptions(), "", -1, randomBoolean()) + ); + SearchRequest searchRequest = SearchRequest.subSearchRequest( + taskId, + request, + Strings.EMPTY_ARRAY, + request.indicesOptions(), + "", + 0, + randomBoolean() ); - SearchRequest searchRequest = SearchRequest.subSearchRequest(taskId, request, Strings.EMPTY_ARRAY, "", 0, randomBoolean()); assertNull(searchRequest.validate()); }