Skip to content

Commit 6aa1eb4

Browse files
Add support for project_routing for _search and _async_search (#137566)
Add `project_routing` support for `_search` and `_async_search`
1 parent 85a4782 commit 6aa1eb4

File tree

11 files changed

+158
-19
lines changed

11 files changed

+158
-19
lines changed

docs/changelog/137566.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 137566
2+
summary: Add support for `project_routing` for `_search` and `_async_search`
3+
area: CCS
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/action/search/SearchRequest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public class SearchRequest extends LegacyActionRequest implements IndicesRequest
117117
*/
118118
private boolean forceSyntheticSource = false;
119119

120+
@Nullable
121+
private String projectRouting;
122+
123+
private static final TransportVersion SEARCH_PROJECT_ROUTING = TransportVersion.fromName("search_project_routing");
124+
120125
public SearchRequest() {
121126
this.localClusterAlias = null;
122127
this.absoluteStartMillis = DEFAULT_ABSOLUTE_START_MILLIS;
@@ -168,6 +173,19 @@ public boolean allowsCrossProject() {
168173
return true;
169174
}
170175

176+
@Override
177+
public String getProjectRouting() {
178+
return projectRouting;
179+
}
180+
181+
public void setProjectRouting(@Nullable String projectRouting) {
182+
if (this.projectRouting != null) {
183+
throw new IllegalArgumentException("project_routing is already set to [" + this.projectRouting + "]");
184+
}
185+
186+
this.projectRouting = projectRouting;
187+
}
188+
171189
/**
172190
* Creates a new sub-search request starting from the original search request that is provided.
173191
* For internal use only, allows to fork a search request into multiple search requests that will be executed independently.
@@ -228,6 +246,7 @@ private SearchRequest(
228246
this.waitForCheckpoints = searchRequest.waitForCheckpoints;
229247
this.waitForCheckpointsTimeout = searchRequest.waitForCheckpointsTimeout;
230248
this.forceSyntheticSource = searchRequest.forceSyntheticSource;
249+
this.projectRouting = searchRequest.projectRouting;
231250
}
232251

233252
/**
@@ -278,6 +297,11 @@ public SearchRequest(StreamInput in) throws IOException {
278297
} else {
279298
forceSyntheticSource = false;
280299
}
300+
if (in.getTransportVersion().supports(SEARCH_PROJECT_ROUTING)) {
301+
this.projectRouting = in.readOptionalString();
302+
} else {
303+
this.projectRouting = null;
304+
}
281305
}
282306

283307
@Override
@@ -324,6 +348,9 @@ public void writeTo(StreamOutput out, boolean skipIndices) throws IOException {
324348
throw new IllegalArgumentException("force_synthetic_source is not supported before 8.4.0");
325349
}
326350
}
351+
if (out.getTransportVersion().supports(SEARCH_PROJECT_ROUTING)) {
352+
out.writeOptionalString(this.projectRouting);
353+
}
327354
}
328355

329356
@Override

server/src/main/java/org/elasticsearch/action/search/TransportSearchAction.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,8 @@ public void onFailure(Exception e) {
620620
}),
621621
forceConnectTimeoutSecs,
622622
resolvesCrossProject,
623-
rewritten.getResolvedIndexExpressions()
623+
rewritten.getResolvedIndexExpressions(),
624+
rewritten.getProjectRouting()
624625
);
625626
}
626627
}
@@ -1065,7 +1066,8 @@ static void collectSearchShards(
10651066
ActionListener<Map<String, SearchShardsResponse>> listener,
10661067
TimeValue forceConnectTimeoutSecs,
10671068
boolean resolvesCrossProject,
1068-
ResolvedIndexExpressions originResolvedIdxExpressions
1069+
ResolvedIndexExpressions originResolvedIdxExpressions,
1070+
String projectRouting
10691071
) {
10701072
RemoteClusterService remoteClusterService = transportService.getRemoteClusterService();
10711073
final CountDown responsesCountDown = new CountDown(remoteIndicesByCluster.size());
@@ -1104,7 +1106,7 @@ Map<String, SearchShardsResponse> createFinalResponse() {
11041106
// We do not use the relaxed index options here when validating indices' existence.
11051107
ElasticsearchException validationEx = CrossProjectIndexResolutionValidator.validate(
11061108
originalIdxOpts,
1107-
null,
1109+
projectRouting,
11081110
originResolvedIdxExpressions,
11091111
resolvedIndexExpressions
11101112
);

server/src/main/java/org/elasticsearch/rest/action/search/RestSearchAction.java

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.elasticsearch.rest.action.RestRefCountedChunkedToXContentListener;
3131
import org.elasticsearch.search.SearchService;
3232
import org.elasticsearch.search.builder.SearchSourceBuilder;
33+
import org.elasticsearch.search.crossproject.CrossProjectModeDecider;
3334
import org.elasticsearch.search.fetch.StoredFieldsContext;
3435
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
3536
import org.elasticsearch.search.internal.SearchContext;
@@ -68,15 +69,13 @@ public class RestSearchAction extends BaseRestHandler {
6869
private final SearchUsageHolder searchUsageHolder;
6970
private final Predicate<NodeFeature> clusterSupportsFeature;
7071
private final Settings settings;
71-
72-
public RestSearchAction(SearchUsageHolder searchUsageHolder, Predicate<NodeFeature> clusterSupportsFeature) {
73-
this(searchUsageHolder, clusterSupportsFeature, null);
74-
}
72+
private final CrossProjectModeDecider crossProjectModeDecider;
7573

7674
public RestSearchAction(SearchUsageHolder searchUsageHolder, Predicate<NodeFeature> clusterSupportsFeature, Settings settings) {
7775
this.searchUsageHolder = searchUsageHolder;
7876
this.clusterSupportsFeature = clusterSupportsFeature;
7977
this.settings = settings;
78+
this.crossProjectModeDecider = new CrossProjectModeDecider(settings);
8079
}
8180

8281
@Override
@@ -109,10 +108,9 @@ public RestChannelConsumer prepareRequest(final RestRequest request, final NodeC
109108
// this might be set by old clients
110109
request.param("min_compatible_shard_node");
111110

112-
final boolean crossProjectEnabled = settings != null && settings.getAsBoolean("serverless.cross_project.enabled", false);
111+
final boolean crossProjectEnabled = crossProjectModeDecider.crossProjectEnabled();
113112
if (crossProjectEnabled) {
114-
// accept but drop project_routing param until fully supported
115-
request.param("project_routing");
113+
searchRequest.setProjectRouting(request.param("project_routing"));
116114
}
117115

118116
/*
@@ -202,11 +200,18 @@ public static void parseSearchRequest(
202200
searchRequest.source(new SearchSourceBuilder());
203201
}
204202
searchRequest.indices(Strings.splitStringByCommaToArray(request.param("index")));
203+
/*
204+
* We pass this object to the request body parser so that we can extract info such as project_routing.
205+
* We only do it if in a Cross Project Environment, though, because outside it, such details are not
206+
* expected and valid.
207+
*/
208+
SearchRequest searchRequestForParsing = crossProjectEnabled ? searchRequest : null;
205209
if (requestContentParser != null) {
206210
if (searchUsageHolder == null) {
207-
searchRequest.source().parseXContent(requestContentParser, true, clusterSupportsFeature);
211+
searchRequest.source().parseXContent(searchRequestForParsing, requestContentParser, true, clusterSupportsFeature);
208212
} else {
209-
searchRequest.source().parseXContent(requestContentParser, true, searchUsageHolder, clusterSupportsFeature);
213+
searchRequest.source()
214+
.parseXContent(searchRequestForParsing, requestContentParser, true, searchUsageHolder, clusterSupportsFeature);
210215
}
211216
}
212217

server/src/main/java/org/elasticsearch/search/builder/SearchSourceBuilder.java

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ public final class SearchSourceBuilder implements Writeable, ToXContentObject, R
129129
public static final ParseField POINT_IN_TIME = new ParseField("pit");
130130
public static final ParseField RUNTIME_MAPPINGS_FIELD = new ParseField("runtime_mappings");
131131
public static final ParseField RETRIEVER = new ParseField("retriever");
132+
public static final ParseField PROJECT_ROUTING = new ParseField("project_routing");
132133

133134
private static final boolean RANK_SUPPORTED = Booleans.parseBoolean(System.getProperty("es.search.rank_supported"), true);
134135

@@ -1329,18 +1330,62 @@ private SearchSourceBuilder shallowCopy(
13291330
/**
13301331
* Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent.
13311332
*
1333+
* @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive
1334+
* the parsed info. Currently, this is non-null only if we expect project_routing to appear in
1335+
* the request body, and we allow it to appear because we're in a Cross Project Search
1336+
* environment and require this info.
13321337
* @param parser The xContent parser.
13331338
* @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object.
13341339
* @param searchUsageHolder holder for the search usage statistics
13351340
* @param clusterSupportsFeature used to check if certain features are available on this cluster
13361341
*/
13371342
public SearchSourceBuilder parseXContent(
1343+
SearchRequest searchRequest,
13381344
XContentParser parser,
13391345
boolean checkTrailingTokens,
13401346
SearchUsageHolder searchUsageHolder,
13411347
Predicate<NodeFeature> clusterSupportsFeature
13421348
) throws IOException {
1343-
return parseXContent(parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature);
1349+
return parseXContent(searchRequest, parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature);
1350+
}
1351+
1352+
/**
1353+
* Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent.
1354+
*
1355+
* @param parser The xContent parser.
1356+
* @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object.
1357+
* @param searchUsageHolder holder for the search usage statistics
1358+
* @param clusterSupportsFeature used to check if certain features are available on this cluster
1359+
*/
1360+
public SearchSourceBuilder parseXContent(
1361+
XContentParser parser,
1362+
boolean checkTrailingTokens,
1363+
SearchUsageHolder searchUsageHolder,
1364+
Predicate<NodeFeature> clusterSupportsFeature
1365+
) throws IOException {
1366+
return parseXContent(null, parser, checkTrailingTokens, searchUsageHolder::updateUsage, clusterSupportsFeature);
1367+
}
1368+
1369+
/**
1370+
* Parse some xContent into this SearchSourceBuilder, overwriting any values specified in the xContent.
1371+
* This variant does not record search features usage. Most times the variant that accepts a {@link SearchUsageHolder} and records
1372+
* usage stats into it is the one to use.
1373+
*
1374+
* @param searchRequest The SearchRequest object that's representing the request we're parsing which shall receive
1375+
* the parsed info. Currently, this is non-null only if we expect project_routing to appear in
1376+
* the request body, and we allow it to appear because we're in a Cross Project Search
1377+
* environment and require this info.
1378+
* @param parser The xContent parser.
1379+
* @param checkTrailingTokens If true throws a parsing exception when extra tokens are found after the main object.
1380+
* @param clusterSupportsFeature used to check if certain features are available on this cluster
1381+
*/
1382+
public SearchSourceBuilder parseXContent(
1383+
SearchRequest searchRequest,
1384+
XContentParser parser,
1385+
boolean checkTrailingTokens,
1386+
Predicate<NodeFeature> clusterSupportsFeature
1387+
) throws IOException {
1388+
return parseXContent(searchRequest, parser, checkTrailingTokens, s -> {}, clusterSupportsFeature);
13441389
}
13451390

13461391
/**
@@ -1357,10 +1402,11 @@ public SearchSourceBuilder parseXContent(
13571402
boolean checkTrailingTokens,
13581403
Predicate<NodeFeature> clusterSupportsFeature
13591404
) throws IOException {
1360-
return parseXContent(parser, checkTrailingTokens, s -> {}, clusterSupportsFeature);
1405+
return parseXContent(null, parser, checkTrailingTokens, s -> {}, clusterSupportsFeature);
13611406
}
13621407

13631408
private SearchSourceBuilder parseXContent(
1409+
SearchRequest searchRequest,
13641410
XContentParser parser,
13651411
boolean checkTrailingTokens,
13661412
Consumer<SearchUsage> searchUsageConsumer,
@@ -1382,7 +1428,13 @@ private SearchSourceBuilder parseXContent(
13821428
if (token == XContentParser.Token.FIELD_NAME) {
13831429
currentFieldName = parser.currentName();
13841430
} else if (token.isValue()) {
1385-
if (FROM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
1431+
if (PROJECT_ROUTING.match(currentFieldName, parser.getDeprecationHandler()) && searchRequest != null) {
1432+
/*
1433+
* If project_routing was specified as a query parameter too, setProjectRouting() will throw
1434+
* an error to prevent setting twice or overwriting previously set value.
1435+
*/
1436+
searchRequest.setProjectRouting(parser.text());
1437+
} else if (FROM_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
13861438
from(parser.intValue());
13871439
} else if (SIZE_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
13881440
size(parser.intValue());
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9219000
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ml_inference_openshift_ai_added,9218000
1+
search_project_routing,9219000

server/src/test/java/org/elasticsearch/action/search/TransportSearchActionTests.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,7 @@ public void testCollectSearchShards() throws Exception {
11041104
new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch),
11051105
null,
11061106
false,
1107+
null,
11071108
null
11081109
);
11091110
awaitLatch(latch, 5, TimeUnit.SECONDS);
@@ -1136,6 +1137,7 @@ public void testCollectSearchShards() throws Exception {
11361137
new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch),
11371138
null,
11381139
false,
1140+
null,
11391141
null
11401142
);
11411143
awaitLatch(latch, 5, TimeUnit.SECONDS);
@@ -1191,6 +1193,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce
11911193
new LatchedActionListener<>(ActionListener.wrap(r -> fail("no response expected"), failure::set), latch),
11921194
null,
11931195
false,
1196+
null,
11941197
null
11951198
);
11961199
awaitLatch(latch, 5, TimeUnit.SECONDS);
@@ -1224,6 +1227,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce
12241227
new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch),
12251228
null,
12261229
false,
1230+
null,
12271231
null
12281232
);
12291233
awaitLatch(latch, 5, TimeUnit.SECONDS);
@@ -1273,6 +1277,7 @@ public void onNodeDisconnected(DiscoveryNode node, @Nullable Exception closeExce
12731277
new LatchedActionListener<>(ActionTestUtils.assertNoFailureListener(response::set), latch),
12741278
null,
12751279
false,
1280+
null,
12761281
null
12771282
);
12781283
awaitLatch(latch, 5, TimeUnit.SECONDS);

server/src/test/java/org/elasticsearch/rest/action/search/RestSearchActionTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.action.search.SearchRequest;
1212
import org.elasticsearch.action.search.SearchResponse;
1313
import org.elasticsearch.action.search.SearchType;
14+
import org.elasticsearch.common.settings.Settings;
1415
import org.elasticsearch.rest.RestRequest;
1516
import org.elasticsearch.search.builder.SearchSourceBuilder;
1617
import org.elasticsearch.search.suggest.SuggestBuilder;
@@ -33,7 +34,7 @@ public final class RestSearchActionTests extends RestActionTestCase {
3334

3435
@Before
3536
public void setUpAction() {
36-
action = new RestSearchAction(new UsageService().getSearchUsageHolder(), nf -> false);
37+
action = new RestSearchAction(new UsageService().getSearchUsageHolder(), nf -> false, Settings.EMPTY);
3738
controller().registerHandler(action);
3839
verifyingClient.setExecuteVerifier((actionType, request) -> mock(SearchResponse.class));
3940
verifyingClient.setExecuteLocallyVerifier((actionType, request) -> mock(SearchResponse.class));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.search;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.client.ResponseException;
12+
import org.elasticsearch.common.util.CollectionUtils;
13+
import org.elasticsearch.plugins.Plugin;
14+
import org.elasticsearch.test.ESIntegTestCase;
15+
import org.hamcrest.Matchers;
16+
17+
import java.io.IOException;
18+
import java.util.Collection;
19+
20+
public class ProjectRoutingDisallowedInNonCpsEnvIT extends ESIntegTestCase {
21+
@Override
22+
protected boolean addMockHttpTransport() {
23+
return false;
24+
}
25+
26+
@Override
27+
protected Collection<Class<? extends Plugin>> nodePlugins() {
28+
return CollectionUtils.appendToCopyNoNullElements(super.nodePlugins(), AsyncSearch.class);
29+
}
30+
31+
public void testDisallowProjectRouting() throws IOException {
32+
Request createAsyncRequest = new Request("POST", "/*,*:*/" + randomFrom("_async_search", "_search"));
33+
createAsyncRequest.setJsonEntity("""
34+
{
35+
"project_routing": "_alias:_origin"
36+
}
37+
""");
38+
39+
ResponseException err = expectThrows(ResponseException.class, () -> getRestClient().performRequest(createAsyncRequest));
40+
assertThat(err.toString(), Matchers.containsString("Unknown key for a VALUE_STRING in [project_routing]"));
41+
}
42+
}

0 commit comments

Comments
 (0)