diff --git a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java index 30d8ebceb635e..a90506c52a6a6 100644 --- a/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java +++ b/server/src/main/java/org/elasticsearch/action/ResolvedIndices.java @@ -240,44 +240,6 @@ 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/TransportSearchAction.java b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java index fcf087684568e..854a8c0421f34 100644 --- a/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java +++ b/server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java @@ -1236,14 +1236,17 @@ private static void mergeResolvedIndices( if (ex != null) { listener.onFailure(ex); } else { - ResolvedIndices resolvedIndicesForCps = ResolvedIndices.resolveWithIndexExpressions( - originalResolvedIndices.getLocalIndices(), - originalResolvedIndices.getConcreteLocalIndicesMetadata(), - resolvedExpressions, - resolutionIdxOpts - ); - - HashMap participatingLinkedProjects = new HashMap<>(resolvedIndicesForCps.getRemoteClusterIndices()); + HashMap participatingLinkedProjects = new HashMap<>(); + for (var entry : resolvedExpressions.entrySet()) { + boolean hasAnyResolvedIndices = entry.getValue().expressions().stream().anyMatch(expression -> { + var localExpressions = expression.localExpressions(); + return localExpressions.localIndexResolutionResult() == ResolvedIndexExpression.LocalIndexResolutionResult.SUCCESS + && localExpressions.indices().isEmpty() == false; + }); + if (hasAnyResolvedIndices) { + participatingLinkedProjects.put(entry.getKey(), originalResolvedIndices.getRemoteClusterIndices().get(entry.getKey())); + } + } /* * Because we're considering unresponsive projects as participating and instantiating a `Clusters` object based @@ -1261,7 +1264,7 @@ private static void mergeResolvedIndices( new ResolvedIndices( participatingLinkedProjects, includeOriginProjectInMetadata ? originalResolvedIndices.getLocalIndices() : null, - resolvedIndicesForCps.getConcreteLocalIndicesMetadata() + originalResolvedIndices.getConcreteLocalIndicesMetadata() ) ); } diff --git a/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java b/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java deleted file mode 100644 index 7b60d14440b4d..0000000000000 --- a/server/src/test/java/org/elasticsearch/action/ResolvedIndicesTests.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 - ); - } -}