Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Group field caps shard requests per node #77047

Merged
merged 10 commits into from
Sep 27, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public void testGetFieldMappings() {
}

public void testFieldCapabilities() {
String fieldCapabilitiesShardAction = FieldCapabilitiesAction.NAME + "[index][s]";
String fieldCapabilitiesShardAction = FieldCapabilitiesAction.NAME + "[n]";
interceptTransportActions(fieldCapabilitiesShardAction);

FieldCapabilitiesRequest fieldCapabilitiesRequest = new FieldCapabilitiesRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,7 @@ public void testFailuresFromRemote() {
.filter(f -> Arrays.asList(f.getIndices()).contains("remote_cluster:" + remoteErrorIndex))
.findFirst().get();
ex = failure.getException();
assertEquals(RemoteTransportException.class, ex.getClass());
cause = ExceptionsHelper.unwrapCause(ex);
assertEquals(IllegalArgumentException.class, cause.getClass());
assertEquals("I throw because I choose to.", cause.getMessage());
assertEquals(IllegalArgumentException.class, ex.getClass());
assertEquals("I throw because I choose to.", ex.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.index.mapper.DocumentParserContext;
import org.elasticsearch.index.mapper.KeywordFieldMapper;
import org.elasticsearch.index.mapper.MetadataFieldMapper;
import org.elasticsearch.index.mapper.DocumentParserContext;
import org.elasticsearch.index.query.AbstractQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
Expand All @@ -29,7 +29,6 @@
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.SearchPlugin;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.transport.RemoteTransportException;
import org.junit.Before;

import java.io.IOException;
Expand Down Expand Up @@ -298,9 +297,8 @@ public void testFailures() throws InterruptedException {
assertEquals(2, response.getFailedIndices().length);
assertThat(response.getFailures().get(0).getIndices(), arrayContainingInAnyOrder("index1-error", "index2-error"));
Exception failure = response.getFailures().get(0).getException();
assertEquals(RemoteTransportException.class, failure.getClass());
assertEquals(IllegalArgumentException.class, failure.getCause().getClass());
assertEquals("I throw because I choose to.", failure.getCause().getMessage());
assertEquals(IllegalArgumentException.class, failure.getClass());
assertEquals("I throw because I choose to.", failure.getMessage());

// the "indices" section should not include failed ones
assertThat(Arrays.asList(response.getIndices()), containsInAnyOrder("old_index", "new_index"));
Expand Down
16 changes: 16 additions & 0 deletions server/src/main/java/org/elasticsearch/action/OriginalIndices.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

/**
* Used to keep track of original indices within internal (e.g. shard level) requests
Expand Down Expand Up @@ -67,4 +68,19 @@ public String toString() {
", indicesOptions=" + indicesOptions +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OriginalIndices that = (OriginalIndices) o;
return Arrays.equals(indices, that.indices) && Objects.equals(indicesOptions, that.indicesOptions);
}

@Override
public int hashCode() {
int result = Objects.hash(indicesOptions);
result = 31 * result + Arrays.hashCode(indices);
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,4 @@ FieldCapabilitiesFailure addIndex(String index) {
this.indices.add(index);
return this;
}

FieldCapabilitiesFailure addIndices(List<String> indices) {
this.indices.addAll(indices);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.action.fieldcaps;

import org.elasticsearch.index.IndexService;
import org.elasticsearch.index.engine.Engine;
import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.ObjectMapper;
import org.elasticsearch.index.mapper.RuntimeField;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.SearchExecutionContext;
import org.elasticsearch.index.shard.IndexShard;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.indices.IndicesService;
import org.elasticsearch.search.SearchService;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.internal.AliasFilter;
import org.elasticsearch.search.internal.ShardSearchRequest;

import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

/**
* Loads the mappings for an index and computes all {@link IndexFieldCapabilities}. This
* helper class performs the core shard operation for the field capabilities action.
*/
class FieldCapabilitiesFetcher {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class lets us use the same per-shard logic for both the new and old execution strategies. It's not completely necessary to add it -- I could have shuffled some inner classes around to let us share this logic. However I found this to be a nice abstraction. It helps breaks up TransportFieldCapabilitiesAction, which is complex, and opens the door to adding unit tests for field caps (which I hope to in a follow-up).

private final IndicesService indicesService;

FieldCapabilitiesFetcher(IndicesService indicesService) {
this.indicesService = indicesService;
}

public FieldCapabilitiesIndexResponse fetch(final FieldCapabilitiesIndexRequest request) throws IOException {
final ShardId shardId = request.shardId();
final IndexService indexService = indicesService.indexServiceSafe(shardId.getIndex());
final IndexShard indexShard = indexService.getShard(request.shardId().getId());
try (Engine.Searcher searcher = indexShard.acquireSearcher(Engine.CAN_MATCH_SEARCH_SOURCE)) {

final SearchExecutionContext searchExecutionContext = indexService.newSearchExecutionContext(shardId.id(), 0,
searcher, request::nowInMillis, null, request.runtimeFields());

if (canMatchShard(request, searchExecutionContext) == false) {
return new FieldCapabilitiesIndexResponse(request.index(), Collections.emptyMap(), false);
}

Set<String> fieldNames = new HashSet<>();
for (String pattern : request.fields()) {
fieldNames.addAll(searchExecutionContext.getMatchingFieldNames(pattern));
}

Predicate<String> fieldPredicate = indicesService.getFieldFilter().apply(shardId.getIndexName());
Map<String, IndexFieldCapabilities> responseMap = new HashMap<>();
for (String field : fieldNames) {
MappedFieldType ft = searchExecutionContext.getFieldType(field);
boolean isMetadataField = searchExecutionContext.isMetadataField(field);
if (isMetadataField || fieldPredicate.test(ft.name())) {
IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(field,
ft.familyTypeName(), isMetadataField, ft.isSearchable(), ft.isAggregatable(), ft.meta());
responseMap.put(field, fieldCap);
} else {
continue;
}

// Check the ancestor of the field to find nested and object fields.
// Runtime fields are excluded since they can override any path.
//TODO find a way to do this that does not require an instanceof check
if (ft instanceof RuntimeField == false) {
int dotIndex = ft.name().lastIndexOf('.');
while (dotIndex > -1) {
String parentField = ft.name().substring(0, dotIndex);
if (responseMap.containsKey(parentField)) {
// we added this path on another field already
break;
}
// checks if the parent field contains sub-fields
if (searchExecutionContext.getFieldType(parentField) == null) {
// no field type, it must be an object field
ObjectMapper mapper = searchExecutionContext.getObjectMapper(parentField);
// Composite runtime fields do not have a mapped type for the root - check for null
if (mapper != null) {
String type = mapper.isNested() ? "nested" : "object";
IndexFieldCapabilities fieldCap = new IndexFieldCapabilities(parentField, type,
false, false, false, Collections.emptyMap());
responseMap.put(parentField, fieldCap);
}
}
dotIndex = parentField.lastIndexOf('.');
}
}
}
return new FieldCapabilitiesIndexResponse(request.index(), responseMap, true);
}
}

private boolean canMatchShard(FieldCapabilitiesIndexRequest req, SearchExecutionContext searchExecutionContext) throws IOException {
if (req.indexFilter() == null || req.indexFilter() instanceof MatchAllQueryBuilder) {
return true;
}
assert req.nowInMillis() != 0L;
ShardSearchRequest searchRequest = new ShardSearchRequest(req.shardId(), null, req.nowInMillis(), AliasFilter.EMPTY);
searchRequest.source(new SearchSourceBuilder().query(req.indexFilter()));
return SearchService.queryStillMatchesAfterRewrite(searchRequest, searchExecutionContext);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.action.fieldcaps;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.IndicesRequest;
import org.elasticsearch.action.OriginalIndices;
import org.elasticsearch.action.support.IndicesOptions;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.shard.ShardId;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;

class FieldCapabilitiesNodeRequest extends ActionRequest implements IndicesRequest {

private final ShardId[] shardIds;
private final String[] fields;
private final OriginalIndices originalIndices;
private final QueryBuilder indexFilter;
private final long nowInMillis;
private final Map<String, Object> runtimeFields;

FieldCapabilitiesNodeRequest(StreamInput in) throws IOException {
super(in);
shardIds = in.readArray(ShardId::new, ShardId[]::new);
fields = in.readStringArray();
originalIndices = OriginalIndices.readOriginalIndices(in);
indexFilter = in.readOptionalNamedWriteable(QueryBuilder.class);
nowInMillis = in.readLong();
runtimeFields = in.readMap();
}

FieldCapabilitiesNodeRequest(ShardId[] shardIds,
String[] fields,
OriginalIndices originalIndices,
QueryBuilder indexFilter,
long nowInMillis,
Map<String, Object> runtimeFields) {
this.fields = fields;
this.shardIds = shardIds;
this.originalIndices = originalIndices;
this.indexFilter = indexFilter;
this.nowInMillis = nowInMillis;
this.runtimeFields = runtimeFields;
}

public String[] fields() {
return fields;
}

public OriginalIndices originalIndices() {
return originalIndices;
}

@Override
public String[] indices() {
return originalIndices.indices();
}

@Override
public IndicesOptions indicesOptions() {
return originalIndices.indicesOptions();
}

public QueryBuilder indexFilter() {
return indexFilter;
}

public Map<String, Object> runtimeFields() {
return runtimeFields;
}

public ShardId[] shardIds() {
return shardIds;
}

public long nowInMillis() {
return nowInMillis;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeArray(shardIds);
out.writeStringArray(fields);
OriginalIndices.writeOriginalIndices(originalIndices, out);
out.writeOptionalNamedWriteable(indexFilter);
out.writeLong(nowInMillis);
out.writeMap(runtimeFields);
}

@Override
public ActionRequestValidationException validate() {
return null;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FieldCapabilitiesNodeRequest that = (FieldCapabilitiesNodeRequest) o;
return nowInMillis == that.nowInMillis && Arrays.equals(shardIds, that.shardIds)
&& Arrays.equals(fields, that.fields) && Objects.equals(originalIndices, that.originalIndices)
&& Objects.equals(indexFilter, that.indexFilter) && Objects.equals(runtimeFields, that.runtimeFields);
}

@Override
public int hashCode() {
int result = Objects.hash(originalIndices, indexFilter, nowInMillis, runtimeFields);
result = 31 * result + Arrays.hashCode(shardIds);
result = 31 * result + Arrays.hashCode(fields);
return result;
}
}
Loading