Skip to content
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