mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 01:22:26 -04:00
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.
This commit is contained in:
parent
ab4cc0c8d0
commit
f16c2ffcaa
1 changed files with 291 additions and 0 deletions
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* 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.admin.indices;
|
||||
|
||||
import org.elasticsearch.client.Request;
|
||||
import org.elasticsearch.client.Response;
|
||||
import org.elasticsearch.client.ResponseException;
|
||||
import org.elasticsearch.cluster.metadata.ProjectId;
|
||||
import org.elasticsearch.common.Strings;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||
import org.elasticsearch.multiproject.MultiProjectRestTestCase;
|
||||
import org.elasticsearch.test.cluster.ElasticsearchCluster;
|
||||
import org.elasticsearch.test.cluster.local.LocalClusterSpecBuilder;
|
||||
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
|
||||
import org.elasticsearch.test.rest.ObjectPath;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Rule;
|
||||
import org.junit.rules.TestName;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.aMapWithSize;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.hamcrest.Matchers.notNullValue;
|
||||
|
||||
public class SearchMultiProjectIT extends MultiProjectRestTestCase {
|
||||
|
||||
private static final String PASSWORD = "hunter2";
|
||||
|
||||
@ClassRule
|
||||
public static ElasticsearchCluster cluster = createCluster();
|
||||
|
||||
@Rule
|
||||
public final TestName testNameRule = new TestName();
|
||||
|
||||
private static ElasticsearchCluster createCluster() {
|
||||
LocalClusterSpecBuilder<ElasticsearchCluster> clusterBuilder = ElasticsearchCluster.local()
|
||||
.nodes(1)
|
||||
.distribution(DistributionType.INTEG_TEST)
|
||||
.module("test-multi-project")
|
||||
.setting("test.multi_project.enabled", "true")
|
||||
.setting("xpack.security.enabled", "true")
|
||||
.user("admin", PASSWORD);
|
||||
return clusterBuilder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTestRestCluster() {
|
||||
return cluster.getHttpAddresses();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Settings restClientSettings() {
|
||||
final String token = basicAuthHeaderValue("admin", new SecureString(PASSWORD.toCharArray()));
|
||||
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
|
||||
}
|
||||
|
||||
public void testSearchIndexThatExistsInMultipleProjects() throws Exception {
|
||||
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId1.id());
|
||||
|
||||
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId2.id());
|
||||
|
||||
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
|
||||
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
|
||||
|
||||
createIndex(projectId1, indexName);
|
||||
String docId1 = putDocument(projectId1, indexName, "{\"project\": 1 }", true);
|
||||
|
||||
createIndex(projectId2, indexName);
|
||||
String docId2a = putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"a\" }", false);
|
||||
String docId2b = putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"b\" }", true);
|
||||
|
||||
List<String> results1 = search(projectId1, indexName);
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
|
||||
List<String> results2 = search(projectId2, indexName);
|
||||
assertThat(results2, containsInAnyOrder(docId2a, docId2b));
|
||||
|
||||
final var query = """
|
||||
{
|
||||
"query": { "term": { "project": 1 } }
|
||||
}
|
||||
""";
|
||||
results1 = getHitIds(search(projectId1, indexPrefix + "-*", query));
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
|
||||
results2 = getHitIds(search(projectId2, indexPrefix + "-*", query));
|
||||
assertThat(results2, empty());
|
||||
|
||||
final String aliasName = indexPrefix + "-" + randomIntBetween(100, 999);
|
||||
addAlias(projectId1, indexName, aliasName);
|
||||
|
||||
results1 = search(projectId1, aliasName);
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
|
||||
assertIndexNotFound(projectId2, aliasName);
|
||||
|
||||
addAlias(projectId2, indexName, aliasName);
|
||||
results2 = search(projectId2, indexName);
|
||||
assertThat(results2, containsInAnyOrder(docId2a, docId2b));
|
||||
|
||||
results1 = search(projectId1, indexPrefix + "-*");
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
}
|
||||
|
||||
public void testIndexNotVisibleAcrossProjects() throws IOException {
|
||||
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId1.id());
|
||||
|
||||
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId2.id());
|
||||
|
||||
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
|
||||
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
|
||||
|
||||
createIndex(projectId1, indexName);
|
||||
String docId1 = putDocument(projectId1, indexName, "{\"project\": 1 }", true);
|
||||
|
||||
List<String> results1 = search(projectId1, indexName);
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
|
||||
assertIndexNotFound(projectId2, indexName);
|
||||
|
||||
results1 = search(projectId1, indexPrefix + "-*");
|
||||
assertThat(results1, containsInAnyOrder(docId1));
|
||||
|
||||
List<String> results2 = search(projectId2, indexPrefix + "-*");
|
||||
assertThat(results2, empty());
|
||||
|
||||
results2 = search(projectId2, "");
|
||||
assertThat(results2, empty());
|
||||
}
|
||||
|
||||
public void testRequestCacheIsNotSharedAcrossProjects() throws IOException {
|
||||
final ProjectId projectId1 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId1.id());
|
||||
|
||||
final ProjectId projectId2 = ProjectId.fromId(randomIdentifier());
|
||||
createProject(projectId2.id());
|
||||
|
||||
final String indexPrefix = getTestName().toLowerCase(Locale.ROOT);
|
||||
final String indexName = indexPrefix + "-" + randomAlphanumericOfLength(6).toLowerCase(Locale.ROOT);
|
||||
|
||||
createIndex(projectId1, indexName);
|
||||
putDocument(projectId1, indexName, "{\"project\": 1 }", true);
|
||||
|
||||
createIndex(projectId2, indexName);
|
||||
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"a\" }", false);
|
||||
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"b\" }", false);
|
||||
putDocument(projectId2, indexName, "{\"project\": 2, \"doc\": \"c\" }", true);
|
||||
|
||||
final long initialCacheSize = getRequestCacheUsage();
|
||||
|
||||
final var query = """
|
||||
{
|
||||
"size": 0,
|
||||
"aggs": {
|
||||
"proj": { "terms": { "field": "project" } }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
// Perform a search in project 1 that should be cached in shard request cache
|
||||
// That is, an aggregation with size:0
|
||||
ObjectPath response = search(projectId1, indexName, query);
|
||||
String context = "In search response: " + response;
|
||||
assertThat(context, response.evaluateArraySize("aggregations.proj.buckets"), equalTo(1));
|
||||
assertThat(context, response.evaluate("aggregations.proj.buckets.0.key"), equalTo(1));
|
||||
assertThat(context, response.evaluate("aggregations.proj.buckets.0.doc_count"), equalTo(1));
|
||||
|
||||
final long agg1CacheSize = getRequestCacheUsage();
|
||||
assertThat("Expected aggregation result to be stored in shard request cache", agg1CacheSize, greaterThan(initialCacheSize));
|
||||
|
||||
// Perform the identical search on project 2 and make sure it returns the right results for the project
|
||||
response = search(projectId2, indexName, query);
|
||||
context = "In search response: " + response;
|
||||
assertThat(context, response.evaluateArraySize("aggregations.proj.buckets"), equalTo(1));
|
||||
assertThat(context, response.evaluate("aggregations.proj.buckets.0.key"), equalTo(2));
|
||||
assertThat(context, response.evaluate("aggregations.proj.buckets.0.doc_count"), equalTo(3));
|
||||
|
||||
final long agg2CacheSize = getRequestCacheUsage();
|
||||
assertThat("Expected aggregation result to be stored in shard request cache", agg2CacheSize, greaterThan(agg1CacheSize));
|
||||
}
|
||||
|
||||
private void createIndex(ProjectId projectId, String indexName) throws IOException {
|
||||
Request request = new Request("PUT", "/" + indexName);
|
||||
setRequestProjectId(request, projectId.id());
|
||||
Response response = client().performRequest(request);
|
||||
assertOK(response);
|
||||
}
|
||||
|
||||
private void addAlias(ProjectId projectId, String indexName, String alias) throws IOException {
|
||||
Request request = new Request("POST", "/_aliases");
|
||||
request.setJsonEntity(Strings.format("""
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"add": {
|
||||
"index": "%s",
|
||||
"alias": "%s"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""", indexName, alias));
|
||||
setRequestProjectId(request, projectId.id());
|
||||
Response response = client().performRequest(request);
|
||||
assertOK(response);
|
||||
}
|
||||
|
||||
private String putDocument(ProjectId projectId, String indexName, String body, boolean refresh) throws IOException {
|
||||
Request request = new Request("POST", "/" + indexName + "/_doc?refresh=" + refresh);
|
||||
request.setJsonEntity(body);
|
||||
setRequestProjectId(request, projectId.id());
|
||||
Response response = client().performRequest(request);
|
||||
assertOK(response);
|
||||
return String.valueOf(entityAsMap(response).get("_id"));
|
||||
}
|
||||
|
||||
private List<String> search(ProjectId projectId, String indexExpression) throws IOException {
|
||||
return getHitIds(search(projectId, indexExpression, null));
|
||||
}
|
||||
|
||||
private static ObjectPath search(ProjectId projectId, String indexExpression, String body) throws IOException {
|
||||
Request request = new Request("GET", "/" + indexExpression + "/_search");
|
||||
if (body != null) {
|
||||
request.setJsonEntity(body);
|
||||
}
|
||||
setRequestProjectId(request, projectId.id());
|
||||
Response response = client().performRequest(request);
|
||||
assertOK(response);
|
||||
return new ObjectPath(entityAsMap(response));
|
||||
}
|
||||
|
||||
private void assertIndexNotFound(ProjectId projectId2, String indexName) {
|
||||
ResponseException ex = expectThrows(ResponseException.class, () -> search(projectId2, indexName));
|
||||
assertThat(ex.getMessage(), containsString("index_not_found"));
|
||||
assertThat(ex.getMessage(), containsString(indexName));
|
||||
}
|
||||
|
||||
private static List<String> getHitIds(ObjectPath searchResponse) throws IOException {
|
||||
List<Map<String, ?>> ids = searchResponse.evaluate("hits.hits");
|
||||
return ids.stream().map(o -> String.valueOf(o.get("_id"))).toList();
|
||||
}
|
||||
|
||||
private long getRequestCacheUsage() throws IOException {
|
||||
final ObjectPath nodeStats = getNodeStats("indices/request_cache");
|
||||
return evaluateLong(nodeStats, "indices.request_cache.memory_size_in_bytes");
|
||||
}
|
||||
|
||||
private static ObjectPath getNodeStats(String stat) throws IOException {
|
||||
Request request = new Request("GET", "/_nodes/stats/" + stat);
|
||||
Response response = client().performRequest(request);
|
||||
assertOK(response);
|
||||
final Map<String, ?> responseMap = entityAsMap(response);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, ?> nodes = (Map<String, ?>) responseMap.get("nodes");
|
||||
assertThat(nodes, aMapWithSize(1));
|
||||
|
||||
ObjectPath nodeStats = new ObjectPath(nodes.values().iterator().next());
|
||||
return nodeStats;
|
||||
}
|
||||
|
||||
private static long evaluateLong(ObjectPath nodeStats, String path) throws IOException {
|
||||
Object size = nodeStats.evaluate(path);
|
||||
assertThat("did not find " + path + " in " + nodeStats, size, notNullValue());
|
||||
assertThat("incorrect type for " + path + " in " + nodeStats, size, instanceOf(Number.class));
|
||||
return ((Number) size).longValue();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue