Skip to content

Commit f16c2ff

Browse files
authored
Add a Multi-Project Search Rest Test (#128657)
This commit adds a Rest IT specifically for search in MultiProject. Everything was already working as expected, but we were a bit light on explicit testing for search, which as _the_ core capability of Elasticsearch is worth testing thoroughly and clearly.
1 parent ab4cc0c commit f16c2ff

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
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", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.action.admin.indices;
11+
12+
import org.elasticsearch.client.Request;
13+
import org.elasticsearch.client.Response;
14+
import org.elasticsearch.client.ResponseException;
15+
import org.elasticsearch.cluster.metadata.ProjectId;
16+
import org.elasticsearch.common.Strings;
17+
import org.elasticsearch.common.settings.SecureString;
18+
import org.elasticsearch.common.settings.Settings;
19+
import org.elasticsearch.common.util.concurrent.ThreadContext;
20+
import org.elasticsearch.multiproject.MultiProjectRestTestCase;
21+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
22+
import org.elasticsearch.test.cluster.local.LocalClusterSpecBuilder;
23+
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
24+
import org.elasticsearch.test.rest.ObjectPath;
25+
import org.junit.ClassRule;
26+
import org.junit.Rule;
27+
import org.junit.rules.TestName;
28+
29+
import java.io.IOException;
30+
import java.util.List;
31+
import java.util.Locale;
32+
import java.util.Map;
33+
34+
import static org.hamcrest.Matchers.aMapWithSize;
35+
import static org.hamcrest.Matchers.containsInAnyOrder;
36+
import static org.hamcrest.Matchers.containsString;
37+
import static org.hamcrest.Matchers.empty;
38+
import static org.hamcrest.Matchers.equalTo;
39+
import static org.hamcrest.Matchers.greaterThan;
40+
import static org.hamcrest.Matchers.instanceOf;
41+
import static org.hamcrest.Matchers.notNullValue;
42+
43+
public class SearchMultiProjectIT extends MultiProjectRestTestCase {
44+
45+
private static final String PASSWORD = "hunter2";
46+
47+
@ClassRule
48+
public static ElasticsearchCluster cluster = createCluster();
49+
50+
@Rule
51+
public final TestName testNameRule = new TestName();
52+
53+
private static ElasticsearchCluster createCluster() {
54+
LocalClusterSpecBuilder<ElasticsearchCluster> clusterBuilder = ElasticsearchCluster.local()
55+
.nodes(1)
56+
.distribution(DistributionType.INTEG_TEST)
57+
.module("test-multi-project")
58+
.setting("test.multi_project.enabled", "true")
59+
.setting("xpack.security.enabled", "true")
60+
.user("admin", PASSWORD);
61+
return clusterBuilder.build();
62+
}
63+
64+
@Override
65+
protected String getTestRestCluster() {
66+
return cluster.getHttpAddresses();
67+
}
68+
69+
@Override
70+
protected Settings restClientSettings() {
71+
final String token = basicAuthHeaderValue("admin", new SecureString(PASSWORD.toCharArray()));
72+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
73+
}
74+
75+
public void testSearchIndexThatExistsInMultipleProjects() throws Exception {
76+
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
77+
createProject(projectId1.id());
78+
79+
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
80+
createProject(projectId2.id());
81+
82+
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
83+
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
84+
85+
createIndex(projectId1, indexName);
86+
String docId1 = putDocument(projectId1, indexName, "{\"project\": 1 }", true);
87+
88+
createIndex(projectId2, indexName);
89+
String docId2a = putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"a\" }", false);
90+
String docId2b = putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"b\" }", true);
91+
92+
List<String> results1 = search(projectId1, indexName);
93+
assertThat(results1, containsInAnyOrder(docId1));
94+
95+
List<String> results2 = search(projectId2, indexName);
96+
assertThat(results2, containsInAnyOrder(docId2a, docId2b));
97+
98+
final var query = """
99+
{
100+
"query": { "term": { "project": 1 } }
101+
}
102+
""";
103+
results1 = getHitIds(search(projectId1, indexPrefix + "-*", query));
104+
assertThat(results1, containsInAnyOrder(docId1));
105+
106+
results2 = getHitIds(search(projectId2, indexPrefix + "-*", query));
107+
assertThat(results2, empty());
108+
109+
final String aliasName = indexPrefix + "-" + randomIntBetween(100, 999);
110+
addAlias(projectId1, indexName, aliasName);
111+
112+
results1 = search(projectId1, aliasName);
113+
assertThat(results1, containsInAnyOrder(docId1));
114+
115+
assertIndexNotFound(projectId2, aliasName);
116+
117+
addAlias(projectId2, indexName, aliasName);
118+
results2 = search(projectId2, indexName);
119+
assertThat(results2, containsInAnyOrder(docId2a, docId2b));
120+
121+
results1 = search(projectId1, indexPrefix + "-*");
122+
assertThat(results1, containsInAnyOrder(docId1));
123+
}
124+
125+
public void testIndexNotVisibleAcrossProjects() throws IOException {
126+
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
127+
createProject(projectId1.id());
128+
129+
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
130+
createProject(projectId2.id());
131+
132+
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
133+
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
134+
135+
createIndex(projectId1, indexName);
136+
String docId1 = putDocument(projectId1, indexName, "{\"project\": 1 }", true);
137+
138+
List<String> results1 = search(projectId1, indexName);
139+
assertThat(results1, containsInAnyOrder(docId1));
140+
141+
assertIndexNotFound(projectId2, indexName);
142+
143+
results1 = search(projectId1, indexPrefix + "-*");
144+
assertThat(results1, containsInAnyOrder(docId1));
145+
146+
List<String> results2 = search(projectId2, indexPrefix + "-*");
147+
assertThat(results2, empty());
148+
149+
results2 = search(projectId2, "");
150+
assertThat(results2, empty());
151+
}
152+
153+
public void testRequestCacheIsNotSharedAcrossProjects() throws IOException {
154+
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
155+
createProject(projectId1.id());
156+
157+
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
158+
createProject(projectId2.id());
159+
160+
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
161+
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
162+
163+
createIndex(projectId1, indexName);
164+
putDocument(projectId1, indexName, "{\"project\": 1 }", true);
165+
166+
createIndex(projectId2, indexName);
167+
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"a\" }", false);
168+
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"b\" }", false);
169+
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"c\" }", true);
170+
171+
final long initialCacheSize = getRequestCacheUsage();
172+
173+
final var query = """
174+
{
175+
"size": 0,
176+
"aggs": {
177+
"proj": { "terms": { "field": "project" } }
178+
}
179+
}
180+
""";
181+
182+
// Perform a search in project 1 that should be cached in shard request cache
183+
// That is, an aggregation with size:0
184+
ObjectPath response = search(projectId1, indexName, query);
185+
String context = "In search response: " + response;
186+
assertThat(context, response.evaluateArraySize("aggregations.proj.buckets"), equalTo(1));
187+
assertThat(context, response.evaluate("aggregations.proj.buckets.0.key"), equalTo(1));
188+
assertThat(context, response.evaluate("aggregations.proj.buckets.0.doc_count"), equalTo(1));
189+
190+
final long agg1CacheSize = getRequestCacheUsage();
191+
assertThat("Expected aggregation result to be stored in shard request cache", agg1CacheSize, greaterThan(initialCacheSize));
192+
193+
// Perform the identical search on project 2 and make sure it returns the right results for the project
194+
response = search(projectId2, indexName, query);
195+
context = "In search response: " + response;
196+
assertThat(context, response.evaluateArraySize("aggregations.proj.buckets"), equalTo(1));
197+
assertThat(context, response.evaluate("aggregations.proj.buckets.0.key"), equalTo(2));
198+
assertThat(context, response.evaluate("aggregations.proj.buckets.0.doc_count"), equalTo(3));
199+
200+
final long agg2CacheSize = getRequestCacheUsage();
201+
assertThat("Expected aggregation result to be stored in shard request cache", agg2CacheSize, greaterThan(agg1CacheSize));
202+
}
203+
204+
private void createIndex(ProjectId projectId, String indexName) throws IOException {
205+
Request request = new Request("PUT", "/" + indexName);
206+
setRequestProjectId(request, projectId.id());
207+
Response response = client().performRequest(request);
208+
assertOK(response);
209+
}
210+
211+
private void addAlias(ProjectId projectId, String indexName, String alias) throws IOException {
212+
Request request = new Request("POST", "/_aliases");
213+
request.setJsonEntity(Strings.format("""
214+
{
215+
"actions": [
216+
{
217+
"add": {
218+
"index": "%s",
219+
"alias": "%s"
220+
}
221+
}
222+
]
223+
}
224+
""", indexName, alias));
225+
setRequestProjectId(request, projectId.id());
226+
Response response = client().performRequest(request);
227+
assertOK(response);
228+
}
229+
230+
private String putDocument(ProjectId projectId, String indexName, String body, boolean refresh) throws IOException {
231+
Request request = new Request("POST", "/" + indexName + "/_doc?refresh=" + refresh);
232+
request.setJsonEntity(body);
233+
setRequestProjectId(request, projectId.id());
234+
Response response = client().performRequest(request);
235+
assertOK(response);
236+
return String.valueOf(entityAsMap(response).get("_id"));
237+
}
238+
239+
private List<String> search(ProjectId projectId, String indexExpression) throws IOException {
240+
return getHitIds(search(projectId, indexExpression, null));
241+
}
242+
243+
private static ObjectPath search(ProjectId projectId, String indexExpression, String body) throws IOException {
244+
Request request = new Request("GET", "/" + indexExpression + "/_search");
245+
if (body != null) {
246+
request.setJsonEntity(body);
247+
}
248+
setRequestProjectId(request, projectId.id());
249+
Response response = client().performRequest(request);
250+
assertOK(response);
251+
return new ObjectPath(entityAsMap(response));
252+
}
253+
254+
private void assertIndexNotFound(ProjectId projectId2, String indexName) {
255+
ResponseException ex = expectThrows(ResponseException.class, () -> search(projectId2, indexName));
256+
assertThat(ex.getMessage(), containsString("index_not_found"));
257+
assertThat(ex.getMessage(), containsString(indexName));
258+
}
259+
260+
private static List<String> getHitIds(ObjectPath searchResponse) throws IOException {
261+
List<Map<String, ?>> ids = searchResponse.evaluate("hits.hits");
262+
return ids.stream().map(o -> String.valueOf(o.get("_id"))).toList();
263+
}
264+
265+
private long getRequestCacheUsage() throws IOException {
266+
final ObjectPath nodeStats = getNodeStats("indices/request_cache");
267+
return evaluateLong(nodeStats, "indices.request_cache.memory_size_in_bytes");
268+
}
269+
270+
private static ObjectPath getNodeStats(String stat) throws IOException {
271+
Request request = new Request("GET", "/_nodes/stats/" + stat);
272+
Response response = client().performRequest(request);
273+
assertOK(response);
274+
final Map<String, ?> responseMap = entityAsMap(response);
275+
276+
@SuppressWarnings("unchecked")
277+
final Map<String, ?> nodes = (Map<String, ?>) responseMap.get("nodes");
278+
assertThat(nodes, aMapWithSize(1));
279+
280+
ObjectPath nodeStats = new ObjectPath(nodes.values().iterator().next());
281+
return nodeStats;
282+
}
283+
284+
private static long evaluateLong(ObjectPath nodeStats, String path) throws IOException {
285+
Object size = nodeStats.evaluate(path);
286+
assertThat("did not find " + path + " in " + nodeStats, size, notNullValue());
287+
assertThat("incorrect type for " + path + " in " + nodeStats, size, instanceOf(Number.class));
288+
return ((Number) size).longValue();
289+
}
290+
291+
}

0 commit comments

Comments
 (0)